AI 智能管家 JSBridge 深度问答(面试 + 实战)
用途:把 JSBridge 双向通信机制讲透,含 6 个深度问题 + 完整开门案例
适合:被追问「getToken 是步骤几」「method 命名规则」「原生→H5 怎么回传数据」时脱口而出
核心思想:method 名字串起 H5→RN→H5 三步,requestId 串起一次会话
一、JSBridge 双向通信机制(10 步)
先记住这张图,后面所有问题都围绕它展开。
业务后端 原生 handler webViewHandlers.js WebView H5 (web/agent) 业务后端 原生 handler webViewHandlers.js WebView H5 (web/agent) #mermaid-svg-QsisKGIStXRz1YrO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QsisKGIStXRz1YrO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QsisKGIStXRz1YrO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QsisKGIStXRz1YrO .error-icon{fill:#552222;}#mermaid-svg-QsisKGIStXRz1YrO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QsisKGIStXRz1YrO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QsisKGIStXRz1YrO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QsisKGIStXRz1YrO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QsisKGIStXRz1YrO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QsisKGIStXRz1YrO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QsisKGIStXRz1YrO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QsisKGIStXRz1YrO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QsisKGIStXRz1YrO .marker.cross{stroke:#333333;}#mermaid-svg-QsisKGIStXRz1YrO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QsisKGIStXRz1YrO p{margin:0;}#mermaid-svg-QsisKGIStXRz1YrO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QsisKGIStXRz1YrO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QsisKGIStXRz1YrO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QsisKGIStXRz1YrO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QsisKGIStXRz1YrO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QsisKGIStXRz1YrO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QsisKGIStXRz1YrO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QsisKGIStXRz1YrO .sequenceNumber{fill:white;}#mermaid-svg-QsisKGIStXRz1YrO #sequencenumber{fill:#333;}#mermaid-svg-QsisKGIStXRz1YrO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QsisKGIStXRz1YrO .messageText{fill:#333;stroke:none;}#mermaid-svg-QsisKGIStXRz1YrO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QsisKGIStXRz1YrO .labelText,#mermaid-svg-QsisKGIStXRz1YrO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QsisKGIStXRz1YrO .loopText,#mermaid-svg-QsisKGIStXRz1YrO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QsisKGIStXRz1YrO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QsisKGIStXRz1YrO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QsisKGIStXRz1YrO .noteText,#mermaid-svg-QsisKGIStXRz1YrO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QsisKGIStXRz1YrO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QsisKGIStXRz1YrO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QsisKGIStXRz1YrO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QsisKGIStXRz1YrO .actorPopupMenu{position:absolute;}#mermaid-svg-QsisKGIStXRz1YrO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QsisKGIStXRz1YrO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QsisKGIStXRz1YrO .actor-man circle,#mermaid-svg-QsisKGIStXRz1YrO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QsisKGIStXRz1YrO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} H5 ->> 原生 的入口 WebView 的 onMessage 回调 查 handlers 表 找到对应方法 注意是 ctx.sendMessage不是 webViewRef.sendMessageToWebView step1 postMessage method params requestId1step2 onMessage 事件触发2step3 handleWebViewMessage 分发3step4 调用对应 handler4step5 调业务 API5step6 返回数据6step7 调用 ctx.sendMessage method data requestId7step8 调用 webViewRef.injectJavaScript 注入 JS8step9 dispatchEvent nativeMessage 事件9step10 拿到数据 渲染到聊天界面10
步骤速查表
| 步骤 | 谁 | 调什么 | 代码位置 |
|---|---|---|---|
| step1 | H5 | window.ReactNativeWebView.postMessage |
H5 端 SDK |
| step2 | WebView | onMessage 回调 自动触发 |
WebView.js prop |
| step3 | webViewHandlers.js | handleWebViewMessage 函数 |
utils/webViewHandlers.js |
| step4 | webViewHandlers.js | handlers[method] 按 method 查表 |
同上 |
| step5 | 原生 handler | 调业务 API(fetch) | utils/agentToolHandlers/doorHandlers.js |
| step6 | 业务后端 | 返回数据 | --- |
| step7 | 原生 handler | ctx.sendMessage(method, data, requestId) |
doorHandlers.js |
| step8 | webViewHandlers.js 到 WebView.js | ctx.sendMessage 到 webViewRef.injectJavaScript |
WebView.js |
| step9 | H5 | window.dispatchEvent nativeMessage 注入的 JS 触发 |
H5 监听 |
| step10 | H5 | 拿到数据 渲染聊天界面 | H5 端 SDK |
二、6 个深度问题速答
Q1:智能体云服务(nexa.newbest.com.cn)是哪个厂商?
A :公司域名 newbest.com.cn 的子域名,大概率是采购的第三方 SaaS (思必驰/涂鸦/大厂之一)。LLM 包含在云服务里,前端不需要单独买。
判断依据:
| 证据 | 推断 |
|---|---|
域名 <YOUR_AGENT_DOMAIN> |
公司自有域名,不是第三方云厂商 |
tenantId + agentId 多租户配置 |
SaaS 服务典型特征 |
SDK AgentChatSDK |
第三方厂商提供的标准 SaaS SDK 命名 |
LLM 处理 :智能体云服务内部封装,前端零感知。买的套餐里包含 LLM 调用次数。
Q2:前端需要理解这么详细吗?RN 项目能区分「智能体云服务」和「LLM」吗?
A :前端不用理解这么深 。RN 代码完全看不见 LLM ,把云服务当黑盒。
前端只需要理解 3 句话:
1. RN 端把用户说的话发给智能体云服务
2. 等结果
3. 把结果显示出来
中间的「云服务怎么调 LLM、怎么过滤权限 」完全在云服务内部,前端零感知。
| 层级 | RN 项目能看到 | 看不到 |
|---|---|---|
| 智能体云服务 | 能看到(URL <YOUR_AGENT_DOMAIN>) | --- |
| LLM | 完全看不到 | 模型名称 调用次数 prompt |
| 工具注册表 | 完全看不到 | 哪些工具注册了 |
| 权限过滤 | 完全看不到 | 用户能用哪些工具 |
Q3:智能体云服务怎么处理工具权限?
A :大致流程是用户身份 → 权限过滤 → 工具列表 + 用户消息 → LLM → 二次校验 → 返回。前端不可见。
推测的流程:
用户发请求「帮我开一号楼大门」
智能体云服务 收到请求
1. 识别用户身份 拿到 userId/token
2. 查该用户的权限(A 能调哪些工具)
3. 过滤工具注册表(A 只能用 [查天气, 开门])
4. 把用户身份 + 过滤后的工具列表 + 用户消息 一起发给 LLM
5. LLM 返回 要调开门
6. 智能体云服务再次校验权限(防止 LLM 幻觉)
7. 返回结果给前端
关键 :LLM 不可信 ,可能幻觉瞎说或被 prompt 注入越权,云服务永远要做最终权限校验。
Q4:onMessage / injectedJavaScript / injectJavaScript() 是啥?
答:3 个相似但完全不同的东西
| 名称 | 类型 | 何时调用 | 用途 |
|---|---|---|---|
injectedJavaScript |
WebView 的 prop | 初始化时 跑一次 | 注入静态配置 BASE_URL 禁用缩放 |
injectJavaScript() |
WebView 实例的 方法 | 任何时候手动调用 | 动态注入 JS 业务数据回传 |
onMessage |
WebView 的 prop 回调 | H5 发消息时触发 | 接收 H5 发来的数据 |
对照代码:
javascript
// WebView.js 第 421 440 行
<WebView
onMessage={this.handleWebViewMessage} // Q4-A 监听 H5 发来的消息
injectedJavaScript={injectedJavaScript} // Q4-B 初始化时跑一次
injectedJavaScriptBeforeContentLoaded={...} // Q4-C DOM 加载前注入
/>
javascript
// WebView.js sendMessageToWebView 方法
sendMessageToWebView = (method, data, requestId) => {
const jsCode = `
window.dispatchEvent(new CustomEvent('nativeMessage', {
detail: { method: '${method}', requestId: '${requestId}', data: ${JSON.stringify(data)} }
}));
true;
`;
// 这里调用的是 webViewRef 的实例方法 不是 prop
this.webViewRef.current?.injectJavaScript(jsCode);
};
对照表:
| 业务数据回调用哪个 | 用 prop 还是方法 |
|---|---|
| 初始化静态配置 | injectedJavaScript prop |
| 业务数据回传 | webViewRef.current.injectJavaScript() 方法 |
三个 prop / 方法分别注入什么?
| 名称 | 注入时机 | 注入内容 |
|---|---|---|
injectedJavaScriptBeforeContentLoaded |
DOM 加载前 | 宿主 App 信息 安全区域 console 转发 |
injectedJavaScript |
DOM 加载后 | BASE_URL isInXingzhijia 标记 viewport |
onLoad 回调里 injectJavaScript() |
加载完成后 | 最新的 __SAFE_AREA_INSETS__ |
sendMessageToWebView() 里的 injectJavaScript() |
任何时候 | 业务数据回传(dispatchEvent nativeMessage) |
业务 API 数据回传走的是 sendMessageToWebView 里的 injectJavaScript(),不是 injectedJavaScript prop。
Q5:onShouldStartLoadWithRequest 是 WebView 自带的吗?
A :是 react-native-webview 提供的 prop,业务自定义 URL 拦截逻辑。
项目代码:
javascript
handleShouldStartLoadWithRequest = (request) => {
const currentUri = this.props.route?.params?.uri;
const requestUrl = request?.url;
// 规则 1: 空 URL 放行
if (!requestUrl) return true;
// 规则 2: 非顶层 frame 放行
if (request?.isTopFrame === false) return true;
// 规则 3: 不是 xingzhijia-agent 场景放行
if (!currentUri.includes('xingzhijia-agent')) return true;
// 规则 4: javascript: 或 about:blank 放行
if (requestUrl === 'about:blank' || requestUrl.startsWith('javascript:')) return true;
// 规则 5: 相对路径放行
if (!requestUrl.startsWith('//') && !/^[a-z][a-z0-9+.-]*:/i.test(requestUrl)) return true;
// 规则 6: 相同 URL 放行
if (currentUri.split('#')[0] === requestUrl.split('#')[0]) return true;
// 规则 7: 外部链接 用 Linking.openURL 在外部浏览器打开
Linking.openURL(requestUrl).catch(console.warn);
return false; // 阻止 WebView 内导航
};
用途 :安全防护 ,防止用户在 H5 里点恶意链接导致 WebView 跳到钓鱼网站。拦截后用外部浏览器打开,用户能看到地址栏。
⚠️ 项目里这个 prop 目前被注释了 (前面有 //),目前没有启用。
Q6:流程图对不对?injectedJavaScript 是不是传业务 API 数据?
A :你的发现完全正确!
injectedJavaScript 这个 prop ≠ 业务 API 数据回调。
业务 API 数据回调用的是:
sendMessageToWebView(method, data, requestId)(自定义函数)- 内部调用
webViewRef.current.injectJavaScript(jsCode)(实例方法) - 注入的 JS 里
window.dispatchEvent('nativeMessage', {detail}) - H5 监听
nativeMessage事件拿到数据
injectedJavaScript prop 注入的是 :BASE_URL isInXingzhijia 标记 viewport 禁用缩放(静态配置)。
修正文档时的核心澄清:
| 步骤 | 用什么 | 文档之前描述 | 修正后 |
|---|---|---|---|
| step8 | 原生→H5 注入 JS | injectJavaScript(歧义) |
webViewRef.current.injectJavaScript() 实例方法 |
| 初始化 | 静态配置 | injectedJavaScript prop |
明确为 prop 加实例方法 |
三、method 命名规则 + 完整开门案例
你提的核心问题
H5 给 RN 通信定义的方法是咋样的?RN 返回消息给 H5 又是怎样的方法命名?
getToken/getUserInfo是步骤 step4 的方法,还是步骤 step7 的 sendMessage 方法?
核心答案:method 名 = handler 名 = sendMessage 参数,三者必须一致
getToken 既是:
- 步骤 step4 的 handler 名(
handlers['getToken']) - 步骤 step7 的 sendMessage 第一参数(
sendMessage('getToken', ...)) - 步骤 step9 的 H5 响应 method 字段(
{method: 'getToken', ...})
三者必须同名,因为:
- 步骤 step4:
handlers['getToken']表示「H5 想调 getToken 这个能力」 - 步骤 step7:
sendMessage('getToken', ...)表示「回传的数据 method 是 getToken」 - 步骤 step9:H5 用 method 名字区分是哪个请求的响应
method 命名规范
| 类型 | 命名规则 | 例子 |
|---|---|---|
| 拉数据 查 | getXxx |
getToken / getUserInfo / getAgentContext / getServicePhone |
| 查列表 | queryXxx / getUserXxx |
queryBuildings / getUserAllDoor |
| 执行动作 | verbXxx |
doorOpen / verifyDoorHealth / callPhone |
| 跳转 | goXxx |
goBack |
H5 发起:
javascript
{
method: '方法名', // 驼峰命名
params: {...}, // 参数对象
requestId: '唯一ID' // 关联响应
}
RN 响应:
javascript
ctx.sendMessage('方法名', result, requestId);
// ↑ 同一个方法名
四、完整案例:「帮我开一号楼大门」(3 轮 H5↔RN 通信)
这是 AI 智能管家最经典的场景,包含 3 个 handler 调用 + 4 次 H5↔RN 通信。
全流程时序图
业务后端 原生 handlers webViewHandlers WebView 大模型 智能体云服务 H5 聊天页 用户 业务后端 原生 handlers webViewHandlers WebView 大模型 智能体云服务 H5 聊天页 用户 #mermaid-svg-uLwxE4ExgEyF49BN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uLwxE4ExgEyF49BN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uLwxE4ExgEyF49BN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uLwxE4ExgEyF49BN .error-icon{fill:#552222;}#mermaid-svg-uLwxE4ExgEyF49BN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uLwxE4ExgEyF49BN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uLwxE4ExgEyF49BN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uLwxE4ExgEyF49BN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uLwxE4ExgEyF49BN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uLwxE4ExgEyF49BN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uLwxE4ExgEyF49BN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uLwxE4ExgEyF49BN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uLwxE4ExgEyF49BN .marker.cross{stroke:#333333;}#mermaid-svg-uLwxE4ExgEyF49BN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uLwxE4ExgEyF49BN p{margin:0;}#mermaid-svg-uLwxE4ExgEyF49BN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uLwxE4ExgEyF49BN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-uLwxE4ExgEyF49BN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uLwxE4ExgEyF49BN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-uLwxE4ExgEyF49BN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-uLwxE4ExgEyF49BN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-uLwxE4ExgEyF49BN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-uLwxE4ExgEyF49BN .sequenceNumber{fill:white;}#mermaid-svg-uLwxE4ExgEyF49BN #sequencenumber{fill:#333;}#mermaid-svg-uLwxE4ExgEyF49BN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-uLwxE4ExgEyF49BN .messageText{fill:#333;stroke:none;}#mermaid-svg-uLwxE4ExgEyF49BN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uLwxE4ExgEyF49BN .labelText,#mermaid-svg-uLwxE4ExgEyF49BN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-uLwxE4ExgEyF49BN .loopText,#mermaid-svg-uLwxE4ExgEyF49BN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-uLwxE4ExgEyF49BN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uLwxE4ExgEyF49BN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-uLwxE4ExgEyF49BN .noteText,#mermaid-svg-uLwxE4ExgEyF49BN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-uLwxE4ExgEyF49BN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uLwxE4ExgEyF49BN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uLwxE4ExgEyF49BN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uLwxE4ExgEyF49BN .actorPopupMenu{position:absolute;}#mermaid-svg-uLwxE4ExgEyF49BN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-uLwxE4ExgEyF49BN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uLwxE4ExgEyF49BN .actor-man circle,#mermaid-svg-uLwxE4ExgEyF49BN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-uLwxE4ExgEyF49BN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第 1 轮 拿楼宇信息 从 redux 直接读 不调 API 第 2 轮 拿门禁列表 第 3 轮 执行开门 帮我开一号楼大门1对话请求2理解意图3method getBuildingInfo4返回工具调用5step1 postMessage method getBuildingInfo requestId req-0016step2 onMessage7step3 step4 查 handlers.getBuildingInfo8step7 ctx.sendMessage getBuildingInfo buildingId 1 req-0019step8 webViewRef.injectJavaScript10step9 dispatchEvent nativeMessage11拿到 buildingId 112自动追问 buildingId 113理解14method getUserAllDoor params buildingId 115返回工具调用16step1 postMessage method getUserAllDoor params buildingId 1 requestId req-00217step2 onMessage18step3 step4 查 handlers.getUserAllDoor19step5 step6 调 api.getUserAllDoorNew 返回门禁列表20step7 ctx.sendMessage getUserAllDoor 门禁列表 req-00221step8 webViewRef.injectJavaScript22step9 dispatchEvent nativeMessage23拿到门禁列表24自动追问 deviceSerial abc25理解26method doorOpen params deviceSerial abc buildingId 127返回工具调用28step1 postMessage method doorOpen params req-00329step2 onMessage30step3 step4 查 handlers.doorOpen31step5 step6 调 api.doorOpen 返回 success true32step7 ctx.sendMessage doorOpen success true req-00333step8 webViewRef.injectJavaScript34step9 dispatchEvent nativeMessage35显示「已为您打开一号楼大门」36
关键代码对照
step1 H5 发请求(H5 端 SDK 调用)
javascript
// 第一轮 拿楼宇
window.ReactNativeWebView.postMessage(JSON.stringify({
method: 'getBuildingInfo',
params: {},
requestId: 'req-001'
}));
// 第二轮 拿门禁列表
window.ReactNativeWebView.postMessage(JSON.stringify({
method: 'getUserAllDoor',
params: {buildingId: 1, employeeNo: 'xxx'},
requestId: 'req-002'
}));
// 第三轮 开门
window.ReactNativeWebView.postMessage(JSON.stringify({
method: 'doorOpen',
params: {
deviceSerial: 'abc',
buildingId: 1,
employeeNo: 'xxx'
},
requestId: 'req-003'
}));
step4 + step7 原生 handler 实现(RN 项目代码)
javascript
// src/utils/agentToolHandlers/doorHandlers.js
// handler 名 = method 名 = sendMessage 名(必须一致)
export async function getBuildingInfoHandler(params, requestId, context) {
const {sendMessage} = context;
// 从 redux 拿 不调 API
const buildingId = context.building?.curId;
sendMessage('getBuildingInfo', {buildingId}, requestId);
// ↑ 步骤 step7 调用 sendMessage
}
export async function getUserAllDoorHandler(params, requestId, context) {
const {sendMessage} = context;
const {buildingId, employeeNo} = params;
// 调业务 API
const result = await getUserAllDoorNew(buildingId, employeeNo);
sendMessage('getUserAllDoor', result, requestId);
// ↑ 步骤 step7
}
export async function doorOpenHandler(params, requestId, context) {
const {sendMessage} = context;
const {deviceSerial, buildingId, employeeNo} = params;
// 调开门 API
const result = await doorOpen({deviceSerial, buildingId, employeeNo});
sendMessage('doorOpen', result, requestId);
// ↑ 步骤 step7
}
step9 H5 接收响应(H5 端 SDK 内部)
javascript
window.addEventListener('nativeMessage', (e) => {
const {method, requestId, data} = e.detail;
// method 区分是哪个请求的响应
if (method === 'getBuildingInfo') {
// 拿到 buildingId 继续下一步
} else if (method === 'getUserAllDoor') {
// 拿到门禁列表
} else if (method === 'doorOpen') {
// 开门成功 显示结果
}
});
4 个观察点(最容易混的地方)
| 观察点 | 谁 | 啥时候调 |
|---|---|---|
| method 名字一致性 | H5 method = handler 名 = sendMessage 参数 | 全程保持一致 |
| requestId 唯一性 | 每次新请求生成 | H5 发起时 |
| ctx.sendMessage 是步骤 step7 | 原生 handler 完成后调 | handler 内部 |
| sendMessageToWebView 是步骤 step8 | ctx.sendMessage 内部调 | WebView.js 内 |
核心口诀 :「method 名字串起 H5 RN H5 三步,requestId 串起一次会话,handler 名 = sendMessage 的 method 参数」
五、11 个工具速查表
| 工具名 | 作用 | 内部调什么 |
|---|---|---|
getCloudTalkUserInfo |
拿云对讲用户信息 | api.getCloudTalkUserInfo() |
queryBuildings |
拿楼栋列表 | api.queryBuildingsNew() |
getUserAllDoor |
拿门禁列表 | api.getUserAllDoorNew() |
verifyDoorHealth |
验证开门权限 | api.verifyDoorHealth() |
doorOpen |
执行开门 | api.doorOpen() |
getBuildingInfo |
拿当前楼宇 | 直接从 redux 取 |
getBindingStatus |
拿房屋绑定状态 | 直接从 redux 取 |
getToken |
拿登录 token | storage.getToken() |
getUserInfo |
拿用户信息 | 直接从 redux 取 |
getAgentContext |
拿 agent 上下文 | 从 redux 拼 |
getServicePhone |
拿服务中心电话 | 直接从 redux 取 |
callPhone |
拨打电话 | Linking.openURL tel ... |
六、5 个观察口诀
口诀 1:JSBridge 三要素
onMessage 接收 + injectJavaScript 发送 + requestId 关联
口诀 2:method 三名一致
handler 名 = H5 method = sendMessage 第一参数
口诀 3:业务数据回传路径
sendMessageToWebView → webViewRef.injectJavaScript → dispatchEvent nativeMessage
口诀 4:3 个相似但不同
injectedJavaScript 是 prop 初始化用
injectJavaScript() 是方法 动态调用
sendMessageToWebView 是自定义函数 封装了方法调用
口诀 5:method 命名规范
查数据用 getXxx 查列表用 queryXxx 执行动作用 verbXxx 跳转用 goXxx
七、面试常见 5 问速答
Q1:什么是 JSBridge?
A :JSBridge 是 JavaScript 与原生代码之间的通信桥梁。在 RN WebView 里:
- H5 → 原生:
window.ReactNativeWebView.postMessage→ WebViewonMessage - 原生 → H5:
webViewRef.injectJavaScript→ 派发CustomEvent nativeMessage
Q2:onMessage / injectedJavaScript / injectJavaScript() 三者关系?
| 名称 | 性质 | 用途 |
|---|---|---|
onMessage |
WebView prop | 接收 H5 发来的消息 |
injectedJavaScript |
WebView prop | 初始化时注入静态配置 |
injectJavaScript() |
WebView 实例方法 | 任何时候动态注入 JS 业务数据回传 |
Q3:getToken 是步骤几?
A :同时是 step4 和 step7:
- step4:
handlers['getToken'](handler 名) - step7:
ctx.sendMessage('getToken', ...)(sendMessage 第一参数) - step9:H5 收到的响应 method 字段是
'getToken'
三者必须同名,这就是 JSBridge 的 method 约定。
Q4:method 命名有什么规范?
A:4 类命名:
- 查数据:
getXxx(getToken getUserInfo getAgentContext) - 查列表:
queryXxx或getUserXxx(queryBuildings getUserAllDoor) - 执行动作:
verbXxx(doorOpen callPhone) - 跳转:
goXxx(goBack)
Q5:为什么需要 requestId?
A :关联异步请求和响应 。H5 可能同时发多个 postMessage,原生处理完不一定按顺序返回,用 requestId 让 H5 知道这个响应是对应哪个请求。
面试前 5 分钟 :看第一节(10 步机制图)+ 第四节(method 命名规则)+ 第七节(5 问速答),足够应付 80% 追问。
被追问细节:看第二节(6 个深度问题)+ 第四节(完整开门案例)。