2026-06-16 12:14:52 +08:00
# @xuqm/rn-update
2026-07-17 13:50:30 +08:00
Android 整包更新与 RN 多 Bundle 插件运行时。它依赖 `@xuqm/rn-common` 的唯一配置和会话,不提供第二套初始化、登录、网络或文件实现。
2026-06-16 12:14:52 +08:00
2026-07-17 13:50:30 +08:00
当前发布范围为 Android。iOS 插件事务实现完成并通过验证前,不作为本包的已支持能力发布。
## 最小接入
2026-06-16 12:14:52 +08:00
```bash
2026-07-17 13:50:30 +08:00
pnpm add @xuqm/rn -common @xuqm/rn -update \
@react -native-async-storage/async-storage
pnpm exec xuqm-rn init
pnpm run xuqm:doctor
2026-06-16 12:14:52 +08:00
```
2026-07-26 23:47:30 +08:00
`xuqm-rn init` 生成 schema v3 `xuqm.modules.json` 并补充最少脚本。版本只有两条明确来源:
2026-06-16 12:14:52 +08:00
2026-07-17 13:50:30 +08:00
- 完整 App 版本来自宿主 `package.json.version` ,医网信新包从 `8.0.0` 开始。
2026-07-26 23:47:30 +08:00
- 插件默认版本来自 `xuqm.modules.json.pluginVersion` ,从 `1.0.0` 开始;仅独立发布的模块覆盖 `moduleVersion` 。
2026-07-27 00:39:26 +08:00
`xuqm.modules.json` 分别使用 Android `packageName` 和 iOS `iosBundleId` 保存平台宿主
身份,只在目标平台 build/package/release 时要求相应字段。ZIP wire 字段仍统一为
`packageName` ;CLI 通过唯一平台解析器写入对应身份,发布前从 ZIP 内重新读取并与同一
解析结果精确比较,缺失或不一致时在联网前拒绝发布。
2026-07-26 23:47:30 +08:00
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。
2026-06-16 12:14:52 +08:00
2026-07-17 13:50:30 +08:00
app/buz 使用 `commonVersionRange` 声明 SemVer 兼容范围,并使用 `minNativeApiLevel` 声明最低原生能力。Bundle 只接受 SHA-256,不使用 MD5。
2026-06-16 12:14:52 +08:00
2026-07-18 08:59:40 +08:00
宿主的唯一 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 配置。
2026-07-17 13:50:30 +08:00
## 开发与打包
2026-06-16 12:14:52 +08:00
2026-07-17 13:50:30 +08:00
- `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。
2026-06-16 12:14:52 +08:00
2026-07-27 16:32:38 +08:00
`withXuqmModuleConfig()` 会让开发服务器公开当前工程标识;`xuqm-rn run` 在复用
已有 Metro 前必须校验该标识。若端口属于其他工程或不支持标识校验的旧服务,命令
会明确失败并要求重启 Metro,禁止静默连接后运行错误 bundle。确认属于当前工程后
仍完全使用标准 Metro 与 Fast Refresh,不生成或读取 Debug 插件包。
2026-07-26 23:47:30 +08:00
发布使用的 `XUQM_API_TOKEN` 只能由 Jenkins Credentials 注入环境变量,不属于
`xuqm.modules.json` 或 `config.xuqmconfig` 。CLI 不读取项目文件中的 Token,避免可用凭据
进入源码、插件包或构建日志。
2026-07-27 00:39:26 +08:00
配置校验会直接拒绝 `release.apiToken` ;发布凭据只能来自 `XUQM_API_TOKEN` 。
2026-07-26 23:47:30 +08:00
2026-07-17 13:50:30 +08:00
Android 宿主只应用一次 SDK 脚本:
2026-06-16 12:14:52 +08:00
2026-07-17 13:50:30 +08:00
```groovy
apply from: file("../../node_modules/@xuqm/rn-update/android/xuqm-bundles.gradle")
```
2026-06-16 12:14:52 +08:00
2026-07-17 13:50:30 +08:00
该脚本关闭 React Native 默认单体 Bundle,避免宿主同时维护两套启动产物。
2026-06-16 12:14:52 +08:00
2026-07-20 19:31:43 +08:00
多 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 分别执行同名原生组件注册。
2026-07-18 06:59:57 +08:00
插件产物的引擎只有一个事实来源: Android 宿主的 `android/gradle.properties` 。当
`hermesEnabled=true` 时,CLI 自动使用与宿主 React Native 匹配的 `hermes-compiler` 将每个
startup/common/app/buz 编译成 Hermes bytecode,并组合 Metro/Hermes source map;无需也不允许在
2026-07-26 23:47:30 +08:00
`xuqm.modules.json` 再声明一份引擎。manifest 和发布请求会记录 `bundleFormat` 。
2026-07-18 06:59:57 +08:00
2026-07-17 13:50:30 +08:00
## 运行时规则
2026-06-16 12:14:52 +08:00
2026-07-20 19:31:43 +08:00
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 依赖,未访问插件不复制、不解压、不执行。
2026-07-17 14:12:02 +08:00
安装仅包含 buz 的 release set 后,使用
`XuqmRuntime.activate(moduleId, { reloadBundle: true })` 重新求值并激活新 bundle。若 release set
包含 common,宿主必须冷启动,不能在同一 JavaScript VM 中混用新旧 common。
2026-06-16 12:14:52 +08:00
```ts
2026-07-17 13:50:30 +08:00
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,
})
2026-06-16 13:25:46 +08:00
2026-07-17 13:50:30 +08:00
const startup = await XuqmRuntime.start()
2026-06-16 13:25:46 +08:00
```
2026-07-17 13:50:30 +08:00
`start()` 的固定顺序是:准备未确认事务 → 检查完整 App → 没有整包更新时检查 app+common。返回 `kind: 'app'` 时由宿主显示整包更新 UI;返回 `kind: 'plugins'` 且 `requiresColdStart` 时重启后加载并确认。
2026-06-16 13:25:46 +08:00
2026-07-17 13:50:30 +08:00
进入 buz 前只检查当前入口及 common:
2026-06-16 13:25:46 +08:00
```ts
2026-07-17 13:50:30 +08:00
const plan = await UpdateSDK.checkAndInstallPlugin('prescription', {
onProgress(moduleId, progress) {
console.log(moduleId, progress.percent)
2026-06-16 13:25:46 +08:00
},
})
2026-07-17 13:50:30 +08:00
```
2026-06-16 13:25:46 +08:00
2026-07-20 19:31:43 +08:00
SDK 会验证所有已安装模块在目标 common 下仍兼容,并拒绝 `builtAgainstNativeBaselineId` 与当前完整包不一致的候选,然后整组下载、校验、staging、激活、确认或回滚。Bundle 版本与事务状态只保存在原生 state;宿主不得另建 AsyncStorage 版本表,也不得逐层传递版本或路径状态。
2026-07-17 13:50:30 +08:00
2026-07-28 20:29:06 +08:00
整包检查返回结构化结果,不展示 UI,也不以 `null` 混淆状态:
```ts
const result = await UpdateSDK.checkAppUpdate()
if (result.status === 'UPDATE_AVAILABLE' & & result.update) {
showUpdateDialog(result.update, result.network)
}
```
状态固定为 `UPDATE_AVAILABLE / NO_UPDATE / LOGIN_REQUIRED / SERVICE_DISABLED / FAILED` 。
Android 同时返回 `WIFI / CELLULAR / CELLULAR_4G / CELLULAR_5G / ETHERNET / OTHER / NONE`
及是否计费网络,宿主自行决定是否允许下载安装。检查失败、未登录或服务关闭均不阻断
宿主业务。下载、安装和插件 API 仍要求平台明确开启 `features.update` ,关闭时在任何
网络或原生操作前抛出 `UpdateDisabledError` ( `code: UPDATE_DISABLED`)。
Android 整包能力全部桥接到原生权威实现:
- `downloadApk()` :只下载和校验,可选 `destination: 'PUBLIC_DOWNLOADS'` 。
- `downloadAndInstallApp()` : 下载、SHA-256/包名/签名校验并打开系统安装器。
- `NativeAppUpdate.installDownloaded()` :安装本次检查授权的已下载版本。
- `openStore(updateInfo)` :设备商店不可用时回退租户平台返回的通用下载页。
任务取消、失败或进程结束后不会在下次启动自动续传。
2026-07-27 00:39:26 +08:00
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 逐项比较。
2026-07-17 13:50:30 +08:00
需要先展示插件更新提示的项目,分别调用纯检查和安装 API:
```ts
const plan = await UpdateSDK.checkPluginRelease('prescription')
if (plan & & (await showPluginUpdateDialog(plan))) {
await UpdateSDK.installPluginRelease(plan)
}
2026-06-16 12:14:52 +08:00
```
2026-07-17 13:50:30 +08:00
检查不会下载或修改状态;安装前会重新比对检查时的全部本地版本,计划过期时抛出 `StaleReleaseSetError` ,调用方重新检查即可。
2026-06-16 12:14:52 +08:00
2026-07-17 13:50:30 +08:00
服务端使用单次 `POST /api/v1/rn/release-set/check` 返回目标 app/buz 与 common 的依赖闭包,不能让客户端并发拼接多个独立更新结果。