【webview】原生 App 与 H5 双向通信完全指南:JSBridge 原理与实战
目录
一、Hybrid 通信整体概述
二、两大通信方向底层原理
2.1 原生调用 H5(App → H5)
通信原理
H5 基础回调示例
两端原生调用 API 对比
2.2 H5 调用原生(H5 → App)
2.2.1 Android:addJavascriptInterface 对象注入
2.2.2 iOS:WKScriptMessageHandler 消息通道
三、双平台原生完整实现代码
3.1 Android Kotlin 端实现
3.2 iOS Swift 端实现
四、通用跨平台 JSBridge 封装(bridge.js)
4.1 设计思路
4.2 完整源码
五、业务层实战调用示例(main.js)
原生主动回调 H5 执行语句
六、生产环境避坑注意事项
七、全文总结
一、Hybrid 通信整体概述
在混合开发(Hybrid App)场景中,H5 页面嵌入 App WebView 后,存在两类核心通信需求:
- H5 主动调用原生:唤起相册、定位、支付、弹窗等 App 原生能力;
- 原生主动回调 H5:原生处理完成业务后,将结果数据回传给页面渲染。
Android、iOS 两套 WebView 底层通信 API 完全不兼容,直接业务层写分支判断会造成大量冗余代码。本文从底层原理出发,实现一套屏蔽平台差异、支持 Promise 异步、可直接上生产的通用 JSBridge 方案。
二、两大通信方向底层原理
2.1 原生调用 H5(App → H5)
通信原理
H5 提前在window全局挂载约定名称的回调函数,Android /iOS 通过 WebView 提供的evaluateJavascript/evaluateJavaScript动态执行 JS 代码,携带业务数据调用全局函数。
H5 基础回调示例
// 防止页面重复注册覆盖函数 if (!window.nativeCallback) { /** * 原生统一回调入口 * @param {string|object} res 原生返回数据 */ window.nativeCallback = function (res) { // 兼容 Android JSON字符串 / iOS JS对象两种格式 const data = typeof res === 'string' ? JSON.parse(res) : res; console.log('接收原生回调数据:', data); } }两端原生调用 API 对比
| 平台 | 核心 API | 调用示例 |
|---|---|---|
| Android | evaluateJavascript | webView.evaluateJavascript("window.nativeCallback('{\"code\":0}')", null) |
| iOS | evaluateJavaScript | webView.evaluateJavaScript("window.nativeCallback({\"code\":0})") |
2.2 H5 调用原生(H5 → App)
2.2.1 Android:addJavascriptInterface 对象注入
- 原生通过
addJavascriptInterface将 Java/Kotlin 对象注入 H5 window; - H5 通过
window.[注入别名].[方法名]()调用原生逻辑; - 限制:参数仅支持字符串,H5 必须手动
JSON.stringify序列化对象; - 强制要求:暴露给 JS 的方法必须添加
@JavascriptInterface安全注解。
2.2.2 iOS:WKScriptMessageHandler 消息通道
- WKWebView 注册
WKUserContentController消息处理器; - H5 使用系统内置固定 API:
window.webkit.messageHandlers.[名称].postMessage(); - 优势:可直接传递 JS 对象,无需强制转 JSON 字符串;
- 限制:仅 WKWebView 支持,废弃 UIWebView 无此能力。
三、双平台原生完整实现代码
3.1 Android Kotlin 端实现
import android.webkit.JavascriptInterface import android.webkit.WebView import com.google.gson.Gson class WebBridge(private val webView: WebView) { private val gson = Gson() init { // 将原生对象注入window,H5通过 window.NativeBridge 访问 webView.addJavascriptInterface(BridgeObj(), "NativeBridge") } inner class BridgeObj { /** * H5调用获取用户信息 * @param jsonStr H5传递的JSON字符串参数 */ @JavascriptInterface fun getUserInfo(jsonStr: String) { // 解析H5参数 val params = gson.fromJson(jsonStr, Map::class.java) println("收到H5请求:$params") // 模拟业务逻辑,组装返回数据 val result = mapOf( "code" to 0, "data" to mapOf("name" to "张三", "userId" to 10001) ) val callbackJson = gson.toJson(result) // 调用H5全局回调,回传数据 webView.evaluateJavascript("window.Bridge.onCallback('cb_get_user', '$callbackJson')", null) } /** * H5调用弹出Toast提示 */ @JavascriptInterface fun showToast(jsonStr: String) { val params = gson.fromJson(jsonStr, Map::class.java) val msg = params["message"] ?: "" println("Toast提示:$msg") } } }3.2 iOS Swift 端实现
import WebKit class WebViewController: UIViewController, WKScriptMessageHandler { var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() let config = WKWebViewConfiguration() // 注册消息处理器,名称与H5约定 NativeBridge config.userContentController.add(self, name: "NativeBridge") webView = WKWebView(frame: view.bounds, configuration: config) view.addSubview(webView) } // 接收H5 postMessage 消息统一入口 func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == "NativeBridge", let payload = message.body as? [String: Any], let funcName = payload["funcName"] as? String else { return } switch funcName { case "getUserInfo": let callbackId = payload["__callbackId__"] as! String let res: [String: Any] = [ "code": 0, "data": ["name": "张三", "userId": 10001] ] // 回调H5 let jsonData = try! JSONSerialization.data(withJSONObject: res) let jsonStr = String(data: jsonData, encoding: .utf8)! webView.evaluateJavaScript("window.Bridge.onCallback('\(callbackId)', '\(jsonStr)')") case "showToast": guard let msg = payload["message"] as? String else { return } print("Toast提示:\(msg)") default: break } } }四、通用跨平台 JSBridge 封装(bridge.js)
4.1 设计思路
- 使用 IIFE 闭包隔离内部变量,不污染全局;
- UA 自动识别 Android /iOS/macOS / Windows / Linux 环境;
- 底层统一分发双端通信逻辑,业务层无需区分平台;
- 基于 CallbackId + Promise 实现异步回调,解决回调地狱;
- 全局缓存回调函数,原生回调后自动销毁,避免内存泄漏;
- 兼容 CommonJS 打包与浏览器直接引入两种使用方式。
4.2 完整源码
/** * 跨平台通用 JSBridge 通信模块 * 兼容 Android / iOS / macOS / Windows / Linux * 能力:同步调用、Promise异步回调、环境检测、原生回调分发 */ const Bridge = (() => { // 1. 识别当前运行系统 const detectOS = () => { const ua = navigator.userAgent.toLowerCase(); const platform = navigator.platform.toLowerCase(); if (/android/i.test(ua)) return 'android'; if (/iphone|ipad|ipod/i.test(ua)) return 'ios'; if (/macintosh|macintel/i.test(platform)) return 'macos'; if (/win32|windows/i.test(platform)) return 'windows'; if (/linux/i.test(platform) && !/android/i.test(ua)) return 'linux'; return 'unknown'; }; // 2. 底层分发:统一调用原生方法 const _invokeNative = (objName, funcName, data) => { const os = detectOS(); const jsonString = typeof data === 'string' ? data : JSON.stringify(data); // Android / Windows / Linux 注入对象调用 if (['android', 'windows', 'linux'].includes(os)) { const nativeObj = window[objName]; if (nativeObj && typeof nativeObj[funcName] === 'function') { nativeObj[funcName](jsonString); return true; } } // iOS / macOS webkit messageHandler if (['ios', 'macos'].includes(os)) { const handler = window.webkit?.messageHandlers?.[objName]; if (handler && typeof handler.postMessage === 'function') { handler.postMessage(jsonString); return true; } } console.warn(`[Bridge Warn] 当前环境${os}不支持 ${objName}.${funcName}`); return false; }; // 对外暴露API return { /** * 同步单向调用原生,无需返回结果 * @param {string} objName 桥接对象名 * @param {string} funcName 原生方法名 * @param {object|string} data 请求参数 * @returns {boolean} 是否成功发起调用 */ invoke(objName, funcName, data = {}) { return _invokeNative(objName, funcName, data); }, /** * 异步调用原生,Promise 返回业务结果 * @param {string} objName * @param {string} funcName * @param {object} data * @param {string} [callbackId] 自定义回调标识,不传自动生成 * @returns {Promise<any>} */ invokeAsync(objName, funcName, data = {}, callbackId) { return new Promise((resolve, reject) => { // 生成唯一回调ID const payload = { ...data, funcName, __callbackId__: callbackId || `cb_${Date.now()}_${Math.random().toString(36).slice(2)}` }; // 全局缓存Promise回调 window.__bridge_callbacks__ = window.__bridge_callbacks__ || {}; window.__bridge_callbacks__[payload.__callbackId__] = { resolve, reject }; const invokeSuccess = _invokeNative(objName, funcName, payload); if (!invokeSuccess) { delete window.__bridge_callbacks__[payload.__callbackId__]; reject(new Error('Native Bridge 环境不存在')); } }); }, /** * 原生统一回调入口,由App主动执行 * @param {string} callbackId 异步请求唯一标识 * @param {string|object} resultJson 原生返回数据 */ onCallback(callbackId, resultJson) { const cbCache = window.__bridge_callbacks__?.[callbackId]; if (!cbCache) return; // 统一格式化返回数据 const result = typeof resultJson === 'string' ? JSON.parse(resultJson) : resultJson; // 约定协议:code=0 成功,其余失败 if (result.code === 0 || result.success) { cbCache.resolve(result.data); } else { cbCache.reject(result); } // 销毁缓存,防止内存泄漏 delete window.__bridge_callbacks__[callbackId]; }, /** 获取当前系统标识 */ getOS: detectOS }; })(); // 兼容打包工具导出 if (typeof module !== 'undefined' && module.exports) { module.exports = Bridge; }五、业务层实战调用示例(main.js)
import Bridge from './bridge.js'; // 同步单向调用(仅通知原生,不需要返回) function showNativeToast() { Bridge.invoke('NativeBridge', 'showToast', { message: '操作成功', duration: 2000 }); } // Promise 异步调用,等待原生返回数据 async function fetchUserInfo() { try { const user = await Bridge.invokeAsync( 'NativeBridge', 'getUserInfo', {}, 'cb_get_user' ); console.log('获取用户信息成功:', user); } catch (err) { console.error('获取用户信息失败:', err); } } // 平台差异化UI适配 function adaptSafeArea() { if (Bridge.getOS() === 'ios') { // iPhone底部小黑条安全区适配 document.body.style.paddingBottom = '34px'; } } // 页面初始化执行 document.addEventListener('DOMContentLoaded', () => { adaptSafeArea(); showNativeToast(); fetchUserInfo(); });原生主动回调 H5 执行语句
App 处理完业务后,执行下面 JS 将结果回传给页面 Promise:
// Android / iOS 统一执行脚本示例 window.Bridge.onCallback("cb_get_user", '{"code":0,"data":{"name":"张三","userId":10001}}')六、生产环境避坑注意事项
- Android 安全强制注解所有暴露给 JS 的方法必须添加
@JavascriptInterface,Android 4.2+ 无注解会直接拦截调用,出现无响应。 - 数据格式兼容问题Android 仅支持字符串参数,H5 调用原生方法必须
JSON.stringify;iOS postMessage 可直接传对象,但为双端统一建议全部序列化。 - 回调内存泄漏异步请求回调执行完成后必须删除全局缓存的 resolve/reject,页面频繁刷新、多次请求会造成内存堆积。
- 命名强约定H5 与原生的桥对象名、方法名、回调字段
__callbackId__必须完全一致,大小写敏感。 - WebView 生命周期页面销毁前清空全局
window.__bridge_callbacks__,避免页面卸载后原生回调空白页面报错。 - iOS 废弃 UIWebViewUIWebView 无
webkit.messageHandlers通道,必须使用 WKWebView。
七、全文总结
- 原生调 H5:依靠 WebView 执行 JS,调用 window 全局挂载的回调函数;
- H5 调原生:Android 对象注入、iOS WKWebView 消息通道,两套底层 API 完全隔离;
- 工程化方案:通过 JSBridge 闭包封装屏蔽平台差异,对外提供统一
invoke/invokeAsync; - 异步闭环:CallbackId 映射 Promise 缓存,原生通过统一
onCallback分发结果,替代传统多层回调; - 落地价值:一套 JS 代码同时兼容双端,业务层无需写大量平台判断,可直接投入线上生产。
