你真的了解JSBridge吗?

23 阅读4分钟

前言

如果在此之前你从事过H5/Hybrid开发,那么你一定听说过JSBridge这个概念,简单理解就是:一个提供给H5页面去调用原生能力的工具,同时还能从原生应用中传递数据到H5页面。那么在实际应用中这层能力究竟是如何实现的呢?下面我将以IOS应用中的H5开发为例,构建一个简单的JSBridge.

搭建一个简易JSBridge

定义规范

明确在H5中JSBridge的接口规范

/** JSBridge */

interface IH5JSBridgeCallOptions {
    /** 原生能力对应方法名称 */
    funcKey: string;
    /** 调用方法的参数(可选) */
    params?: Record<string, unknown>;
}

interface Window {
    /** 定义JSBridge在H5暴露的全局对象 */
    H5JSBridge: {
        /** 提供call方法供H5调用指定原生能力 */
        call: <T>(
          options: IH5JSBridgeCallOptions, 
          /** 通过回调获取调用原生能力的返回结果 */
          callback?: (response: T) =>void
        ) => void,
        /** 其他属性/方法 */
    }
}

注入JS脚本

  1. JS脚本
const GlobalCallbackKey = "__js_bridge_callback__";
window[GlobalCallbackKey] = window[GlobalCallbackKey] || {};

window.H5JSBridge = {
  call: (options, callback) => {
    const { funcKey } = options || {};
    
    if (!funcKey) {
      throw new Error("funcKey params must be given.");
    }
    
    if (!callback || typeof callback !== "function") {
      
      window.webkit.messageHandlers.nativeBridge.postMessage(options);
      return;
    }
    
    // 生成一个callbackId简易,作为key存储callback
    const callbackId = `__js_bridge_cb_${Date.now()}_${Math.floor(Math.random() * 10)}__`;

    
    window[GlobalCallbackKey][callbackId] = callback;
    
    window.webkit.messageHandlers.nativeBridge.postMessage(
      Object.assign({}, options, { __callbackId: callbackId }),
    );
  },
}; 
  1. 注入脚本
struct WebView: UIViewRepresentable {
    func makeUIView(context: Context) -> WKWebView {
        // 配置 WKWebView
        let configuration = WKWebViewConfiguration()
        let contentController = WKUserContentController()
        
        // 注入JS脚本
        let userScript = WKUserScript(
            source: bridgeScript,
            injectionTime: .atDocumentStart,
            forMainFrameOnly: true
        )
        contentController.addUserScript(userScript)
        
        // 注册消息处理器,名称为 "nativeBridge"(与 H5 约定)
        contentController.add(context.coordinator, name: "nativeBridge")
        configuration.userContentController = contentController
        
        let webView = WKWebView(frame: .zero, configuration: configuration)
        
        webView.navigationDelegate = context.coordinator  // 可选,用于监听加载状态
        context.coordinator.webView = webView
        
        return webView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        let request = URLRequest(url: URL(string: url)!)
        uiView.load(request)
    }
}

通信逻辑

流程:原生应用接收到H5消息 -> 执行原生代码逻辑 -> 封装传递给H5数据体 -> 执行在window上挂载的回调函数(JS代码)

struct WebView UIViewRepresentable {

    func makeCoordinator() -> Coordinator {
        Coordinator(onMessage: onMessage)
    }
    
    // 自定义协议
    class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
        let onMessage: (String) -> Void
        weak var webView: WKWebView?
        
        init(onMessage: @escaping (String) -> Void) {
            self.onMessage = onMessage
        }
        
        //【核心部分】接收H5消息
        func userContentController(
            _ userContentController: WKUserContentController,
            didReceive message: WKScriptMessage
        ) {
            /**
            * 匹配JS代码: window.webkit.messageHandlers[message.name]
            */
            guard message.name == "nativeBridge" else { return }
            
            let dict = message.body as? [String: Any],
                let jsonData = try? JSONSerialization.data(
                    withJSONObject: dict
                ),
                let jsonString = String(data: jsonData, encoding: .utf8)
            {
                // onMessage(jsonString)
                
                // 处理消息
                handleMessage([
                    "callbackId": dict["__callbackId"],
                    "funcKey": dict["funcKey"],
                    "params": dict["params"],
                ])
            } else {
                // onMessage("\(message.body)")
            }
        }
        
        // 处理H5的消息,原生应用所要处理的一些业务逻辑
        func handleMessage(_ params: [String: Any?]) {
            
            let funcKey = params["funcKey"] as? String
            
            // 根据funKey执行不同的原生能力
            switch funcKey {
            
            // 一个获取设备信息的方法(demo)
            case "getDeviceInfo":
            
                let deviceInfo: [String: Any] = [
                    "model": UIDevice.current.model,
                    "systemName": UIDevice.current.systemName,
                    "systemVersion": UIDevice.current.systemVersion,
                    "name": UIDevice.current.name,
                ]
                
                
                // 有回调,将设备信息作为参数调用回调函数
                if let callbackId = params["callbackId"] as? String {
                    callJSFunction(
                        "window.__js_bridge_callback__",
                        arguments: [
                            "callbackId": callbackId,
                            "response": deviceInfo,
                        ]
                    )
                } else {
                    // onMessage("设备信息: \(deviceInfo)")
                }
                
            // 其他 case
                
            default:
                // onMessage("未知 funcKey: \( funcKey ?? "")")
            }
        }
        
        //【核心部分】执行JS脚本
        func callJSFunction(_ functionName: String, arguments: [String: Any?]) {
            let response = arguments["response"] as? [String: Any?] ?? [:]
            let callbackId = arguments["callbackId"] as? String ?? ""
            var jsCode = "\(functionName)"
            
            if callbackId != "" {
                jsCode += ".\(callbackId)"
            } else {
                // print("未获取到callbackId参数,无法执行")
                return
            }
            
            // 拼接出一个调用JS函数的字符串:`window.__js_bridge_callback__.${callbackId}(${response})`
            if let jsonData = try? JSONSerialization.data(
                withJSONObject: response
            ),
                let jsonString = String(data: jsonData, encoding: .utf8)
            {
                jsCode += "(\(jsonString))"
            } else {
                jsCode += "({})"
            }
            
            // 删除已经完成调用的callback
            jsCode += ";(function() { delete window.\(functionName).\(callbackId) })()"
            
            // 执行JS代码
            webView?.evaluateJavaScript(jsCode) { result, error in
                if let error = error {
                    print("执行JS 失败: \(error.localizedDescription)")
                } else {
                    print("执行 JS 成功,返回值: \(result ?? "nil")")
                }
            }
        }
    }
}

H5调用

window.H5JSBridge.call<DeviceInfo>(
  { funcKey: "getDeviceInfo" },
  (response) => {
     setDeviceInfo(response);
  },
);

思考

  1. JSBridge是一段提前注入的脚本,对外暴露了一些方法可以让H5在JS环境下直接调用,那么这个注入是由谁去执行(H5还是原生)?

优先考虑在原生应用中注入,原因如下:

  1. 提供更早的执行时机:可以在H5页面解析之前直接注入,实现在执行<script>脚本时就能调用
  2. 逻辑解耦:在H5中只需调用window.JSBridge上的方法,无需再做其他逻辑,如果涉及不同操作系统的应用时,只要统一JSBridge接口,即可实现H5的无缝衔接
  3. 安全考虑:H5存在被第三方恶意篡改的风险
  1. 使用callbackId管理每个回调函数的意义?

保障每一次的调用中,原生应用都能找到关联的回调去执行脚本

3.【探索】在WebView容器,是如何实现调用window.webkit.messageHandlers.nativeBridge.postMessage就能触发原生应用的消息接收?

4.【探索】原生应用中,是如何做到evaluateJavaScript

最后

以上仅为个人观点,如有错误欢迎指正。代码仅为Demo,切勿直接应用到生产环境。

参考资料