iOS Hybrid交互

自己最近重新梳理了下iOS 中 Hybrid 交互方面的知識,這里簡單記錄一下:

環(huán)境說明:

  • Web容器:WKWebView
  • iOS10.0以上
  • Swift: 5.0

術(shù)語說明:

  • JS: 指H5前端層面
  • Native: 指iOS原生層面

1.通信方式選擇

JS與原生交互的前提是保證JS與Native能進行相互通信,這里選擇的通信方案是通過注入JS對象的方式來實現(xiàn)。

具體實現(xiàn)方式是Native在創(chuàng)建好wkwebview容器后向其注入 JSBridge 對象。后續(xù)消息收發(fā)均通過注入的JSBridge對象進行。

webView.configuration.userContentController.add(self, name: "JSBridge")

注入成功后,可以在web瀏覽器中控制臺輸入window.webkit.messageHandlers.JSBridge 進行查看

打印JSBridge對象

其他幾種通信方案及詳細說明請看
從零收拾一個hybrid框架(一)-- 從選擇JS通信方案開始

需要注意的地方:
直接使用addScriptMessageHandler: name方法會存在內(nèi)存泄漏問題, 解決辦法可以參考:iOS 解決WKWebView中WKScriptMessageHandler方法引起內(nèi)存不釋放問題

2.消息結(jié)構(gòu)約定

需要Native與JS雙方約定一個消息結(jié)構(gòu),作為通信時消息傳遞的規(guī)范:
這里約定消息結(jié)構(gòu)如下:

//js
{
  action: "foo", //兩端約定的方法或協(xié)議名
  data: {}   //傳輸數(shù)據(jù),json對象類型
  callbackId: "xxxx-xxxxx-xxx", //消息回調(diào)id
}

//swift
class JSMessage: NSObject {
    var action: String
    var data: Dictionary<String, Any>?
    var callbackId: String
}

其中callbackId 主要用于當(dāng)該消息有響應(yīng)數(shù)據(jù)時,將callbackId與傳入的回調(diào)方法進行綁定并寫入當(dāng)前環(huán)境的map中。當(dāng)獲取到回調(diào)數(shù)據(jù)后, 再從map中獲取保存的回調(diào)方法,并將回調(diào)數(shù)據(jù)傳入該方法。

3.具體通信流程

3.1 JS向Native發(fā)送消息

//js
function Bridge() {
    this.sendMsg = function(msg) {
        // TODO: 判斷不同平臺,目前只處理iOS
        window.webkit.messageHandlers.JSBridge.postMessage(msg);
    }
}

3.2 Native接收消息

//swift
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    print("收到JS消息: \(message.name), \(message.body)")
    handleJSMsg(message)
}

/// 處理H5發(fā)送過來的消息
private func handleJSMsg(_ msg: WKScriptMessage) {
    if !isSupportMsg(msg) { return }
    if let body = msg.body as? Dictionary<String, Any>,
        let action = body["action"] as? String,
        let data = body["data"] as? Dictionary<String, Any>,
        let callbackId = body["callbackId"] as? String {
        let jsMsg = JSMessage(action: action, data: data, callbackId: callbackId)
        //消息分發(fā)
        dispatchMsg(jsMsg)
    }
}

3.3 Native向JS發(fā)送消息

//swift
private func sendMsg(_ msg: JSMessage) {
    let msgDict: [String : Any] = [
        "action": msg.action,
        "data": msg.data ?? "",
        "callbackId": msg.callbackId
    ]
    let jsonString = msgDict.formatJSON() ?? ""
    let js = """
    bridge.handleNativeMsg(\(jsonString))
    """
    evaluateJavaScript(js)
}

private func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
    print("調(diào)用js方法:\(javaScriptString)")
    DispatchQueue.main.async {[weak self] in
        self?.webView?.evaluateJavaScript(javaScriptString, completionHandler: completionHandler)
    }
}

3.4 JS接收消息

//js
this.handleNativeMsg = function(msg) {
    if (msg.action == "handleNativeResponse") {
        this.handleNativeResponse(msg);
        return;
    }
    let functionName = "bridgeMsgListener." + msg.action;
    let func = eval(functionName);
    // 消息轉(zhuǎn)發(fā)
    new func(msg);
}

完整的時序圖如下


消息時序圖

4.回調(diào)處理

針對一些異步操作或需要獲取響應(yīng)狀態(tài)的操作, 例如網(wǎng)絡(luò)請求,我們需要在對方執(zhí)行完畢后獲取到對應(yīng)的響應(yīng)數(shù)據(jù)。 針對這種情況,我們可以在發(fā)送message時申明兩個特殊的action

  • handleH5Response 用于表示這是一條來自h5側(cè)的響應(yīng)數(shù)據(jù),native需要進行回調(diào)處理

  • handleNativeResponse 用于表示這是一條來自native側(cè)的響應(yīng)數(shù)據(jù),h5需要進行回調(diào)處理


舉例說明: 例如,JS調(diào)用Native的一個網(wǎng)絡(luò)請求request方法,在Native獲取到網(wǎng)絡(luò)請求響應(yīng)數(shù)據(jù)responseData后,需要將responseData 返回給JS側(cè);

  1. JS發(fā)送消息:其中callbackId 是由JS生成一個的UUID字符串
{
  action: "request",
  data:  {
    url: "https://www.baidu.com"
  },
  callbackId: "xxxx-xxxxx-xx",
}
  1. Native收到消息后,解析消息內(nèi)容,并在本地發(fā)起網(wǎng)絡(luò)請求,在獲取到響應(yīng)數(shù)據(jù)responseData后,封裝成JSMessage對象發(fā)送給
{
  action: "handleNativeResponse",
  data: responseData,
  callbackId: "xxxx-xxxxx-xx",
}
  1. JS在 handleNativeResponse方法中獲取到 msg 消息中的callbackIddata響應(yīng)數(shù)據(jù),然后進行后續(xù)的回調(diào)操作
    // 回調(diào)處理
    this.handleNativeResponse = function(msg) {
        let callbackFunction = this.callBackMaps[msg.callbackId];
        if (callbackFunction != undefined && callbackFunction != null) {
            this.callBackMaps.delete(msg.callbackId);
            callbackFunction(msg.data);
        }
    }

5.最后

補個簡單的Demo: Demo

PS: 由于本人前端知識淺薄,demo中js部分建議根據(jù)實際需要進行改動。

后續(xù)應(yīng)該會增加一些其他的常用功能:

  1. 離線加載(從bundle、 沙盒 加載html文件)
  2. 逐步實現(xiàn)類似微信小程序的API功能
  3. 增加結(jié)構(gòu)配置項,如 tabbar、導(dǎo)航欄樣式 等...

參考鏈接:
從零收拾一個hybrid框架(一)-- 從選擇JS通信方案開始
從零收拾一個hybrid框架(二)-- WebView容器基礎(chǔ)功能設(shè)計思路
WebViewJsBridge-iOS 做一個好用的橋接庫

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容