diff --git a/packages/update/README.md b/packages/update/README.md index 1b5c661..4124aa3 100644 --- a/packages/update/README.md +++ b/packages/update/README.md @@ -56,7 +56,7 @@ if (pluginInfo.needsUpdate) { | API | 说明 | |-----|------| | `UpdateSDK.checkAppUpdate(bypassIgnore?)` | 检查 App 更新(30 分钟缓存) | -| `UpdateSDK.downloadApk(info, options?)` | 下载 APK,返回 ArrayBuffer | +| `UpdateSDK.downloadApk(info, options?)` | 下载 APK,返回 ArrayBuffer(支持进度回调) | | `UpdateSDK.downloadAndInstallApk(url, options?)` | 下载 APK 并调起系统安装器(Android) | | `UpdateSDK.openStore(appStoreUrl?, marketUrl?)` | 跳转应用商店 | | `UpdateSDK.getAppVersionCode()` | 获取当前 versionCode | @@ -67,6 +67,7 @@ if (pluginInfo.needsUpdate) { | API | 说明 | |-----|------| | `UpdateSDK.checkPluginUpdate(moduleId)` | 检查插件更新(30 分钟缓存) | +| `UpdateSDK.downloadPlugin(moduleId, info, options?)` | 下载插件 bundle,返回 JS 源码(支持进度回调) | | `UpdateSDK.updatePlugin(moduleId, options?)` | 一步完成:检查 → 下载 → 写文件 → 重载 | ### 类型定义 @@ -106,6 +107,31 @@ interface CachedRnBundle { downloadedAt: string source: string } + +type UpdateDownloadProgress = { + bytesDownloaded: number + totalBytes: number + /** 0-100 */ + percent: number +} +``` + +### 下载进度回调 + +`downloadApk()` 和 `downloadPlugin()` 支持 `onProgress` 回调,使用 `fetch + ReadableStream` 实现流式进度计算: + +```ts +// 下载 APK 带进度 +const buffer = await UpdateSDK.downloadApk(info, { + onProgress: ({ bytesDownloaded, totalBytes, percent }) => { + console.log(`下载进度: ${percent}% (${bytesDownloaded}/${totalBytes})`) + }, +}) + +// 下载插件 bundle 带进度 +const source = await UpdateSDK.downloadPlugin('szyx', pluginInfo, { + onProgress: ({ percent }) => setProgress(percent), +}) ``` ## 工作原理 diff --git a/packages/xwebview/README.md b/packages/xwebview/README.md index f9e47a1..e944771 100644 --- a/packages/xwebview/README.md +++ b/packages/xwebview/README.md @@ -75,4 +75,59 @@ window.XWebViewBridge.postMessage(JSON.stringify({ type: 'login', token: '...' } controller.postMessageToWeb("window.dispatchEvent(new CustomEvent('nativeMsg', { detail: { key: 'value' } }))") ``` -详细 handler 列表见 `@xuqm/rn-common` 的 `xwebview/` 目录和 `docs/XWebView-JSBridge.md`。 +### 内置 JSBridge Handler + +| Action | 说明 | 参数 | +|--------|------|------| +| `xuqm.getDeviceInfo` | 获取设备信息 | — | +| `xuqm.getToken` | 获取当前用户 token | — | +| `xuqm.getUserInfo` | 获取用户信息 | — | +| `xuqm.openNativePage` | 打开原生页面 | `{ route, params? }` | +| `xuqm.closeWebView` | 关闭 WebView | — | +| `xuqm.showToast` | 显示 Toast | `{ message }` | +| `xuqm.scanQRCode` | 打开扫码页(需宿主注册 handler) | — | + +## 文件下载 + +XWebView 内置文件下载支持,自动拦截以下场景: + +1. URL 以常见下载扩展名结尾(`.pdf`, `.zip`, `.apk` 等) +2. `` 标签点击 +3. `blob:` URL 下载(通过 injected JS 拦截) + +### 使用 `handleDownloadRequest` + +```ts +import { handleDownloadRequest } from '@xuqm/rn-xwebview' + +const handle = await handleDownloadRequest(url, hintFilename, { + autoDownload: true, + downloadConflict: 'rename', + onDownloadProgress: (p) => console.log(`${p.percentage}%`), + onDownloadComplete: (r) => console.log('saved to', r.filePath), + onDownloadError: (url, err) => console.error(err), +}) +// handle?.cancel() 可取消下载 +``` + +### XWebViewConfig 下载相关参数 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `autoDownload` | boolean | 自动下载(默认 true),false 时需 onDownloadDecide | +| `downloadConflict` | `'rename' \| 'overwrite'` | 文件名冲突策略(默认 rename) | +| `onDownloadStart` | (request) => void | 下载开始回调 | +| `onDownloadProgress` | (progress) => void | 下载进度回调 | +| `onDownloadComplete` | (result) => void | 下载完成回调 | +| `onDownloadError` | (url, error) => void | 下载失败回调 | +| `onDownloadDecide` | (request) => Decision | 非自动模式下的下载决策回调 | + +### 低级 API + +| API | 说明 | +|-----|------| +| `fetchDownloadInfo(url, hintFilename?)` | HEAD 请求获取文件名/MIME/大小 | +| `saveBase64File(base64, filename, savePath?, conflict)` | 保存 base64 数据为文件 | +| `startDownload(url, filename, savePath?, conflict, onProgress, onComplete, onError)` | 开始下载,返回 DownloadHandle | + +详细 handler 列表见 `@xuqm/rn-common` 的 `xwebview/` 目录。 diff --git a/packages/xwebview/src/XWebViewDownload.ts b/packages/xwebview/src/XWebViewDownload.ts index 058b184..6af349e 100644 --- a/packages/xwebview/src/XWebViewDownload.ts +++ b/packages/xwebview/src/XWebViewDownload.ts @@ -2,6 +2,7 @@ import { Platform } from 'react-native' import RNFetchBlob from 'react-native-blob-util' import type { + XWebViewDownloadDecision, XWebViewDownloadProgress, XWebViewDownloadRequest, XWebViewDownloadResult, @@ -105,6 +106,67 @@ export async function saveBase64File( export type DownloadHandle = { cancel: () => void } +/** + * 统一的下载请求处理函数。 + * + * 封装 fetchDownloadInfo → onDownloadDecide → startDownload 的完整流程, + * 供 XWebViewScreen / XWebViewView 及外部调用方使用。 + * + * @returns DownloadHandle(可取消),若用户拒绝或无需下载则返回 null + */ +export async function handleDownloadRequest( + url: string, + hintFilename: string | undefined, + options: { + autoDownload?: boolean + downloadConflict?: 'rename' | 'overwrite' + savePath?: string + onDownloadDecide?: ( + request: XWebViewDownloadRequest, + ) => XWebViewDownloadDecision | Promise + onDownloadStart?: (request: XWebViewDownloadRequest) => void + onDownloadProgress?: (progress: XWebViewDownloadProgress) => void + onDownloadComplete?: (result: XWebViewDownloadResult) => void + onDownloadError?: (url: string, error: string) => void + } = {}, +): Promise { + const { + autoDownload = true, + downloadConflict = 'rename', + savePath, + onDownloadDecide, + onDownloadStart, + onDownloadProgress, + onDownloadComplete, + onDownloadError, + } = options + + const info = await fetchDownloadInfo(url, hintFilename) + let finalFilename = info.suggestedFilename + let finalSavePath = savePath + + // 非自动下载模式:需要宿主决定是否允许 + if (!autoDownload) { + if (!onDownloadDecide) return null + const decision = await Promise.resolve(onDownloadDecide(info)) + if (!decision.allowed) return null + if (decision.filename) finalFilename = decision.filename + if (decision.savePath) finalSavePath = decision.savePath + } + + onDownloadStart?.({ ...info, suggestedFilename: finalFilename }) + + return startDownload( + url, + finalFilename, + finalSavePath, + downloadConflict, + p => onDownloadProgress?.(p), + r => onDownloadComplete?.(r), + err => onDownloadError?.(url, err), + ) +} + export function startDownload( url: string, filename: string, diff --git a/packages/xwebview/src/index.ts b/packages/xwebview/src/index.ts index f219633..e9e8c53 100644 --- a/packages/xwebview/src/index.ts +++ b/packages/xwebview/src/index.ts @@ -21,3 +21,5 @@ export type { export { default as XWebViewProgress } from './XWebViewProgress' export { XWebViewView } from './XWebViewView' export { XWebViewScreen } from './XWebViewScreen' +export { handleDownloadRequest, fetchDownloadInfo, saveBase64File, startDownload } from './XWebViewDownload' +export type { DownloadHandle } from './XWebViewDownload' diff --git a/src/index.ts b/src/index.ts index 25826ce..06fec6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ export type { export { PushSDK } from '@xuqm/rn-push' export type { PushVendor } from '@xuqm/rn-push' export { UpdateSDK } from '@xuqm/rn-update' -export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from '@xuqm/rn-update' +export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle, UpdateDownloadProgress } from '@xuqm/rn-update' export { XWebViewControl, XWebViewScreen, @@ -31,8 +31,14 @@ export { getXWebViewConfig, openXWebView, setXWebViewController, + setXWebViewScanQRCodeHandler, + handleDownloadRequest, + fetchDownloadInfo, + saveBase64File, + startDownload, } from '@xuqm/rn-xwebview' export type { + ScanQRCodeHandler, XWebViewClickMenu, XWebViewConfig, XWebViewControllerAPI, @@ -42,4 +48,5 @@ export type { XWebViewDownloadResult, XWebViewMessageEvent, XWebViewPermissionRequest, + DownloadHandle, } from '@xuqm/rn-xwebview'