如何使用 react 和 react-native 设计跨平台插件化软件?
可以实现。面向第三方开发者的插件市场,建议由宿主内置跨平台组件库,插件通过接口返回 UI 描述和数据。插件便可以在宿主已有能力范围内更新页面,无需重新发布 Android、iOS 或 Web 应用。
接口也可以返回 JavaScript 模块,但宿主需要额外处理代码加载、依赖兼容、权限隔离,并满足应用商店规则,不能按普通的 React 组件请求来处理。
跨平台 UI-kit 有哪些选择?
支持 Android、iOS 和 Web 的组件库可以考虑以下几种:
| 组件库 | 跨平台方式 | 适合的需求 |
|---|---|---|
| React Native Elements | 官方明确支持 Android、iOS 和 Web | 使用现成的通用组件构建界面 |
| Tamagui | 为 React Native 和 Web 提供组件、主题及样式系统 | 需要统一设计规范,并适配不同屏幕 |
| React Native Paper | 提供通过 React Native for Web 使用的文档 | 希望采用 Material Design 风格 |
支持范围可查阅 React Native Elements 官网、Tamagui 文档和 React Native Paper 的 Web 指南。
“Elements 只支持 Android 和 iOS”这一判断需要修正。选型时,仍应核对所选版本与 React、React Native、Expo 的兼容要求,并验证实际会用到的组件。
跨平台支持还需要区分两种情况:
- React Native for Web 将兼容的 React Native 组件和 API 映射到浏览器。
- 基于 DOM 的 React 组件库不会因此自动变成原生组件库。
React Native for Web 使用 React DOM 实现浏览器渲染;React DOM API 本身不适用于 React Native。Polaris 一类组件库的设计规范可以作为参考,但依赖 DOM 的具体组件需要通过 Web 页面承载,或者另外实现原生版本。
建议在宿主中封装自己的组件接口,例如 PluginButton、PluginForm、PluginCard。插件依赖这套稳定接口,宿主负责维护底层组件库。
插件接口能直接返回 React 组件吗?
普通 JSON 接口不能直接传输可执行的组件函数。例如,下面的组件包含 JavaScript 逻辑,无法通过 JSON.stringify() 完整传给另一端:
function PluginPanel() {
return <Button title="刷新" onPress={() => refreshData()} />;
}
接口可以返回源代码或编译后的模块,宿主加载并执行代码后才能得到组件。这需要处理:
- JSX、模块格式和平台差异。
- React、UI-kit 及其他依赖的版本兼容。
- 插件能够访问哪些宿主数据和设备能力。
- 加载失败、运行异常和版本回滚。
React.lazy 可以在模块加载后取得组件,但不提供插件下载协议或安全沙箱。普通 React Native 应用也不能直接照搬浏览器加载远程脚本的方式。
可选方案包括:
| 方案 | 插件提供的内容 | 无需更新宿主的边界 |
|---|---|---|
| 声明式 UI | JSON 组件树、数据、动作描述 | 可以组合宿主已经支持的组件和动作 |
| Web 插件 | 独立 Web 页面 | Web 使用 iframe,移动端使用 WebView,界面在网页环境中渲染 |
| 远程 JavaScript | 编译后的插件代码 | 需要专门的加载机制,且受运行时和分发规则约束 |
| 内置插件 | 随宿主构建的组件模块 | 可以动态启用已有插件,新增代码通常需要更新 |
表单、设置页、列表和业务卡片通常可以用声明式 UI 实现。需要复杂编辑器或高度自由的布局时,可以再提供 Web 插件入口。
推荐架构:插件描述界面,宿主负责渲染
1. 定义插件清单和扩展位置
先规定插件可以出现在哪里,例如详情页卡片、设置页面或工具栏操作,避免让插件任意修改宿主组件树。
插件清单至少应包含:
- 插件 ID 和版本。
- 支持的 UI 协议版本。
- 页面入口或 UI 描述接口。
- 申请的数据权限。
- 所需的宿主能力。
这些字段属于平台自行定义的协议,React 或 Expo 没有内置这套标准。宿主应在安装时检查兼容性,并保存用户或管理员授予的权限。
声明式插件的“安装”可以只包含建立安装记录、授权和注册入口,不必下载可执行代码。
2. 定义可序列化的 UI 协议
插件接口可以返回这样的 UI 描述:
{
"schemaVersion": 1,
"root": {
"id": "summary",
"type": "stack",
"children": [
{
"id": "message",
"type": "text",
"value": "当前有 3 个待处理任务"
},
{
"id": "refresh-button",
"type": "button",
"label": "刷新",
"action": "refresh"
}
]
}
}
schemaVersion: 1 是示例协议版本。宿主负责解释 stack、text 和 button;refresh 是动作标识,插件没有传入函数。
正式协议还可以增加表单字段、加载状态和主题 token。属性应有明确的边界,例如 tone: "danger",避免开放任意样式或表达式。
3. 在宿主实现跨平台渲染器
渲染器可以使用 React Native 基础组件,Web 端通过 React Native for Web 渲染。以下示例只包含渲染逻辑,实际项目中可以换成统一 UI-kit 的封装组件。
import type { ReactElement } from "react";
import { Button, Text, View } from "react-native";
type UiNode =
| {
id: string;
type: "text";
value: string;
}
| {
id: string;
type: "button";
label: string;
action: string;
}
| {
id: string;
type: "stack";
children: UiNode[];
};
type Props = {
node: UiNode;
onAction: (action: string) => void;
};
export function PluginUI({
node,
onAction,
}: Props): ReactElement {
switch (node.type) {
case "text":
return <Text>{node.value}</Text>;
case "button":
return (
<Button
title={node.label}
onPress={() => onAction(node.action)}
/>
);
case "stack":
return (
<View>
{node.children.map((child) => (
<PluginUI
key={child.id}
node={child}
onAction={onAction}
/>
))}
</View>
);
default:
return <Text>暂不支持此组件</Text>;
}
}
网络返回值必须先经过运行时校验,再交给这个组件。TypeScript 类型声明不会验证 JSON。可以使用 JSON Schema、Zod 等工具检查协议版本、节点类型和属性,同时限制树的深度、节点数量与字符串长度。
宿主提供 onAction,并将它绑定到当前安装实例。执行动作前,宿主先检查是否允许,再请求后端网关。插件返回的 JSON 不应自行决定使用哪个用户身份或访问哪个内部接口。
例如,刷新流程可以是:
用户点击按钮
→ 宿主提交 refresh 动作
→ 网关校验用户、安装实例和权限
→ 插件后端处理请求
→ 返回新的页面数据
→ 宿主更新 UI 描述并重新渲染
如果开发者需要用 React 编写插件界面,可以提供 JSX 风格的 SDK,由 SDK 的构建工具或专用运行时生成 UI 描述。这部分需要额外实现,任意 React 组件无法自动转换为这种协议。
后端如何共享数据?
建议通过受控 API 共享业务数据,由核心后端负责访问控制和数据校验。
请求关系可以设计为:
插件后端 → 核心 API → 核心业务数据
宿主前端 → 插件网关 → 插件后端
OAuth 可以用于委托授权,但仍需明确授权范围、租户归属、资源权限和撤销机制。例如,插件获准读取当前租户的任务,并不代表它可以修改任务或读取其他租户的数据。
插件自己的数据可以放在独立存储中,通过核心资源 ID 建立关联。需要同步时,通过 API 或经过鉴权的 Webhook 传递事件。长期凭证和 client_secret 应保存在后端,不能打包进 Web 或移动端代码。
什么时候使用 WebView 或远程代码?
如果插件需要自行编写完整的 React 页面,可以在 Web 端使用 iframe,在移动端使用 WebView。这种方式能够复用 Web UI,包括依赖 DOM 的组件库,但需要适配页面导航、键盘、尺寸和无障碍体验。
宿主与页面之间应只开放有限的消息协议。浏览器端要验证 postMessage 的来源和发送窗口;移动端要限制加载地址、页面跳转及桥接消息。不要把宿主的通用网络请求能力或长期令牌直接交给插件页面。
远程 JavaScript 更适合经过审核、与宿主紧密配合的插件。即使使用 Expo 的更新机制,更新内容也必须与已安装的原生运行时兼容。如果插件新增了宿主尚未包含的原生模块,就需要重新构建应用,具体限制见 Expo 的运行时版本文档。
移动端分发也需要提前验证。Apple 的审核指南限制下载并执行会改变应用功能的代码,同时在第 4.7 节规定了部分迷你应用和插件的允许范围及附加要求。采用 WebView、JSON UI 或远程 JavaScript,都不能单独保证通过审核,需要根据实际功能核对 Apple App Review Guidelines 中的适用条款。
落地时需要保留哪些边界?
- UI 协议和插件业务版本分别管理。旧宿主遇到不支持的组件时,应显示兼容提示。
- 前端按钮是否可见不代表权限有效,每次后端操作仍需鉴权。
- 动作采用白名单,避免在 UI 描述中引入
eval、任意 JavaScript 表达式或任意请求地址。 - 为插件提供请求超时、错误展示、缓存和停用机制。服务端异常可以通过错误状态处理,渲染异常可以用 Error Boundary 限制影响范围。
- “无需更新宿主”适用于已有能力的重新组合。新增组件实现、设备能力或原生依赖,仍可能需要发布宿主更新。
备注:内容仅供参考。
