🌉 JSBridge 框架全解析:Web 与 Native 的魔法翻译官

24 阅读6分钟

📖 故事背景:两个王国的贸易通道

想象在移动开发的大陆上,存在两个繁荣的王国:

  • Web 王国:擅长快速建造「数字商店」(网页),但缺乏制造「魔法道具」(调用摄像头、支付等原生功能)的技术

  • Native 帝国:拥有强大的「魔法工坊」,能制造各种神奇道具,但建造商店的速度很慢

为了实现互利共赢,两国决定在边境(WebView)上修建一座「贸易大桥」(JSBridge),让:

  • Web 商人能使用 Native 帝国的魔法道具
  • Native 工匠能快速搭建 Web 风格的商店并控制其行为

🏗️ 大桥的核心构造:双向通信通道

1. Native → Web:魔法道具的远程展示

Native 帝国要向 Web 王国展示魔法道具的效果,相当于「远程表演魔术」。

实现原理:Native 通过 WebView 执行 JS 代码,就像在 Web 王国的广场上立一块「直播屏幕」。

Android 代码示例

java

// Native 端调用 Web 端的 showWebDialog 方法
String magicShow = "展示火焰魔法";
String jsCode = String.format("window.showWebDialog('%s')", magicShow);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String result) {
        // Web 端返回的魔术反馈
        Log.d("JSBridge", "Web 观众反馈:" + result);
    }
});

Web 端接收代码

javascript

// Web 王国的魔术展示区
function showWebDialog(message) {
    alert("Native 帝国正在表演:" + message);
    // 可以返回观众的掌声(回调结果)
    return "观众欢呼!";
}

2. Web → Native:魔法道具的定制订单

Web 商人需要向 Native 帝国订购魔法道具,有两种「下单方式」。

📮 方式一:信使传信(URL Scheme 拦截)

Web 商人在信封上写下特殊地址(URL Scheme),Native 守卫拦截信件并处理订单。

Web 端下单代码

javascript

// 订购「扫码魔法」道具
function orderMagic(orderType, params) {
    // 生成特殊地址:jsbridge://magic/scan?params=...
    const url = `jsbridge://magic/${orderType}?${JSON.stringify(params)}`;
    // 用隐形信鸽(iframe)发送信件
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
    setTimeout(() => iframe.remove(), 100);
}

// 调用示例:订购扫码魔法
orderMagic("scan", { type: "二维码" });

Native 端收信处理

java

// Native 帝国的邮差(WebViewClient)
webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith("jsbridge://")) {
            // 解析信件内容
            String order = url.substring(12); // 去掉 jsbridge://
            String orderType = order.split("\?")[0];
            String params = order.split("\?")[1];
            
            // 处理订单:如果是扫码魔法
            if ("magic/scan".equals(orderType)) {
                showScanPage(params); // 打开扫码页面
            }
            return true; // 拦截成功,不再发送到 Web 邮局
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
});

🏢 方式二:设立办事处(注入 JS API)

Native 帝国在 Web 王国设立办事处(注入全局对象),Web 商人可直接上门下单。

Native 端设立办事处

java

// Native 帝国的办事处人员
class MagicOffice {
    private Context context;
    
    public MagicOffice(Context context) {
        this.context = context;
    }
    
    // 处理「打开宝箱」的订单
    @JavascriptInterface
    public void openChest(String chestType) {
        Toast.makeText(context, "Native 正在打开" + chestType + "宝箱", 
                      Toast.LENGTH_SHORT).show();
        // 打开宝箱后通知 Web 商人
        String jsCode = "window.onChestOpened('" + chestType + "宝箱已打开')";
        webView.evaluateJavascript(jsCode, null);
    }
}

// 把办事处注册到 Web 王国
webView.addJavascriptInterface(new MagicOffice(this), "NativeMagic");

Web 端上门下单

javascript

// Web 商人直接访问办事处
function orderChest(chestType) {
    // 调用 Native 办事处的 openChest 方法
    NativeMagic.openChest(chestType);
    
    // 注册宝箱打开的回调
    window.onChestOpened = function(result) {
        document.getElementById("result").textContent = result;
    };
}

// 调用示例:订购黄金宝箱
orderChest("黄金");

🧩 复杂订单的处理:带回调的双向通信

当 Web 商人订购需要定制的魔法道具时,需要「订单回执」机制。

1. Web 下订单时附带回执地址

javascript

// Web 端下单并等待回执
function orderCustomMagic(magicType, params, callback) {
    // 生成唯一订单号
    const orderId = "ORD" + Date.now();
    // 保存回执地址(回调函数)
    window.magicCallbacks[orderId] = callback;
    
    // 下单时附带订单号
    const data = {
        type: magicType,
        params: params,
        orderId: orderId
    };
    NativeMagic.processOrder(JSON.stringify(data));
}

// 注册回执处理中心
window.magicCallbacks = {};
window.onMagicResult = function(result) {
    const { orderId, data } = JSON.parse(result);
    if (window.magicCallbacks[orderId]) {
        // 按订单号找到对应的回执地址(回调函数)
        window.magicCallbacks[orderId](data);
        delete window.magicCallbacks[orderId]; // 销毁回执地址
    }
};

2. Native 处理订单并按地址回执

java

// Native 端处理带回执的订单
@JavascriptInterface
public void processOrder(String orderData) {
    try {
        JSONObject order = new JSONObject(orderData);
        String magicType = order.getString("type");
        String orderId = order.getString("orderId");
        
        // 处理魔法订单(耗时操作)
        new Handler().postDelayed(() -> {
            String result = "魔法[" + magicType + "]已制作完成";
            // 按订单号回执
            String jsCode = "window.onMagicResult('{" +
                    ""orderId":"" + orderId + ""," +
                    ""data":"" + result + ""}')";
            webView.evaluateJavascript(jsCode, null);
        }, 2000); // 模拟魔法制作时间
    } catch (JSONException e) {
        e.printStackTrace();
    }
}

🛡️ 大桥的安全与兼容性

1. Android 的版本兼容问题

  • 4.2 之前:使用 addJavascriptInterface 有安全漏洞,像年久失修的旧桥

  • 4.2 之后:新增 @JavascriptInterface 注解,如同加固后的新桥

java

// 兼容新旧桥的代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    // 新桥:使用 @JavascriptInterface 注解
    webView.addJavascriptInterface(new SafeMagicOffice(this), "SafeMagic");
} else {
    // 旧桥:使用拦截 prompt 的方式
    webView.setWebChromeClient(new WebChromeClient() {
        @Override
        public boolean onJsPrompt(WebView view, String url, String message, 
                                  String defaultValue, JsPromptResult result) {
            if (message.startsWith("JSBridge:")) {
                // 解析旧桥的通信内容
                String data = message.substring(9);
                handleLegacyBridge(data);
                result.confirm("旧桥已收到");
                return true;
            }
            return super.onJsPrompt(view, url, message, defaultValue, result);
        }
    });
}

2. URL Scheme 的长度限制

太长的订单内容(URL 参数)会导致信件超重,解决方案:

  • 使用 POST 方式传递大数据(部分 WebView 支持)
  • 分拆成多封小信件(分段传输)
  • 优先使用办事处方式(注入 API)传递复杂数据

🌐 实战:完整的 JSBridge 框架封装

1. Native 端框架核心

java

public class JSBridge {
    private WebView webView;
    private Context context;
    private static JSBridge instance;
    
    private JSBridge(Context context, WebView webView) {
        this.context = context;
        this.webView = webView;
        initBridge();
    }
    
    public static JSBridge getInstance(Context context, WebView webView) {
        if (instance == null) {
            instance = new JSBridge(context, webView);
        }
        return instance;
    }
    
    private void initBridge() {
        // 注册办事处(注入 API)
        webView.addJavascriptInterface(new BridgeInterface(), "JSBridge");
        
        // 配置信鸽拦截器(URL Scheme)
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                if (url.startsWith("jsbridge://")) {
                    handleUrlScheme(url);
                    return true;
                }
                return super.shouldOverrideUrlLoading(view, url);
            }
        });
    }
    
    // 处理信鸽信件(URL Scheme)
    private void handleUrlScheme(String url) {
        // 解析 URL 中的命令和参数
        // ... 省略解析逻辑
        String action = "默认命令";
        String params = "{}";
        
        // 分发命令到对应的处理函数
        dispatchAction(action, params);
    }
    
    // 命令分发中心
    private void dispatchAction(String action, String params) {
        if ("showCamera".equals(action)) {
            openCamera(params);
        } else if ("pay".equals(action)) {
            processPayment(params);
        }
        // 其他命令处理...
    }
    
    // 桥接接口实现
    class BridgeInterface {
        @JavascriptInterface
        public void callNative(String action, String params) {
            dispatchAction(action, params);
        }
    }
    
    // 打开相机的 Native 实现
    private void openCamera(String params) {
        // 启动相机 Intent
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        ((Activity) context).startActivityForResult(intent, 1001);
    }
    
    // 处理支付的 Native 实现
    private void processPayment(String params) {
        // 调用支付 SDK
        Log.d("JSBridge", "处理支付请求:" + params);
        // 支付完成后通知 Web 端
        String jsCode = "window.onPaymentResult('支付成功')";
        webView.evaluateJavascript(jsCode, null);
    }
}

2. Web 端框架核心

javascript

// Web 端 JSBridge 框架
window.JSBridge = {
    // 命令队列(处理桥未就绪的情况)
    commandQueue: [],
    isBridgeReady: false,
    
    // 初始化桥接
    init: function() {
        // 检查 Native 办事处是否存在
        if (window.JSBridge_Native) {
            this.isBridgeReady = true;
            // 发送积压的命令
            this.commandQueue.forEach(cmd => this.executeCommand(cmd));
            this.commandQueue = [];
        } else {
            // 桥未就绪,500ms 后重试
            setTimeout(() => this.init(), 500);
        }
    },
    
    // 执行命令
    executeCommand: function(command) {
        if (this.isBridgeReady) {
            // 直接调用 Native 办事处
            window.JSBridge_Native.callNative(command.action, command.params);
        } else {
            // 命令入队等待
            this.commandQueue.push(command);
        }
    },
    
    // 调用 Native 功能
    call: function(action, params, callback) {
        // 生成唯一回调 ID
        const callbackId = "CB_" + Date.now();
        window.JSBridge_callbacks[callbackId] = callback;
        
        // 构造命令参数
        const command = {
            action: action,
            params: JSON.stringify({...params, callbackId: callbackId})
        };
        
        this.executeCommand(command);
    }
};

// 注册回调处理中心
window.JSBridge_callbacks = {};
window.onJSBridgeResult = function(result) {
    const { callbackId, data } = JSON.parse(result);
    if (window.JSBridge_callbacks[callbackId]) {
        window.JSBridge_callbacks[callbackId](data);
        delete window.JSBridge_callbacks[callbackId];
    }
};

// 初始化桥接
JSBridge.init();

📌 总结:JSBridge 的核心要点

  1. 双向通信的本质

    • Native→Web:通过 WebView 执行 JS 代码,如同远程操控
    • Web→Native:通过 URL 拦截或注入 API,如同写信或上门拜访
  2. 回调机制的实现

    • 核心是「唯一标识 + 回调映射」,就像订单号对应回执地址
  3. 实际应用建议

    • 优先使用注入 API 方式(办事处),性能和体验更优

    • 复杂数据传输避免使用 URL Scheme(信鸽),防止信件超重

    • 注意 Android 版本兼容,4.2 后必须使用 @JavascriptInterface 注解

通过这座「JSBridge 贸易大桥」,Web 王国和 Native 帝国实现了互利共赢,就像现实中 Hybrid 开发通过 JSBridge 结合了 Web 的灵活和 Native 的强大能力。掌握了这座桥的构造原理,你就能在移动开发的大陆上自由穿梭啦! 🌉