docs(rn): rebuild public sdk documentation
这个提交包含在:
父节点
94e5ee0ec4
当前提交
eb2baae07e
@ -58,11 +58,15 @@ export default defineConfig({
|
||||
],
|
||||
'/rn/': [
|
||||
{ text: '概览', link: '/rn/' },
|
||||
{ text: '安装配置', link: '/rn/setup' },
|
||||
{ text: '安装与自动配置', link: '/rn/setup' },
|
||||
{ text: 'Common 基础能力', link: '/rn/common' },
|
||||
{ text: '会话与隐私授权', link: '/rn/session' },
|
||||
{ text: '版本与插件更新', link: '/rn/update' },
|
||||
{ text: 'BugCollect', link: '/rn/bugcollect' },
|
||||
{ text: 'XWebView', link: '/rn/xwebview' },
|
||||
{ text: 'IM 接入', link: '/rn/im' },
|
||||
{ text: '群聊', link: '/rn/group' },
|
||||
{ text: '推送接入', link: '/rn/push' },
|
||||
{ text: '版本管理', link: '/rn/update' },
|
||||
{ text: '错误处理与排障', link: '/rn/troubleshooting' },
|
||||
],
|
||||
'/vue3/': [
|
||||
{ text: '概览', link: '/vue3/' },
|
||||
|
||||
128
docs-site/docs/rn/bugcollect.md
普通文件
128
docs-site/docs/rn/bugcollect.md
普通文件
@ -0,0 +1,128 @@
|
||||
# BugCollect
|
||||
|
||||
`@xuqm/rn-bugcollect` 用于采集 JS 异常、原生崩溃、业务面包屑和自定义事件。它共享
|
||||
Common 的自动配置与会话,不提供第二套初始化或登录入口。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pnpm add @xuqm/rn-bugcollect @xuqm/rn-common
|
||||
```
|
||||
|
||||
完成 [自动配置](./setup) 后无需在 App 入口调用 BugCollect 初始化方法。
|
||||
|
||||
## 隐私前置条件
|
||||
|
||||
只有宿主明确同步用户授权后才允许采集:
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
await XuqmSDK.setPrivacyConsent(true)
|
||||
```
|
||||
|
||||
撤回授权:
|
||||
|
||||
```ts
|
||||
await XuqmSDK.setPrivacyConsent(false)
|
||||
```
|
||||
|
||||
SDK 不负责展示隐私声明,也不会把一次历史授权永久推断为当前授权。
|
||||
|
||||
## 自动采集
|
||||
|
||||
满足以下条件时,Release 应用自动启用异常采集:
|
||||
|
||||
- 租户平台已开启 BugCollect。
|
||||
- 当前配置有效。
|
||||
- 用户已同意隐私声明。
|
||||
- App 构建没有使用应急关闭选项。
|
||||
|
||||
服务关闭、网络失败、限流或上传失败不会阻断宿主业务。服务端明确返回关闭状态后,SDK 会
|
||||
停止后续采集,直到获取到新的有效平台配置。
|
||||
|
||||
## 手动记录
|
||||
|
||||
```ts
|
||||
import { BugCollect } from '@xuqm/rn-bugcollect'
|
||||
|
||||
BugCollect.addBreadcrumb({
|
||||
type: 'navigation',
|
||||
category: 'order',
|
||||
message: '进入订单详情',
|
||||
data: { orderType: 'outpatient' },
|
||||
})
|
||||
|
||||
BugCollect.info('开始提交订单')
|
||||
BugCollect.warn('接口响应较慢', { elapsedMs: 2200 })
|
||||
|
||||
try {
|
||||
await submitOrder()
|
||||
} catch (error) {
|
||||
BugCollect.captureError(error, {
|
||||
scene: 'submit-order',
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
不要把密码、Token、身份证号、手机号、病历正文或完整请求响应放入 metadata。
|
||||
|
||||
## 自定义事件与漏斗
|
||||
|
||||
```ts
|
||||
BugCollect.event('checkout_started', {
|
||||
source: 'cart',
|
||||
})
|
||||
|
||||
BugCollect.defineFunnel({
|
||||
id: 'checkout',
|
||||
steps: ['checkout_started', 'payment_started', 'payment_completed'],
|
||||
})
|
||||
|
||||
const progress = BugCollect.getFunnelProgress('checkout')
|
||||
```
|
||||
|
||||
事件名应保持稳定,属性只记录分析所需的非敏感枚举和数值。
|
||||
|
||||
## 日志级别与环境
|
||||
|
||||
```ts
|
||||
BugCollect.setLogLevel(__DEV__ ? 'debug' : 'warn')
|
||||
BugCollect.setEnvironment(__DEV__ ? 'development' : 'production')
|
||||
BugCollect.setSampleRate(0.2)
|
||||
```
|
||||
|
||||
采样率只影响非致命事件,致命异常不参与客户端采样。线上采样与服务端限流共同生效,宿主
|
||||
不需要自行实现重试队列。
|
||||
|
||||
## Debug 验证
|
||||
|
||||
Debug 默认不自动上传。开发者工具页可以在用户已同意隐私声明后显式执行:
|
||||
|
||||
```ts
|
||||
await BugCollect.sendTestErrorAndFlush('BugCollect integration test')
|
||||
```
|
||||
|
||||
该方法用于验证配置和上传链路,不应放在正常业务流程中。
|
||||
|
||||
## 主动刷新
|
||||
|
||||
```ts
|
||||
await BugCollect.flush()
|
||||
```
|
||||
|
||||
通常不需要主动调用。应用即将退出且确实需要等待当前队列时才使用;上传失败仍应当允许
|
||||
宿主继续退出。
|
||||
|
||||
## 应急关闭
|
||||
|
||||
当 BugCollect 服务不可用且影响生产打包时,宿主可以通过自己的 Release 构建参数关闭本次
|
||||
构建的 BugCollect。该设置只用于应急,不写入平台下发配置,也不允许业务页面在运行时
|
||||
反向开启平台已关闭的服务。
|
||||
|
||||
## 数据与错误边界
|
||||
|
||||
- SDK 会对常见敏感字段进行脱敏,但调用方仍须避免主动传入敏感正文。
|
||||
- 网络和上传错误不会抛到全局导致 App 崩溃。
|
||||
- 未授权、服务关闭和配置不可用时,自定义记录调用安全跳过。
|
||||
- `sendTestErrorAndFlush` 是诊断方法,会把错误明确返回给开发者页面。
|
||||
157
docs-site/docs/rn/common.md
普通文件
157
docs-site/docs/rn/common.md
普通文件
@ -0,0 +1,157 @@
|
||||
# Common 基础能力
|
||||
|
||||
`@xuqm/rn-common` 是所有 RN 扩展包共享的基础依赖,也可以不接入平台配置、不开启登录而
|
||||
独立使用。
|
||||
|
||||
## 网络请求
|
||||
|
||||
Common-only 工程可以设置自己的业务地址:
|
||||
|
||||
```ts
|
||||
import {
|
||||
apiRequest,
|
||||
configureHttp,
|
||||
setGlobalApiErrorHandler,
|
||||
} from '@xuqm/rn-common'
|
||||
|
||||
configureHttp({
|
||||
baseUrl: 'https://api.example.com',
|
||||
})
|
||||
|
||||
setGlobalApiErrorHandler((report) => {
|
||||
// report 只包含经过脱敏的请求诊断信息
|
||||
console.warn(report)
|
||||
})
|
||||
|
||||
const profile = await apiRequest<{ name: string }>('/profile', {
|
||||
timeoutMs: 15_000,
|
||||
})
|
||||
```
|
||||
|
||||
安装扩展包并完成自动配置后,扩展模块共享平台地址和当前会话。页面不要重复创建平台
|
||||
Axios 实例,也不要逐层传递用户 ID 或 Token。
|
||||
|
||||
React 页面需要请求状态和运行时响应校验时,可以使用 `useRequest`、`useApi` 和
|
||||
`usePageApi`。这些 Hooks 接收 URL、请求方法、参数和可选的 Zod Schema,具体类型由
|
||||
TypeScript 自动提示。
|
||||
|
||||
## 文件与下载
|
||||
|
||||
```ts
|
||||
import {
|
||||
downloadFileToPath,
|
||||
fileExists,
|
||||
openLocalFile,
|
||||
resolveAvailableFilePath,
|
||||
} from '@xuqm/rn-common'
|
||||
|
||||
const target = await resolveAvailableFilePath('/downloads', 'report.pdf', 'rename')
|
||||
const task = downloadFileToPath('https://example.com/report.pdf', target, {
|
||||
onProgress(progress) {
|
||||
console.log(progress.percent)
|
||||
},
|
||||
})
|
||||
|
||||
await task.result
|
||||
if (await fileExists(target)) await openLocalFile(target)
|
||||
```
|
||||
|
||||
常用能力还包括:
|
||||
|
||||
- `ensureDirectory`
|
||||
- `readFileAsBase64`
|
||||
- `writeBase64File`
|
||||
- `deleteFile`
|
||||
- `registerAndroidDownloadedFile`
|
||||
- `fileNameFromUrl`
|
||||
- `parseContentDispositionFileName`
|
||||
- `sanitizeFileName`
|
||||
- `inferMimeType`
|
||||
|
||||
下载任务支持取消,页面卸载时可以调用 `task.cancel()`。
|
||||
|
||||
## 日期与过期状态
|
||||
|
||||
```ts
|
||||
import {
|
||||
expirationStatus,
|
||||
formatDateTime,
|
||||
millisecondsUntil,
|
||||
toTimestamp,
|
||||
} from '@xuqm/rn-common'
|
||||
|
||||
const status = expirationStatus(endTime)
|
||||
if (status === 'expired') {
|
||||
// 已过期
|
||||
}
|
||||
|
||||
const remaining = millisecondsUntil(endTime)
|
||||
const display = formatDateTime(endTime)
|
||||
const timestamp = toTimestamp(endTime)
|
||||
```
|
||||
|
||||
协议要求固定数字格式时使用 `formatNumericDateTime`,不要依赖设备 Locale 的标点顺序。
|
||||
|
||||
## 版本判断
|
||||
|
||||
```ts
|
||||
import {
|
||||
compareVersions,
|
||||
isVersionUpgrade,
|
||||
satisfiesVersion,
|
||||
} from '@xuqm/rn-common'
|
||||
|
||||
compareVersions('1.2.0', '1.10.0')
|
||||
isVersionUpgrade('1.0.0', '1.1.0')
|
||||
satisfiesVersion('1.2.3', '>=1.0.0 <2.0.0')
|
||||
```
|
||||
|
||||
## 设备信息
|
||||
|
||||
```ts
|
||||
import { getDeviceId, getDeviceInfo, detectPushVendor } from '@xuqm/rn-common'
|
||||
|
||||
const deviceId = await getDeviceId()
|
||||
const device = await getDeviceInfo()
|
||||
const vendor = detectPushVendor(device.brand)
|
||||
```
|
||||
|
||||
## 公共提示
|
||||
|
||||
```ts
|
||||
import { showAlert, showConfirm, showToast } from '@xuqm/rn-common'
|
||||
|
||||
showToast('保存成功')
|
||||
await showAlert({ title: '提示', message: '操作已完成' })
|
||||
showConfirm({
|
||||
title: '确认删除',
|
||||
message: '删除后无法恢复',
|
||||
onConfirm() {
|
||||
// 执行删除
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
宿主可在应用入口统一调用 `configureToast` 对接自己的提示组件,业务页面不应复制多套
|
||||
Alert/Toast 样式。
|
||||
|
||||
## 错误处理
|
||||
|
||||
```ts
|
||||
import { XuqmError } from '@xuqm/rn-common'
|
||||
|
||||
try {
|
||||
await someOperation()
|
||||
} catch (error) {
|
||||
if (error instanceof XuqmError) {
|
||||
switch (error.code) {
|
||||
case 'XUQM_NOT_READY':
|
||||
case 'XUQM_SERVICE_DISABLED':
|
||||
// 非阻断提示或跳过功能
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
不要根据英文错误文案判断业务分支,统一使用结构化错误码。
|
||||
@ -1,109 +0,0 @@
|
||||
# React Native 群聊
|
||||
|
||||
基于 `@xuqm/rn-im` 模块实现群组相关功能。
|
||||
|
||||
---
|
||||
|
||||
## 创建群聊
|
||||
|
||||
```ts
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
|
||||
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
|
||||
// group.id — 群 ID
|
||||
// group.name — 群名称
|
||||
// group.creatorId — 创建者
|
||||
```
|
||||
|
||||
> `createGroup` 第二个参数为初始成员列表,创建者自动加入。
|
||||
|
||||
---
|
||||
|
||||
## 邀请成员
|
||||
|
||||
```ts
|
||||
// 添加单个成员
|
||||
await ImSDK.addGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 批量添加成员
|
||||
await ImSDK.batchAddGroupMembers('group_xxx', ['user_004', 'user_005'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 发送群消息
|
||||
|
||||
```ts
|
||||
const msg = await ImSDK.sendMessage(
|
||||
'group_xxx', // toId
|
||||
'GROUP', // chatType
|
||||
'TEXT', // msgType
|
||||
'大家好!' // content
|
||||
)
|
||||
```
|
||||
|
||||
发送多媒体群消息:
|
||||
|
||||
```ts
|
||||
// 图片
|
||||
await ImSDK.sendImageMessage('group_xxx', 'GROUP', '/path/to/image.jpg', 800, 600)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群成员管理
|
||||
|
||||
```ts
|
||||
// 移除成员
|
||||
await ImSDK.removeGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 批量移除
|
||||
await ImSDK.batchRemoveGroupMembers('group_xxx', ['user_004', 'user_005'])
|
||||
|
||||
// 退出群聊
|
||||
await ImSDK.leaveGroup('group_xxx')
|
||||
|
||||
// 设置管理员角色(示例)
|
||||
await ImSDK.setGroupRole('group_xxx', 'user_004', 'ADMIN')
|
||||
|
||||
// 禁言成员(示例)
|
||||
await ImSDK.muteGroupMember('group_xxx', 'user_004', 60)
|
||||
|
||||
// 转让群主(示例)
|
||||
await ImSDK.transferGroupOwner('group_xxx', 'user_002')
|
||||
|
||||
// 解散群聊(示例)
|
||||
await ImSDK.dismissGroup('group_xxx')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群信息查询
|
||||
|
||||
```ts
|
||||
// 群列表(仅返回当前用户所在的群)
|
||||
const groups = await ImSDK.listGroups()
|
||||
|
||||
// 群详情
|
||||
const group = await ImSDK.getGroupInfo('group_xxx')
|
||||
|
||||
// 群成员
|
||||
const members = await ImSDK.listGroupMembers('group_xxx')
|
||||
|
||||
// 群历史消息
|
||||
const history = await ImSDK.fetchGroupHistory('group_xxx', page, size)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群类型
|
||||
|
||||
创建群时可指定 `groupType`:
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| `WORK` | 工作群(默认)|
|
||||
| `PUBLIC` | 公开群 |
|
||||
| `PRIVATE` | 私有群 |
|
||||
|
||||
[→ 返回 RN IM 接入文档](./im)
|
||||
@ -1,199 +1,232 @@
|
||||
# React Native IM 接入
|
||||
# IM 接入
|
||||
|
||||
基于 `@xuqm/rn-im` 模块实现即时通讯功能,使用 WatermelonDB 进行本地消息存储。
|
||||
`@xuqm/rn-im` 提供单聊、群聊、会话、好友关系、黑名单和离线消息能力。宿主仍然只通过
|
||||
Common 同步一次登录状态。
|
||||
|
||||
---
|
||||
## 安装
|
||||
|
||||
## 初始化与登录
|
||||
|
||||
### 初始化
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
await XuqmSDK.initialize({
|
||||
appKey: 'your_app_key',
|
||||
logLevel: __DEV__ ? 'debug' : 'warn',
|
||||
})
|
||||
```bash
|
||||
pnpm add @xuqm/rn-common @xuqm/rn-im @nozbe/watermelondb
|
||||
```
|
||||
|
||||
### 登录
|
||||
## 登录与连接
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
await XuqmSDK.login({
|
||||
userId: 'user_001',
|
||||
userSig: 'your_user_sig_jwt',
|
||||
userId: 'user-001',
|
||||
accessToken: 'host-access-token',
|
||||
userSig: 'im-user-signature',
|
||||
})
|
||||
```
|
||||
|
||||
> 如果集成了 `rn-im`,`XuqmSDK.login` 会自动触发 `ImSDK` 连接,业务侧无需单独调用 `ImSDK.login`。
|
||||
安装 IM 后,公共会话会自动建立连接。不要再调用第二套 IM 登录方法。
|
||||
|
||||
---
|
||||
|
||||
## 消息收发
|
||||
|
||||
### 监听实时消息
|
||||
连接状态:
|
||||
|
||||
```ts
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
|
||||
ImSDK.addEventListener('message', (msg) => {
|
||||
console.log('收到消息:', msg.msgType, msg.content)
|
||||
})
|
||||
|
||||
ImSDK.addEventListener('read', (msg) => {
|
||||
console.log('已读回执:', msg.id)
|
||||
})
|
||||
|
||||
ImSDK.addEventListener('revoke', (data) => {
|
||||
console.log('消息被撤回:', data.msgId)
|
||||
})
|
||||
```
|
||||
|
||||
### 发送文本消息
|
||||
|
||||
```ts
|
||||
const msg = await ImSDK.sendMessage(
|
||||
'user_002', // toId
|
||||
'SINGLE', // chatType: 'SINGLE' | 'GROUP'
|
||||
'TEXT', // msgType
|
||||
'Hello!' // content
|
||||
)
|
||||
```
|
||||
|
||||
### 发送图片
|
||||
|
||||
```ts
|
||||
const msg = await ImSDK.sendImageMessage(
|
||||
'user_002',
|
||||
'SINGLE',
|
||||
'/path/to/image.jpg', // 本地 URI
|
||||
800, // 宽
|
||||
600 // 高
|
||||
)
|
||||
```
|
||||
|
||||
### 获取历史消息
|
||||
|
||||
```ts
|
||||
// 单聊历史
|
||||
const history = await ImSDK.fetchHistory('user_002', page, size)
|
||||
|
||||
// 群历史
|
||||
const groupHistory = await ImSDK.fetchGroupHistory('group_xxx', page, size)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WatermelonDB 本地存储
|
||||
|
||||
`rn-im` 使用 WatermelonDB(SQLite)存储本地消息,按 `appKey + userId` 自动隔离。
|
||||
|
||||
```ts
|
||||
import { ImDatabase } from '@xuqm/rn-im'
|
||||
|
||||
// 消息搜索(本地)
|
||||
const params: MessageSearchParams = {
|
||||
keyword: '会议',
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgTypes: ['TEXT'],
|
||||
limit: 20,
|
||||
if (!ImSDK.isConnected()) {
|
||||
await ImSDK.reconnect()
|
||||
}
|
||||
const results = await ImSDK.searchMessages(params)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群聊
|
||||
## 事件监听
|
||||
|
||||
```ts
|
||||
// 创建群
|
||||
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
|
||||
const listener = {
|
||||
onConnected() {
|
||||
console.log('IM 已连接')
|
||||
},
|
||||
onDisconnected(reason?: string) {
|
||||
console.log('IM 已断开', reason)
|
||||
},
|
||||
onMessage(message) {
|
||||
appendMessage(message)
|
||||
},
|
||||
onGroupMessage(message) {
|
||||
appendGroupMessage(message)
|
||||
},
|
||||
onRead(message) {
|
||||
markMessageRead(message.id)
|
||||
},
|
||||
onRevoke({ msgId }) {
|
||||
markMessageRevoked(msgId)
|
||||
},
|
||||
onError(error) {
|
||||
console.warn(error)
|
||||
},
|
||||
}
|
||||
|
||||
// 群列表
|
||||
const groups = await ImSDK.listGroups()
|
||||
ImSDK.addListener(listener)
|
||||
|
||||
// 添加成员
|
||||
await ImSDK.addGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 移除成员
|
||||
await ImSDK.removeGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 退出群聊
|
||||
await ImSDK.leaveGroup('group_xxx')
|
||||
// 页面或服务销毁时
|
||||
ImSDK.removeListener(listener)
|
||||
```
|
||||
|
||||
详见 [RN 群聊文档 →](./group)
|
||||
同一个 listener 对象用于添加和移除。
|
||||
|
||||
---
|
||||
## 发送消息
|
||||
|
||||
文本:
|
||||
|
||||
```ts
|
||||
const message = await ImSDK.sendTextMessage(
|
||||
'user-002',
|
||||
'SINGLE',
|
||||
'你好',
|
||||
)
|
||||
```
|
||||
|
||||
群聊:
|
||||
|
||||
```ts
|
||||
await ImSDK.sendTextMessage('group-001', 'GROUP', '大家好')
|
||||
```
|
||||
|
||||
图片:
|
||||
|
||||
```ts
|
||||
await ImSDK.sendImageMessage(
|
||||
'user-002',
|
||||
'SINGLE',
|
||||
imageUri,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
)
|
||||
```
|
||||
|
||||
文件、音频和视频使用对应方法:
|
||||
|
||||
```ts
|
||||
await ImSDK.sendFileMessage(
|
||||
'user-002',
|
||||
'SINGLE',
|
||||
fileUri,
|
||||
'report.pdf',
|
||||
fileSize,
|
||||
)
|
||||
await ImSDK.sendAudioMessage('user-002', 'SINGLE', audioUri, durationSeconds)
|
||||
await ImSDK.sendVideoMessage(
|
||||
'user-002',
|
||||
'SINGLE',
|
||||
videoUri,
|
||||
thumbnailUri,
|
||||
durationSeconds,
|
||||
)
|
||||
```
|
||||
|
||||
自定义、位置、富文本、通知、引用、合并转发和音视频通话信令也有对应的类型化方法。消息
|
||||
业务内容由双方约定,不要在普通文本字段中拼接不可验证的脚本。
|
||||
|
||||
撤回与编辑:
|
||||
|
||||
```ts
|
||||
await ImSDK.revokeMessage(message.id)
|
||||
await ImSDK.editMessage(message.id, '修改后的内容')
|
||||
```
|
||||
|
||||
## 历史和离线消息
|
||||
|
||||
```ts
|
||||
const history = await ImSDK.fetchHistory(
|
||||
'user-002',
|
||||
0,
|
||||
20,
|
||||
'SINGLE',
|
||||
)
|
||||
|
||||
const filtered = await ImSDK.fetchHistoryWithFilters('user-002', {
|
||||
keyword: '会议',
|
||||
msgType: 'TEXT',
|
||||
startTime: '2026-07-01 00:00:00',
|
||||
endTime: '2026-07-31 23:59:59',
|
||||
page: 0,
|
||||
size: 20,
|
||||
})
|
||||
|
||||
const offlineCount = await ImSDK.offlineMessageCount()
|
||||
const offlineMessages = await ImSDK.syncOfflineMessages(100)
|
||||
```
|
||||
|
||||
SDK 负责当前登录用户的本地消息缓存与同步,宿主不需要依赖其内部存储结构。
|
||||
|
||||
## 会话列表
|
||||
|
||||
```ts
|
||||
// 订阅会话变化
|
||||
const unsub = ImSDK.subscribeConversations((conversations) => {
|
||||
console.log(conversations)
|
||||
const conversations = await ImSDK.listConversations()
|
||||
|
||||
const unsubscribe = ImSDK.subscribeConversations((next) => {
|
||||
setConversations(next)
|
||||
})
|
||||
|
||||
// 置顶
|
||||
await ImSDK.setConversationPinned('user_002', 'SINGLE', true)
|
||||
await ImSDK.markRead('user-002', 'SINGLE')
|
||||
await ImSDK.setConversationPinned('user-002', 'SINGLE', true)
|
||||
await ImSDK.setConversationMuted('user-002', 'SINGLE', true)
|
||||
await ImSDK.setDraft('user-002', 'SINGLE', '稍后继续')
|
||||
|
||||
// 免打扰
|
||||
await ImSDK.setConversationMuted('group_xxx', 'GROUP', true)
|
||||
|
||||
// 标记已读
|
||||
await ImSDK.markRead('user_002')
|
||||
unsubscribe()
|
||||
```
|
||||
|
||||
---
|
||||
还可以隐藏、删除会话或将会话加入自定义分组。
|
||||
|
||||
## 离线消息同步
|
||||
## 群聊
|
||||
|
||||
```ts
|
||||
// 查询离线消息数量(示例)
|
||||
const count = await ImSDK.offlineMessageCount()
|
||||
const group = await ImSDK.createGroup(
|
||||
'项目讨论',
|
||||
['user-002', 'user-003'],
|
||||
'WORK',
|
||||
)
|
||||
|
||||
// 同步离线消息(示例)
|
||||
const messages = await ImSDK.syncOfflineMessages()
|
||||
const groups = await ImSDK.listGroups()
|
||||
const detail = await ImSDK.getGroupInfo(group.id)
|
||||
const members = await ImSDK.listGroupMembers(group.id)
|
||||
|
||||
await ImSDK.addGroupMember(group.id, 'user-004')
|
||||
await ImSDK.removeGroupMember(group.id, 'user-004')
|
||||
await ImSDK.leaveGroup(group.id)
|
||||
```
|
||||
|
||||
---
|
||||
管理员能力包括群资料、角色、禁言、群主转让、入群申请和解散群聊。调用失败时使用服务端
|
||||
返回的权限错误,不要仅靠前端角色隐藏来替代服务端校验。
|
||||
|
||||
## 断开连接
|
||||
## 好友和黑名单
|
||||
|
||||
```ts
|
||||
ImSDK.disconnect()
|
||||
const friends = await ImSDK.listFriends()
|
||||
await ImSDK.addFriend('user-002')
|
||||
await ImSDK.removeFriend('user-002')
|
||||
|
||||
const incoming = await ImSDK.listFriendRequests('incoming')
|
||||
const request = await ImSDK.sendFriendRequest('user-003', '我是张医生')
|
||||
await ImSDK.acceptFriendRequest(request.id)
|
||||
|
||||
await ImSDK.addToBlacklist('user-004')
|
||||
const blocked = await ImSDK.checkBlacklist('user-004')
|
||||
await ImSDK.removeFromBlacklist('user-004')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 类型参考
|
||||
## 用户与消息搜索
|
||||
|
||||
```ts
|
||||
interface ImMessage {
|
||||
id: string
|
||||
fromId: string
|
||||
toId: string
|
||||
chatType: 'SINGLE' | 'GROUP'
|
||||
msgType: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'FILE' |
|
||||
'LOCATION' | 'NOTIFY' | 'CUSTOM' | 'RICH_TEXT' |
|
||||
'CALL_AUDIO' | 'CALL_VIDEO' | 'FORWARD' | 'REVOKED'
|
||||
content: string
|
||||
status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' | 'REVOKED'
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
interface ConversationData {
|
||||
targetId: string
|
||||
chatType: 'SINGLE' | 'GROUP'
|
||||
lastMsgContent: string
|
||||
lastMsgType: string
|
||||
lastMsgTime: number
|
||||
unreadCount: number
|
||||
isMuted: boolean
|
||||
isPinned: boolean
|
||||
}
|
||||
const users = await ImSDK.searchUsers('张医生')
|
||||
const groups = await ImSDK.searchGroups('项目')
|
||||
const messages = await ImSDK.searchMessages({
|
||||
keyword: '会议',
|
||||
chatType: 'SINGLE',
|
||||
toId: 'user-002',
|
||||
msgTypes: ['TEXT'],
|
||||
limit: 20,
|
||||
})
|
||||
```
|
||||
|
||||
## 登出
|
||||
|
||||
```ts
|
||||
await XuqmSDK.logout()
|
||||
```
|
||||
|
||||
公共登出会自动断开 IM。`ImSDK.disconnect()` 仅用于 IM 专用诊断或明确的临时断开场景。
|
||||
|
||||
@ -1,288 +1,88 @@
|
||||
# React Native SDK 接入指南
|
||||
# React Native SDK
|
||||
|
||||
**包名**:`@xuqm/rn-sdk` · **版本**:0.2.x(内部基础包,业务方不直接引用)
|
||||
XuqmGroup React Native SDK 由一个可独立使用的基础包和多个按需安装的扩展包组成。宿主只需
|
||||
维护一份平台下发的配置文件,并在登录成功后同步一次会话;已安装的扩展包会共享这份配置
|
||||
和会话状态。
|
||||
|
||||
> **注意**:`rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
|
||||
## 选择需要的模块
|
||||
|
||||
---
|
||||
| 包 | 用途 | 是否需要平台配置 |
|
||||
| --- | --- | --- |
|
||||
| `@xuqm/rn-common` | 网络、文件、时间、设备、通用 UI 与 API Hooks | 独立使用时不需要 |
|
||||
| `@xuqm/rn-update` | Android 整包更新与 RN 插件更新 | 需要 |
|
||||
| `@xuqm/rn-bugcollect` | JS 异常、原生崩溃、面包屑和自定义事件 | 需要 |
|
||||
| `@xuqm/rn-xwebview` | 独立 WebView 页面、导航栏、权限、下载和宿主 Bridge | 使用页面能力时需要 |
|
||||
| `@xuqm/rn-im` | 单聊、群聊、会话、好友和离线消息 | 需要,并需要登录 |
|
||||
| `@xuqm/rn-push` | 厂商推送绑定、离线推送和免打扰 | 需要,并需要登录 |
|
||||
|
||||
## 功能模块
|
||||
只安装实际使用的模块。扩展包会声明对 `rn-common` 的依赖范围,不需要宿主再创建第二套
|
||||
初始化器、网络层或用户 Store。
|
||||
|
||||
| 包 | 功能 |
|
||||
|----|------|
|
||||
| `@xuqm/rn-common` | 初始化、网络、设备信息,可独立使用 |
|
||||
| `@xuqm/rn-im` | 单聊、群聊、消息收发、本地 DB(WatermelonDB)|
|
||||
| `@xuqm/rn-push` | 推送设备 Token 上报 |
|
||||
| `@xuqm/rn-update` | App 版本检查、RN Bundle 热更新 |
|
||||
| `@xuqm/rn-sdk` | 内部基础包,随 IM / Push / Update 自动安装,不建议业务方直接引用 |
|
||||
## 两种接入方式
|
||||
|
||||
---
|
||||
### 只使用 Common
|
||||
|
||||
## 安装
|
||||
不放置 `.xuqmconfig`,也不包装 Metro:
|
||||
|
||||
在项目根目录创建 `.npmrc`:
|
||||
```ts
|
||||
import { configureHttp, apiRequest, expirationStatus } from '@xuqm/rn-common'
|
||||
|
||||
```
|
||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
|
||||
configureHttp({ baseUrl: 'https://api.example.com' })
|
||||
const health = await apiRequest('/health', { skipAuth: true })
|
||||
const certState = expirationStatus('2027-12-31T23:59:59+08:00')
|
||||
```
|
||||
|
||||
只使用基础能力时,直接安装 `rn-common`,不会带入 IM / Push / Update:
|
||||
这种模式不需要初始化和登录。
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-common
|
||||
### 使用任意扩展包
|
||||
|
||||
1. 从租户平台下载当前应用的 `.xuqmconfig`。
|
||||
2. 将唯一一份配置放到 `src/assets/config/`。
|
||||
3. 使用 SDK 提供的 Metro 包装器。
|
||||
4. 宿主隐私声明获得用户同意后调用一次 `setPrivacyConsent(true)`。
|
||||
5. 宿主登录成功后调用一次 `XuqmSDK.login(...)`。
|
||||
|
||||
SDK 不要求页面逐层传递 AppKey、平台地址、用户 ID 或登录凭证。
|
||||
|
||||
## 最小示例
|
||||
|
||||
```js
|
||||
// metro.config.js
|
||||
const { getDefaultConfig } = require('@react-native/metro-config')
|
||||
const { withXuqmConfig } = require('@xuqm/rn-common/metro')
|
||||
|
||||
module.exports = withXuqmConfig(getDefaultConfig(__dirname))
|
||||
```
|
||||
|
||||
按需安装模块时,`rn-im` / `rn-push` / `rn-update` 都会自动带上 `rn-common` 和 `rn-sdk`:
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-common @xuqm/rn-im
|
||||
```
|
||||
|
||||
`rn-sdk` 不作为业务方直接安装入口;它会随着 `rn-im` / `rn-push` / `rn-update` 自动进入依赖树。
|
||||
|
||||
---
|
||||
|
||||
## 快速接入(当前 v0.2.x)
|
||||
|
||||
### 1. 初始化
|
||||
|
||||
初始化只需传入 `appKey`,平台地址由 SDK 内置,开发者无需额外配置。
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
// App 入口(如 App.tsx 的顶层)
|
||||
await XuqmSDK.initialize({
|
||||
appKey: 'your_app_key',
|
||||
logLevel: __DEV__ ? 'debug' : 'warn',
|
||||
})
|
||||
```
|
||||
await XuqmSDK.setPrivacyConsent(true)
|
||||
|
||||
> SDK 内部自动处理服务器地址、IM 实时连接、文件服务等配置,开发者无需关心。
|
||||
|
||||
### 2. 统一登录
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
|
||||
// 登录只需要 userId + userSig
|
||||
// nickname / avatar 不再通过登录接口传入
|
||||
await XuqmSDK.login({
|
||||
userId: 'user_001',
|
||||
userSig: 'your_user_sig_jwt',
|
||||
userId: 'user-001',
|
||||
accessToken: 'host-access-token',
|
||||
// 仅使用 IM 时传入:
|
||||
userSig: 'im-user-signature',
|
||||
})
|
||||
|
||||
// 如果集成了 rn-im,XuqmSDK.login 会自动触发 ImSDK 连接
|
||||
// 业务侧无需单独调用 ImSDK.login
|
||||
```
|
||||
|
||||
### 3. 消息收发
|
||||
## 默认行为
|
||||
|
||||
```ts
|
||||
// 监听实时消息
|
||||
ImSDK.addEventListener('message', (msg) => {
|
||||
console.log('收到消息:', msg.msgType, msg.content)
|
||||
})
|
||||
- 配置由构建工具自动发现,运行时没有手动初始化 API。
|
||||
- 配置错误会阻断构建,避免生成安装后才无法初始化的包。
|
||||
- 扩展服务初始化失败不会阻断宿主应用自身启动;具体操作会返回结构化状态或错误。
|
||||
- Update、BugCollect 等服务是否启用由租户平台控制。
|
||||
- Debug 不会自动检查更新,也不会自动上报 BugCollect。
|
||||
- Common 独立模式不会访问 XuqmGroup 服务。
|
||||
|
||||
// 发送文本消息
|
||||
const msg = await ImSDK.sendMessage(
|
||||
'user_002', // toId
|
||||
'SINGLE', // chatType: 'SINGLE' | 'GROUP'
|
||||
'TEXT', // msgType
|
||||
'Hello!' // content
|
||||
)
|
||||
## 下一步
|
||||
|
||||
// 发送图片(自动上传到 file-service)
|
||||
const msg = await ImSDK.sendImageMessage(
|
||||
'user_002', 'SINGLE',
|
||||
'/path/to/image.jpg', // 本地 URI
|
||||
800, 600 // 宽高(可选)
|
||||
)
|
||||
|
||||
// 获取历史消息(单聊)
|
||||
const history = await ImSDK.fetchHistory('user_002', page, size)
|
||||
|
||||
// 获取群历史消息
|
||||
const groupHistory = await ImSDK.fetchGroupHistory('group_xxx', page, size)
|
||||
```
|
||||
|
||||
### 4. 会话列表
|
||||
|
||||
```ts
|
||||
// 订阅会话变化
|
||||
const unsub = ImSDK.subscribeConversations((conversations) => {
|
||||
// conversations: ConversationData[]
|
||||
console.log(conversations)
|
||||
})
|
||||
|
||||
// 置顶会话
|
||||
await ImSDK.setConversationPinned('user_002', 'SINGLE', true)
|
||||
|
||||
// 免打扰
|
||||
await ImSDK.setConversationMuted('group_xxx', 'GROUP', true)
|
||||
|
||||
// 标记已读
|
||||
await ImSDK.markRead('user_002')
|
||||
```
|
||||
|
||||
### 5. 群组管理
|
||||
|
||||
```ts
|
||||
// 创建群组
|
||||
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
|
||||
|
||||
// 群组列表(仅返回当前用户所在的群)
|
||||
const groups = await ImSDK.listGroups()
|
||||
|
||||
// 添加成员
|
||||
await ImSDK.addGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 移除成员(需要管理员权限)
|
||||
await ImSDK.removeGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 退出群聊
|
||||
await ImSDK.leaveGroup('group_xxx')
|
||||
```
|
||||
|
||||
### 6. 好友关系
|
||||
|
||||
```ts
|
||||
// 好友列表
|
||||
const friends = await ImSDK.listFriends()
|
||||
|
||||
// 添加好友(双向)
|
||||
await ImSDK.addFriend('user_002')
|
||||
|
||||
// 删除好友(双向)
|
||||
await ImSDK.removeFriend('user_002')
|
||||
```
|
||||
|
||||
### 7. 消息搜索(本地)
|
||||
|
||||
```ts
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
import type { MessageSearchParams } from '@xuqm/rn-im'
|
||||
|
||||
const params: MessageSearchParams = {
|
||||
keyword: '会议', // 关键词搜索
|
||||
toId: 'user_002', // 限定会话(可选)
|
||||
chatType: 'SINGLE', // 限定类型(可选)
|
||||
msgTypes: ['TEXT'], // 限定消息类型(可选)
|
||||
limit: 20,
|
||||
}
|
||||
const results = await ImSDK.searchMessages(params)
|
||||
```
|
||||
|
||||
### 8. 推送 SDK
|
||||
|
||||
登录成功后 SDK **自动**注册设备 Token,业务侧无需手动调用。
|
||||
|
||||
如需自定义 Push 行为,可引入 `@xuqm/rn-push`:
|
||||
|
||||
```ts
|
||||
import { PushSDK } from '@xuqm/rn-push'
|
||||
|
||||
// 手动设置接收推送开关
|
||||
await PushSDK.setReceivePush(false)
|
||||
```
|
||||
|
||||
### 9. 版本更新 SDK
|
||||
|
||||
```ts
|
||||
import { UpdateSDK } from '@xuqm/rn-update'
|
||||
|
||||
// 检查 App 原生更新
|
||||
const appUpdate = await UpdateSDK.checkAppUpdate()
|
||||
if (appUpdate?.needsUpdate) {
|
||||
console.log('新版本:', appUpdate.versionName)
|
||||
// Linking.openURL(appUpdate.downloadUrl)
|
||||
}
|
||||
|
||||
// 检查 RN Bundle 热更新
|
||||
const rnUpdate = await UpdateSDK.checkRnUpdate('your_module_id')
|
||||
if (rnUpdate?.needsUpdate) {
|
||||
await UpdateSDK.downloadAndApplyBundle(rnUpdate)
|
||||
}
|
||||
```
|
||||
|
||||
### 10. 断开连接
|
||||
|
||||
```ts
|
||||
ImSDK.disconnect()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript 类型参考
|
||||
|
||||
```ts
|
||||
// 消息
|
||||
interface ImMessage {
|
||||
id: string
|
||||
fromId: string
|
||||
toId: string
|
||||
chatType: 'SINGLE' | 'GROUP'
|
||||
msgType: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'FILE' |
|
||||
'LOCATION' | 'NOTIFY' | 'CUSTOM' | 'RICH_TEXT' |
|
||||
'CALL_AUDIO' | 'CALL_VIDEO' | 'FORWARD' | 'REVOKED'
|
||||
content: string // JSON 字符串,结构按 msgType 不同
|
||||
status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' | 'REVOKED'
|
||||
createdAt: number // Unix 毫秒
|
||||
}
|
||||
|
||||
// 会话
|
||||
interface ConversationData {
|
||||
targetId: string
|
||||
chatType: 'SINGLE' | 'GROUP'
|
||||
lastMsgContent: string
|
||||
lastMsgType: string
|
||||
lastMsgTime: number
|
||||
unreadCount: number
|
||||
isMuted: boolean
|
||||
isPinned: boolean
|
||||
}
|
||||
|
||||
// 群组
|
||||
interface ImGroup {
|
||||
id: string
|
||||
name: string
|
||||
creatorId: string
|
||||
memberIds: string // JSON 数组字符串
|
||||
adminIds: string
|
||||
createdAt: number
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前接入
|
||||
|
||||
当前接入方式如下:
|
||||
|
||||
```ts
|
||||
// 初始化
|
||||
await XuqmSDK.initialize({ appKey: 'xxx' }) // 平台地址内置,无需传入
|
||||
|
||||
// IM 登录(UserSig 由业务服务端签发)
|
||||
const userSig = await yourServer.getUserSig(userId)
|
||||
await ImSDK.login(userId, userSig)
|
||||
|
||||
// dbName 自动由 appKey + userId 派生,无需传入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 如何获取 appKey?**
|
||||
在 [平台控制台](https://dev.xuqinmin.com) 注册账号后,创建应用即可获得 AppKey。
|
||||
|
||||
**Q: 为什么不需要传平台地址参数?**
|
||||
XuqmGroup 是托管平台,服务地址统一管理,与腾讯云 IM 等平台的设计一致,开发者只需关心业务逻辑。
|
||||
|
||||
**Q: UserSig 是什么?**
|
||||
UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证。当前项目的 IM 登录不做过期功能,只校验 `userId + UserSig` 是否匹配。AppSecret 绝不下发到客户端。
|
||||
|
||||
**Q: 本地消息存储在哪里?**
|
||||
使用 WatermelonDB(SQLite),按 `appKey + userId` 自动隔离,多账号切换安全。
|
||||
- [安装与自动配置](./setup)
|
||||
- [Common 基础能力](./common)
|
||||
- [会话与隐私授权](./session)
|
||||
- [版本与插件更新](./update)
|
||||
- [BugCollect](./bugcollect)
|
||||
- [XWebView](./xwebview)
|
||||
- [IM 接入](./im)
|
||||
- [推送接入](./push)
|
||||
|
||||
@ -1,138 +1,80 @@
|
||||
# React Native 推送接入指南
|
||||
# 推送接入
|
||||
|
||||
**包名**:`@xuqm/rn-push` · **支持**:华为、小米、OPPO、vivo、荣耀、APNs(iOS)
|
||||
`@xuqm/rn-push` 负责把宿主登录用户同步给原生推送模块,并提供离线推送开关与免打扰
|
||||
设置。厂商识别、Token 获取和绑定由原生实现完成,RN 页面不接收或上传厂商 Token。
|
||||
|
||||
---
|
||||
|
||||
## 1. 安装
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-push
|
||||
pnpm add @xuqm/rn-common @xuqm/rn-push
|
||||
```
|
||||
|
||||
iOS 需要执行 `pod install`:
|
||||
Android 原生推送依赖按租户平台为当前应用生成的配置接入。宿主只启用实际使用的厂商
|
||||
渠道,不要把其他项目的厂商 App ID 或密钥复制过来。
|
||||
|
||||
```bash
|
||||
cd ios && pod install
|
||||
```
|
||||
## 登录后自动绑定
|
||||
|
||||
---
|
||||
|
||||
## 2. Android 厂商推送集成
|
||||
|
||||
各厂商推送 SDK 需在原生 Android 层集成。在 `android/app/build.gradle` 中按需添加:
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
// 华为 HMS Push
|
||||
implementation 'com.huawei.hms:push:6.9.0.300'
|
||||
// 小米 Push
|
||||
implementation 'com.xiaomi.mipush:sdk:5.0.6'
|
||||
// OPPO Push
|
||||
implementation 'com.heytap.msp:push:3.5.0'
|
||||
// vivo Push
|
||||
implementation 'com.vivo.push:sdk:3.0.0.4_484'
|
||||
// 荣耀 Push
|
||||
implementation 'com.hihonor.mcs:push:7.0.41.301'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 请求原生推送权限并注册
|
||||
|
||||
```ts
|
||||
import { PushSDK } from '@xuqm/rn-push'
|
||||
|
||||
// 触发原生推送注册(Android 请求厂商 token;iOS 请求 APNs 权限)
|
||||
await PushSDK.requestNativeRegistration()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 监听推送 Token
|
||||
|
||||
```ts
|
||||
import { PushSDK } from '@xuqm/rn-push'
|
||||
|
||||
// 在 App 启动时监听 token 回调,并向服务端注册
|
||||
const unsubscribe = PushSDK.onPushToken(async (token, vendor) => {
|
||||
console.log('获取到 Token:', token, '厂商:', vendor)
|
||||
// 登录后调用注册接口
|
||||
await PushSDK.setDeviceToken(token, vendor as PushVendor)
|
||||
})
|
||||
|
||||
// 在组件卸载时取消监听
|
||||
unsubscribe()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 手动注册 Token
|
||||
|
||||
```ts
|
||||
import { PushSDK } from '@xuqm/rn-push'
|
||||
import type { PushVendor } from '@xuqm/rn-push'
|
||||
|
||||
// 登录成功后,将 token 注册到服务端
|
||||
await PushSDK.registerToken('user_001', 'device_token_here', 'HUAWEI')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 登出时注销 Token
|
||||
|
||||
```ts
|
||||
await PushSDK.unregisterToken('user_001')
|
||||
// 或简写
|
||||
await PushSDK.logout('user_001')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 多模块统一登录
|
||||
|
||||
Push 模块与 IM、Update 模块共享同一套登录态:
|
||||
宿主仍然只调用公共登录入口:
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
await XuqmSDK.login({
|
||||
userId: 'user-001',
|
||||
accessToken: 'host-access-token',
|
||||
})
|
||||
```
|
||||
|
||||
已安装 Push 模块时,会话变化自动同步到原生层。不要调用不存在的
|
||||
Token 注册方法,也不要建立第二套 Push 登录 API。
|
||||
|
||||
登出:
|
||||
|
||||
```ts
|
||||
await XuqmSDK.logout()
|
||||
```
|
||||
|
||||
## 离线推送开关
|
||||
|
||||
```ts
|
||||
import { PushSDK } from '@xuqm/rn-push'
|
||||
|
||||
await XuqmSDK.initialize({ appKey: 'your_app_key' })
|
||||
await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig' })
|
||||
// ↓ 登录后调用 PushSDK.initialize() 完成 token 注册
|
||||
await PushSDK.initialize('user_001')
|
||||
await PushSDK.setOfflinePushEnabled(false)
|
||||
await PushSDK.setOfflinePushEnabled(true)
|
||||
```
|
||||
|
||||
---
|
||||
该开关同步到当前登录用户。未登录时不要在业务页面展示为已经保存成功。
|
||||
|
||||
## 8. iOS APNs 配置
|
||||
## 免打扰
|
||||
|
||||
在 `AppDelegate.m` 或 `AppDelegate.swift` 中:
|
||||
|
||||
```objc
|
||||
// AppDelegate.m
|
||||
- (void)application:(UIApplication *)application
|
||||
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||
// 转发 token,由 rn-push 原生模块处理
|
||||
[RCTEventEmitter ...] // 通过 Bridge 传至 JS
|
||||
}
|
||||
```ts
|
||||
await PushSDK.setQuietHours('22:00', '08:00')
|
||||
```
|
||||
|
||||
> 使用 `PushSDK.requestNativeRegistration()` 会自动触发 iOS APNs 注册流程,无需额外原生代码(前提是 RN 0.76+ 自动链接)。
|
||||
时间使用 24 小时制的 `HH:mm`。清除设置:
|
||||
|
||||
---
|
||||
```ts
|
||||
await PushSDK.clearQuietHours()
|
||||
```
|
||||
|
||||
## 9. 厂商渠道自动检测
|
||||
## 权限和厂商配置
|
||||
|
||||
`@xuqm/rn-common` 的 `detectPushVendor` 会根据 `device.brand` 自动识别厂商:
|
||||
- Android 13 及以上由宿主在合适的业务时机申请通知权限。
|
||||
- 厂商推送应用标识和服务端密钥在租户平台维护。
|
||||
- 客户端只包含厂商要求的公开配置,不得包含服务端密钥。
|
||||
- Push 服务关闭或注册失败不能阻断宿主登录。
|
||||
- 重新登录或切换用户后,SDK 会使用新的公共会话重新绑定。
|
||||
|
||||
| 品牌关键字 | 识别厂商 |
|
||||
|-----------|---------|
|
||||
| xiaomi / redmi | XIAOMI |
|
||||
| huawei | HUAWEI |
|
||||
| honor | HONOR |
|
||||
| oppo / realme | OPPO |
|
||||
| vivo / iqoo | VIVO |
|
||||
| iOS | APNS |
|
||||
| 其他 | FCM |
|
||||
## 常见错误
|
||||
|
||||
### 登录成功但没有离线通知
|
||||
|
||||
依次检查:
|
||||
|
||||
1. 系统通知权限是否开启。
|
||||
2. 当前构建是否包含目标设备厂商通道。
|
||||
3. 租户平台是否启用 Push。
|
||||
4. App 包名、签名和厂商控制台配置是否一致。
|
||||
5. 当前用户是否关闭了离线推送或设置了免打扰。
|
||||
|
||||
不要在 RN 页面手工缓存 Token 作为“绑定成功”的判断依据。
|
||||
|
||||
83
docs-site/docs/rn/session.md
普通文件
83
docs-site/docs/rn/session.md
普通文件
@ -0,0 +1,83 @@
|
||||
# 会话与隐私授权
|
||||
|
||||
使用扩展模块的应用只有一个公共会话入口。Common 独立模式不需要登录。
|
||||
|
||||
## 隐私授权
|
||||
|
||||
SDK 不展示隐私弹窗,也不会自行推断用户是否同意。宿主在自己的隐私声明获得授权后调用:
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
await XuqmSDK.setPrivacyConsent(true)
|
||||
```
|
||||
|
||||
用户撤回授权时:
|
||||
|
||||
```ts
|
||||
await XuqmSDK.setPrivacyConsent(false)
|
||||
```
|
||||
|
||||
撤回后 BugCollect 会停止采集并清理尚未发送的数据。宿主业务是否继续运行由宿主决定。
|
||||
|
||||
## 登录
|
||||
|
||||
宿主业务登录成功后调用一次:
|
||||
|
||||
```ts
|
||||
await XuqmSDK.login({
|
||||
userId: 'user-001',
|
||||
accessToken: 'host-access-token',
|
||||
name: '张医生',
|
||||
phone: '13800000000',
|
||||
avatar: 'https://example.com/avatar.png',
|
||||
// 只有使用 IM 时才需要:
|
||||
userSig: 'im-user-signature',
|
||||
})
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 必填 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `userId` | 是 | XuqmGroup 扩展服务共享的用户标识 |
|
||||
| `accessToken` | 按业务 | 公共 HTTP Bearer Token |
|
||||
| `userSig` | 仅 IM | IM 登录凭证 |
|
||||
| `name/email/phone/avatar` | 否 | 扩展模块需要时使用的用户资料 |
|
||||
|
||||
`accessToken` 和 `userSig` 是两种不同凭证,不要互相替代。
|
||||
|
||||
## 登出
|
||||
|
||||
```ts
|
||||
await XuqmSDK.logout()
|
||||
```
|
||||
|
||||
登出会通知所有已安装扩展包清理当前会话。业务页面不需要分别调用 Update、Push 或 IM 的
|
||||
登录/登出方法。
|
||||
|
||||
## 获取当前状态
|
||||
|
||||
```ts
|
||||
const userId = XuqmSDK.getUserId()
|
||||
const userInfo = XuqmSDK.getUserInfo()
|
||||
const initState = XuqmSDK.getInitializationState()
|
||||
```
|
||||
|
||||
这些方法适合应用入口、调试页或基础设施层读取。业务组件应从宿主自己的用户领域 Store
|
||||
展示资料,不要把 SDK 当作宿主用户资料数据库。
|
||||
|
||||
## 登录门禁与宿主行为
|
||||
|
||||
租户平台可以把 Update 等服务配置为“登录后检查”。未登录时 SDK 会返回
|
||||
`LOGIN_REQUIRED`,宿主应当跳过更新提示并继续正常使用应用:
|
||||
|
||||
```ts
|
||||
const result = await UpdateSDK.checkAppUpdate()
|
||||
|
||||
if (result.status === 'LOGIN_REQUIRED') {
|
||||
return // 不弹错误、不阻断启动
|
||||
}
|
||||
```
|
||||
|
||||
平台临时不可用、服务关闭或登录要求发生变化,都不能替代宿主自身的登录和启动流程。
|
||||
@ -1,147 +1,156 @@
|
||||
# React Native 安装配置
|
||||
# 安装与自动配置
|
||||
|
||||
**包名**:`@xuqm/rn-sdk` · **版本**:0.2.x · **RN 版本**:≥ 0.76.0
|
||||
## 环境要求
|
||||
|
||||
> `rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
|
||||
- React Native `0.76` 或更高版本
|
||||
- Node.js 当前 LTS
|
||||
- Android `minSdk 24` 或更高版本
|
||||
- 使用原生模块时,需要标准 React Native 原生工程或完成 Prebuild 的 Expo 工程
|
||||
|
||||
---
|
||||
SDK 当前以 Android 为完整验证平台。页面中明确标记为 Android 的能力不能据此推断为
|
||||
iOS 已支持。
|
||||
|
||||
## npm / yarn 安装
|
||||
## 配置 npm 仓库
|
||||
|
||||
在项目根目录创建 `.npmrc`:
|
||||
宿主从只读聚合仓库安装依赖:
|
||||
|
||||
```
|
||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
|
||||
```ini
|
||||
# .npmrc
|
||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm/
|
||||
```
|
||||
|
||||
只使用基础能力时,直接安装 `rn-common`:
|
||||
请使用租户平台为当前应用推荐的精确版本。开发和测试阶段可能使用带 `alpha`、`beta`、
|
||||
`rc` 或 `SNAPSHOT` 标识的预发布版本,不要自行改成无预发布标识的版本。
|
||||
|
||||
## 按需安装
|
||||
|
||||
只使用通用基础能力:
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-common
|
||||
pnpm add @xuqm/rn-common axios \
|
||||
@react-native-async-storage/async-storage
|
||||
```
|
||||
|
||||
按需安装模块时,`rn-im` / `rn-push` / `rn-update` 都会自动带上 `rn-common` 和 `rn-sdk`:
|
||||
使用版本更新:
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-common @xuqm/rn-im
|
||||
# 或全量安装
|
||||
yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update @xuqm/rn-bugcollect
|
||||
pnpm add @xuqm/rn-common @xuqm/rn-update \
|
||||
@react-native-async-storage/async-storage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自动 / 手动链接
|
||||
|
||||
React Native 0.60+ 支持**自动链接**,安装后执行:
|
||||
使用 WebView:
|
||||
|
||||
```bash
|
||||
cd ios && pod install
|
||||
pnpm add @xuqm/rn-common @xuqm/rn-xwebview \
|
||||
react-native-webview react-native-safe-area-context react-native-svg
|
||||
```
|
||||
|
||||
> 若使用 Expo,需先执行 `expo prebuild` 生成原生项目后再执行 `pod install`。
|
||||
|
||||
---
|
||||
|
||||
## iOS 配置
|
||||
|
||||
### Pod 安装
|
||||
其他模块同样按需安装:
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
pod install
|
||||
pnpm add @xuqm/rn-bugcollect
|
||||
pnpm add @xuqm/rn-im @nozbe/watermelondb
|
||||
pnpm add @xuqm/rn-push
|
||||
```
|
||||
|
||||
### 权限声明
|
||||
项目使用 npm 或 Yarn 时,可以使用等价安装命令;不要同时维护多份锁文件。
|
||||
|
||||
在 `Info.plist` 中添加:
|
||||
## 获取配置文件
|
||||
|
||||
```xml
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问相机</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问相册</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要访问麦克风</string>
|
||||
使用扩展包时,在租户平台进入目标应用并下载配置文件。将下载文件原名放到:
|
||||
|
||||
```text
|
||||
src/assets/config/<平台生成的文件名>.xuqmconfig
|
||||
```
|
||||
|
||||
---
|
||||
约束:
|
||||
|
||||
## Android 配置
|
||||
- 目录中只能存在一个 `.xuqmconfig`。
|
||||
- 不要重命名内容、手工编辑或提交到公共仓库。
|
||||
- 公有化和私有化平台生成的配置不能混用。
|
||||
- 重新生成配置后,下一次构建必须使用新文件。
|
||||
- Common 独立模式不要放置配置文件。
|
||||
|
||||
### Gradle 仓库
|
||||
建议加入忽略规则:
|
||||
|
||||
在 `android/build.gradle` 中确保包含:
|
||||
|
||||
```gradle
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url "https://nexus.xuqinmin.com/repository/android/" }
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
```gitignore
|
||||
src/assets/config/*.xuqmconfig
|
||||
```
|
||||
|
||||
### 最低版本
|
||||
## 配置 Metro
|
||||
|
||||
- `minSdkVersion = 24`
|
||||
- `compileSdkVersion = 34`
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
@xuqm/rn-sdk(meta-package,不建议业务方直接引用)
|
||||
├── @xuqm/rn-common ← 初始化、网络、设备信息
|
||||
├── @xuqm/rn-im ← IM 模块(依赖 WatermelonDB)
|
||||
├── @xuqm/rn-push ← Push 模块
|
||||
├── @xuqm/rn-update ← 更新模块
|
||||
└── @xuqm/rn-bugcollect ← 异常采集模块(崩溃、ANR 上报)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Metro 配置
|
||||
|
||||
使用 `withXuqmConfig` Metro 插件替代手动 alias 配置。该插件会自动处理 RN SDK 的模块解析,并注入 `.xuqmconfig` 配置文件:
|
||||
没有启用插件化时,只包装一次 Common:
|
||||
|
||||
```js
|
||||
// metro.config.js
|
||||
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
|
||||
const { withXuqmConfig } = require('@xuqm/rn-common/metro')
|
||||
|
||||
module.exports = withXuqmConfig({
|
||||
// 你的原始 Metro 配置
|
||||
const config = mergeConfig(getDefaultConfig(__dirname), {
|
||||
// 宿主自己的 Metro 配置
|
||||
})
|
||||
|
||||
module.exports = withXuqmConfig(config)
|
||||
```
|
||||
|
||||
> 不再需要手动配置 `resolver.extraNodeModules` 或 alias。
|
||||
启用 RN 插件化时使用 Update 提供的组合器,不要再叠加 `withXuqmConfig`:
|
||||
|
||||
---
|
||||
```js
|
||||
const { getDefaultConfig } = require('@react-native/metro-config')
|
||||
const { withXuqmModuleConfig } = require('@xuqm/rn-update/metro')
|
||||
|
||||
## 配置文件
|
||||
|
||||
从平台控制台下载应用的 `.xuqmconfig` 配置文件(例如 `识校宝.xuqmconfig`),放置到 `src/assets/config/` 目录。`withXuqmConfig` 插件会自动发现并注入该配置,SDK 运行时自动读取,无需手动指定路径。
|
||||
|
||||
文件名不限,只要扩展名为 `.xuqmconfig` 即可。
|
||||
|
||||
---
|
||||
|
||||
## BugCollect
|
||||
|
||||
异常采集模块可自动收集 JS 异常、原生崩溃日志并上报到服务端。
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-bugcollect
|
||||
module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname))
|
||||
```
|
||||
|
||||
集成了 `withXuqmConfig` 插件后,BugCollect 会随 SDK 自动初始化,无需额外代码。采集服务地址从 `.xuqmconfig` 配置中自动读取。
|
||||
两种配置二选一。项目只能存在一个 Metro 配置出口。
|
||||
|
||||
---
|
||||
## Android 仓库
|
||||
|
||||
## 下一步
|
||||
原生依赖从 XuqmGroup Android 聚合仓库读取:
|
||||
|
||||
- [RN IM 接入 →](./im)
|
||||
- [RN 群聊 →](./group)
|
||||
- [RN 推送接入 →](./push)
|
||||
- [RN 版本更新 →](./update)
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
maven("https://nexus.xuqinmin.com/repository/android/")
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
React Native 自动链接会处理已安装模块。只有 Update 插件化宿主需要按其页面说明应用
|
||||
一次 Bundle 构建脚本。
|
||||
|
||||
## 启动前检查
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
const state = XuqmSDK.getInitializationState()
|
||||
// idle | initializing | ready | degraded | failed
|
||||
```
|
||||
|
||||
一般业务无需等待初始化。确实需要在首屏执行扩展操作时:
|
||||
|
||||
```ts
|
||||
await XuqmSDK.awaitInitialization()
|
||||
```
|
||||
|
||||
持续失败时按错误码展示非阻断提示,不要让 SDK 服务异常替代宿主首页。
|
||||
|
||||
## Debug 策略
|
||||
|
||||
- `pnpm start` 和 `pnpm android` 使用标准 Metro 与 Fast Refresh。
|
||||
- Debug 默认不自动检查整包或插件更新。
|
||||
- Debug 默认不自动上传 BugCollect。
|
||||
- 需要验证服务时,通过对应模块的显式测试入口触发。
|
||||
|
||||
## 验收清单
|
||||
|
||||
- 工程中只有一份 `.xuqmconfig`。
|
||||
- Metro 只使用一个 Xuqm 包装器。
|
||||
- Common-only 工程不要求初始化。
|
||||
- App 启动时没有手写 AppKey、平台地址或内部服务地址。
|
||||
- 登录只调用一次公共会话入口。
|
||||
- Release 构建使用租户平台当前有效配置。
|
||||
|
||||
142
docs-site/docs/rn/troubleshooting.md
普通文件
142
docs-site/docs/rn/troubleshooting.md
普通文件
@ -0,0 +1,142 @@
|
||||
# 错误处理与排障
|
||||
|
||||
## 公共错误码
|
||||
|
||||
需要初始化的扩展能力可能抛出 `XuqmError`:
|
||||
|
||||
| 错误码 | 含义 | 宿主处理 |
|
||||
| --- | --- | --- |
|
||||
| `XUQM_NOT_READY` | 自动初始化尚未完成或没有启动 | 检查配置和 Metro;不要阻断宿主首页 |
|
||||
| `XUQM_NOT_LOGGED_IN` | 当前操作要求登录 | 完成宿主登录后重试 |
|
||||
| `XUQM_CONFIG_INVALID` | 配置格式或签名无效 | 重新从租户平台下载配置并构建 |
|
||||
| `XUQM_CONFIG_EXPIRED` | 构建使用的配置已过期 | 重新生成并构建 |
|
||||
| `XUQM_CONFIG_REVOKED` | 配置已被租户平台撤销 | 使用平台当前有效配置重新构建 |
|
||||
| `XUQM_SERVICE_DISABLED` | 对应服务未开启 | 隐藏入口或跳过功能 |
|
||||
|
||||
示例:
|
||||
|
||||
```ts
|
||||
import { XuqmError } from '@xuqm/rn-common'
|
||||
|
||||
try {
|
||||
await operation()
|
||||
} catch (error) {
|
||||
if (error instanceof XuqmError) {
|
||||
reportNonBlockingError(error.code)
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
## App 启动正常,但扩展功能不可用
|
||||
|
||||
1. 确认 `src/assets/config/` 只有一个 `.xuqmconfig`。
|
||||
2. 确认 Metro 只使用一个 Xuqm 包装器。
|
||||
3. 确认配置来自当前公有化或私有化租户平台,不能混用。
|
||||
4. 查看 `XuqmSDK.getInitializationState()`。
|
||||
5. 检查租户平台是否已开启对应服务。
|
||||
6. 如果服务要求登录,确认已经调用公共 `login`。
|
||||
|
||||
扩展服务失败不应导致宿主白屏。不要在根组件渲染前无限等待网络初始化。
|
||||
|
||||
## Common-only 工程提示未初始化
|
||||
|
||||
Common 的网络、文件、时间和 UI 能力不要求初始化。出现未初始化错误通常表示调用了
|
||||
需要平台配置的扩展 API,或直接调用了 `getConfig()`。
|
||||
|
||||
Common-only 工程应当:
|
||||
|
||||
- 不放 `.xuqmconfig`。
|
||||
- 不使用 Metro 自动配置包装器。
|
||||
- 不调用 `XuqmSDK.awaitInitialization()`。
|
||||
- 业务网络使用绝对 URL 或 `configureHttp`。
|
||||
|
||||
## 配置构建失败
|
||||
|
||||
构建期失败是预期的安全门禁,常见原因:
|
||||
|
||||
- 配置文件超过一个。
|
||||
- 文件内容被编辑、损坏或不属于当前应用。
|
||||
- 配置过期或已撤销。
|
||||
- 公有化配置用于私有化应用,或反向混用。
|
||||
|
||||
不要通过跳过校验生成安装包,应从租户平台重新下载配置。
|
||||
|
||||
## Update 不弹更新
|
||||
|
||||
先检查 `checkAppUpdate()` 的结构化状态:
|
||||
|
||||
- `LOGIN_REQUIRED`:未登录时正常跳过。
|
||||
- `SERVICE_DISABLED`:租户平台未启用 Update。
|
||||
- `NO_UPDATE`:没有适用于当前版本、设备或灰度身份的更新。
|
||||
- `FAILED`:查看 `errorCode` 和 `message`,但不要阻断应用。
|
||||
|
||||
网络状态随检查结果返回,SDK 不替宿主决定是否允许移动网络下载。
|
||||
|
||||
## 插件打不开
|
||||
|
||||
1. Debug 确认 Metro 属于当前工程并能正常 Fast Refresh。
|
||||
2. 运行 `pnpm run xuqm:doctor`。
|
||||
3. 确认模块 ID 与 `xuqm.modules.json` 一致。
|
||||
4. 检查插件入口是否调用 `definePlugin`。
|
||||
5. 更新检查失败时,确认宿主仍会尝试打开本地版本。
|
||||
6. 只有插件实际启动失败才记录启动失败,网络检查失败不是插件启动失败。
|
||||
|
||||
## BugCollect 没有数据
|
||||
|
||||
确认:
|
||||
|
||||
- 用户已经同意隐私声明。
|
||||
- 租户平台已开启 BugCollect。
|
||||
- 当前不是默认禁止自动上报的 Debug 行为。
|
||||
- Release 构建没有启用应急关闭。
|
||||
- 测试页调用 `sendTestErrorAndFlush` 时能看到明确错误。
|
||||
|
||||
BugCollect 失败不能影响宿主功能,不要通过抛出全局异常验证生产采集。
|
||||
|
||||
## XWebView 摄像头或麦克风失败
|
||||
|
||||
同时满足以下条件才会授权:
|
||||
|
||||
- 宿主声明并获得系统权限。
|
||||
- `permissions.camera` 或 `permissions.microphone` 已开启。
|
||||
- 当前网页 Origin 精确命中 `allowedOrigins`。
|
||||
- 自定义 `onRequest` 没有拒绝。
|
||||
|
||||
协议、端口或子域名变化都会形成不同 Origin。
|
||||
|
||||
## Push 没有通知
|
||||
|
||||
检查系统通知权限、设备厂商通道、包名与签名、租户平台服务状态、用户离线推送开关和免
|
||||
打扰时间。RN 页面不应以是否缓存到厂商 Token 判断最终绑定状态。
|
||||
|
||||
## 从历史接入方式迁移
|
||||
|
||||
当前版本不再使用以下方式:
|
||||
|
||||
- 手动传 AppKey 初始化。
|
||||
- 页面或子模块分别初始化、登录。
|
||||
- 宿主手工传入应用商店地址。
|
||||
- Update 的旧 Bundle 下载与应用方法。
|
||||
- RN 页面手工注册推送 Token。
|
||||
- XWebView 的扁平参数和全局控制器。
|
||||
|
||||
迁移步骤:
|
||||
|
||||
1. 下载并放置唯一配置文件。
|
||||
2. 使用一个 Metro 包装器。
|
||||
3. 隐私授权同步一次。
|
||||
4. 登录和登出只调用 Common。
|
||||
5. 按本文各模块页面替换旧 API。
|
||||
6. 删除宿主重复的 SDK 状态、地址和 Token 缓存。
|
||||
|
||||
## 提交问题时提供
|
||||
|
||||
- SDK 包名与精确版本。
|
||||
- React Native、Android/iOS 和设备系统版本。
|
||||
- 公有化或私有化类型,不提供服务端秘密。
|
||||
- 初始化状态、结构化错误码和最小复现路径。
|
||||
- 已脱敏日志。
|
||||
|
||||
不要上传 `.xuqmconfig`、Token、用户隐私数据或完整业务响应。
|
||||
@ -1,161 +1,233 @@
|
||||
# React Native 版本更新接入指南
|
||||
# 版本与插件更新
|
||||
|
||||
**包名**:`@xuqm/rn-update` · **功能**:App 版本检查、RN Bundle 热更新、打开商店
|
||||
`@xuqm/rn-update` 提供两类能力:
|
||||
|
||||
---
|
||||
- Android 整包检查、下载、安装和应用商店跳转。
|
||||
- 可选的 RN 插件检查、安装和启动。
|
||||
|
||||
## 1. 安装
|
||||
SDK 返回更新信息和当前网络状态,是否弹窗、是否允许使用移动网络以及何时开始下载由宿主
|
||||
决定。
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-update
|
||||
```
|
||||
## 整包更新
|
||||
|
||||
`rn-update` 会自动依赖 `@xuqm/rn-common` 和 `@xuqm/rn-sdk`。
|
||||
|
||||
---
|
||||
|
||||
## 2. App 版本检查
|
||||
### 检查
|
||||
|
||||
```ts
|
||||
import { UpdateSDK } from '@xuqm/rn-update'
|
||||
|
||||
const appUpdate = await UpdateSDK.checkAppUpdate()
|
||||
if (appUpdate.needsUpdate) {
|
||||
console.log('新版本:', appUpdate.versionName)
|
||||
console.log('更新日志:', appUpdate.changeLog)
|
||||
console.log('下载地址:', appUpdate.downloadUrl)
|
||||
const result = await UpdateSDK.checkAppUpdate()
|
||||
|
||||
if (appUpdate.forceUpdate) {
|
||||
// 强制更新
|
||||
showForceUpdateModal(appUpdate)
|
||||
} else {
|
||||
// 可选更新
|
||||
showOptionalUpdateModal(appUpdate)
|
||||
}
|
||||
switch (result.status) {
|
||||
case 'UPDATE_AVAILABLE':
|
||||
showUpdateDialog(result.update, result.network)
|
||||
break
|
||||
case 'LOGIN_REQUIRED':
|
||||
case 'SERVICE_DISABLED':
|
||||
case 'NO_UPDATE':
|
||||
// 正常跳过,不阻断应用
|
||||
break
|
||||
case 'FAILED':
|
||||
// 记录一次非阻断诊断
|
||||
console.warn(result.errorCode, result.message)
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
App 版本号自动从原生模块读取,无需手动传入。开发/模拟器环境可调用 `_devSetAppVersion` 覆盖:
|
||||
检查状态:
|
||||
|
||||
| 状态 | 宿主建议 |
|
||||
| --- | --- |
|
||||
| `UPDATE_AVAILABLE` | 根据 `forceUpdate` 展示宿主自己的更新弹窗 |
|
||||
| `NO_UPDATE` | 继续启动 |
|
||||
| `LOGIN_REQUIRED` | 未登录时跳过,登录后可再次检查 |
|
||||
| `SERVICE_DISABLED` | 服务未启用,继续运行 |
|
||||
| `FAILED` | 记录诊断,不阻断宿主 |
|
||||
|
||||
`result.network` 包含连接类型、是否已连接和是否按流量计费。租户平台不替宿主配置网络
|
||||
策略。
|
||||
|
||||
### 下载并安装
|
||||
|
||||
用户确认后调用:
|
||||
|
||||
```ts
|
||||
// 仅限开发环境使用
|
||||
UpdateSDK._devSetAppVersion(100, '1.0.0')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 打开商店
|
||||
|
||||
```ts
|
||||
await UpdateSDK.openStore(appUpdate.appStoreUrl, appUpdate.marketUrl)
|
||||
```
|
||||
|
||||
- iOS:打开 `appStoreUrl`
|
||||
- Android:打开 `marketUrl`
|
||||
|
||||
---
|
||||
|
||||
## 4. RN 插件更新
|
||||
|
||||
插件更新以服务端返回的 release-set 为最小事务单位。一个 release-set 只包含目标
|
||||
app/buz 及其 Common 依赖;SDK 会统一完成签名、宿主身份、整包版本、原生基线、
|
||||
SHA-256 和最终依赖闭包校验。宿主不能分别下载后自行拼接版本。
|
||||
|
||||
进入 buz 前自动检查并安装:
|
||||
|
||||
```ts
|
||||
await UpdateSDK.checkAndInstall('home', {
|
||||
onProgress(moduleId, progress) {
|
||||
console.log(moduleId, progress.percent)
|
||||
await UpdateSDK.downloadAndInstallApp(result.update!, {
|
||||
destination: 'SDK_PRIVATE',
|
||||
onProgress(progress) {
|
||||
setProgress(progress.percent)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
需要由宿主展示确认弹窗时,检查与安装分开调用:
|
||||
需要把安装包保留在系统 Downloads:
|
||||
|
||||
```ts
|
||||
const plan = await UpdateSDK.check('home')
|
||||
if (plan && (await showPluginUpdateDialog(plan))) {
|
||||
await UpdateSDK.install(plan)
|
||||
}
|
||||
await UpdateSDK.downloadAndInstallApp(result.update!, {
|
||||
destination: 'PUBLIC_DOWNLOADS',
|
||||
})
|
||||
```
|
||||
|
||||
`check` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
|
||||
记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
|
||||
管理,宿主不得另建一套缓存版本表。
|
||||
下载失败或应用退出后不会在下次启动时自动继续。
|
||||
|
||||
---
|
||||
|
||||
## 5. 强制更新处理
|
||||
|
||||
当 `appUpdate.forceUpdate` 为 `true` 时,建议业务层:
|
||||
|
||||
1. 弹出不可关闭的 Modal
|
||||
2. 只允许用户点击「立即更新」
|
||||
3. 调用 `UpdateSDK.openStore()` 跳转商店
|
||||
### 只下载、稍后安装
|
||||
|
||||
```ts
|
||||
function showForceUpdateModal(update: AppUpdateInfo) {
|
||||
// 使用 RN Modal 组件,设置不可取消
|
||||
Alert.alert(
|
||||
'发现重要更新',
|
||||
`当前版本已不可用,请升级至 ${update.versionName}`,
|
||||
[
|
||||
{
|
||||
text: '立即更新',
|
||||
onPress: () => UpdateSDK.openStore(update.appStoreUrl, update.marketUrl),
|
||||
},
|
||||
],
|
||||
{ cancelable: false }
|
||||
)
|
||||
}
|
||||
import { NativeAppUpdate, UpdateSDK } from '@xuqm/rn-update'
|
||||
|
||||
const downloaded = await UpdateSDK.downloadApk(result.update!, {
|
||||
destination: 'SDK_PRIVATE',
|
||||
})
|
||||
|
||||
console.log(downloaded.localPath)
|
||||
|
||||
await NativeAppUpdate.installDownloaded({
|
||||
versionCode: result.update!.versionCode!,
|
||||
sha256: result.update!.sha256,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 多模块统一登录
|
||||
|
||||
Update 模块与 IM、Push 模块共享同一套登录态:
|
||||
取消当前下载:
|
||||
|
||||
```ts
|
||||
await XuqmSDK.initialize({ appKey: 'your_app_key' })
|
||||
await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
|
||||
// UpdateSDK 在 checkAppUpdate 时自动携带 appKey,无需额外登录操作
|
||||
await NativeAppUpdate.cancel()
|
||||
```
|
||||
|
||||
---
|
||||
如果系统禁止安装未知来源应用:
|
||||
|
||||
## 7. 一键打包上传
|
||||
```ts
|
||||
await NativeAppUpdate.openInstallPermissionSettings()
|
||||
```
|
||||
|
||||
SDK 提供 `xuqm_release.mjs` 脚本,可一键完成 RN Bundle 构建并上传至 XuqmGroup 版本管理服务:
|
||||
### 跳转应用商店
|
||||
|
||||
宿主不传商店地址,只传检查接口返回的更新对象:
|
||||
|
||||
```ts
|
||||
const opened = await UpdateSDK.openStore(result.update!)
|
||||
```
|
||||
|
||||
Android 会优先打开当前设备对应的应用商店,不可用时使用平台提供的通用下载页。iOS 或
|
||||
HarmonyOS 没有配置商店地址时返回 `false`,宿主应提示当前无法提供下载服务。
|
||||
|
||||
### 忽略可选版本
|
||||
|
||||
```ts
|
||||
await UpdateSDK.ignoreAppVersion(result.update!.versionCode!)
|
||||
const ignoredVersion = await UpdateSDK.getIgnoredAppVersion()
|
||||
await UpdateSDK.clearIgnoredAppVersion()
|
||||
```
|
||||
|
||||
强制更新不能被忽略。
|
||||
|
||||
## 不启用插件化
|
||||
|
||||
普通项目可以只使用上述整包更新 API,不创建 `common/app/buz` 模块,也不需要接入插件
|
||||
运行时。后续需要插件化时再执行初始化命令即可。
|
||||
|
||||
## 启用插件化
|
||||
|
||||
在项目根目录执行交互式初始化:
|
||||
|
||||
```bash
|
||||
XUQM_API_TOKEN=*** node scripts/xuqm_release.mjs --platform android --publish
|
||||
pnpm exec xuqm-rn init
|
||||
pnpm run xuqm:doctor
|
||||
```
|
||||
|
||||
脚本执行流程:
|
||||
1. 按模块构建 JS/Hermes Bundle 和 Source Map
|
||||
2. 生成根目录唯一的 `rn-manifest.json`
|
||||
3. 生成 `<moduleId>.<platform>.xuqm.zip`
|
||||
4. 上传归档并由服务端校验、签名
|
||||
5. 按用户选择发布,返回不可变发布记录
|
||||
初始化工具会创建 `xuqm.modules.json`、补充必要脚本并提示宿主完成最少原生配置。不要
|
||||
手工复制其他项目的模块表。
|
||||
|
||||
常用参数:
|
||||
Metro 使用:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--platform` | 平台:`android` / `ios` |
|
||||
| `--module` | 可重复传入,只构建或发布指定模块 |
|
||||
| `--note` | 本次发布说明 |
|
||||
| `--publish` | 上传后立即发布 |
|
||||
| `--bugcollect` | `platform` 上传 Source Map;`disabled` 仅保留本地产物 |
|
||||
```js
|
||||
const { getDefaultConfig } = require('@react-native/metro-config')
|
||||
const { withXuqmModuleConfig } = require('@xuqm/rn-update/metro')
|
||||
|
||||
在 `package.json` 中添加快捷命令:
|
||||
module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname))
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"xuqm:release": "node scripts/xuqm_release.mjs --platform android --publish"
|
||||
}
|
||||
Android 宿主应用一次构建脚本:
|
||||
|
||||
```groovy
|
||||
apply from: file("../../node_modules/@xuqm/rn-update/android/xuqm-bundles.gradle")
|
||||
```
|
||||
|
||||
### 注册宿主运行时
|
||||
|
||||
```ts
|
||||
import { XuqmRuntime } from '@xuqm/rn-update'
|
||||
|
||||
XuqmRuntime.configure({
|
||||
plugins: pluginRegistrations,
|
||||
context: {
|
||||
navigate(route, params) {
|
||||
navigationRef.navigate(route, params)
|
||||
},
|
||||
emit(event, payload) {
|
||||
hostEvents.emit(event, payload)
|
||||
},
|
||||
getHostState() {
|
||||
return appStore.getState()
|
||||
},
|
||||
},
|
||||
async loadBundle(_moduleId, bundlePath) {
|
||||
await loadNativeBundle(bundlePath)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
模块入口注册:
|
||||
|
||||
```ts
|
||||
import { definePlugin } from '@xuqm/rn-update'
|
||||
|
||||
definePlugin({
|
||||
moduleId: 'orders',
|
||||
activate(context) {
|
||||
context.navigate('OrdersHome')
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 检查与安装插件
|
||||
|
||||
自动完成检查和安装:
|
||||
|
||||
```ts
|
||||
const plan = await UpdateSDK.checkAndInstallPlugin('orders', {
|
||||
onProgress(moduleId, progress) {
|
||||
console.log(moduleId, progress.percent)
|
||||
},
|
||||
})
|
||||
|
||||
await XuqmRuntime.activate('orders', {
|
||||
reloadBundle: plan !== null,
|
||||
})
|
||||
```
|
||||
|
||||
宿主需要先展示确认弹窗时:
|
||||
|
||||
```ts
|
||||
const plan = await UpdateSDK.checkPluginRelease('orders')
|
||||
|
||||
if (plan && (await showPluginUpdateDialog(plan))) {
|
||||
await UpdateSDK.installPluginRelease(plan)
|
||||
}
|
||||
```
|
||||
|
||||
只检查不会下载或修改本地状态。插件检查失败时,宿主应继续打开已有的本地健康版本。
|
||||
|
||||
## 启动策略
|
||||
|
||||
- 启动时先检查整包更新。
|
||||
- 不需要整包更新时,再检查 App 插件。
|
||||
- 业务插件在进入前分别检查。
|
||||
- Debug 默认不自动检查;开发页可以显式使用 `UpdateSDK.developer` 下的测试入口。
|
||||
- 更新 SDK 不绘制业务弹窗,所有提示和确认交互由宿主实现。
|
||||
|
||||
## 打包与发布
|
||||
|
||||
初始化生成的脚本支持 Debug、Release、插件构建和上传。发布令牌只通过 CI 环境变量
|
||||
`XUQM_API_TOKEN` 注入,不写入项目配置、源码或产物。
|
||||
|
||||
SDK 自身制品发布与应用/插件发布是不同操作。应用开发者不需要发布 SDK,也不得把 SDK
|
||||
发布凭据加入宿主工程。
|
||||
|
||||
249
docs-site/docs/rn/xwebview.md
普通文件
249
docs-site/docs/rn/xwebview.md
普通文件
@ -0,0 +1,249 @@
|
||||
# XWebView
|
||||
|
||||
`@xuqm/rn-xwebview` 提供可直接打开的独立 WebView 页面,也支持在业务页面中嵌入浏览器
|
||||
内核。SDK 只提供通用容器和 Bridge 接入入口,不内置任何宿主业务协议。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pnpm add @xuqm/rn-common @xuqm/rn-xwebview \
|
||||
react-native-webview react-native-svg \
|
||||
@react-native-clipboard/clipboard react-native-safe-area-context
|
||||
```
|
||||
|
||||
这些原生依赖需要由宿主直接声明,以便 React Native 自动链接。
|
||||
|
||||
## 根部挂载 Host
|
||||
|
||||
独立页面模式需要在应用根部挂载一次:
|
||||
|
||||
```tsx
|
||||
import { XWebViewHost } from '@xuqm/rn-xwebview'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<>
|
||||
<RootNavigation />
|
||||
<XWebViewHost />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
全局只能存在一个 `XWebViewHost`。
|
||||
|
||||
## 打开独立页面
|
||||
|
||||
```ts
|
||||
import { openWebView } from '@xuqm/rn-xwebview'
|
||||
|
||||
const page = openWebView({
|
||||
source: {
|
||||
uri: 'https://example.com',
|
||||
headers: {
|
||||
'X-From-App': 'true',
|
||||
},
|
||||
},
|
||||
navigationBar: {
|
||||
title: {
|
||||
text: '服务页面',
|
||||
mode: 'web',
|
||||
},
|
||||
},
|
||||
statusBar: {
|
||||
translucent: true,
|
||||
contentStyle: 'dark-content',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await page.closed
|
||||
console.log(result.reason, result.data)
|
||||
```
|
||||
|
||||
返回的页面 Handle 可以:
|
||||
|
||||
```ts
|
||||
page.reload()
|
||||
page.goBack()
|
||||
page.goForward()
|
||||
page.postMessage(JSON.stringify({ type: 'refresh' }))
|
||||
page.evaluateJavaScript('window.refreshPage?.()')
|
||||
page.suspend()
|
||||
page.resume()
|
||||
page.close('api', { completed: true })
|
||||
```
|
||||
|
||||
`suspend()` 用于临时展示宿主原生页面,页面实例和网页历史会保留;完成后调用
|
||||
`resume()`。
|
||||
|
||||
## 默认导航和返回规则
|
||||
|
||||
- 左上角返回按钮只在当前网页存在历史页面时显示,只负责网页后退。
|
||||
- 右上角关闭按钮始终显示,点击立即关闭当前 WebView 页面。
|
||||
- Android 物理返回键优先返回网页历史。
|
||||
- 没有网页历史时,物理返回键需要在一秒内再次按下才关闭页面。
|
||||
- 默认菜单包含刷新、复制链接和在系统浏览器打开,不包含关闭页面。
|
||||
|
||||
## 导航栏与状态栏
|
||||
|
||||
```ts
|
||||
openWebView({
|
||||
source: { uri: 'https://example.com' },
|
||||
navigationBar: {
|
||||
background: {
|
||||
gradient: ['#1769E0', '#55A8FF'],
|
||||
gradientAngle: 90,
|
||||
// 也可以使用 color 或 image
|
||||
},
|
||||
title: {
|
||||
text: '渐变导航栏',
|
||||
color: '#FFFFFF',
|
||||
mode: 'fixed',
|
||||
},
|
||||
backButton: { color: '#FFFFFF' },
|
||||
closeButton: { color: '#FFFFFF' },
|
||||
},
|
||||
statusBar: {
|
||||
backgroundColor: '#1769E0',
|
||||
contentStyle: 'light-content',
|
||||
translucent: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
网络背景图片异步加载,不会阻塞 `openWebView` 返回。
|
||||
|
||||
完整替换导航栏时使用 `navigationBar.render(context)`。自定义返回按钮应调用
|
||||
`context.onBackPress()`,以保留统一的网页后退规则。
|
||||
|
||||
## 自定义菜单
|
||||
|
||||
```tsx
|
||||
openWebView({
|
||||
source: { uri: 'https://example.com' },
|
||||
navigationBar: {
|
||||
menu: {
|
||||
visible: true,
|
||||
items: [
|
||||
{
|
||||
id: 'feedback',
|
||||
label: '问题反馈',
|
||||
onPress({ navigationState }) {
|
||||
openFeedback({ url: navigationState.url })
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
不需要菜单时设置 `menu.visible: false`。
|
||||
|
||||
## 摄像头和麦克风
|
||||
|
||||
宿主先声明系统权限,然后为具体网页配置精确 Origin:
|
||||
|
||||
```ts
|
||||
openWebView({
|
||||
source: { uri: 'https://verify.example.com/start' },
|
||||
permissions: {
|
||||
allowedOrigins: ['https://verify.example.com'],
|
||||
camera: true,
|
||||
microphone: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Android Manifest 示例:
|
||||
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
```
|
||||
|
||||
Origin 必须包含协议和主机,跨域后的页面不会继承原页面授权。宿主需要自定义决策时:
|
||||
|
||||
```ts
|
||||
permissions: {
|
||||
allowedOrigins: ['https://verify.example.com'],
|
||||
camera: true,
|
||||
microphone: true,
|
||||
onRequest(request) {
|
||||
if (canUseMedia(request.origin)) request.grant()
|
||||
else request.deny()
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 文件下载
|
||||
|
||||
```ts
|
||||
openWebView({
|
||||
source: { uri: 'https://example.com/files' },
|
||||
downloads: {
|
||||
auto: true,
|
||||
destination: 'publicDownloads',
|
||||
conflict: 'rename',
|
||||
onProgress(progress) {
|
||||
console.log(progress.percentage)
|
||||
},
|
||||
onComplete(result) {
|
||||
console.log(result.filePath)
|
||||
},
|
||||
onError(url, error) {
|
||||
console.warn(url, error)
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
默认保存到应用沙盒。保存到系统 Downloads 只在目标平台支持时生效。
|
||||
|
||||
## 宿主 Bridge
|
||||
|
||||
宿主与 H5 自行约定消息协议,通过 `bridge` 注入:
|
||||
|
||||
```ts
|
||||
openWebView({
|
||||
source: { uri: 'https://example.com' },
|
||||
bridge: {
|
||||
async onMessage(raw, context) {
|
||||
const message = JSON.parse(raw)
|
||||
|
||||
if (message.method === 'close') {
|
||||
context.close('h5_close', message.data)
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.method === 'setTitle') {
|
||||
context.updateNavigation({
|
||||
title: { text: String(message.title ?? '') },
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
XWebView 不规定方法名、App ID、厂商、用户或业务参数。协议校验、权限和业务结果由宿主
|
||||
自己的 Bridge 负责。
|
||||
|
||||
## 内嵌 WebView
|
||||
|
||||
不需要独立页面和内置导航栏时:
|
||||
|
||||
```tsx
|
||||
import { XWebViewView } from '@xuqm/rn-xwebview'
|
||||
|
||||
<XWebViewView
|
||||
config={{
|
||||
source: { uri: 'https://example.com' },
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
内嵌模式由所在页面负责尺寸、导航和关闭交互。
|
||||
@ -4,6 +4,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check:rn-contract": "node scripts/check-rn-docs.mjs",
|
||||
"prebuild": "node scripts/check-rn-docs.mjs",
|
||||
"dev": "vitepress dev docs",
|
||||
"build": "vitepress build docs",
|
||||
"preview": "vitepress preview docs"
|
||||
|
||||
171
docs-site/scripts/check-rn-docs.mjs
普通文件
171
docs-site/scripts/check-rn-docs.mjs
普通文件
@ -0,0 +1,171 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const docsSiteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const rnDocsRoot = path.join(docsSiteRoot, 'docs', 'rn')
|
||||
const vitepressConfig = path.join(docsSiteRoot, 'docs', '.vitepress', 'config.ts')
|
||||
|
||||
const requiredPages = [
|
||||
'index.md',
|
||||
'setup.md',
|
||||
'common.md',
|
||||
'session.md',
|
||||
'update.md',
|
||||
'bugcollect.md',
|
||||
'xwebview.md',
|
||||
'im.md',
|
||||
'push.md',
|
||||
'troubleshooting.md',
|
||||
]
|
||||
|
||||
const forbiddenPatterns = [
|
||||
[/@xuqm\/rn-sdk\b/g, '内部聚合包不应进入对外文档'],
|
||||
[/\bXuqmSDK\.initialize\s*\(/g, '已删除的手动初始化 API'],
|
||||
[/\bcheckRnUpdate\s*\(/g, '已删除的旧 Update API'],
|
||||
[/\bdownloadAndApplyBundle\s*\(/g, '已删除的旧 Bundle API'],
|
||||
[/\brequestNativeRegistration\s*\(/g, '已删除的 RN Push Token API'],
|
||||
[/\bregisterToken\s*\(/g, '已删除的 RN Push Token API'],
|
||||
[/\bsetDeviceToken\s*\(/g, '已删除的 RN Push Token API'],
|
||||
[/@xuqm\/rn-common\/internal(?:-security)?\b/g, '官方扩展专用内部入口'],
|
||||
[/\b(?:release[- ]set|Ed25519|NativeBundle|planReleaseSet)\b/gi, '内部更新实现细节'],
|
||||
[/(?:原生基线|签名算法|数据库隔离规则|服务拓扑)/g, '内部架构细节'],
|
||||
]
|
||||
|
||||
const requiredPackagePages = new Map([
|
||||
['@xuqm/rn-common', ['index.md', 'setup.md', 'common.md', 'session.md']],
|
||||
['@xuqm/rn-update', ['update.md']],
|
||||
['@xuqm/rn-bugcollect', ['bugcollect.md']],
|
||||
['@xuqm/rn-xwebview', ['xwebview.md']],
|
||||
['@xuqm/rn-im', ['im.md']],
|
||||
['@xuqm/rn-push', ['push.md']],
|
||||
])
|
||||
|
||||
const failures = []
|
||||
const sourceByPage = new Map()
|
||||
|
||||
for (const page of requiredPages) {
|
||||
const file = path.join(rnDocsRoot, page)
|
||||
if (!fs.existsSync(file)) {
|
||||
failures.push(`缺少 RN 对外文档页面:${page}`)
|
||||
continue
|
||||
}
|
||||
sourceByPage.set(page, fs.readFileSync(file, 'utf8'))
|
||||
}
|
||||
|
||||
const actualMarkdownPages = fs
|
||||
.readdirSync(rnDocsRoot, { withFileTypes: true })
|
||||
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.map(entry => entry.name)
|
||||
.sort()
|
||||
|
||||
for (const page of actualMarkdownPages) {
|
||||
if (!requiredPages.includes(page)) {
|
||||
failures.push(`存在未纳入公开信息架构的 RN 页面:${page}`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [page, source] of sourceByPage) {
|
||||
for (const [pattern, reason] of forbiddenPatterns) {
|
||||
pattern.lastIndex = 0
|
||||
if (pattern.test(source)) {
|
||||
failures.push(`${page} 包含禁止内容(${reason}):${pattern}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [packageName, pages] of requiredPackagePages) {
|
||||
for (const page of pages) {
|
||||
if (!sourceByPage.get(page)?.includes(packageName)) {
|
||||
failures.push(`${page} 未明确记录公开包 ${packageName}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const navigation = fs.readFileSync(vitepressConfig, 'utf8')
|
||||
for (const page of requiredPages) {
|
||||
const route = page === 'index.md' ? "/rn/'" : `/rn/${page.replace(/\.md$/, '')}'`
|
||||
if (!navigation.includes(route)) {
|
||||
failures.push(`VitePress RN 导航缺少 ${page}`)
|
||||
}
|
||||
}
|
||||
if (navigation.includes('/rn/group')) {
|
||||
failures.push('VitePress RN 导航仍引用已经合并的重复群聊页面')
|
||||
}
|
||||
|
||||
// 本地多仓工作区存在 RN SDK 源码时,继续核对文档中的命名导入确实由 package index 导出。
|
||||
// Jenkins 单仓构建没有兄弟仓库时跳过此增强检查,但上面的静态契约仍然生效。
|
||||
const sdkRoot =
|
||||
process.env.XUQM_RN_SDK_DIR ??
|
||||
path.resolve(docsSiteRoot, '..', '..', 'XuqmGroup-RNSDK')
|
||||
|
||||
if (fs.existsSync(path.join(sdkRoot, 'packages'))) {
|
||||
const indexByPackage = new Map()
|
||||
for (const packageDir of ['common', 'update', 'bugcollect', 'xwebview', 'im', 'push']) {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(sdkRoot, 'packages', packageDir, 'package.json'), 'utf8'),
|
||||
)
|
||||
indexByPackage.set(
|
||||
packageJson.name,
|
||||
fs.readFileSync(path.join(sdkRoot, 'packages', packageDir, 'src', 'index.ts'), 'utf8'),
|
||||
)
|
||||
}
|
||||
|
||||
const namedImport =
|
||||
/import\s*\{([\s\S]*?)\}\s*from\s*['"](@xuqm\/rn-(?:common|update|bugcollect|xwebview|im|push))['"]/g
|
||||
for (const [page, source] of sourceByPage) {
|
||||
for (const match of source.matchAll(namedImport)) {
|
||||
const packageSource = indexByPackage.get(match[2])
|
||||
const symbols = match[1]
|
||||
.split(',')
|
||||
.map(value => value.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0])
|
||||
.filter(value => /^[A-Za-z_$][\w$]*$/.test(value))
|
||||
for (const symbol of symbols) {
|
||||
const exported = new RegExp(
|
||||
`(?:export\\s+(?:const|class|function|type|interface)\\s+${symbol}\\b|export\\s*\\{[\\s\\S]*?\\b${symbol}\\b[\\s\\S]*?\\})`,
|
||||
).test(packageSource)
|
||||
if (!exported) {
|
||||
failures.push(`${page} 从 ${match[2]} 导入未公开的符号:${symbol}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const objectSources = new Map([
|
||||
['XuqmSDK', path.join(sdkRoot, 'packages', 'common', 'src', 'sdk.ts')],
|
||||
['UpdateSDK', path.join(sdkRoot, 'packages', 'update', 'src', 'UpdateSDK.ts')],
|
||||
[
|
||||
'NativeAppUpdate',
|
||||
path.join(sdkRoot, 'packages', 'update', 'src', 'NativeAppUpdate.ts'),
|
||||
],
|
||||
['XuqmRuntime', path.join(sdkRoot, 'packages', 'update', 'src', 'PluginRuntime.ts')],
|
||||
['BugCollect', path.join(sdkRoot, 'packages', 'bugcollect', 'src', 'BugCollect.ts')],
|
||||
['ImSDK', path.join(sdkRoot, 'packages', 'im', 'src', 'ImSDK.ts')],
|
||||
['PushSDK', path.join(sdkRoot, 'packages', 'push', 'src', 'PushSDK.ts')],
|
||||
])
|
||||
|
||||
for (const [objectName, sourceFile] of objectSources) {
|
||||
const implementation = fs.readFileSync(sourceFile, 'utf8')
|
||||
const usage = new RegExp(`\\b${objectName}\\.([A-Za-z_$][\\w$]*)\\s*\\(`, 'g')
|
||||
for (const [page, source] of sourceByPage) {
|
||||
for (const match of source.matchAll(usage)) {
|
||||
const method = match[1]
|
||||
const declared = new RegExp(
|
||||
`(?:^|\\n)\\s*(?:async\\s+)?${method}\\s*(?:\\([^\\n]*|:)`,
|
||||
).test(implementation)
|
||||
if (!declared) {
|
||||
failures.push(`${page} 调用 ${objectName} 中不存在的公开方法:${method}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('RN 对外文档契约检查失败:')
|
||||
for (const failure of failures) console.error(`- ${failure}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`RN 对外文档契约检查通过:${requiredPages.length} 个页面,6 个公开模块`)
|
||||
@ -0,0 +1,62 @@
|
||||
# RN 对外文档边界
|
||||
|
||||
## 目的
|
||||
|
||||
`docs-site/docs/rn/` 是面向宿主集成方的公开文档,只描述安装、配置、公开 API、宿主交互、
|
||||
错误处理、平台差异和可执行的排障步骤。内部设计继续保留在对应 SDK 仓库,不进入公开
|
||||
站点导航、搜索索引或示例。
|
||||
|
||||
## 事实来源
|
||||
|
||||
文档修改按以下优先级核对:
|
||||
|
||||
1. `XuqmGroup-RNSDK/packages/*/src/index.ts` 的真实公开导出。
|
||||
2. 公开类型定义和公开对象方法。
|
||||
3. 当前测试、可运行示例和构建脚本。
|
||||
4. 租户平台当前实际提供的配置与功能。
|
||||
|
||||
包内 README、历史文档、注释和旧站点内容只能作为线索,不能直接复制到公开站点。
|
||||
|
||||
## 当前公开模块
|
||||
|
||||
- `@xuqm/rn-common`
|
||||
- `@xuqm/rn-update`
|
||||
- `@xuqm/rn-bugcollect`
|
||||
- `@xuqm/rn-xwebview`
|
||||
- `@xuqm/rn-im`
|
||||
- `@xuqm/rn-push`
|
||||
|
||||
未实现、未发布或没有稳定公开契约的模块和 API 不得预先出现在文档站。
|
||||
|
||||
## 可以公开
|
||||
|
||||
- 安装依赖和只读制品仓库。
|
||||
- 平台配置文件的下载位置与宿主放置方式。
|
||||
- 自动配置、公共登录和隐私授权的调用方式。
|
||||
- 宿主需要调用的公开 API、参数、返回状态和错误码。
|
||||
- Android/iOS 已验证范围及权限、Manifest、Info.plist 要求。
|
||||
- 插件化的创建、打包、上传和宿主调用方式。
|
||||
- 不涉及安全实现的默认行为、升级说明与排障清单。
|
||||
|
||||
## 禁止公开
|
||||
|
||||
- 官方扩展包专用的 internal 子路径和内部聚合包。
|
||||
- 配置解密、签名算法、可信密钥、不可变清单和校验实现。
|
||||
- 插件安装事务、内部状态表、回滚存储和版本规划算法。
|
||||
- 数据库选型、表结构、用户隔离键和内部缓存格式。
|
||||
- 服务拓扑、内部接口、内部令牌、私有化部署脚本和运维地址。
|
||||
- App4、医网信或其他单一宿主的页面、厂商、域名和业务协议。
|
||||
- 凭据、Token、配置文件正文、用户隐私数据和真实业务响应。
|
||||
- 没有源码依据的示例 API。
|
||||
|
||||
## 自动检查
|
||||
|
||||
`yarn workspace @xuqm/docs-site check:rn-contract` 检查:
|
||||
|
||||
- 十个 RN 公开页面和导航完整性。
|
||||
- 六个公开包是否均有入口。
|
||||
- 历史 API、internal 入口和内部实现词汇。
|
||||
- 在本地多仓工作区中,文档命名导入和对象方法是否确实由当前 RN SDK 源码公开。
|
||||
|
||||
文档站构建前自动执行同一检查。Jenkins 单仓构建无法读取兄弟 SDK 仓库时仍执行静态
|
||||
边界检查;合并前必须在完整本地工作区至少执行一次源码增强检查。
|
||||
@ -1,6 +1,8 @@
|
||||
# 租户平台 SDK V2 页面接手说明
|
||||
|
||||
SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOFF.md`。
|
||||
RN 对外文档的内容边界、事实来源和自动检查见
|
||||
[`RN_PUBLIC_DOCUMENTATION_POLICY.md`](./RN_PUBLIC_DOCUMENTATION_POLICY.md)。
|
||||
|
||||
当前 Web 变更:
|
||||
|
||||
@ -25,6 +27,7 @@ SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOF
|
||||
yarn install --frozen-lockfile
|
||||
yarn build:tenant
|
||||
yarn build:ops
|
||||
yarn workspace @xuqm/docs-site check:rn-contract
|
||||
yarn workspace @xuqm/docs-site build
|
||||
yarn test:web-cache
|
||||
nginx -t -p "$PWD/" -c scripts/nginx-test.conf
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户