feat(update): manage terminal RN plugin packages
这个提交包含在:
父节点
985c7f78d7
当前提交
7c4915bdb6
@ -55,64 +55,34 @@ await UpdateSDK.openStore(appUpdate.appStoreUrl, appUpdate.marketUrl)
|
||||
|
||||
---
|
||||
|
||||
## 4. RN Bundle 热更新
|
||||
## 4. RN 插件更新
|
||||
|
||||
### 4.1 注册插件
|
||||
插件更新以服务端返回的 release-set 为最小事务单位。一个 release-set 只包含目标
|
||||
app/buz 及其 Common 依赖;SDK 会统一完成签名、宿主身份、整包版本、原生基线、
|
||||
SHA-256 和最终依赖闭包校验。宿主不能分别下载后自行拼接版本。
|
||||
|
||||
在插件 Bundle 的入口文件顶部注册插件元数据:
|
||||
进入 buz 前自动检查并安装:
|
||||
|
||||
```ts
|
||||
// plugin/index.ts
|
||||
import { UpdateSDK } from '@xuqm/rn-update'
|
||||
import meta from './plugin.json'
|
||||
|
||||
UpdateSDK.registerPlugin(meta)
|
||||
await UpdateSDK.checkAndInstallPlugin('home', {
|
||||
onProgress(moduleId, progress) {
|
||||
console.log(moduleId, progress.percent)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`plugin.json` 示例:
|
||||
需要由宿主展示确认弹窗时,检查与安装分开调用:
|
||||
|
||||
```json
|
||||
{
|
||||
"moduleId": "home",
|
||||
"version": "1.2.3"
|
||||
```ts
|
||||
const plan = await UpdateSDK.checkPluginRelease('home')
|
||||
if (plan && (await showPluginUpdateDialog(plan))) {
|
||||
await UpdateSDK.installPluginRelease(plan)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 检查热更新
|
||||
|
||||
```ts
|
||||
const rnUpdate = await UpdateSDK.checkRnUpdate('home')
|
||||
if (rnUpdate.needsUpdate) {
|
||||
console.log('最新版本:', rnUpdate.latestVersion)
|
||||
console.log('下载地址:', rnUpdate.downloadUrl)
|
||||
console.log('更新说明:', rnUpdate.note)
|
||||
console.log('最低依赖版本:', rnUpdate.minCommonVersion)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 下载并缓存 Bundle
|
||||
|
||||
```ts
|
||||
// 下载 Bundle 源码
|
||||
const source = await UpdateSDK.downloadRnBundle(rnUpdate.downloadUrl)
|
||||
|
||||
// 缓存到本地
|
||||
await UpdateSDK.cacheRnBundle('home', rnUpdate.latestVersion, rnUpdate.md5, source)
|
||||
|
||||
// 读取已缓存的 Bundle
|
||||
const cached = await UpdateSDK.getCachedRnBundle('home')
|
||||
if (cached) {
|
||||
console.log('缓存版本:', cached.version)
|
||||
console.log('缓存时间:', cached.downloadedAt)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 获取已注册插件版本
|
||||
|
||||
```ts
|
||||
const version = UpdateSDK.getRegisteredPluginVersion('home')
|
||||
console.log('当前运行版本:', version)
|
||||
```
|
||||
`checkPluginRelease` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
|
||||
记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
|
||||
管理,宿主不得另建一套缓存版本表。
|
||||
|
||||
---
|
||||
|
||||
@ -160,30 +130,32 @@ await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
|
||||
SDK 提供 `xuqm_release.mjs` 脚本,可一键完成 RN Bundle 构建并上传至 XuqmGroup 版本管理服务:
|
||||
|
||||
```bash
|
||||
node scripts/xuqm_release.mjs --platform android --env production
|
||||
XUQM_API_TOKEN=*** node scripts/xuqm_release.mjs --platform android --publish
|
||||
```
|
||||
|
||||
脚本执行流程:
|
||||
1. 执行 `react-native bundle` 构建 JS Bundle
|
||||
2. 自动提取模块 ID、版本号、更新日志
|
||||
3. 上传 Bundle 到 XuqmGroup 后台
|
||||
4. 返回发布结果
|
||||
1. 按模块构建 JS/Hermes Bundle 和 Source Map
|
||||
2. 生成根目录唯一的 `rn-manifest.json`
|
||||
3. 生成 `<moduleId>.<platform>.xuqm.zip`
|
||||
4. 上传归档并由服务端校验、签名
|
||||
5. 按用户选择发布,返回不可变发布记录
|
||||
|
||||
常用参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--platform` | 平台:`android` / `ios` |
|
||||
| `--env` | 环境:`production` / `staging` |
|
||||
| `--moduleId` | 模块 ID(如 `home`)|
|
||||
| `--forceUpdate` | 是否强制更新 |
|
||||
| `--module` | 可重复传入,只构建或发布指定模块 |
|
||||
| `--note` | 本次发布说明 |
|
||||
| `--publish` | 上传后立即发布 |
|
||||
| `--bugcollect` | `platform` 上传 Source Map;`disabled` 仅保留本地产物 |
|
||||
|
||||
在 `package.json` 中添加快捷命令:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"xuqm:release": "node scripts/xuqm_release.mjs --platform android --env production"
|
||||
"xuqm:release": "node scripts/xuqm_release.mjs --platform android --publish"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -43,8 +43,7 @@ updateClient.interceptors.request.use((config) => {
|
||||
url.startsWith('/api/v1/updates/app/check') ||
|
||||
url.startsWith('/api/v1/updates/app/inspect') ||
|
||||
url.startsWith('/api/v1/updates/public/') ||
|
||||
url.startsWith('/api/v1/rn/update/check') ||
|
||||
url.startsWith('/api/v1/rn/inspect') ||
|
||||
url.startsWith('/api/v1/rn/release-set/check') ||
|
||||
url.startsWith('/api/v1/rn/files/')
|
||||
)
|
||||
if (!skipAuth) {
|
||||
@ -186,12 +185,21 @@ export interface AppPackageInspectResult {
|
||||
export interface RnBundle {
|
||||
id: string
|
||||
appKey: string
|
||||
packageName: string
|
||||
moduleId: string
|
||||
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||
type: 'common' | 'app' | 'buz'
|
||||
platform: 'ANDROID' | 'IOS'
|
||||
version: string
|
||||
md5: string
|
||||
minCommonVersion?: string
|
||||
packageName?: string
|
||||
appVersionRange: string
|
||||
builtAgainstNativeBaselineId: string
|
||||
buildId: string
|
||||
bundleFormat: string
|
||||
commonVersionRange?: string
|
||||
minNativeApiLevel: number
|
||||
bundleSha256: string
|
||||
archiveSha256: string
|
||||
keyId: string
|
||||
signature: string
|
||||
note?: string
|
||||
publishStatus: 'DRAFT' | 'PUBLISHED' | 'DEPRECATED'
|
||||
publishMode?: PublishMode
|
||||
@ -203,16 +211,6 @@ export interface RnBundle {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface RnBundleInspectResult {
|
||||
moduleId?: string
|
||||
platform?: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||
version?: string
|
||||
minCommonVersion?: string
|
||||
packageName?: string
|
||||
fileName?: string
|
||||
detected: boolean
|
||||
}
|
||||
|
||||
export interface UnifiedAppUploadItem {
|
||||
fileKey: string
|
||||
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||
@ -226,19 +224,8 @@ export interface UnifiedAppUploadItem {
|
||||
publishImmediately: boolean
|
||||
}
|
||||
|
||||
export interface UnifiedRnUploadItem {
|
||||
fileKey: string
|
||||
moduleId: string
|
||||
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||
version: string
|
||||
minCommonVersion?: string
|
||||
packageName?: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
export interface UnifiedReleaseManifest {
|
||||
appVersions: UnifiedAppUploadItem[]
|
||||
rnBundles: UnifiedRnUploadItem[]
|
||||
}
|
||||
|
||||
export interface DownloadPageHistoryItem {
|
||||
@ -342,10 +329,6 @@ export const updateAdminApi = {
|
||||
return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, uploadProgressConfig(onProgress))
|
||||
},
|
||||
|
||||
inspectRnBundle(formData: FormData, onProgress?: UploadProgressHandler) {
|
||||
return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, uploadProgressConfig(onProgress))
|
||||
},
|
||||
|
||||
uploadUnifiedRelease(formData: FormData, onProgress?: UploadProgressHandler) {
|
||||
return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress))
|
||||
},
|
||||
|
||||
@ -178,8 +178,18 @@
|
||||
<div class="table-wrap">
|
||||
<el-table :data="rnBundles" v-loading="loadingRn" border stripe>
|
||||
<el-table-column prop="moduleId" label="模块ID" width="140" />
|
||||
<el-table-column prop="type" label="类型" width="90" />
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
<el-table-column prop="platform" label="平台" width="90" />
|
||||
<el-table-column prop="appVersionRange" label="整包版本范围" width="170" />
|
||||
<el-table-column prop="commonVersionRange" label="Common 版本范围" width="180">
|
||||
<template #default="{row}">{{ row.commonVersionRange || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="builtAgainstNativeBaselineId" label="原生基线" width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="buildId" label="构建 ID" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="bundleFormat" label="Bundle 格式" width="140" />
|
||||
<el-table-column prop="minNativeApiLevel" label="最低原生 API" width="130" />
|
||||
<el-table-column prop="archiveSha256" label="归档 SHA-256" width="220" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="140">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
|
||||
@ -191,7 +201,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="minCommonVersion" label="最低 Common 版本" width="160" />
|
||||
<el-table-column prop="note" label="说明" show-overflow-tooltip />
|
||||
<el-table-column prop="createdAt" label="上传时间" width="160">
|
||||
<template #default="{row}">{{ formatTime(row.createdAt) }}</template>
|
||||
@ -924,44 +933,25 @@
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="onRnBundleChange"
|
||||
accept=".bundle,.js"
|
||||
accept=".xuqm.zip"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将 RN Bundle 拖到这里,或 <em>点击选择文件</em>
|
||||
将 CLI 生成的 .xuqm.zip 发布包拖到这里,或 <em>点击选择文件</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
选择或拖入文件后会自动识别模块、平台、版本和 Common 版本。
|
||||
发布身份只读取压缩包根目录的 rn-manifest.json,页面不能修改。
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="rnInspectUploadProgress > 0" label="识别进度">
|
||||
<div class="upload-progress-block">
|
||||
<el-progress :percentage="rnInspectUploadProgress" :status="rnInspectUploadProgress === 100 ? 'success' : undefined" />
|
||||
<span class="upload-progress-text">{{ rnInspectUploadProgress }}%</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="推荐文件名格式:moduleId__ANDROID__1.0.0__1.0.0__com.example.app.bundle,系统会按命名自动识别模块、平台、版本、最低 Common 版本和包名。"
|
||||
title="只接受由发布 CLI 生成的 .xuqm.zip。服务端会校验清单、Bundle SHA-256、归档 SHA-256、应用归属并签署不可变发布身份。"
|
||||
/>
|
||||
<el-form-item label="模块ID"><el-input v-model="rnUploadForm.moduleId" placeholder="可由文件名自动识别" /></el-form-item>
|
||||
<el-form-item label="平台">
|
||||
<el-select v-model="rnUploadForm.platform">
|
||||
<el-option value="ANDROID" label="Android" />
|
||||
<el-option value="IOS" label="iOS" />
|
||||
<el-option value="HARMONY" label="Harmony" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本"><el-input v-model="rnUploadForm.version" placeholder="可由文件名自动识别" /></el-form-item>
|
||||
<el-form-item label="最低 Common 版本"><el-input v-model="rnUploadForm.minCommonVersion" placeholder="可由文件名自动识别" /></el-form-item>
|
||||
<el-form-item label="包名 / Bundle">
|
||||
<el-input v-model="rnUploadForm.packageName" placeholder="可选,建议与应用包名一致" />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item v-if="rnBundleUploadProgress > 0" label="提交进度">
|
||||
<div class="upload-progress-block">
|
||||
@ -981,7 +971,7 @@
|
||||
<div class="drag-overlay-content">
|
||||
<el-icon size="64"><UploadFilled /></el-icon>
|
||||
<p class="drag-overlay-title">释放文件以上传</p>
|
||||
<p class="drag-overlay-hint">支持 .apk、.bundle、.js</p>
|
||||
<p class="drag-overlay-hint">支持 .apk、.xuqm.zip</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -1005,7 +995,6 @@ import {
|
||||
type GrayTag,
|
||||
type PublishMode,
|
||||
type RnBundle,
|
||||
type RnBundleInspectResult,
|
||||
type StoreConfig,
|
||||
type StoreType,
|
||||
} from '@/api/update'
|
||||
@ -1132,12 +1121,12 @@ async function handleDrop(e: DragEvent) {
|
||||
openUploadAppDialog()
|
||||
await nextTick()
|
||||
await onAppPackageChange({ raw: file })
|
||||
} else if (ext === '.bundle' || ext === '.js') {
|
||||
} else if (file.name.toLowerCase().endsWith('.xuqm.zip')) {
|
||||
showUploadRn.value = true
|
||||
await nextTick()
|
||||
await onRnBundleChange({ raw: file })
|
||||
} else {
|
||||
ElMessage.warning(`不支持的文件类型:${file.name},请拖入 .apk、.bundle 或 .js 文件`)
|
||||
ElMessage.warning(`不支持的文件类型:${file.name},请拖入 .apk 或 .xuqm.zip 文件`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1167,7 +1156,6 @@ const appPackageInspecting = ref(false)
|
||||
const appPackageUploadProgress = ref(0)
|
||||
const appVersionUploadProgress = ref(0)
|
||||
const appUploadRef = ref<any>(null)
|
||||
const rnInspectUploadProgress = ref(0)
|
||||
const rnBundleUploadProgress = ref(0)
|
||||
const operationLogs = ref<{
|
||||
id: string
|
||||
@ -2155,81 +2143,28 @@ async function submitAppUpload() {
|
||||
const showUploadRn = ref(false)
|
||||
const uploadingRn = ref(false)
|
||||
const rnUploadForm = ref({
|
||||
moduleId: '',
|
||||
platform: 'ANDROID' as 'ANDROID' | 'IOS' | 'HARMONY',
|
||||
version: '',
|
||||
minCommonVersion: '',
|
||||
packageName: '',
|
||||
note: '',
|
||||
file: null as File | null,
|
||||
})
|
||||
|
||||
function parseRnBundleName(fileName: string): RnBundleInspectResult | null {
|
||||
const baseName = fileName.replace(/\.[^.]+$/, '')
|
||||
const parts = baseName.split('__')
|
||||
if (parts.length >= 4) {
|
||||
return {
|
||||
moduleId: parts[0],
|
||||
platform: parts[1].toUpperCase() as 'ANDROID' | 'IOS' | 'HARMONY',
|
||||
version: parts[2],
|
||||
minCommonVersion: parts[3],
|
||||
packageName: parts[4],
|
||||
fileName,
|
||||
detected: true,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function onRnBundleChange(uploadFile: { raw?: File } | null) {
|
||||
const file = uploadFile?.raw ?? null
|
||||
rnUploadForm.value.file = file
|
||||
if (!file) return
|
||||
|
||||
const local = parseRnBundleName(file.name)
|
||||
if (local) {
|
||||
if (local.moduleId) rnUploadForm.value.moduleId = local.moduleId
|
||||
if (local.platform) rnUploadForm.value.platform = local.platform
|
||||
if (local.version) rnUploadForm.value.version = local.version
|
||||
if (local.minCommonVersion) rnUploadForm.value.minCommonVersion = local.minCommonVersion
|
||||
if (local.packageName) rnUploadForm.value.packageName = local.packageName
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('bundle', file)
|
||||
rnInspectUploadProgress.value = 0
|
||||
try {
|
||||
const res = await updateAdminApi.inspectRnBundle(formData, (percent) => {
|
||||
rnInspectUploadProgress.value = percent
|
||||
})
|
||||
rnInspectUploadProgress.value = 100
|
||||
const inspected = res.data.data as RnBundleInspectResult
|
||||
if (inspected.moduleId) rnUploadForm.value.moduleId = inspected.moduleId
|
||||
if (inspected.platform) rnUploadForm.value.platform = inspected.platform
|
||||
if (inspected.version) rnUploadForm.value.version = inspected.version
|
||||
if (inspected.minCommonVersion) rnUploadForm.value.minCommonVersion = inspected.minCommonVersion
|
||||
if (inspected.packageName) rnUploadForm.value.packageName = inspected.packageName
|
||||
} catch {
|
||||
ElMessage.warning('已选择文件,但未能从文件名识别出 RN Bundle 元数据,请补全后上传')
|
||||
} finally {
|
||||
rnInspectUploadProgress.value = 0
|
||||
if (!file.name.toLowerCase().endsWith('.xuqm.zip')) {
|
||||
rnUploadForm.value.file = null
|
||||
ElMessage.warning('RN 插件只接受发布 CLI 生成的 .xuqm.zip')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRnUpload() {
|
||||
const f = rnUploadForm.value
|
||||
if (!f.moduleId || !f.version || !f.file) return ElMessage.warning('请填写模块ID、版本和 Bundle 文件')
|
||||
if (!f.file) return ElMessage.warning('请选择 CLI 生成的 .xuqm.zip 发布包')
|
||||
uploadingRn.value = true
|
||||
rnBundleUploadProgress.value = 0
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('appKey', appKey.value)
|
||||
fd.append('moduleId', f.moduleId)
|
||||
fd.append('platform', f.platform)
|
||||
fd.append('version', f.version)
|
||||
if (f.minCommonVersion) fd.append('minCommonVersion', f.minCommonVersion)
|
||||
if (f.packageName) fd.append('packageName', f.packageName)
|
||||
if (f.note) fd.append('note', f.note)
|
||||
fd.append('bundle', f.file)
|
||||
await updateAdminApi.uploadRnBundle(fd, (percent) => {
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户