【webview】原生 App 与 H5 双向通信完全指南:JSBridge 原理与实战

目录

[一、Hybrid 通信整体概述](#一、Hybrid 通信整体概述)

二、两大通信方向底层原理

[2.1 原生调用 H5(App → H5)](#2.1 原生调用 H5(App → H5))

通信原理

[H5 基础回调示例](#H5 基础回调示例)

[两端原生调用 API 对比](#两端原生调用 API 对比)

[2.2 H5 调用原生(H5 → App)](#2.2 H5 调用原生(H5 → App))

[2.2.1 Android:addJavascriptInterface 对象注入](#2.2.1 Android:addJavascriptInterface 对象注入)

[2.2.2 iOS:WKScriptMessageHandler 消息通道](#2.2.2 iOS:WKScriptMessageHandler 消息通道)

三、双平台原生完整实现代码

[3.1 Android Kotlin 端实现](#3.1 Android Kotlin 端实现)

[3.2 iOS Swift 端实现](#3.2 iOS Swift 端实现)

[四、通用跨平台 JSBridge 封装(bridge.js)](#四、通用跨平台 JSBridge 封装(bridge.js))

[4.1 设计思路](#4.1 设计思路)

[4.2 完整源码](#4.2 完整源码)

五、业务层实战调用示例(main.js)

[原生主动回调 H5 执行语句](#原生主动回调 H5 执行语句)

六、生产环境避坑注意事项

七、全文总结


一、Hybrid 通信整体概述

在混合开发(Hybrid App)场景中,H5 页面嵌入 App WebView 后,存在两类核心通信需求:

  1. H5 主动调用原生:唤起相册、定位、支付、弹窗等 App 原生能力;
  2. 原生主动回调 H5:原生处理完成业务后,将结果数据回传给页面渲染。

Android、iOS 两套 WebView 底层通信 API 完全不兼容,直接业务层写分支判断会造成大量冗余代码。本文从底层原理出发,实现一套屏蔽平台差异、支持 Promise 异步、可直接上生产的通用 JSBridge 方案。

二、两大通信方向底层原理

2.1 原生调用 H5(App → H5)

通信原理

H5 提前在 window 全局挂载约定名称的回调函数,Android /iOS 通过 WebView 提供的 evaluateJavascript / evaluateJavaScript 动态执行 JS 代码,携带业务数据调用全局函数。

H5 基础回调示例
javascript 复制代码
// 防止页面重复注册覆盖函数
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 对象注入
  1. 原生通过 addJavascriptInterface 将 Java/Kotlin 对象注入 H5 window;
  2. H5 通过 window.[注入别名].[方法名]() 调用原生逻辑;
  3. 限制:参数仅支持字符串,H5 必须手动 JSON.stringify 序列化对象;
  4. 强制要求:暴露给 JS 的方法必须添加 @JavascriptInterface 安全注解。
2.2.2 iOS:WKScriptMessageHandler 消息通道
  1. WKWebView 注册 WKUserContentController 消息处理器;
  2. H5 使用系统内置固定 API:window.webkit.messageHandlers.[名称].postMessage()
  3. 优势:可直接传递 JS 对象,无需强制转 JSON 字符串;
  4. 限制:仅 WKWebView 支持,废弃 UIWebView 无此能力。

三、双平台原生完整实现代码

3.1 Android Kotlin 端实现

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 端实现

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 设计思路

  1. 使用 IIFE 闭包隔离内部变量,不污染全局;
  2. UA 自动识别 Android /iOS/macOS / Windows / Linux 环境;
  3. 底层统一分发双端通信逻辑,业务层无需区分平台;
  4. 基于 CallbackId + Promise 实现异步回调,解决回调地狱;
  5. 全局缓存回调函数,原生回调后自动销毁,避免内存泄漏;
  6. 兼容 CommonJS 打包与浏览器直接引入两种使用方式。

4.2 完整源码

javascript 复制代码
/**
 * 跨平台通用 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)

javascript 复制代码
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:

javascript 复制代码
// Android / iOS 统一执行脚本示例
window.Bridge.onCallback("cb_get_user", '{"code":0,"data":{"name":"张三","userId":10001}}')

六、生产环境避坑注意事项

  1. Android 安全强制注解 所有暴露给 JS 的方法必须添加 @JavascriptInterface,Android 4.2+ 无注解会直接拦截调用,出现无响应。
  2. 数据格式兼容问题 Android 仅支持字符串参数,H5 调用原生方法必须 JSON.stringify;iOS postMessage 可直接传对象,但为双端统一建议全部序列化。
  3. 回调内存泄漏 异步请求回调执行完成后必须删除全局缓存的 resolve/reject,页面频繁刷新、多次请求会造成内存堆积。
  4. 命名强约定 H5 与原生的桥对象名、方法名、回调字段 __callbackId__ 必须完全一致,大小写敏感。
  5. WebView 生命周期 页面销毁前清空全局 window.__bridge_callbacks__,避免页面卸载后原生回调空白页面报错。
  6. iOS 废弃 UIWebView UIWebView 无 webkit.messageHandlers 通道,必须使用 WKWebView。

七、全文总结

  1. 原生调 H5:依靠 WebView 执行 JS,调用 window 全局挂载的回调函数;
  2. H5 调原生:Android 对象注入、iOS WKWebView 消息通道,两套底层 API 完全隔离;
  3. 工程化方案 :通过 JSBridge 闭包封装屏蔽平台差异,对外提供统一 invoke / invokeAsync
  4. 异步闭环 :CallbackId 映射 Promise 缓存,原生通过统一 onCallback 分发结果,替代传统多层回调;
  5. 落地价值:一套 JS 代码同时兼容双端,业务层无需写大量平台判断,可直接投入线上生产。