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 规则 / 调试指南
302 行
10 KiB
TypeScript
302 行
10 KiB
TypeScript
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:应用标识(来自配置文件)
|
||
* - platform:ANDROID / 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)
|
||
},
|
||
}
|