JS 与原生 App 交互方法总结

34 阅读6分钟

JS 与原生 App 交互方法总结

一、概述

JS 与原生 App(Android/iOS)交互是混合开发(Hybrid App)的核心技术,主要实现 Web 页面与原生功能的双向通信。

二、主要交互方法

目前主流的 JS 与原生 App 交互方法有三种:

  1. URL Scheme 方式
  2. 注入 API 方式
  3. WebViewJavascriptBridge 方式(消息队列机制)

三、详细分析

1. URL Scheme 方式

底层原理
  • 通过构造自定义 URL Scheme(如 jsbridge://method?params=...
  • 利用 WebView 的 URL 加载拦截机制
  • 解析 URL 获取方法名和参数,执行对应原生功能
调用顺序
JS 端 → 构造 URL → 设置 window.location.hrefWebView 拦截 URL → 原生解析处理 → 返回结果
代码示例

JS 端代码

// 构造并触发 URL Scheme
function callNative(method, params = {}) {
    const url = `jsbridge://${method}?params=${encodeURIComponent(JSON.stringify(params))}`;
    window.location.href = url;
}

// 接收原生返回结果(由原生通过 evaluateJavascript 调用)
function nativeCallback(method, result) {
    console.log(`原生响应 ${method}:`, result);
    // 处理返回结果
}

// 调用示例
callNative('share', { title: '分享标题', content: '分享内容' });

Android 端代码

// 设置 WebViewClient 拦截 URL
webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith("jsbridge://")) {
            handleJSBridgeCall(url);
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
});

// 处理 JSBridge 调用
private void handleJSBridgeCall(String url) {
    // 解析 URL
    Uri uri = Uri.parse(url);
    String method = uri.getHost();
    String paramsStr = uri.getQueryParameter("params");
    JSONObject params = new JSONObject(paramsStr);
    
    // 根据方法名处理
    JSONObject result = new JSONObject();
    if ("share".equals(method)) {
        // 执行分享逻辑
        result.put("success", true);
        result.put("message", "分享成功");
    }
    
    // 返回结果给 JS
    String js = String.format("javascript:nativeCallback('%s', JSON.parse('%s'));", 
        method, result.toString());
    webView.evaluateJavascript(js, null);
}

iOS 端代码

// WKWebView 拦截 URL
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let url = navigationAction.request.url, url.scheme == "jsbridge" {
        handleJSBridgeCall(url: url)
        decisionHandler(.cancel) // 取消默认加载
        return
    }
    decisionHandler(.allow)
}

// 处理 JSBridge 调用
private func handleJSBridgeCall(url: URL) {
    let method = url.host()
    let paramsStr = url.queryParameters?["params"] ?? "{}"
    let params = try? JSONSerialization.jsonObject(with: paramsStr.data(using: .utf8)!, options: []) as? [String: Any]
    
    // 根据方法名处理
    var result = [String: Any]()
    if method == "share" {
        // 执行分享逻辑
        result = ["success": true, "message": "分享成功"]
    }
    
    // 返回结果给 JS
    let resultJson = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted)
    let resultStr = String(data: resultJson!, encoding: .utf8)! 
    let js = "nativeCallback('\(method!)', JSON.parse('\(resultStr)'))"
    webView.evaluateJavaScript(js, completionHandler: nil)
}

2. 注入 API 方式

底层原理
  • 通过 WebView 提供的 API 向 JS 环境注入原生对象
  • JS 直接调用注入对象的方法
  • 支持同步/异步调用,依赖平台提供的安全机制
调用顺序
原生 → 向 JS 环境注入对象 → JS 直接调用对象方法 → 原生执行对应功能 → 返回结果
代码示例

JS 端代码

// 调用原生注入的对象方法
if (window.nativeBridge) {
    // 同步调用示例
    const result = window.nativeBridge.getDeviceInfo();
    console.log('设备信息:', result);
    
    // 异步调用示例(通过回调)
    window.nativeBridge.share({
        title: '分享标题',
        content: '分享内容'
    }, (success) => {
        console.log('分享结果:', success);
    });
}

Android 端代码

// 创建要注入的对象
public class NativeBridge {
    private Context context;
    private WebView webView;
    
    public NativeBridge(Context context, WebView webView) {
        this.context = context;
        this.webView = webView;
    }
    
    // 供 JS 调用的方法必须添加此注解
    @JavascriptInterface
    public String getDeviceInfo() {
        // 获取设备信息
        JSONObject info = new JSONObject();
        info.put("device", "Android Device");
        info.put("os", "Android 12");
        return info.toString();
    }
    
    @JavascriptInterface
    public void share(String paramsStr, final String callback) {
        JSONObject params = new JSONObject(paramsStr);
        String title = params.optString("title");
        String content = params.optString("content");
        
        // 执行分享逻辑
        boolean success = true;
        
        // 通过 JS 回调返回结果
        String js = String.format("javascript:eval('%s')(%b);", callback, success);
        webView.post(() -> webView.evaluateJavascript(js, null));
    }
}

// 注入对象到 JS 环境
webView.addJavascriptInterface(new NativeBridge(this, webView), "nativeBridge");

iOS 端代码

// 创建 JavaScript 桥接对象
class NativeBridge: NSObject {
    private weak var webView: WKWebView?
    
    init(webView: WKWebView) {
        self.webView = webView
    }
    
    // 暴露给 JS 的方法
    @objc func getDeviceInfo(_ completion: @escaping (String) -> Void) {
        // 获取设备信息
        let info = [
            "device": "iOS Device",
            "os": "iOS 16"
        ]
        let jsonData = try? JSONSerialization.data(withJSONObject: info, options: [])
        let jsonStr = String(data: jsonData!, encoding: .utf8)! 
        completion(jsonStr)
    }
    
    @objc func share(_ params: [String: Any], completion: @escaping (Bool) -> Void) {
        let title = params["title"] as? String ?? ""
        let content = params["content"] as? String ?? ""
        
        // 执行分享逻辑
        let success = true
        completion(success)
    }
}

// 配置 WKWebView
let configuration = WKWebViewConfiguration()
let nativeBridge = NativeBridge(webView: webView)
configuration.userContentController.add(nativeBridge, name: "nativeBridge")

// JS 调用示例(WKScriptMessageHandler)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "nativeBridge" {
        let data = message.body as! [String: Any]
        let method = data["method"] as! String
        let params = data["params"] as! [String: Any]
        let callbackId = data["callbackId"] as! String
        
        // 根据方法名处理
        if method == "share" {
            share(params) { success in
                let js = "nativeBridge.callback('\(callbackId)', \(success))"
                self.webView?.evaluateJavaScript(js, completionHandler: nil)
            }
        }
    }
}

3. WebViewJavascriptBridge 方式

底层原理
  • 基于消息队列机制,通过 window.prompt/console.log 等方式传递消息
  • 在 JS 环境中创建桥接对象,统一通信接口
  • 支持异步回调,解决了参数限制和安全问题
调用顺序
JS → 创建 Bridge 对象 → 注册处理函数 → 调用 bridge.callHandler → 构造消息 → 通过 prompt 发送 → 原生拦截 → 处理消息 → 调用 bridge.registerHandler 注册的处理函数
代码示例

JS 端代码

// Bridge 初始化
(function() {
    if (window.WebViewJavascriptBridge) return;
    
    window.WebViewJavascriptBridge = {
        callHandler: function(handlerName, data, callback) {
            const message = {
                handlerName: handlerName,
                data: data || {},
                callbackId: 'cb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
            };
            
            if (callback) {
                this._callbacks[message.callbackId] = callback;
            }
            
            // 通过 prompt 发送消息
            window.prompt('WebViewJavascriptBridge:' + JSON.stringify(message), '');
        },
        
        registerHandler: function(handlerName, handler) {
            this._handlers[handlerName] = handler;
        },
        
        _callbacks: {},
        _handlers: {}
    };
    
    // 触发初始化完成事件
    const event = new Event('WebViewJavascriptBridgeReady');
    document.dispatchEvent(event);
})();

// 调用原生方法
window.WebViewJavascriptBridge.callHandler('share', {
    title: '分享标题',
    content: '分享内容'
}, function(response) {
    console.log('分享结果:', response);
});

// 注册供原生调用的方法
window.WebViewJavascriptBridge.registerHandler('onLoginSuccess', function(data) {
    console.log('登录成功:', data);
    // 处理登录成功逻辑
});

Android 端代码

// 设置 WebChromeClient 拦截 prompt
webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        if (message.startsWith("WebViewJavascriptBridge:")) {
            String jsonStr = message.substring("WebViewJavascriptBridge:".length());
            handleBridgeMessage(jsonStr);
            result.confirm("success");
            return true;
        }
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
});

// 处理 Bridge 消息
private void handleBridgeMessage(String jsonStr) {
    JSONObject message = new JSONObject(jsonStr);
    String handlerName = message.optString("handlerName");
    JSONObject data = message.optJSONObject("data");
    String callbackId = message.optString("callbackId");
    
    // 根据 handlerName 处理
    JSONObject result = new JSONObject();
    if ("share".equals(handlerName)) {
        // 执行分享逻辑
        result.put("success", true);
        result.put("message", "分享成功");
    }
    
    // 调用 JS 回调
    if (!callbackId.isEmpty()) {
        String js = String.format(
            "javascript:window.WebViewJavascriptBridge._callbacks['%s'](JSON.parse('%s'));delete window.WebViewJavascriptBridge._callbacks['%s'];",
            callbackId, result.toString(), callbackId
        );
        webView.evaluateJavascript(js, null);
    }
}

// 调用 JS 方法
private void callJsMethod() {
    JSONObject data = new JSONObject();
    data.put("userId", "123456");
    data.put("token", "abcdef123456");
    
    String js = String.format(
        "javascript:window.WebViewJavascriptBridge._handlers['onLoginSuccess'](JSON.parse('%s'));",
        data.toString()
    );
    webView.evaluateJavascript(js, null);
}

iOS 端代码

// 设置 WKUIDelegate 拦截 prompt
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
    if prompt.hasPrefix("WebViewJavascriptBridge:") {
        let jsonStr = prompt.dropFirst("WebViewJavascriptBridge:".count)
        handleBridgeMessage(jsonStr: String(jsonStr))
        completionHandler("success")
        return
    }
    completionHandler(nil)
}

// 处理 Bridge 消息
private func handleBridgeMessage(jsonStr: String) {
    let message = try? JSONSerialization.jsonObject(with: jsonStr.data(using: .utf8)!, options: []) as? [String: Any]
    let handlerName = message?["handlerName"] as? String
    let data = message?["data"] as? [String: Any]
    let callbackId = message?["callbackId"] as? String
    
    // 根据 handlerName 处理
    var result = [String: Any]()
    if handlerName == "share" {
        // 执行分享逻辑
        result = ["success": true, "message": "分享成功"]
    }
    
    // 调用 JS 回调
    if let callbackId = callbackId {
        let resultJson = try? JSONSerialization.data(withJSONObject: result, options: .prettyPrinted)
        let resultStr = String(data: resultJson!, encoding: .utf8)! 
        let js = """
        window.WebViewJavascriptBridge._callbacks['\(callbackId)'](JSON.parse('\(resultStr)'));
        delete window.WebViewJavascriptBridge._callbacks['\(callbackId)'];
        """
        webView.evaluateJavaScript(js, completionHandler: nil)
    }
}

// 调用 JS 方法
private func callJsMethod() {
    let data = [
        "userId": "123456",
        "token": "abcdef123456"
    ]
    let dataJson = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
    let dataStr = String(data: dataJson!, encoding: .utf8)! 
    let js = "window.WebViewJavascriptBridge._handlers['onLoginSuccess'](JSON.parse('\(dataStr)'))"
    webView.evaluateJavaScript(js, completionHandler: nil)
}

四、三种方法的优缺点比较

方法优点缺点适用场景
URL Scheme兼容性好,实现简单,无需修改原生代码结构参数长度有限制,安全性低,易被拦截简单交互,旧版本兼容
注入 API使用简单,调用方便,性能好Android 4.2 以下有安全漏洞,参数类型限制多高性能要求,简单交互
WebViewJavascriptBridge功能完善,支持异步回调,安全性高,参数灵活实现复杂,需要引入框架或自行实现复杂交互,高安全性要求

五、最佳实践建议

  1. 优先选择 WebViewJavascriptBridge:功能完善,安全性高,是目前主流方案
  2. 安全第一:无论使用哪种方式,都要进行参数验证和过滤
  3. 统一接口:定义清晰的通信协议,规范方法名和参数格式
  4. 错误处理:完善的错误处理机制,提高稳定性
  5. 性能优化:避免频繁通信,批量处理请求

六、常用框架推荐

  • WebViewJavascriptBridge:iOS/Android 跨平台支持
  • DSBridge:更强大的跨平台 JS Bridge 框架
  • EasyJSWebView:轻量级的 JS Bridge 实现
  • Cordova/PhoneGap:成熟的混合开发框架,内置完善的 JS Bridge 机制