XuqmGroup-RNSDK/packages/update
XuqmGroup d009c3b616 fix(common): ValidationError 改为继承 Error 修复 Hermes 校验崩溃
- Babel 转译子类 apply 调用原生 ZodError 在 Hermes 抛 TypeError,掩盖真实校验失败
- unexpected 分支补安全诊断日志(构造名+截断消息)
- xuqm-bundles.gradle 将 file: 依赖的 src/metro 纳入任务输入
2026-07-21 01:10:41 +08:00
..
android fix(common): ValidationError 改为继承 Error 修复 Hermes 校验崩溃 2026-07-21 01:10:41 +08:00
ios feat: T-B01~B04 — XuqmBundleModule + onProgress + JSBridge/厂商文档 2026-06-15 02:36:11 +08:00
metro feat(update): release-set 事务制品与原生层职责拆分 2026-07-20 19:31:43 +08:00
scripts feat(update): release-set 事务制品与原生层职责拆分 2026-07-20 19:31:43 +08:00
src feat(update): release-set 事务制品与原生层职责拆分 2026-07-20 19:31:43 +08:00
tests feat(update): release-set 事务制品与原生层职责拆分 2026-07-20 19:31:43 +08:00
package.json feat(update): release-set 事务制品与原生层职责拆分 2026-07-20 19:31:43 +08:00
react-native.config.js chore: sync local changes 2026-05-07 19:39:41 +08:00
README.md feat(update): release-set 事务制品与原生层职责拆分 2026-07-20 19:31:43 +08:00
tsconfig.json feat: rebuild RN sdk runtime and plugin updates 2026-07-17 13:50:30 +08:00

@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 大于等于内嵌版本,且 nativeBaselineIdminNativeApiLevel 和文件长度仍兼容时,直接返回现有 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 的依赖闭包,不能让客户端并发拼接多个独立更新结果。