docs(rn): rebuild public sdk documentation
这个提交包含在:
父节点
94e5ee0ec4
当前提交
eb2baae07e
@ -58,11 +58,15 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
'/rn/': [
|
'/rn/': [
|
||||||
{ text: '概览', link: '/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: 'IM 接入', link: '/rn/im' },
|
||||||
{ text: '群聊', link: '/rn/group' },
|
|
||||||
{ text: '推送接入', link: '/rn/push' },
|
{ text: '推送接入', link: '/rn/push' },
|
||||||
{ text: '版本管理', link: '/rn/update' },
|
{ text: '错误处理与排障', link: '/rn/troubleshooting' },
|
||||||
],
|
],
|
||||||
'/vue3/': [
|
'/vue3/': [
|
||||||
{ text: '概览', link: '/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 同步一次登录状态。
|
||||||
|
|
||||||
---
|
## 安装
|
||||||
|
|
||||||
## 初始化与登录
|
```bash
|
||||||
|
pnpm add @xuqm/rn-common @xuqm/rn-im @nozbe/watermelondb
|
||||||
### 初始化
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { XuqmSDK } from '@xuqm/rn-common'
|
|
||||||
|
|
||||||
await XuqmSDK.initialize({
|
|
||||||
appKey: 'your_app_key',
|
|
||||||
logLevel: __DEV__ ? 'debug' : 'warn',
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 登录
|
## 登录与连接
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { XuqmSDK } from '@xuqm/rn-common'
|
import { XuqmSDK } from '@xuqm/rn-common'
|
||||||
|
|
||||||
await XuqmSDK.login({
|
await XuqmSDK.login({
|
||||||
userId: 'user_001',
|
userId: 'user-001',
|
||||||
userSig: 'your_user_sig_jwt',
|
accessToken: 'host-access-token',
|
||||||
|
userSig: 'im-user-signature',
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
> 如果集成了 `rn-im`,`XuqmSDK.login` 会自动触发 `ImSDK` 连接,业务侧无需单独调用 `ImSDK.login`。
|
安装 IM 后,公共会话会自动建立连接。不要再调用第二套 IM 登录方法。
|
||||||
|
|
||||||
---
|
连接状态:
|
||||||
|
|
||||||
## 消息收发
|
|
||||||
|
|
||||||
### 监听实时消息
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { ImSDK } from '@xuqm/rn-im'
|
import { ImSDK } from '@xuqm/rn-im'
|
||||||
|
|
||||||
ImSDK.addEventListener('message', (msg) => {
|
if (!ImSDK.isConnected()) {
|
||||||
console.log('收到消息:', msg.msgType, msg.content)
|
await ImSDK.reconnect()
|
||||||
})
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
const results = await ImSDK.searchMessages(params)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## 事件监听
|
||||||
|
|
||||||
## 群聊
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// 创建群
|
const listener = {
|
||||||
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
|
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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// 群列表
|
ImSDK.addListener(listener)
|
||||||
const groups = await ImSDK.listGroups()
|
|
||||||
|
|
||||||
// 添加成员
|
// 页面或服务销毁时
|
||||||
await ImSDK.addGroupMember('group_xxx', 'user_004')
|
ImSDK.removeListener(listener)
|
||||||
|
|
||||||
// 移除成员
|
|
||||||
await ImSDK.removeGroupMember('group_xxx', 'user_004')
|
|
||||||
|
|
||||||
// 退出群聊
|
|
||||||
await ImSDK.leaveGroup('group_xxx')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
详见 [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
|
```ts
|
||||||
// 订阅会话变化
|
const conversations = await ImSDK.listConversations()
|
||||||
const unsub = ImSDK.subscribeConversations((conversations) => {
|
|
||||||
console.log(conversations)
|
const unsubscribe = ImSDK.subscribeConversations((next) => {
|
||||||
|
setConversations(next)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 置顶
|
await ImSDK.markRead('user-002', 'SINGLE')
|
||||||
await ImSDK.setConversationPinned('user_002', 'SINGLE', true)
|
await ImSDK.setConversationPinned('user-002', 'SINGLE', true)
|
||||||
|
await ImSDK.setConversationMuted('user-002', 'SINGLE', true)
|
||||||
|
await ImSDK.setDraft('user-002', 'SINGLE', '稍后继续')
|
||||||
|
|
||||||
// 免打扰
|
unsubscribe()
|
||||||
await ImSDK.setConversationMuted('group_xxx', 'GROUP', true)
|
|
||||||
|
|
||||||
// 标记已读
|
|
||||||
await ImSDK.markRead('user_002')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
还可以隐藏、删除会话或将会话加入自定义分组。
|
||||||
|
|
||||||
## 离线消息同步
|
## 群聊
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// 查询离线消息数量(示例)
|
const group = await ImSDK.createGroup(
|
||||||
const count = await ImSDK.offlineMessageCount()
|
'项目讨论',
|
||||||
|
['user-002', 'user-003'],
|
||||||
|
'WORK',
|
||||||
|
)
|
||||||
|
|
||||||
// 同步离线消息(示例)
|
const groups = await ImSDK.listGroups()
|
||||||
const messages = await ImSDK.syncOfflineMessages()
|
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
|
```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
|
```ts
|
||||||
interface ImMessage {
|
const users = await ImSDK.searchUsers('张医生')
|
||||||
id: string
|
const groups = await ImSDK.searchGroups('项目')
|
||||||
fromId: string
|
const messages = await ImSDK.searchMessages({
|
||||||
toId: string
|
keyword: '会议',
|
||||||
chatType: 'SINGLE' | 'GROUP'
|
chatType: 'SINGLE',
|
||||||
msgType: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'FILE' |
|
toId: 'user-002',
|
||||||
'LOCATION' | 'NOTIFY' | 'CUSTOM' | 'RICH_TEXT' |
|
msgTypes: ['TEXT'],
|
||||||
'CALL_AUDIO' | 'CALL_VIDEO' | 'FORWARD' | 'REVOKED'
|
limit: 20,
|
||||||
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
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 登出
|
||||||
|
|
||||||
|
```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'
|
||||||
|
|
||||||
```
|
configureHttp({ baseUrl: 'https://api.example.com' })
|
||||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
|
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
|
```ts
|
||||||
import { XuqmSDK } from '@xuqm/rn-common'
|
import { XuqmSDK } from '@xuqm/rn-common'
|
||||||
|
|
||||||
// App 入口(如 App.tsx 的顶层)
|
await XuqmSDK.setPrivacyConsent(true)
|
||||||
await XuqmSDK.initialize({
|
|
||||||
appKey: 'your_app_key',
|
|
||||||
logLevel: __DEV__ ? 'debug' : 'warn',
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
> SDK 内部自动处理服务器地址、IM 实时连接、文件服务等配置,开发者无需关心。
|
|
||||||
|
|
||||||
### 2. 统一登录
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { XuqmSDK } from '@xuqm/rn-common'
|
|
||||||
import { ImSDK } from '@xuqm/rn-im'
|
|
||||||
|
|
||||||
// 登录只需要 userId + userSig
|
|
||||||
// nickname / avatar 不再通过登录接口传入
|
|
||||||
await XuqmSDK.login({
|
await XuqmSDK.login({
|
||||||
userId: 'user_001',
|
userId: 'user-001',
|
||||||
userSig: 'your_user_sig_jwt',
|
accessToken: 'host-access-token',
|
||||||
|
// 仅使用 IM 时传入:
|
||||||
|
userSig: 'im-user-signature',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果集成了 rn-im,XuqmSDK.login 会自动触发 ImSDK 连接
|
|
||||||
// 业务侧无需单独调用 ImSDK.login
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 消息收发
|
## 默认行为
|
||||||
|
|
||||||
```ts
|
- 配置由构建工具自动发现,运行时没有手动初始化 API。
|
||||||
// 监听实时消息
|
- 配置错误会阻断构建,避免生成安装后才无法初始化的包。
|
||||||
ImSDK.addEventListener('message', (msg) => {
|
- 扩展服务初始化失败不会阻断宿主应用自身启动;具体操作会返回结构化状态或错误。
|
||||||
console.log('收到消息:', msg.msgType, msg.content)
|
- Update、BugCollect 等服务是否启用由租户平台控制。
|
||||||
})
|
- Debug 不会自动检查更新,也不会自动上报 BugCollect。
|
||||||
|
- Common 独立模式不会访问 XuqmGroup 服务。
|
||||||
|
|
||||||
// 发送文本消息
|
## 下一步
|
||||||
const msg = await ImSDK.sendMessage(
|
|
||||||
'user_002', // toId
|
|
||||||
'SINGLE', // chatType: 'SINGLE' | 'GROUP'
|
|
||||||
'TEXT', // msgType
|
|
||||||
'Hello!' // content
|
|
||||||
)
|
|
||||||
|
|
||||||
// 发送图片(自动上传到 file-service)
|
- [安装与自动配置](./setup)
|
||||||
const msg = await ImSDK.sendImageMessage(
|
- [Common 基础能力](./common)
|
||||||
'user_002', 'SINGLE',
|
- [会话与隐私授权](./session)
|
||||||
'/path/to/image.jpg', // 本地 URI
|
- [版本与插件更新](./update)
|
||||||
800, 600 // 宽高(可选)
|
- [BugCollect](./bugcollect)
|
||||||
)
|
- [XWebView](./xwebview)
|
||||||
|
- [IM 接入](./im)
|
||||||
// 获取历史消息(单聊)
|
- [推送接入](./push)
|
||||||
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` 自动隔离,多账号切换安全。
|
|
||||||
|
|||||||
@ -1,138 +1,80 @@
|
|||||||
# React Native 推送接入指南
|
# 推送接入
|
||||||
|
|
||||||
**包名**:`@xuqm/rn-push` · **支持**:华为、小米、OPPO、vivo、荣耀、APNs(iOS)
|
`@xuqm/rn-push` 负责把宿主登录用户同步给原生推送模块,并提供离线推送开关与免打扰
|
||||||
|
设置。厂商识别、Token 获取和绑定由原生实现完成,RN 页面不接收或上传厂商 Token。
|
||||||
|
|
||||||
---
|
## 安装
|
||||||
|
|
||||||
## 1. 安装
|
|
||||||
|
|
||||||
```bash
|
```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
|
```ts
|
||||||
import { XuqmSDK } from '@xuqm/rn-common'
|
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'
|
import { PushSDK } from '@xuqm/rn-push'
|
||||||
|
|
||||||
await XuqmSDK.initialize({ appKey: 'your_app_key' })
|
await PushSDK.setOfflinePushEnabled(false)
|
||||||
await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig' })
|
await PushSDK.setOfflinePushEnabled(true)
|
||||||
// ↓ 登录后调用 PushSDK.initialize() 完成 token 注册
|
|
||||||
await PushSDK.initialize('user_001')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
该开关同步到当前登录用户。未登录时不要在业务页面展示为已经保存成功。
|
||||||
|
|
||||||
## 8. iOS APNs 配置
|
## 免打扰
|
||||||
|
|
||||||
在 `AppDelegate.m` 或 `AppDelegate.swift` 中:
|
```ts
|
||||||
|
await PushSDK.setQuietHours('22:00', '08:00')
|
||||||
```objc
|
|
||||||
// AppDelegate.m
|
|
||||||
- (void)application:(UIApplication *)application
|
|
||||||
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
|
||||||
// 转发 token,由 rn-push 原生模块处理
|
|
||||||
[RCTEventEmitter ...] // 通过 Bridge 传至 JS
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> 使用 `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 |
|
1. 系统通知权限是否开启。
|
||||||
| iOS | APNS |
|
2. 当前构建是否包含目标设备厂商通道。
|
||||||
| 其他 | FCM |
|
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`:
|
宿主从只读聚合仓库安装依赖:
|
||||||
|
|
||||||
```
|
```ini
|
||||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
|
# .npmrc
|
||||||
|
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm/
|
||||||
```
|
```
|
||||||
|
|
||||||
只使用基础能力时,直接安装 `rn-common`:
|
请使用租户平台为当前应用推荐的精确版本。开发和测试阶段可能使用带 `alpha`、`beta`、
|
||||||
|
`rc` 或 `SNAPSHOT` 标识的预发布版本,不要自行改成无预发布标识的版本。
|
||||||
|
|
||||||
|
## 按需安装
|
||||||
|
|
||||||
|
只使用通用基础能力:
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
yarn add @xuqm/rn-common @xuqm/rn-im
|
pnpm add @xuqm/rn-common @xuqm/rn-update \
|
||||||
# 或全量安装
|
@react-native-async-storage/async-storage
|
||||||
yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update @xuqm/rn-bugcollect
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
使用 WebView:
|
||||||
|
|
||||||
## 自动 / 手动链接
|
|
||||||
|
|
||||||
React Native 0.60+ 支持**自动链接**,安装后执行:
|
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
cd ios
|
pnpm add @xuqm/rn-bugcollect
|
||||||
pod install
|
pnpm add @xuqm/rn-im @nozbe/watermelondb
|
||||||
|
pnpm add @xuqm/rn-push
|
||||||
```
|
```
|
||||||
|
|
||||||
### 权限声明
|
项目使用 npm 或 Yarn 时,可以使用等价安装命令;不要同时维护多份锁文件。
|
||||||
|
|
||||||
在 `Info.plist` 中添加:
|
## 获取配置文件
|
||||||
|
|
||||||
```xml
|
使用扩展包时,在租户平台进入目标应用并下载配置文件。将下载文件原名放到:
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>需要访问相机</string>
|
```text
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
src/assets/config/<平台生成的文件名>.xuqmconfig
|
||||||
<string>需要访问相册</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
|
||||||
<string>需要访问麦克风</string>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
约束:
|
||||||
|
|
||||||
## Android 配置
|
- 目录中只能存在一个 `.xuqmconfig`。
|
||||||
|
- 不要重命名内容、手工编辑或提交到公共仓库。
|
||||||
|
- 公有化和私有化平台生成的配置不能混用。
|
||||||
|
- 重新生成配置后,下一次构建必须使用新文件。
|
||||||
|
- Common 独立模式不要放置配置文件。
|
||||||
|
|
||||||
### Gradle 仓库
|
建议加入忽略规则:
|
||||||
|
|
||||||
在 `android/build.gradle` 中确保包含:
|
```gitignore
|
||||||
|
src/assets/config/*.xuqmconfig
|
||||||
```gradle
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
maven { url "https://nexus.xuqinmin.com/repository/android/" }
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 最低版本
|
## 配置 Metro
|
||||||
|
|
||||||
- `minSdkVersion = 24`
|
没有启用插件化时,只包装一次 Common:
|
||||||
- `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` 配置文件:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// metro.config.js
|
// metro.config.js
|
||||||
|
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
|
||||||
const { withXuqmConfig } = require('@xuqm/rn-common/metro')
|
const { withXuqmConfig } = require('@xuqm/rn-common/metro')
|
||||||
|
|
||||||
module.exports = withXuqmConfig({
|
const config = mergeConfig(getDefaultConfig(__dirname), {
|
||||||
// 你的原始 Metro 配置
|
// 宿主自己的 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')
|
||||||
|
|
||||||
## 配置文件
|
module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname))
|
||||||
|
|
||||||
从平台控制台下载应用的 `.xuqmconfig` 配置文件(例如 `识校宝.xuqmconfig`),放置到 `src/assets/config/` 目录。`withXuqmConfig` 插件会自动发现并注入该配置,SDK 运行时自动读取,无需手动指定路径。
|
|
||||||
|
|
||||||
文件名不限,只要扩展名为 `.xuqmconfig` 即可。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BugCollect
|
|
||||||
|
|
||||||
异常采集模块可自动收集 JS 异常、原生崩溃日志并上报到服务端。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn add @xuqm/rn-bugcollect
|
|
||||||
```
|
```
|
||||||
|
|
||||||
集成了 `withXuqmConfig` 插件后,BugCollect 会随 SDK 自动初始化,无需额外代码。采集服务地址从 `.xuqmconfig` 配置中自动读取。
|
两种配置二选一。项目只能存在一个 Metro 配置出口。
|
||||||
|
|
||||||
---
|
## Android 仓库
|
||||||
|
|
||||||
## 下一步
|
原生依赖从 XuqmGroup Android 聚合仓库读取:
|
||||||
|
|
||||||
- [RN IM 接入 →](./im)
|
```kotlin
|
||||||
- [RN 群聊 →](./group)
|
// settings.gradle.kts
|
||||||
- [RN 推送接入 →](./push)
|
dependencyResolutionManagement {
|
||||||
- [RN 版本更新 →](./update)
|
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
|
```ts
|
||||||
import { UpdateSDK } from '@xuqm/rn-update'
|
import { UpdateSDK } from '@xuqm/rn-update'
|
||||||
|
|
||||||
const appUpdate = await UpdateSDK.checkAppUpdate()
|
const result = await UpdateSDK.checkAppUpdate()
|
||||||
if (appUpdate.needsUpdate) {
|
|
||||||
console.log('新版本:', appUpdate.versionName)
|
|
||||||
console.log('更新日志:', appUpdate.changeLog)
|
|
||||||
console.log('下载地址:', appUpdate.downloadUrl)
|
|
||||||
|
|
||||||
if (appUpdate.forceUpdate) {
|
switch (result.status) {
|
||||||
// 强制更新
|
case 'UPDATE_AVAILABLE':
|
||||||
showForceUpdateModal(appUpdate)
|
showUpdateDialog(result.update, result.network)
|
||||||
} else {
|
break
|
||||||
// 可选更新
|
case 'LOGIN_REQUIRED':
|
||||||
showOptionalUpdateModal(appUpdate)
|
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
|
```ts
|
||||||
// 仅限开发环境使用
|
await UpdateSDK.downloadAndInstallApp(result.update!, {
|
||||||
UpdateSDK._devSetAppVersion(100, '1.0.0')
|
destination: 'SDK_PRIVATE',
|
||||||
```
|
onProgress(progress) {
|
||||||
|
setProgress(progress.percent)
|
||||||
---
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
需要由宿主展示确认弹窗时,检查与安装分开调用:
|
需要把安装包保留在系统 Downloads:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const plan = await UpdateSDK.check('home')
|
await UpdateSDK.downloadAndInstallApp(result.update!, {
|
||||||
if (plan && (await showPluginUpdateDialog(plan))) {
|
destination: 'PUBLIC_DOWNLOADS',
|
||||||
await UpdateSDK.install(plan)
|
})
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`check` 只检查,不下载、不修改本地状态。安装阶段会再次验证签名和检查时
|
下载失败或应用退出后不会在下次启动时自动继续。
|
||||||
记录的本地基线;计划已过期时必须重新检查。插件版本、路径和事务状态由原生层统一
|
|
||||||
管理,宿主不得另建一套缓存版本表。
|
|
||||||
|
|
||||||
---
|
### 只下载、稍后安装
|
||||||
|
|
||||||
## 5. 强制更新处理
|
|
||||||
|
|
||||||
当 `appUpdate.forceUpdate` 为 `true` 时,建议业务层:
|
|
||||||
|
|
||||||
1. 弹出不可关闭的 Modal
|
|
||||||
2. 只允许用户点击「立即更新」
|
|
||||||
3. 调用 `UpdateSDK.openStore()` 跳转商店
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function showForceUpdateModal(update: AppUpdateInfo) {
|
import { NativeAppUpdate, UpdateSDK } from '@xuqm/rn-update'
|
||||||
// 使用 RN Modal 组件,设置不可取消
|
|
||||||
Alert.alert(
|
const downloaded = await UpdateSDK.downloadApk(result.update!, {
|
||||||
'发现重要更新',
|
destination: 'SDK_PRIVATE',
|
||||||
`当前版本已不可用,请升级至 ${update.versionName}`,
|
})
|
||||||
[
|
|
||||||
{
|
console.log(downloaded.localPath)
|
||||||
text: '立即更新',
|
|
||||||
onPress: () => UpdateSDK.openStore(update.appStoreUrl, update.marketUrl),
|
await NativeAppUpdate.installDownloaded({
|
||||||
},
|
versionCode: result.update!.versionCode!,
|
||||||
],
|
sha256: result.update!.sha256,
|
||||||
{ cancelable: false }
|
})
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
取消当前下载:
|
||||||
|
|
||||||
## 6. 多模块统一登录
|
|
||||||
|
|
||||||
Update 模块与 IM、Push 模块共享同一套登录态:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
await XuqmSDK.initialize({ appKey: 'your_app_key' })
|
await NativeAppUpdate.cancel()
|
||||||
await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
|
|
||||||
// UpdateSDK 在 checkAppUpdate 时自动携带 appKey,无需额外登录操作
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
如果系统禁止安装未知来源应用:
|
||||||
|
|
||||||
## 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
|
```bash
|
||||||
XUQM_API_TOKEN=*** node scripts/xuqm_release.mjs --platform android --publish
|
pnpm exec xuqm-rn init
|
||||||
|
pnpm run xuqm:doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本执行流程:
|
初始化工具会创建 `xuqm.modules.json`、补充必要脚本并提示宿主完成最少原生配置。不要
|
||||||
1. 按模块构建 JS/Hermes Bundle 和 Source Map
|
手工复制其他项目的模块表。
|
||||||
2. 生成根目录唯一的 `rn-manifest.json`
|
|
||||||
3. 生成 `<moduleId>.<platform>.xuqm.zip`
|
|
||||||
4. 上传归档并由服务端校验、签名
|
|
||||||
5. 按用户选择发布,返回不可变发布记录
|
|
||||||
|
|
||||||
常用参数:
|
Metro 使用:
|
||||||
|
|
||||||
| 参数 | 说明 |
|
```js
|
||||||
|------|------|
|
const { getDefaultConfig } = require('@react-native/metro-config')
|
||||||
| `--platform` | 平台:`android` / `ios` |
|
const { withXuqmModuleConfig } = require('@xuqm/rn-update/metro')
|
||||||
| `--module` | 可重复传入,只构建或发布指定模块 |
|
|
||||||
| `--note` | 本次发布说明 |
|
|
||||||
| `--publish` | 上传后立即发布 |
|
|
||||||
| `--bugcollect` | `platform` 上传 Source Map;`disabled` 仅保留本地产物 |
|
|
||||||
|
|
||||||
在 `package.json` 中添加快捷命令:
|
module.exports = withXuqmModuleConfig(getDefaultConfig(__dirname))
|
||||||
|
```
|
||||||
|
|
||||||
```json
|
Android 宿主应用一次构建脚本:
|
||||||
{
|
|
||||||
"scripts": {
|
```groovy
|
||||||
"xuqm:release": "node scripts/xuqm_release.mjs --platform android --publish"
|
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,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"check:rn-contract": "node scripts/check-rn-docs.mjs",
|
||||||
|
"prebuild": "node scripts/check-rn-docs.mjs",
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
"build": "vitepress build docs",
|
"build": "vitepress build docs",
|
||||||
"preview": "vitepress preview 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 V2 页面接手说明
|
||||||
|
|
||||||
SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOFF.md`。
|
SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOFF.md`。
|
||||||
|
RN 对外文档的内容边界、事实来源和自动检查见
|
||||||
|
[`RN_PUBLIC_DOCUMENTATION_POLICY.md`](./RN_PUBLIC_DOCUMENTATION_POLICY.md)。
|
||||||
|
|
||||||
当前 Web 变更:
|
当前 Web 变更:
|
||||||
|
|
||||||
@ -25,6 +27,7 @@ SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOF
|
|||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
yarn build:tenant
|
yarn build:tenant
|
||||||
yarn build:ops
|
yarn build:ops
|
||||||
|
yarn workspace @xuqm/docs-site check:rn-contract
|
||||||
yarn workspace @xuqm/docs-site build
|
yarn workspace @xuqm/docs-site build
|
||||||
yarn test:web-cache
|
yarn test:web-cache
|
||||||
nginx -t -p "$PWD/" -c scripts/nginx-test.conf
|
nginx -t -p "$PWD/" -c scripts/nginx-test.conf
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户