# @xuqm/rn-update Android 整包更新与 RN 多 Bundle 插件运行时。它依赖 `@xuqm/rn-common` 的唯一配置和会话,不提供第二套初始化、登录、网络或文件实现。 当前发布范围为 Android。iOS 插件事务实现完成并通过验证前,不作为本包的已支持能力发布。 ## 最小接入 ```bash pnpm add @xuqm/rn-common @xuqm/rn-update \ @react-native-async-storage/async-storage pnpm exec xuqm-rn init pnpm run xuqm:doctor ``` `xuqm-rn init` 生成 schema v3 `xuqm.modules.json` 并补充最少脚本。版本只有两条明确来源: - 完整 App 版本来自宿主 `package.json.version`,医网信新包从 `8.0.0` 开始。 - 插件默认版本来自 `xuqm.modules.json.pluginVersion`,从 `1.0.0` 开始;仅独立发布的模块覆盖 `moduleVersion`。 `xuqm.modules.json` 分别使用 Android `packageName` 和 iOS `iosBundleId` 保存平台宿主 身份,只在目标平台 build/package/release 时要求相应字段。ZIP wire 字段仍统一为 `packageName`;CLI 通过唯一平台解析器写入对应身份,发布前从 ZIP 内重新读取并与同一 解析结果精确比较,缺失或不一致时在联网前拒绝发布。 Debug 启动只注册并校验插件,不访问远程更新服务。开发页如需验证平台能力,显式调用 `UpdateSDK.developer.checkStartupOnce()` 或 `UpdateSDK.developer.checkAndInstallPluginOnce(moduleId)`;该状态不会跨重启保留。 `xuqm-rn package android` 会为每次 Release 自动生成递增 `versionCode` 与不可变 `buildId`,并同时注入 APK、插件 manifest 和 Source Map 身份。宿主不得手工维护 versionCode;Jenkins 可通过 `XUQM_VERSION_CODE`、`XUQM_BUILD_ID` 固定本次流水线身份。 同一 `versionName` 的重复测试包仍会获得新的 versionCode/buildId,从而正常覆盖安装并 加载本次 Bundle。 app/buz 使用 `commonVersionRange` 声明 SemVer 兼容范围,并使用 `minNativeApiLevel` 声明最低原生能力。Bundle 只接受 SHA-256,不使用 MD5。 宿主的唯一 Metro 配置使用 update 组合器;它已经包含 common 的自动初始化配置: ```js const { getDefaultConfig } = require('@react-native/metro-config') const { withXuqmModuleConfig } = require('@xuqm/rn-update/metro') module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname)) ``` 日常 `start/android` 没有模块构建上下文,保持标准 Metro 行为。构建 app/buz 时 CLI 自动先生成 startup/common 共享模块表,并为每个插件分配互不重叠的稳定区间;宿主不得复制模块 ID 缓存或维护多份 Metro 配置。 ## 开发与打包 - `pnpm android`:安装 Debug 包、连接 Metro、支持热刷新。 - `pnpm release:android`:重新构建并内嵌全部 startup/common/app/buz 后生成 Release AAB。 - `pnpm release:android -- --apk`:生成包含同一批插件的 Release APK。 - `pnpm publish:android`:上传插件产物;SDK npm 制品发布只能通过 Jenkins。 发布使用的 `XUQM_API_TOKEN` 只能由 Jenkins Credentials 注入环境变量,不属于 `xuqm.modules.json` 或 `config.xuqmconfig`。CLI 不读取项目文件中的 Token,避免可用凭据 进入源码、插件包或构建日志。 配置校验会直接拒绝 `release.apiToken`;发布凭据只能来自 `XUQM_API_TOKEN`。 Android 宿主只应用一次 SDK 脚本: ```groovy apply from: file("../../node_modules/@xuqm/rn-update/android/xuqm-bundles.gradle") ``` 该脚本关闭 React Native 默认单体 Bundle,避免宿主同时维护两套启动产物。 多 Bundle 启动壳只需要查询原生分包路径时使用轻量子路径,禁止为了两个原生调用导入完整更新、网络与 release-set 实现: ```ts import { NativeBundle } from '@xuqm/rn-update/native-bundle' await NativeBundle.preparePendingRelease() const commonPath = await NativeBundle.getLaunchPath('common') const appPath = await NativeBundle.getLaunchPath('app') ``` 启动壳只能依次准备并装载 common、app,不得读取 manifest 后枚举或释放 buz。普通业务继续从 `@xuqm/rn-update` 根入口使用 `UpdateSDK`/`XuqmRuntime`;该子路径只用于首屏启动壳和底层宿主编排,不是第二套状态或兼容 API。 多 bundle 宿主还必须在 common 入口保留轻量注册表: ```ts import '@xuqm/rn-update/plugin-registry' ``` `app` 与各 `buz` 拥有隔离的模块编号区间,common 中预注册的 `plugin-registry` 保证插件执行 `definePlugin()` 后,宿主 `XuqmRuntime` 读取到同一份定义。SDK 的构建/脚手架应生成该入口,业务项目不得自行复制注册表。 同一规则适用于跨 Bundle React Context 和具有原生注册副作用的依赖:Provider 在 app 挂载、由 buz 消费的 Context,以及 app/buz 都会使用的原生 View/Module,必须由 common 入口静态持有并分配共享编号。禁止在插件中复制 Context、注册表,或让多个 Bundle 分别执行同名原生组件注册。 插件产物的引擎只有一个事实来源:Android 宿主的 `android/gradle.properties`。当 `hermesEnabled=true` 时,CLI 自动使用与宿主 React Native 匹配的 `hermes-compiler` 将每个 startup/common/app/buz 编译成 Hermes bytecode,并组合 Metro/Hermes source map;无需也不允许在 `xuqm.modules.json` 再声明一份引擎。manifest 和发布请求会记录 `bundleFormat`。 ## 运行时规则 Android 启动只把 APK 内嵌版本当作可恢复基线,不会每次重新读取或复制大 Bundle: - 原生模块每个进程只解析一次内嵌 `manifest.json`,随后以本地 `state.json` 为版本事实源。 - `getBundleState()` 是纯状态查询:已有本地状态时直接读取,尚未访问的模块只从 manifest 返回内嵌基线元数据,不创建目录、不复制 Bundle。安装/恢复只允许由目标模块的 launch path 请求触发。 - 本地下载版本 SemVer 大于等于内嵌版本,且 `nativeBaselineId`、`minNativeApiLevel` 和文件长度仍兼容时,直接返回现有 `active.bundle`。 - 本地文件由当前 APK 内嵌基线产生时,版本相同仍比较 manifest SHA-256;这样开发期同版本的新 APK 可以替换旧基线,完全相同的 APK 则直接短路。 - 只有首次安装、本地版本落后、原生基线不兼容、摘要/长度异常或 APK 内嵌内容变化时,才读取 APK 中的 Bundle,校验 SHA-256,并连同资源原子恢复。 因此“版本检查”不等于每次网络检查,也不等于解析 Bundle 内容。启动路径只做小状态读取;插件网络更新仍按下述入口策略执行。 上述恢复是严格的单模块按需操作:冷启动只触发 common、app;buz 在用户进入相应功能时才调用 `checkAndInstallPlugin(moduleId)` 和 `XuqmRuntime.activate(moduleId)`。健康的本地 buz 直接执行,缺失或不兼容时只处理目标 buz 与其 common 依赖,未访问插件不复制、不解压、不执行。 安装仅包含 buz 的 release set 后,使用 `XuqmRuntime.activate(moduleId, { reloadBundle: true })` 重新求值并激活新 bundle。若 release set 包含 common,宿主必须冷启动,不能在同一 JavaScript VM 中混用新旧 common。 ```ts import { UpdateSDK, XuqmRuntime } from '@xuqm/rn-update' XuqmRuntime.configure({ plugins: [ { moduleId: 'common', type: 'common', appVersionRange: '>=8.0.0 <9.0.0', }, { moduleId: 'app', type: 'app', appVersionRange: '>=8.0.0 <9.0.0', commonVersionRange: '>=1.0.0 <2.0.0', }, { moduleId: 'prescription', type: 'buz', appVersionRange: '>=8.0.0 <9.0.0', commonVersionRange: '>=1.0.0 <2.0.0', }, ], context, loadBundle, }) const startup = await XuqmRuntime.start() ``` `start()` 的固定顺序是:准备未确认事务 → 检查完整 App → 没有整包更新时检查 app+common。返回 `kind: 'app'` 时由宿主显示整包更新 UI;返回 `kind: 'plugins'` 且 `requiresColdStart` 时重启后加载并确认。 进入 buz 前只检查当前入口及 common: ```ts const plan = await UpdateSDK.checkAndInstallPlugin('prescription', { onProgress(moduleId, progress) { console.log(moduleId, progress.percent) }, }) ``` SDK 会验证所有已安装模块在目标 common 下仍兼容,并拒绝 `builtAgainstNativeBaselineId` 与当前完整包不一致的候选,然后整组下载、校验、staging、激活、确认或回滚。Bundle 版本与事务状态只保存在原生 state;宿主不得另建 AsyncStorage 版本表,也不得逐层传递版本或路径状态。 所有公开检查、下载和安装 API 都要求平台明确开启 `features.update`。关闭时在任何网络 或原生操作前抛出 `UpdateDisabledError`(`code: UPDATE_DISABLED`);登录、启动等自动 后台路径会吞掉该状态并正常返回,不影响宿主使用。 release-set 检查请求必须携带当前 `appVersion`、`nativeApiLevel` 和 `nativeBaselineId`;原生基线缺失时客户端在请求前阻断。服务端候选必须提供 `keyId/signature`,签名覆盖以下不可变 canonical manifest: `appKey, packageName, platform, moduleId, type, version, appVersionRange, builtAgainstNativeBaselineId, buildId, bundleFormat, minNativeApiLevel, bundleSha256, archiveSha256` app/buz 另包含 `commonVersionRange`;common 模块不构造该字段。canonical JSON 与 Config V2 一致:UTF-8、键按字典序、无空白、null/undefined 字段省略。 客户端在生成计划前及安装前各验签一次,未知 keyId、签名错误、宿主身份不匹配或任一 字段被篡改都会拒绝。响应 `sha256` 以 `archiveSha256` 进入签名清单,下载后使用同值 校验 ZIP;Android 解包层还会把已签名的 `buildId/bundleFormat/bundleSha256` 与包内 manifest 逐项比较。 需要先展示插件更新提示的项目,分别调用纯检查和安装 API: ```ts const plan = await UpdateSDK.checkPluginRelease('prescription') if (plan && (await showPluginUpdateDialog(plan))) { await UpdateSDK.installPluginRelease(plan) } ``` 检查不会下载或修改状态;安装前会重新比对检查时的全部本地版本,计划过期时抛出 `StaleReleaseSetError`,调用方重新检查即可。 服务端使用单次 `POST /api/v1/rn/release-set/check` 返回目标 app/buz 与 common 的依赖闭包,不能让客户端并发拼接多个独立更新结果。