WKWebViewJavascriptBridge 源码解读

1,690 阅读3分钟

使用方法

  1. 原生
class ViewController: UIViewController {
    let webView = ...
    var bridge: WKWebviewJavascriptBridge!
    ...
    override func viewDidLoad() {
        super.viewDidLoad()
        bridge = WKWebViewJavascriptBridge(webview: webView)
        bridge.register(handlerName: "xxx") { params, callback in
            
        }
    }
    ...
    func click() {
        bridge.call(handlerName: "xxxx") { params, callback in
        
        }
    }
}
  1. vue 改造
function setupWebViewJavascriptBridge(callback) {
  if (window.WebViewJavascriptBridge) {
    return callback(window.WebViewJavascriptBridge);
  }
  if (window.WVJBCallbacks) {
    return window.WVJBCallbacks.push(callback);
  }
  window.WVJBCallbacks = [callback];
  let WVJBIframe = document.createElement("iframe");
  WVJBIframe.style.display = "none";
  WVJBIframe.src = "https://__bridge_loaded__";
  document.documentElement.appendChild(WVJBIframe);
  setTimeout(() => {
    document.documentElement.removeChild(WVJBIframe);
  }, 0);
}
const callhandler = (name, data, callback) => {
  setupWebViewJavascriptBridge(function(bridge) {
    bridge.callHandler(name, data, callback);
  });
};
const registerhandler = (name, callback) => {
  setupWebViewJavascriptBridge(function(bridge) {
    bridge.registerHandler(name, function(data, responseCallback) {
      callback(data, responseCallback);
    });
  });
};
export { registerhandler, callhandler };
  1. vue 中使用
import { callhandler, registerhandler } from './bridge';
...
mounted() {
    registerhandler(<name>, () => {
    });
},
methods: {
    callMobile() {
        callhandler(<name>, params, (result) => {
        });
    }
}

或者 在 main.js

import Bridge from './bridge.js'
Vue.prototype.$bridge = Bridge
this.$bridge.callhandler(...)
this.$bridge.register(...)

原生通信

当我们不借助这个三方库,我们原生应该怎样直接交互呢?

js 调用原生

  1. js 易错点:这里需要注意如果 messageBody 有且必须有一个参数,如果不需要就传 null
// <name> 就是和移动端协商的函数名
window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
  1. 原生
class ViewController: UIViewController {
    override func viewdidLoad() {
        super.viewDidLoad()
        // 这里有个细节就是需要注意循环引用的问题
        webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: <name>)
    }
    // 这里一定要记得释放
    deinit {
        webView?.configuration.userContentController.removeScriptMessageHandler(forName: <name>)
    }
}
extension ViewController: WKScriptMessageHandler {
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == <name> {
        // doSomething(message.body)
    }
}

原生调用 js

  1. 原生
webview.evaluateJavaScript(<name>, completionHandler: completion)
  1. js
window.<name> = function(param) {
    // doSomething()
}

原理

初始化

  1. js 调用window.webkit.messageHandlers.iOS_Native_InjectJavascript.postMessage(null)
  2. iOS 执行 webView.evaluateJavascript(javascript: WKWebViewJavascriptBridgeJS, ...) 这样在 js 和原生都会有一个 messageHandlersresponseCallbacks,后续可以根据字符串去匹配对应的函数执行,并且会有一个自增的 uniqeID 记录对应的回调。

两端通信原理

两端的原理都是一样的。以 iOS 调用 js 为例。

大体就是 iOS 发送一个 message 给到 jsmessage 里面包含了函数名、参数及回调的 callbackID。回调的 callbackID 及回调函数的地址是以字典的方式存在 responseCallbacks 中的。

给到 js 执行完成之后,他会将请求的 callbackID 封装成 responseID,然后再调用原生,不然他就没有办法区分是回调还是 js 主动调用的了。

同理 js 调用 iOS 也是如此。只不过两端的调用对方的方式的写法不同,在上面的原生也介绍了,也仅此区别了。

细节点

  1. 通信是同步还是异步的? iOS 为主线同步执行的,如果两端有耗时代码均会造成卡顿,所以要注意耗时任务需要放到子线程中或者挂起。如同步的读取一个大文件。

Androidjavascriptcore 线程(即子线程),如果有任务需要强制在主线程中使用,要切换至主线程。

  1. 如果卡顿后消失会丢失吗? 不会。因为是加入队列中,一条一条的执行。

  2. 为什么 responseCallbacks 要删除对应的 uniqeID? 由于 uniqeID 是自增的,相当于唯一标识符。那么用过一次,后续就不可能再次用到,继续放在字典中会造成空间的浪费,故删除后可节约空间。

  3. h5 桥是怎么初始化的? 第一次调用 register 或者 call 的时候初始化的。

扩展(2021.12.16 更新)

如何支持同步?

h5 调用 prompt 方法,然后 iOS 端使用协议拦截,处理好使用 completeHandler 把结果回传回去即可。

尤其需要注意的是,这个 prompt 就相当于是一个弹窗,如果不调用 completeHandler(其实就相当于你弹出了一个弹窗,但是你不点击确定按钮是一样的)会导致 h5 页面卡死。

prompt("method", "params")
// 这个 prompt 就相当于是 h5 传过来的 method
// defaultText 就相当于是 params
public func webView(_ webView: WKWebView,
                    runJavaScriptTextInputPanelWithPrompt prompt: String, 
                    defaultText: String?,
                    initiatedByFrame frame: WKFrameInfo,
                    completionHandler: @escaping (String?) -> Void) {
                    
    completionHandler("xxx")
}

我把源码里面的细节都加了注释放在了 github 上,有需要的可以自行下载,细节后续更新。