- 插件包改为 .xuqm.zip 事务制品,manifest/bundle/资源/SHA-256 统一校验 - 原生层拆分为桥接编排、插件包暂存、底层存储三个单一职责类 - 状态查询改为纯读取,不再误触发安装;新增 NATIVE_BASELINE_INCOMPATIBLE 拒绝 - SemVer 解析拆分为独立原生类并补原生测试 - XWebView 收口唯一内核,错误态统一中文层与重试 - common 增加请求头/运行时契约收口与 http 测试
7.7 KiB
@xuqm/rn-update
Android 整包更新与 RN 多 Bundle 插件运行时。它依赖 @xuqm/rn-common 的唯一配置和会话,不提供第二套初始化、登录、网络或文件实现。
当前发布范围为 Android。iOS 插件事务实现完成并通过验证前,不作为本包的已支持能力发布。
最小接入
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.config.json 并补充最少脚本。版本只有两条明确来源:
- 完整 App 版本来自宿主
package.json.version,医网信新包从8.0.0开始。 - 插件默认版本来自
xuqm.config.json.pluginVersion,从1.0.0开始;仅独立发布的模块覆盖moduleVersion。
app/buz 使用 commonVersionRange 声明 SemVer 兼容范围,并使用 minNativeApiLevel 声明最低原生能力。Bundle 只接受 SHA-256,不使用 MD5。
宿主的唯一 Metro 配置使用 update 组合器;它已经包含 common 的自动初始化配置:
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。
Android 宿主只应用一次 SDK 脚本:
apply from: file("../../node_modules/@xuqm/rn-update/android/xuqm-bundles.gradle")
该脚本关闭 React Native 默认单体 Bundle,避免宿主同时维护两套启动产物。
多 Bundle 启动壳只需要查询原生分包路径时使用轻量子路径,禁止为了两个原生调用导入完整更新、网络与 release-set 实现:
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 入口保留轻量注册表:
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.config.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。
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:
const plan = await UpdateSDK.checkAndInstallPlugin('prescription', {
onProgress(moduleId, progress) {
console.log(moduleId, progress.percent)
},
})
SDK 会验证所有已安装模块在目标 common 下仍兼容,并拒绝 builtAgainstNativeBaselineId 与当前完整包不一致的候选,然后整组下载、校验、staging、激活、确认或回滚。Bundle 版本与事务状态只保存在原生 state;宿主不得另建 AsyncStorage 版本表,也不得逐层传递版本或路径状态。
需要先展示插件更新提示的项目,分别调用纯检查和安装 API:
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 的依赖闭包,不能让客户端并发拼接多个独立更新结果。