1. 为什么需要JSBridge:一道绕不过去的桥
先讲一个大多数团队都经历过的场景:你的App里有一段内嵌H5,运营要做一个活动页,里面需要调用手机摄像头扫码、读取本地相册、甚至是获取当前网络状态。这时候H5和App是两个世界,H5跑在WebView的沙箱里,原生代码才有权限碰系统能力。那怎么让H5安全、稳定地把原生的“手”伸出去,再让原生的结果顺利返回给H5?这就是JSBridge要解决的核心问题。
早期大家用最土的办法:把要调用的操作拼成一个特殊链接,比如myapp://scan?type=qrcode,让H5通过window.location跳转过去,原生拦截这个链接后做扫码,再把结果通过另一个链接回调给H5。这种做法能跑通,但极其脆弱:URL长度有限制,参数值需要各种encode,回调时机没法精确控制,一旦链接被WebView提前处理就整个断掉。更麻烦的是,如果页面里同时还有别的跳转逻辑,两个“跳转”互相打架的情况能把人逼疯。
JSBridge本质上就是在H5和原生之间建立一条稳定的双向通信通道。它不只是把函数暴露出来,还包括一套消息协议、回调机制、生命周期管理和错误处理。很多刚接触这个领域的开发者容易把JSBridge理解成一个工具库,实际上它是一个通信架构。这套架构能同时解决“调用”和“回调”两个方向的问题:H5可以调用原生能力,原生也能主动推送数据给H5,比如网络状态变化、App进入后台这类事件。
这篇文章适合正在做混合开发、跨端开发或者对WebView通信机制感兴趣的开发者,无论你是前端、Android还是iOS方向,都能在里面找到可直接落地的思路。标题里的“核心原理”和“最佳实践”各占一半,前者帮你建立底层认知,后者帮你避开我在实战中踩过的那些坑。
2. JSBridge的核心原理拆解
2.1 注入方案:把你的能力挂到window上
目前主流的JSBridge实现,第一步都是让H5能拿到一个全局对象,比如window.WebViewJavascriptBridge或者window.NativeBridge。这个对象上挂了一些方法,前端调用bridge.callHandler()时,本质上就是在调用原生暴露出来的能力。
在Android上,最常见的做法是addJavascriptInterface。原生把某个Java对象注册进WebView的JavaScript上下文,H5就能直接访问这个对象的方法。这个方案的优点是简单直接,缺点是存在版本差异和安全问题。现在Android开发中更推荐通过WebViewJavascriptBridge自实现的方案,用更可控的方式建立通信,避免直接暴露Java对象。而在iOS上,早期很多人用JavaScriptCore框架,后来主流是WKWebView提供的WKScriptMessageHandler机制:原生通过userContentController.add(scriptMessageHandler:name:)注册一个handler,JS端通过window.webkit.messageHandlers.xxx.postMessage()来调用。
这个机制的精髓在于“注入时机”。我见过不少团队在didStartProvisionalNavigation就把Bridge对象注入进去,导致H5在页面还没加载完时调用不到方法。正确的做法一般是:等WebView的didFinishNavigation触发后,或者等H5主动发一个“BridgeReady”消息之后,再正式启用Bridge。因为Bridge造成了不同的加载顺序问题,我见过某团队为了抢时间,把注入提前到了didStart,后来H5出现了大量偶发性的找不到方法报错,排查了很久才发现是注入时机和页面生命周期没对齐。
2.2 拦截方案:用iframe去“敲门”
在WebView的内置能力还没有那么完善的时候,很多人用拦截URL的方式来模拟调用。H5创建一个隐藏的iframe,把iframe.src设成自定义协议,比如bind://call?method=getLocation。原生WebView在shouldOverrideUrlLoading或者decidePolicyForNavigationAction里拦截这个请求,解析URL参数,执行原生逻辑,再通过注入JS的方式把结果传回页面。
还有一种更冷门但同样有效的方案:拦截prompt弹窗。原生WebView可以重写onJsPrompt,H5那边通过window.prompt("callNative", jsonData)来发起调用,原生拿到jsonData后执行对应逻辑,然后把结果以字符串形式返回。这个方案有一个好处:prompt可以同步返回值,不像URL加载那样是异步的。但坏处也很明显——滥用prompt会让页面上正常需要弹窗的场景被混淆,而且现在主流方向已经不推荐这样做。
URL拦截方案看起来简单,但有几个天然缺陷。第一,URL有长度限制,参数多了直接被截断;第二,iframe的跳转本身会触发WebView的导航逻辑,如果原生没有拦截干净,很容易出现页面被重定向的诡异现象;第三,每次调用都要经过完整的URL解析流程,性能开销偏高。所以现在真正成熟的JSBridge都不会把URL拦截作为主力通道,最多把它作为降级方案。
2.3 为什么所有Bridge都绕不开Callback机制
JSBridge里最容易被忽略也最能体现设计水平的部分,就是回调机制。H5调用原生方法之后,原生那边往往是异步操作的,比如扫码需要用户手动操作,读取相册需要弹系统权限框。那原生执行完了,怎么把结果送回H5?
不可能用return直接返回值,因为跨语言边界的调用本来就是异步的。所以主流的做法是:H5发起调用时传一个唯一callbackId,原生把callbackId连同回调闭包一起存到一个字典里,等原生逻辑执行完毕后,再通过注入JS代码或者postMessage的方式,把结果和callbackId一起回传给H5。H5侧维护一个Map,callbackId作为key,回调函数作为value,收到原生回传的消息后,根据callbackId找到对应的回调并执行。
如果只靠“注入一段JS代码回传结果”,那注入执行的时机、线程、是否被WebView拦截都需要考虑。所以现代JSBridge在原生→JS的回传通道上,也做了和JS→原生相似的机制:如果WebView支持WKScriptMessageHandler就优先用,否则退化为注入JS代码。理解了这一层,你就明白为什么网上很多JSBridge源码里都有callbackId、responseId、messageHandlers这些看起来绕来绕去的概念——它们不是在堆术语,而是在解决“异步回调寻址”这个核心问题。
3. 设计一套能上生产环境的JSBridge
3.1 消息结构与协议设计
一个能上生产环境的JSBridge,核心不在于实现一个能通的方法,而在于设计一套可扩展、可维护、可排查的消息协议。建议协议里至少包含这些字段:
| 字段 | 含义 | 示例 |
|---|---|---|
id | 消息唯一标识 | msg_169900000001 |
module | 能力模块名 | camera、network、storage |
method | 模块下的具体方法 | scan、getStatus |
params | 调用参数对象 | {"type": "qrcode"} |
callbackId | 回调标识 | cb_1001 |
source | 消息来源页面信息 | https://xxx.example.com/path |
version | 协议版本号 | 2.1.0 |
为什么需要module+method双层结构?因为如果没有模块划分,所有能力都堆在同一个命名空间里,一旦超过几十个方法,重名和参数混乱是必然的。见过某团队的Bridge初期只有10个方法,大家也不分模块,半年后加到了60多个方法,每天都有方法名写错、参数对不上的线上事故,最后还是推倒重来,变成模块化结构。
协议版本号也很重要。App端的Bridge实现是跟随客户端发版节奏走的,但H5可以随时上线、随时回滚。如果H5用了新方法,而用户手机上的App还没更新,就会找不到方法。方案是Bridge对象上暴露一个getVersion()方法,H5调用前先检查版本,不兼容就降级处理。别嫌这一步啰嗦,线上问题里“版本不匹配”排在坑位前三。
3.2 生命周期:Bridge何时初始化与销毁
一个常被忽略但极其重要的问题:Bridge的生命周期必须跟随WebView页面走。每次页面新开、跳转、关闭,都要处理好对应的事件。
初始化的时候,原生侧要注册好所有可用的handler,H5侧要等待bridgeReady事件再开始调用。销毁的时候,原生要把WebView持有的JS回调、消息通道全部清空,否则很容易造成内存泄漏——尤其是当WebView里保存了指向原生对象的引用,而原生对象又持有WebView时,这个循环引用会让页面关闭后WebView内存迟迟释放不掉。
页面跳转还有个隐藏问题:如果页面正在发起一个扫码调用,用户点击了某个链接导致WebView开始加载新页面,那之前发出的回调id还在等待返回,但页面上下文已经换了。遇到这种事,原生侧要有能力判断当前页面是否匹配,如果发现页面不是发起调用的那个,就直接丢弃回调,避免回调到错误页面上执行JS,导致报错。我们就在线上遇到过这种场景:用户在扫码途中误点了页面广告,跳转后扫码成功回调打到了新页面上,新页面根本没有处理这个回调的函数,结果报了个不相关的JS错误。
3.3 线程模型与线程切换
移动端的JSBridge,线程问题永远是重灾区。原生执行耗时操作时,一定不能在主线程做。传图片、处理大JSON、访问数据库这些操作,要用子线程或异步队列去跑。但原生执行完后把结果回传给JS时,又必须切回WebView所在的主线程,因为WebView的JS执行引擎不是线程安全的。
很多经验不足的开发者会这样写:渲染线程拿到数据后,直接在子线程里调evaluateJavascript去执行JS代码。这样做的后果是偶发性崩溃、页面卡顿、甚至WebView白屏。正确做法是拿到数据后,先dispatch_async(dispatch_get_main_queue())(iOS)或者runOnUiThread(Android),切到主线程后再执行JS注入。
另一个容易踩坑的点是“同步调用”。有JSBridge实现为了图方便,在H5侧用同步XHR的方式去调原生,美其名曰“像函数一样调用”。但实际上JS和原生之间的通信是异步的,强行同步会让原生侧代码被迫在调用方线程上等待结果,一旦原生逻辑比较复杂,整个WebView的渲染都会被卡住。移动设备的性能不比桌面端,一个卡顿就能让用户觉得App是坏的。所以从一开始就做好异步设计是值得的,不要为了代码简洁牺牲体验。
3.4 兼容性:别让一份代码跑崩三端
现实中的移动开发很少只有一套WebView环境。Android上有常规WebView、X5内核、各种厂商定制ROM的WebView;iOS上UIWebView虽然已被废弃,但存量业务里可能还有残留;还可能有小程序容器、Flutter内嵌WebView等环境。一个JSBridge方案如果只适配了标准WebView,上线之后大概率会被环境差异坑一遍。
我的做法是三层兼容策略:最底层抽象出一套通信通道接口,不同环境各自实现通道;中间层是协议解析和回调管理,与环境无关;最上层是业务能力注册表。这样换一个环境,只需要替换最底层的通道实现,上层的协议和数据逻辑完全不用动。
H5那边也要有环境检测的逻辑。比如通过UA判断当前是Android还是iOS,通过navigator.userAgent判断是否运行在某个特定容器里。如果前端检测不到Bridge对象,要给出明确的提示或者降级方案,不能直接甩给用户一个空白页面。
4. 最佳实践:从能跑到好用的距离
4.1 安全:拿好你的钥匙
JSBridge的安全问题真实存在,而且容易被低估。WebView里跑的页面如果可以被重定向或者注入脚本,黑客就可能通过JSBridge拿到原生的敏感能力,比如读取通讯录、获取定位、甚至调用支付能力。所以安全设计必须在Bridge规划的时候就考虑进来,不能等出了问题再补。
第一层校验是来源域名白名单。原生侧在收到调用时,要检查当前页面的URL是否在允许列表内,不在白名单就直接拒绝。这个校验一定要放在WebView的原生侧做,不能只依赖JS传过来的source字段——因为JS传来的信息可以被伪造。第二层校验是敏感操作的二次确认。比如调用支付、读取通讯录、发送短信这类高危能力,原生侧要弹系统确认框,并且要有独立的权限检查逻辑。第三层是调用频控。有些能力不允许短时间内反复调用,比如频繁扫一扫、频繁调投屏,原生侧要有个简单的计数器和时间窗口,超出阈值就拒绝。
还有一个细节:Android的addJavascriptInterface的历史安全漏洞,让很多团队至今对该方案心有余悸。如果你的兼容性要求允许,尽量使用自研消息通道,而不是直接暴露Java对象给JS。
4.2 超时与错误处理
网络请求有超时,JSBridge调用同样应该有。H5调用原生扫一扫,用户扫了半分钟还没扫出来,这时候H5侧一直无响应地等待用户操作,体验其实很差。而且如果用户中途放弃了扫码,H5的那个回调就永远无人执行,Promise一直pending,页面上的loading状态下不来。
建议是所有JSBridge调用都带上超时控制,默认建议500ms到1s。为什么设这么短?因为正常的原生能力调用,比如获取网络状态、读取一段缓存数据,本身应该是毫秒级返回的。超过1s还没返回,说明大概率出了问题,与其让用户干等,不如尽快失败并给出错误提示。对于扫码、拍照这类需要用户交互的调用,超时时间可以放宽到30s甚至更长,但一定要有超时后的清理逻辑。
错误码也要设计得规范。至少要区分:方法不存在、模块不存在、参数错误、原生功能不可用、用户拒绝、超时、内部错误。每个错误码配上一条可读性强的message,H5侧可以根据错误码决定要不要上报监控、要不要降级、要不要给用户展示特定的提示。别小看这一步,错误码不统一,线上出了事故你想排查都无从下手。
4.3 性能:减少调用次数与合并请求
JSBridge的性能瓶颈,多出在“调用次数过多”上。常见场景是H5页面里通过Bridge获取设备信息,同一个页面用例会连续调好几次:一次获取机型、一次获取系统版本、一次获取屏幕宽度。这些完全可以合并成一个getDeviceInfo(),一次性返回一个对象。
另一个典型场景是原生主动推送数据到H5。比如App的网络状态发生变化,原生的网络模块会向所有WebView广播,如果WebView里的H5页面同时监听了networkChange事件,并且每个回调里都做一次新的Bridge调用,就会产生“听一个消息、做三件事”的连环调用。这种场景建议原生侧做节流:高频通知合并成低频推送,或者在JS侧做一个简单的防抖。
对于大量数据传递的场景,比如原生给H5传一个几十KB的日志字符串,直接走Bridge通道,每次都要序列化和注入,非常损耗性能。更优的做法是:数据不走Bridge,走一个原生提供的临时访问地址,H5拿到地址后自己去拉取。这就像送大件物品不直接走门缝,而是给你一个仓库地址让你去取,门缝留给小纸条就好。
4.4 调试与可观测性
没有日志的JSBridge,线上出了问题是灾难现场。当年我们排查过一次诡异问题:H5页面在只有安卓手机上随机出现“方法不存在”报错,iOS上完全正常。最后靠的就是日志——H5侧打的全量调用日志和原生侧拦截到的消息日志一对,发现安卓上的某个WebView版本把Bridge对象注入时机延后了,H5在BridgeReady之前就开始调用,于是方法名挂在window上但还没赋值,自然就“不存在”了。
所以设计JSBridge时,一定要把日志系统一并设计进去。日志至少要包含:调用发起时间、调用耗时、调用的模块方法、参数大小、返回结果、错误码、WebView环境信息。原生侧和H5侧各自打各自的,最后用同一个requestId串联起来。线上排查时,拿着一个请求id就能说清楚“JS这边发生了什么”、“原生那边发生了什么”。
调试工具也有讲究。Android侧可以用chrome://inspect直接调试WebView中的JS;iOS侧通过Safari的“开发”菜单,可以查看真机上WebView的console和元素。需要特别注意的是,iOS的Safari开发者选项要连上Mac才能生效,而且正式包一般要关掉这个能力,否则有安全和性能风险。建议在Debug模式下开放WebView远程调试,Release模式下关闭。
5. 常见问题与排查技巧实录
5.1 Android上收不到原生回调
这是典型的“一看代码都对,但回调就是不来”的问题。Android侧WebView的evaluateJavascript方法必须在UI线程调用,而原生做一些耗时操作之后,默认是在工作线程返回结果的。如果你直接在工作线程里执行evaluateJavascript,Android会把调用丢到队列里,但页面可能已经切走了,回调就永远没有下文。
排查方式:先在原生侧确保回调执行前,通过runOnUiThread切回主线程。其次要检查WebView是否还存活——页面已经destroy了,还在往回传回调,就一定会丢。建议在回调分发前加一个WebView.isDestroyed()判空。
5.2 iOS首次调用Bridge非常慢
很多团队反馈,iOS上的JSBridge第一次调用要等上一两秒,后面就快了。这通常不是Bridge本身的问题,而是WebView在首次加载时还在解析JS、建立上下文。如果你在didFinishNavigation之前注入了Bridge对象,页面JS一开始执行时Bridge可能还没就位。
另外WKWebView有个特性:在主线程执行JS注入时,如果WebView正在忙着加载或者执行别的脚本,调用会被排到后面等待。所以不要频繁在主线程做注入操作,能合并的注入尽量合并成一次,比如原生端连续回传多个结果时,可以积累几个一起塞进同一个JS字符串里执行,而不是一条一条地注入。
5.3 参数带中文变成乱码
这个坑出现在URL拦截方案里比较多。自定义协议通过URL传参数时,中文和特殊字符必须要做编码,前端encodeURIComponent一下,原生端再URLDecoder.decode一次。有些团队的H5同学忘记了编码,直接拼接URL,到了原生拿到的就是一片乱码。
但这里有个更隐蔽的问题:iOS的WKWebView,在处理decidePolicyForNavigationAction时的request.URL是已经解码后的URL,如果你再对这个URL做一次URL解码,反而会把原本合法的%字符破坏掉。所以解码要严格按照“编码一次、解码一次”的原则,不要重复解码。另外,若能不用URL传参就尽量不用,直接走POST式的消息通道,就不会遇到这种破事。
5.4 到底怎么调试WebView里的JS
这里的建议分三层。第一层是H5侧,直接用浏览器开发者工具调试页面本身,这不依赖真机。第二层是真机联调,Android用chrome://inspect,iOS用Safari开发菜单,需要手机和电脑连在同一网络下。第三层是Bridge通信级调试,在H5侧对Bridge对象做一个代理,每次调用都把参数和结果通过console.log打印出来,原生侧同步打印接收到的消息,然后打开远程调试看两边日志的对应关系。
强调一个经验:Debug包一定要开放WebView远程调试能力,Release包一定要关闭。这不是简单的开关,而是统一的配置管理,否则某天某个工程师把Release包发出去还开着调试模式,用户电脑一插上去就能看到App内部WebView的所有信息,这属于低级安全事故。
6. 动手实现一个最小可用的JSBridge
前面的原理和最佳实践讲了很多,这一节我给出一套最精简的实现骨架,帮助你把前面的概念落到代码上。
6.1 前端侧封装
前端侧的核心是维护一个callbacks表,管理回调函数与callbackId的对应关系。
class Bridge { constructor() { this.callbacks = new Map() this.callbackId = 1000 this.bridge = window.NativeBridge || window.webkit?.messageHandlers?.NativeBridge } registerHandler(name, handler) { // 注册H5侧被原生调用的能力 window._bridgeHandlers = window._bridgeHandlers || {} window._bridgeHandlers[name] = handler } call(module, method, params = {}) { return new Promise((resolve, reject) => { const id = ++this.callbackId this.callbacks.set(id, { resolve, reject }) const message = { id: `msg_${Date.now()}`, module, method, params, callbackId: id, version: '1.0.0' } if (!this.bridge) { reject(new Error('BRIDGE_NOT_READY')) return } try { if (window.NativeBridge) { window.NativeBridge.postMessage(JSON.stringify(message)) } else if (window.webkit?.messageHandlers?.NativeBridge) { window.webkit.messageHandlers.NativeBridge.postMessage(message) } } catch (e) { reject(e) } }) } onNativeMessage(jsonString) { const message = typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString // 原生主动调用H5 if (message.type === 'handler') { const handler = window._bridgeHandlers?.[message.method] if (handler) { const result = handler(message.params) this._postBack(message.callbackId, result) } return } // 原生回调H5 const cb = this.callbacks.get(message.callbackId) if (!cb) return this.callbacks.delete(message.callbackId) if (message.code === 0) { cb.resolve(message.data) } else { cb.reject(new Error(message.message)) } } _postBack(callbackId, data) { this.call(module = 'bridge', method = '_callbackResult', params = { callbackId, data }) } } window.Bridge = new Bridge()这段代码里,call方法组装消息对象,根据运行环境选择不同的通道发送出去。Android走window.NativeBridge.postMessage,iOS走window.webkit.messageHandlers.NativeBridge.postMessage。同时维护一个回调表,等原生回传时根据callbackId找到对应Promise执行resolve或reject。
6.2 原生侧实现(Android)
Android侧的核心是注册一个NativeBridge对象,接收JS侧传来的消息字符串,解析后分发到对应模块。
public class NativeBridge { private final Map<String, BridgeHandler> handlerMap = new HashMap<>(); private final WebView webView; public NativeBridge(WebView webView) { this.webView = webView; } public void registerModule(String module, BridgeHandler handler) { handlerMap.put(module, handler); } @JavascriptInterface public void postMessage(String messageJson) { JSONObject message = new JSONObject(messageJson); String module = message.getString("module"); String method = message.getString("method"); JSONObject params = message.getJSONObject("params"); String callbackId = message.getString("callbackId"); BridgeHandler handler = handlerMap.get(module); if (handler == null) { sendError(callbackId, "MODULE_NOT_FOUND", "module not found: " + module); return; } // 这里要注意,handler内部执行耗时操作时,自行用线程池 handler.handle(params, new BridgeCallback() { @Override public void onSuccess(JSONObject data) { sendResponse(callbackId, 0, data); } @Override public void onError(String code, String message) { sendError(callbackId, code, message); } }); } private void sendResponse(final String callbackId, final int code, final JSONObject data) { // 必须回到UI线程再执行JS webView.post(new Runnable() { @Override public void run() { String js = String.format( "window.Bridge && window.Bridge.onNativeMessage(%s);", JSONObject.quote("{\"callbackId\":\"" + callbackId + "\",\"code\":" + code + ",\"data\":" + data + "}") ); webView.evaluateJavascript(js, null); } }); } }这里最关键的是线程处理:@JavascriptInterface方法是在一个单独的JS线程上执行的,这个线程不能直接操作WebView的UI相关逻辑。耗时操作放线程池,执行完后用webView.post切回UI线程,再执行evaluateJavascript回传数据。
6.3 原生侧实现(iOS)
iOS侧的思路类似,但用的是WKScriptMessageHandler。下面给出核心结构。
class NativeBridge: NSObject, WKScriptMessageHandler { private var handlerMap: [String: BridgeHandler] = [:] private weak var webView: WKWebView? init(webView: WKWebView) { self.webView = webView super.init() webView.configuration.userContentController.add(self, name: "NativeBridge") } func registerModule(_ module: String, handler: BridgeHandler) { handlerMap[module] = handler } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let body = message.body as? [String: Any] else { return } let module = body["module"] as? String ?? "" let method = body["method"] as? String ?? "" let params = body["params"] as? [String: Any] ?? [:] let callbackId = body["callbackId"] as? String ?? "" guard let handler = handlerMap[module] else { sendError(callbackId: callbackId, code: "MODULE_NOT_FOUND", message: "module not found") return } handler.handle(params: params) { [weak self] result in switch result { case .success(let data): self?.sendResponse(callbackId: callbackId, data: data) case .failure(let err): self?.sendError(callbackId: callbackId, code: err.code, message: err.message) } } } private func sendResponse(callbackId: String, data: [String: Any]) { DispatchQueue.main.async { let script = "window.Bridge && window.Bridge.onNativeMessage(\(dataToJs(data)));" self.webView?.evaluateJavaScript(script) } } }iOS侧的一个细节是:WKScriptMessageHandler的接收方法默认会在主线程回调,不必另外切线程。但是具体业务处理如果需要耗时,还是要放到后台队列,最后回传时统一在主线程执行JS。另外不要忘了在WebView销毁前调用userContentController.removeScriptMessageHandler(forName:),否则会有内存泄漏。
6.4 完整接入流程
接入的时候按这个流程来,基本不会出大问题:
- 原生侧创建Bridge实例,注册需要的模块(如
camera、network)。 - WebView加载完成后,H5侧确认
window.Bridge存在,发起一次心跳调用。 - H5方法内通过
Bridge.call('camera', 'scan', { type: 'qrcode' })发起调用。 - 原生侧执行扫码逻辑,完成后回传结果。
- H5侧在
.then里拿到结果,或者用catch捕获错误。
这个流程覆盖了前面讲到的注入时机、消息协议、回调管理、线程切换、错误处理这几个关键环节。实际工程中的代码会比这个复杂很多,但骨架就是这些东西。你想要扩展更多能力,只需要在原生侧注册新的模块,在H5侧封装对应的调用方法,其余部分完全不用动。
7. 最后分享一点个人体会
最近这几年做移动端开发,跨端的方案越来越多,从早期的Hybrid到后来的React Native、Flutter、小程序容器。但无论是哪种方案,只要涉及到H5和原生数据的双向流通,JSBridge这套通信思想就一直存在。区别只是换了个壳:有的叫Channel,有的叫MethodChannel,有的叫NativeChannel。底层逻辑还是那几件事:消息格式化、通道建立、回调寻址、生命周期管理。
我自己动手写过几版JSBridge之后,最大的感受是:这个东西的难点从来不是“让JS调到原生”,而是“让整个调用过程稳定、可排查、可降级”。实际能跑通的方案很多,但真正生产级别好用的方案,拼的是对异常情况的处理——页面跳转、WebView销毁、线程切换、版本不匹配、老设备兼容、安全校验。
如果你正在做的项目里要维护一个JSBridge,建议从今天开始做两件事:第一,把完整的调用日志加上,至少日志要能把请求id串起来;第二,把错误码和版本号纳入协议设计,而不是等出事之后再补。这两件事在一开始不做,后面付出的代价是成倍的。按我踩坑的经验,JSBridge出问题的现场,从来不是“不会调用”,而是“调用成功但没人知道它到底经历了什么”。有日志、有协议、有版本,线上的诡异问题至少能砍掉一半。