📚 前言
前两篇我们先建立了两条主线:
1 | 小程序运行在微信客户端宿主环境里 |
以及:
1 | 小程序页面由逻辑层和渲染层共同完成 |
这一篇继续往下讲:
小程序里的
wx.xxxAPI,为什么能调用微信和系统的原生能力?
比如:
1 | wx.login() |
这些 API 看起来像普通 JS 函数。
但它们本质上不是普通 JS 工具函数。
它们背后连接的是:
1 | 微信客户端 Native 能力 |
这中间的通信桥梁,就是:
1 | JSBridge |
一、先用一句话理解 JSBridge
JSBridge 可以理解为:
1 | JS 和 Native 之间的通信通道 |
在小程序里,大概是:
1 | 小程序 JS |
更直白一点:
JSBridge 让 JS 可以请求微信客户端帮它做一些浏览器 JS 做不了的事。
比如 JS 本身不能直接打开微信支付。
但它可以发起请求:
1 | 我要调用支付 |
然后微信 Native 层去处理真正的支付流程。
二、为什么需要 JSBridge
普通 JS 的能力是有限的。
在浏览器里,JS 主要能做:
- 操作页面
- 发网络请求
- 使用浏览器开放的 Web API
- 使用部分设备能力
但很多能力不应该直接开放给网页。
比如:
- 微信登录态
- 微信支付
- 扫码
- 相册
- 相机
- 蓝牙
- 文件系统
- 分享
- 订阅消息
这些能力涉及:
1 | 用户隐私 |
所以不能让 JS 随便直接调用系统底层 API。
微信采用的方式是:
1 | JS 只能调用微信定义好的 wx API |
这就是 JSBridge 存在的原因。
三、wx API 不是普通函数
看一个例子:
1 | wx.scanCode({ |
从写法上看,它像一个普通函数。
但底层更像一次跨端请求:
1 | JS 调用 wx.scanCode |
所以 wx.scanCode 不是:
1 | JS 自己完成扫码 |
而是:
1 | JS 请求微信客户端代为扫码 |
四、一次 wx API 调用的完整链路
可以把一次 API 调用拆成五步。
1. JS 发起调用
开发者写:
1 | wx.getLocation({ |
2. 基础库封装请求
微信基础库会处理:
- API 名称
- 参数
- 回调
- 调用上下文
- 调用 ID
可以理解成生成一条消息:
1 | { |
这里的结构只是帮助理解,不代表真实内部协议完全长这样。
3. JSBridge 发送消息
基础库把这条请求通过 JSBridge 发给 Native。
1 | JS → JSBridge → Native |
4. Native 执行能力
Native 层根据 API 类型做不同事情。
比如定位:
1 | 调用定位模块 |
比如扫码:
1 | 打开扫码页面 |
5. Native 回传结果
Native 执行完成后,把结果通过 JSBridge 回传给 JS。
1 | Native → JSBridge → JS callback |
于是触发:
1 | success(res) {} |
或者:
1 | fail(err) {} |
这条链路可以先用一张图记住:
五、JSBridge 通信到底是怎么实现的
前面一直在说:
1 | JSBridge 把 JS 请求发给 Native |
但这句话还不够底层。
更准确地说,JSBridge 通常由三部分组成:
1 | JS 侧桥对象 |
在小程序里,开发者看到的是:
1 | wx.scanCode() |
但 wx.scanCode 背后不是直接调用系统扫码能力,而是微信基础库帮你把调用转换成一条“消息”。
1. Native 先创建 JS Runtime
先回答一个很容易卡住的问题:
小程序逻辑层没有 DOM,那 JSBridge 怎么通信?
关键是:
1 | JSBridge 不一定依赖 DOM。 |
DOM、postMessage、MessageHandler 这些更常出现在 WebView 场景里。
但小程序逻辑层更像是:
1 | 微信 Native App |
也就是说,小程序 JS 不是直接跑在普通浏览器页面里。
它跑在微信自己管理的 JS 运行环境中。
概念上类似:
1 | 微信小程序逻辑层 = |
这层关系可以先看成下面这张图:
这里的 JS Runtime 可以理解成一个能执行 JavaScript 的环境。
Native 程序可以通过嵌入 JS 引擎来创建它,比如 JavaScriptCore、V8、QuickJS、Hermes 这类引擎。
伪代码大概像这样:
1 | const runtime = createJavaScriptRuntime() |
真实微信内部实现不会这么简单,但模型可以这样理解。
落到微信的真实选型上,不同端用的 JS 引擎并不一样:
| 端 | 逻辑层 JS 引擎 |
|---|---|
| iOS | JavaScriptCore(系统自带) |
| Android | V8 |
| 开发者工具 | Chromium 内核(隐藏 WebView) |
这也解释了一个真实的坑:同一份代码在 iOS、Android、开发者工具上行为偶尔会有细微差异,因为底层引擎并不相同(比如某些 ES 新特性、正则、数字精度的边界表现)。所以重要逻辑一定要在真机上验证,不能只信开发者工具。
2. Native 给 JS 注入桥能力
小程序 JS 运行在微信提供的 JS 运行环境里。
这个环境不是完全裸的 JavaScript。
微信客户端会在运行时准备好一些平台对象和通信能力,比如:
1 | wx |
你可以把它理解成:
1 | Native 先把一根“通信线”接进 JS 环境 |
所以 JS 才能调用:
1 | wx.getLocation() |
如果没有这层注入,普通 JavaScript 是不知道 wx 是什么的。
3. 微信基础库先搭好运行框架
这里还要区分两个东西:
1 | 微信基础库 |
它们通常运行在同一个逻辑层 JS Runtime 里。
但角色不是平级的。
可以理解成:
1 | 微信基础库先加载 |
所以你能在小程序里直接写:
1 | App({}) |
不是因为 JavaScript 原生就有这些函数。
而是微信基础库提前放进了运行环境。
比如 Page 可以理解成:
1 | globalThis.Page = function page(pageOptions) { |
开发者写的:
1 | Page({ |
本质是在告诉基础库:
1 | 这里有一个页面 |
所以基础库和业务代码的关系更像:
1 | 同一个 JS Runtime |
普通业务计算不需要 Native:
1 | const total = list.reduce((sum, item) => sum + item.price, 0) |
但如果涉及微信能力、页面更新、生命周期调度、事件传递等,就会由基础库和 Native / 渲染层协作。
4. wx API 会被基础库包装成标准消息
比如开发者写:
1 | wx.scanCode({ |
基础库不会把 success 函数直接丢给 Native。
因为 JS 函数不能像普通数据一样跨到 Native 层执行。
它通常会做两件事:
1 | 保存回调函数 |
可以理解成这样:
1 | const callbackId = 'scanCode_10001' |
然后把真正能跨层传递的数据整理成消息:
1 | { |
这里的结构是帮助理解的伪代码,不代表微信内部真实协议完全长这样。
关键点是:
1 | 跨桥传的是可序列化消息,不是 JS 函数本身。 |
5. 消息通过宿主提供的通道送到 Native
JSBridge 的“桥”,本质上就是宿主环境提供的一条跨语言通信通道。
在普通 WebView 容器里,常见实现方式可能包括:
1 | iOS:WKScriptMessageHandler、evaluateJavaScript |
小程序内部实现会更复杂,也不一定等同于普通 H5 WebView 的某一种方案。
这里还要补一个区分:逻辑层和渲染层接 Native 的通道并不一样。
1 | 渲染层(WebView,有 DOM)↔ Native |
正因为前面说过逻辑层没有 DOM,它的通信天然不能依赖 WebView 的 postMessage / MessageHandler——它靠的是 Native 在创建 JS Runtime 时注入进去的桥对象。
但抽象模型是类似的:
1 | JS 把消息交给宿主 |
所以这一步不是浏览器网络请求,也不是 JS import。
它是:
1 | JS 运行环境和微信客户端 Native 之间的进程内 / 宿主内通信 |
下面落到 iOS 和 Android 上,看这条通道真实是怎么搭的。先记住一个分水岭:
1 | 逻辑层(JSCore / V8):引擎是微信「自己内嵌」的 → 能直接做引擎级原生绑定(干净、可同步) |
钥匙就在这里:逻辑层的引擎是微信亲手塞进去的,所以能往里直接注入原生函数;而 WebView 的内核不归它管,只能用官方接口。
iOS
逻辑层(JavaScriptCore):Native 把 OC 的 block 直接注入成 JS 函数。
1 | // Native(OC):把原生能力塞进 JS 环境 |
JS 里调 invokeNative(...) 就是在直接调一个 OC block——同进程、同内存,JSValue 自动做类型转换,不用拼 JSON 字符串,甚至能同步返回。回调时 Native 持有 JSValue 直接 callWithArguments:。
渲染层(WKWebView):只能用 WebKit 的消息通道。
1 | // 渲染层 JS |
Native 在 userContentController:didReceiveScriptMessage: 里收(message.body 自动转成 NSDictionary),回传则用 evaluateJavaScript:。注意它不能同步返回,所以渲染层这条桥天然是异步 + callbackId。
📜 WKWebView 之前的老
UIWebView没这接口,得靠 hack:JS 改iframe.src = "wxbridge://...",Native 在shouldStartLoadWithRequest里拦截 URL 来收消息。
Android
逻辑层(内嵌 V8):微信自带 V8(不依赖系统 WebView 的引擎),用 FunctionTemplate 把 C++ 函数暴露给 JS,C++ 再经 JNI 上到 Java 干活。同样是引擎级绑定,快且可控。
渲染层(系统 WebView):用 addJavascriptInterface。
1 | webView.addJavascriptInterface(new Bridge(), "wxBridge"); |
JS 里 wxBridge.postMessage(JSON.stringify({...}))——这里只能传字符串/基本类型,所以要手动序列化。回传用 webView.evaluateJavascript(...)。
⚠️ 安全坑:
addJavascriptInterface在 Android 4.2 之前有重大漏洞——JS 能通过反射拿到Runtime.exec执行任意命令。4.2+ 要求用@JavascriptInterface注解显式声明才修复,这也是现在所有暴露方法都必须加注解的原因。
📜 早期同样用过 URL 拦截(shouldOverrideUrlLoading)或劫持window.prompt(onJsPrompt)来做通信。
一张表对齐两端两层
| JS → Native | Native → JS | 可同步? | |
|---|---|---|---|
| iOS 逻辑层(JSCore) | 注入 OC block | callWithArguments |
✅ |
| iOS 渲染层(WKWebView) | WKScriptMessageHandler |
evaluateJavaScript |
❌ 异步 |
| Android 逻辑层(V8) | FunctionTemplate + JNI |
持有 Function 直接 Call | ✅ |
| Android 渲染层(WebView) | addJavascriptInterface |
evaluateJavascript |
❌ 异步 |
一句话:逻辑层走引擎级原生绑定(因为引擎是微信内嵌的),渲染层走 WebView 暴露的消息通道(因为内核不归微信管)。 这就是前面说的“两条不同通道”落到真实平台上的样子。
6. Native 收到消息后做分发
Native 拿到消息后,会看:
1 | api 是什么 |
然后进入不同的原生模块。
比如:
1 | scanCode → 扫码模块 |
这一步可以理解成 Native 侧有一个分发器:
1 | 收到 scanCode 消息 |
也正是因为 Native 层在这里做了分发和适配,JS 侧才可以始终写统一的 wx.xxx。
7. Native 执行完后带着 callbackId 回来
扫码、定位、选图这类能力不可能同步返回。
Native 执行完成后,会把结果再包装成一条回传消息:
1 | { |
然后通过反向通道通知 JS。
JS 侧基础库收到结果后:
1 | 根据 callbackId 找到之前保存的回调 |
伪代码大概是:
1 | function onNativeCallback(message) { |
所以 success 不是 Native 直接调用你的 JS 函数。
更准确地说是:
1 | Native 回传 callbackId 和结果 |
8. 为什么 JSBridge 调用大多是异步的
因为中间隔了 Native,而且 Native 可能还要继续调用系统能力。
比如扫码:
1 | JS 发起 scanCode |
这个过程完全不可能像下面这样同步返回:
1 | const result = wx.scanCode() |
所以小程序 API 早期大量使用:
1 | success / fail / complete |
后来很多 API 也可以被框架或开发者包装成 Promise,本质仍然是在等 Native 回调回来。
9. 用一张图记住实现模型
完整通信模型可以这样记:
1 | Native 创建 JS Runtime |
这才是 JSBridge 的核心:
1 | 不是 JS 直接碰 Native |
六、这条链路应该怎么记
可以记成一句话:
1 | wx API 是 JS 通过 JSBridge 向 Native 发起的一次消息请求。 |
再完整一点:
1 | 微信 Native 创建 JS Runtime |
这条链路解释了很多事情:
- 为什么没有 DOM 也能做 JSBridge 通信
- 为什么
wx、App、Page不是 JS 原生能力 - 为什么基础库和业务代码在同一个 Runtime 里,但角色不同
- 为什么 API 是异步的
- 为什么会有 success / fail / complete
- 为什么 JS 函数不能直接传给 Native 执行
- 为什么需要 callbackId
- 为什么返回结果要再通过桥回到 JS
🧠 一句话总结
JSBridge 是小程序 JS 和微信 Native 能力之间的桥。
wx API 是桥上的标准入口。
1 | 微信先创建 JS Runtime |
理解这点,就能看懂小程序里很多 API 的行为:
1 | App / Page 来自微信基础库 |
下一篇可以继续讲:
1 | 小程序页面渲染和组件系统:WXML、WXSS、组件生命周期如何工作 |