XuqmGroup-RNSDK/packages/update/src/UpdateSDK.ts
XuqmGroup 07b08a4f5a feat: T-B01~B04 — XuqmBundleModule + onProgress + JSBridge/厂商文档
T-B01: XuqmBundleModule 原生模块
- Android: XuqmBundleModule.java(文件读写/manifest/路径)
- iOS: XuqmBundleModule.m(对应实现)
- JS: NativeBundle.ts 封装
- 注册到 XuqmUpdatePackage

T-B02: downloadPluginBundle 添加 onProgress
- 使用 ReadableStream 实现下载进度追踪
- checkAndCachePlugin 同步支持 onProgress

T-B03: XWebView JSBridge 标准接口文档
- docs/XWebView-JSBridge.md
- H5→RN 消息协议 / RN→H5 通信
- 下载处理 / Dialog 覆盖 / 标准 Bridge 接口

T-B04: PushSDK Android 厂商集成文档
- docs/PushSDK-厂商集成.md
- 6 厂商配置步骤 / ProGuard 规则 / 调试指南
2026-06-15 02:36:11 +08:00

302 行
10 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import AsyncStorage from '@react-native-async-storage/async-storage'
import { Linking, Platform } from 'react-native'
import { apiRequest, getConfig, getUserId } from '@xuqm/rn-common'
import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion'
import { awaitInitialization } from '@xuqm/rn-common'
// ─── Types ────────────────────────────────────────────────────────────────────
/** 插件注册元数据 */
export interface PluginMeta {
/** 插件唯一标识,如 'buz1'、'buz2' */
moduleId: string
/** 当前 bundle 版本号 */
version: string
}
/**
* App 整包更新信息。
*
* 对齐 Android SDK 的 UpdateInfo 模型。
* SDK 只返回数据,UI 由 app 层自行处理。
*/
export interface AppUpdateInfo {
/** 是否需要更新 */
needsUpdate: boolean
/** 最新版本名,如 '2.1.0' */
versionName?: string
/** 最新版本号(整数) */
versionCode?: number
/** APK/安装包直接下载地址Android 有此字段时优先下载) */
downloadUrl?: string
/** 更新日志 */
changeLog?: string
/** 是否强制更新true 时不允许跳过) */
forceUpdate?: boolean
/** iOS App Store 地址 */
appStoreUrl?: string
/** Android 应用商店地址(华为/小米/OPPO 等) */
marketUrl?: string
/** 服务端要求登录后才能检查true 时 needsUpdate 通常为 false */
requiresLogin?: boolean
/** SDK 内部标记APK 是否已下载到本地(仅 Android */
alreadyDownloaded?: boolean
/** APK 文件 SHA-256 校验值 */
apkHash?: string | null
}
/**
* 插件RN Bundle更新信息。
*
* 插件唯一标识 = appKey + platform + moduleId。
* - appKey应用标识来自配置文件
* - platformANDROID / IOS
* - moduleId插件 ID如 'buz1'
*/
export interface PluginUpdateInfo {
/** 是否需要更新 */
needsUpdate: boolean
/** 最新版本号 */
latestVersion: string
/** bundle 下载地址 */
downloadUrl: string
/** bundle 文件 MD5 */
md5: string
/** 要求的最低 common bundle 版本 */
minCommonVersion: string
/** 更新说明 */
note: string
}
/** 已缓存的 bundle 元数据 */
export interface CachedRnBundle {
moduleId: string
version: string
md5: string
downloadedAt: string
source: string
}
// ─── Internal ─────────────────────────────────────────────────────────────────
const _pluginRegistry = new Map<string, PluginMeta>()
function bundleCacheKey(moduleId: string) {
return `@xuqm:bundle:${moduleId}`
}
function normalizeDownloadUrl(rawUrl?: string): string | undefined {
if (!rawUrl) return rawUrl
if (rawUrl.includes('/api/v1/updates/api/v1/rn/files/')) {
return rawUrl.replace('/api/v1/updates/api/v1/rn/files/', '/api/v1/rn/files/')
}
if (rawUrl.includes('/files/apk/')) {
try {
const url = new URL(rawUrl)
if (url.pathname.startsWith('/files/apk/')) {
return `${url.origin}/api/v1/updates${url.pathname}${url.search}`
}
} catch {}
}
return rawUrl
}
// ─── UpdateSDK ────────────────────────────────────────────────────────────────
export const UpdateSDK = {
// ── 插件注册 ──────────────────────────────────────────────────────────────
/**
* 注册插件元数据。在插件 bundle 入口文件顶部调用。
*
* 插件唯一标识 = moduleId如 'buz1')。
* 完整定位 = appKey配置文件+ platform自动检测+ moduleId。
*
* @example
* // src/plugins/buz1/bundle.ts
* UpdateSDK.registerPlugin({ moduleId: 'buz1', version: '1.0.0' })
*/
registerPlugin(meta: PluginMeta): void {
_pluginRegistry.set(meta.moduleId, meta)
},
/**
* 获取已注册的插件版本号。
*/
getRegisteredPluginVersion(moduleId: string): string | undefined {
return _pluginRegistry.get(moduleId)?.version
},
/**
* 获取所有已注册的插件列表。
*/
getRegisteredPlugins(): PluginMeta[] {
return Array.from(_pluginRegistry.values())
},
// ── App 整包更新 ──────────────────────────────────────────────────────────
/**
* 检查 App 整包更新。
*
* SDK 自动检测平台iOS/Android并传给服务端。
* 服务端根据平台返回对应的版本信息和下载/商店地址。
*
* 对齐 Android SDK UpdateSDK.checkAppUpdate()
* - 返回完整更新信息,app 层自行决定 UI
* - Android有 downloadUrl 时下载 APK;无 downloadUrl 时用 marketUrl 跳转商店
* - iOS用 appStoreUrl 跳转 App Store
*
* @param bypassIgnore false默认= 静默检查,跳过用户已忽略的版本
* true = 用户主动检查,不跳过
*/
async checkAppUpdate(bypassIgnore?: boolean): Promise<AppUpdateInfo> {
await awaitInitialization()
const config = getConfig()
const currentVersionCode = getAppVersionCode()
const userId = getUserId()?.trim()
const params: Record<string, string> = {
appKey: config.appKey,
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersionCode: String(currentVersionCode),
}
if (userId) params.userId = userId
if (bypassIgnore) params.bypassIgnore = 'true'
const result = await apiRequest<AppUpdateInfo>('/api/v1/updates/app/check', {
skipAuth: true,
params,
})
return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) }
},
/**
* 打开应用商店。
* iOS 使用 appStoreUrl,Android 使用 marketUrl。
*/
async openStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl
if (url) await Linking.openURL(url)
},
// ── 插件更新 ──────────────────────────────────────────────────────────────
/**
* 检查指定插件的更新。
*
* 插件唯一标识 = appKey + platform + moduleId。
* 插件必须已通过 registerPlugin() 注册。
*
* @param moduleId 插件 ID如 'buz1'
* @returns 更新信息,包含 latestVersion / downloadUrl / md5 等
* @throws 插件未注册时抛出错误
*/
async checkPluginUpdate(moduleId: string): Promise<PluginUpdateInfo> {
await awaitInitialization()
const config = getConfig()
const meta = _pluginRegistry.get(moduleId)
if (!meta) {
throw new Error(
`[UpdateSDK] Plugin "${moduleId}" not registered. ` +
'Call UpdateSDK.registerPlugin({ moduleId, version }) at bundle load time.',
)
}
const result = await apiRequest<PluginUpdateInfo>('/api/v1/rn/update/check', {
skipAuth: true,
params: {
appKey: config.appKey,
moduleId,
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersion: meta.version,
},
})
return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl }
},
/**
* 下载插件 bundle 源码文本。
*
* @param downloadUrl 下载地址
* @param onProgress 下载进度回调 (0~1)
*/
async downloadPluginBundle(downloadUrl: string, onProgress?: (progress: number) => void): Promise<string> {
const response = await fetch(downloadUrl)
if (!response.ok) throw new Error(`[UpdateSDK] Bundle download failed: ${response.status}`)
if (!onProgress) return response.text()
const contentLength = Number(response.headers.get('Content-Length') ?? '0')
const reader = response.body?.getReader()
if (!reader) return response.text()
const chunks: Uint8Array[] = []
let received = 0
for (;;) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
if (contentLength > 0) {
onProgress(received / contentLength)
}
}
const decoder = new TextDecoder()
return chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') + decoder.decode()
},
/**
* 缓存插件 bundle 到本地AsyncStorage
*/
async cachePluginBundle(moduleId: string, version: string, md5: string, source: string): Promise<CachedRnBundle> {
const payload: CachedRnBundle = {
moduleId, version, md5, source,
downloadedAt: new Date().toISOString(),
}
await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(payload))
return payload
},
/**
* 读取已缓存的插件 bundle。
*/
async getCachedPluginBundle(moduleId: string): Promise<CachedRnBundle | null> {
const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId))
return raw ? (JSON.parse(raw) as CachedRnBundle) : null
},
/**
* 检查并下载插件更新(一步完成)。
*
* 如果有更新,下载 bundle 并缓存到本地。
* 调用方需要在下次启动时通过 reloadPlugin() 加载新版本。
*
* @param moduleId 插件 ID
* @param onProgress 下载进度回调 (0~1)
* @returns 如果有更新,返回缓存的 bundle 信息;否则返回 null
*/
async checkAndCachePlugin(moduleId: string, onProgress?: (progress: number) => void): Promise<CachedRnBundle | null> {
const info = await this.checkPluginUpdate(moduleId)
if (!info.needsUpdate) return null
const source = await this.downloadPluginBundle(info.downloadUrl, onProgress)
return this.cachePluginBundle(moduleId, info.latestVersion, info.md5, source)
},
// ── 版本信息 ──────────────────────────────────────────────────────────────
/** 当前 App versionCode原生读取 */
getAppVersionCode,
/** 当前 App versionName原生读取 */
getAppVersionName,
/**
* 开发环境手动设置版本号(仅调试用,生产环境不要调用)。
*/
_devSetAppVersion(versionCode: number, versionName?: string): void {
_devSetAppVersion(versionCode, versionName)
},
}