前言
理解 JSBridge 时,最容易卡住的问题不是:
1 | JSBridge 是什么 |
而是:
1 | 两个不同环境,为什么能互相调用方法? |
比如:
1 | JS 为什么能调用 Native? |
如果只说一句“通过桥通信”,其实还是有点虚。
这篇用一个最小 demo,把这件事拆开。
先放结论:
跨环境调用不是两种语言天然互通,而是宿主在创建 JS 运行时时注入了入口;JS 也会暴露一个接收入口。双方通过约定好的消息格式通信,再用 callbackId 把异步结果和原始调用配对。
一、先分清两个世界
一个 JSBridge 场景里,通常至少有两个世界:
1 | 宿主环境 Host |
在不同场景里,它们的名字不一样。
小程序里:
1 | 微信 Native = 宿主 Host |
WebView 里:
1 | iOS / Android App = 宿主 Host |
Unity WebGL 里:
1 | Unity Runtime / WASM = 宿主 Host |
所以关键不是语言,而是:
1 | 谁创建和管理这个 JS 运行环境 |
谁创建它,谁就有机会往里面注入一些东西。
二、什么叫“宿主给 JS 注入能力”
看一个最小模型。
Node 里有一个 vm 模块,可以创建一个独立的 JS 运行环境。
1 | const vm = require('node:vm') |
这里发生了两件事:
1 | 1. 宿主创建了一个新的 JS 运行环境 jsRuntime |
所以在 jsRuntime 里面执行的代码,可以直接调用:
1 | NativeBridge.postMessage(...) |
这不是 JavaScript 原生能力。
它能调用,是因为宿主提前塞进去了。
换成浏览器 WebView,可能是:
1 | window.webkit.messageHandlers.JSBridge.postMessage(...) |
换成 Android WebView,可能是:
1 | window.NativeBridge.postMessage(...) |
换成 Unity 小游戏,可能是:
1 | GameGlobal.Module.SendMessage(...) |
名字不同,本质相同:
1 | 宿主给 JS 留了一个“发消息给我”的入口 |
三、globalThis 是谁
再看这句:
1 | globalThis.JSBridge = { |
globalThis 是 JavaScript 的全局对象。
不同环境里,全局对象以前有不同名字:
1 | 浏览器:window |
所以:
1 | globalThis.JSBridge = ... |
意思就是:
1 | 在当前 JS 运行环境的全局对象上挂一个 JSBridge |
如果这段代码运行在 jsRuntime 里面,那么它等价于:
1 | 给 jsRuntime 的全局对象挂上 JSBridge |
于是宿主以后就可以通过这个全局对象找到:
1 | jsRuntime.JSBridge.receiveFromNative(...) |
这就是反方向通道:
1 | JS 自己给宿主留了一个“你有结果时调我”的入口 |
四、最小 demo
仓库里有一个最小 demo:
1 | demos/jsbridge-cross-env-minimal.js |
代码如下:
1 | const vm = require('node:vm') |
运行:
1 | node demos/jsbridge-cross-env-minimal.js |
输出:
1 | [Host] received from JS: { api: 'getSomething', params: { value: 1 }, callbackId: 'cb_1' } |
五、这个 demo 到底发生了什么
1. 宿主创建 JS 运行时
1 | const jsRuntime = vm.createContext(...) |
可以理解成:
1 | 宿主开了一个 JS 房间 |
2. 宿主往 JS 里注入 NativeBridge
1 | NativeBridge: { |
可以理解成:
1 | 宿主在 JS 房间里放了一部电话 |
所以 JS 里面可以调用:
1 | NativeBridge.postMessage(...) |
3. JS 在自己的全局对象上挂 JSBridge
1 | globalThis.JSBridge = { |
可以理解成:
1 | JS 在房间公告板上写了一个回电话地址 |
所以宿主可以回调:
1 | jsRuntime.JSBridge.receiveFromNative(...) |
4. JS 调用 invoke
1 | JSBridge.invoke('getSomething', { value: 1 }, callback) |
表面看是调用方法。
但它实际做的是:
1 | 生成 callbackId |
5. callbackMap 保存等待中的回调
1 | callbackMap.set(callbackId, callback) |
这一步很关键。
因为跨环境调用通常不能马上拿到结果,所以 JS 要先记住:
1 | 等 cb_1 的结果回来时,要执行哪个 callback |
6. JS 把消息发给宿主
1 | NativeBridge.postMessage(JSON.stringify({ |
这里跨出去的不是函数,而是一条消息:
1 | { |
7. 宿主收到消息
1 | postMessage(rawMessage) { |
宿主看到:
1 | api = getSomething |
于是它去执行对应能力。
在真实小程序里,这一步可能是:
1 | scanCode → 打开扫码 |
8. 宿主执行完以后回 JS
1 | jsRuntime.JSBridge.receiveFromNative(JSON.stringify(response)) |
回传消息里带着同一个 callbackId:
1 | { |
9. JS 根据 callbackId 找回调
1 | const callback = callbackMap.get(message.callbackId) |
这就是为什么结果能回到最开始那个调用。
不是宿主直接拿到了 JS callback 函数。
而是:
1 | 宿主只回传 callbackId 和结果 |
六、为什么这叫异步 RPC
RPC 是 Remote Procedure Call,远程过程调用。
直白一点:
1 | 像调用本地函数一样,调用另一个环境里的能力 |
比如:
1 | wx.scanCode({ |
看起来像 JS 函数。
但真正执行扫码的是微信 Native。
所以它更像:
1 | JS 请求 Native 执行 scanCode |
为什么是异步?
因为调用后不会马上返回结果。
1 | JSBridge.invoke('getSomething', params, callback) |
输出里也能看到:
1 | [JS] invoke returned immediately |
然后宿主过一会儿才回:
1 | [JS] callback runs |
所以异步 RPC 可以拆成:
1 | RPC:调用另一个环境里的能力 |
七、为什么不同语言能互相调用
严格来说,不是“不同语言直接互相调用”。
真正发生的是:
1 | 运行时提供了一层 binding / glue |
以 Unity WebGL 为例:
1 | C# 编译成 WASM |
C# 里写:
1 | [] |
意思是:
1 | 我需要调用一个外部函数,名字叫 ParkLogin |
.jslib 里定义:
1 | mergeInto(LibraryManager.library, { |
构建时,Unity / Emscripten 会把这个 JS 函数注册成 WASM 能调用的外部函数。
所以 C# 能调 JS,不是 C# 天然懂 JS,而是:
1 | WASM 支持 import 外部函数 |
反过来 JS 回 Unity:
1 | GameGlobal.Module.SendMessage( |
Unity Runtime 会做:
1 | 找到名为 -ParkToJS- 的 GameObject |
所以跨语言调用的本质不是语言互懂,而是:
1 | 宿主运行时暴露了可调用入口 |
八、对应到 js-sdk-mono
js-sdk-mono 里的链路就是这个模型。
它不是微信底层 Native Bridge,而是 Unity / C# 和 JS SDK 之间的一层桥。
完整链路大概是:
1 | C# 调 ParkSDKManager.ParkLogin |
它和微信底层 JSBridge 的思想一样:
1 | 跨环境不能直接传函数 |
只是桥的两端不同。
微信底层 JSBridge:
1 | 小程序 JS ↔ 微信 Native |
js-sdk-mono 这一层:
1 | Unity / C# ↔ JS SDK |
九、对应到微信小程序
微信小程序里的模型也是类似的。
1 | 微信 Native 创建 JS Runtime |
所以你写:
1 | wx.scanCode({ |
不是 JS 自己扫码。
而是:
1 | JS 发消息请求 Native 扫码 |
十、最后用一句话记住
JSBridge 的最小模型是:
1 | 宿主创建 JS 运行时 |
再压缩一下:
1 | 入口靠宿主注入 |
这就是 JSBridge、Native Bridge、Unity SendMessage、WebView Bridge 这些东西背后的共同骨架。