193 行
11 KiB
Markdown
193 行
11 KiB
Markdown
# @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。
|
||
|
||
`withXuqmModuleConfig()` 会让开发服务器公开当前工程标识;`xuqm-rn run` 在复用
|
||
已有 Metro 前必须校验该标识。若端口属于其他工程或不支持标识校验的旧服务,命令
|
||
会明确失败并要求重启 Metro,禁止静默连接后运行错误 bundle。确认属于当前工程后
|
||
仍完全使用标准 Metro 与 Fast Refresh,不生成或读取 Debug 插件包。
|
||
|
||
发布使用的 `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 的依赖闭包,不能让客户端并发拼接多个独立更新结果。
|