swift

WKWebViewJavascriptBridge源码解析

2018-08-19  本文已影响134人  朽木自雕也

文件目录结构

1DA4FDEB-8900-4C4D-BDA9-8B47BDA17D83.png

WKWebViewJavascrtptBridge 问价内部解剖

源码如下
import WebKit

@available(iOS 9.0, *)
public class WKWebViewJavascriptBridge: NSObject {
    private let nativeHandler = "nativeHandler"
    private let iOS_Native_FlushMessageQueue = "iOS_Native_FlushMessageQueue"
    
    private weak var webView: WKWebView?
    private var base: WKWebViewJavascriptBridgeBase!
    /*初始化
     *webView 监听的 webView
     */
    public init(webView: WKWebView) {
        super.init()
        self.webView = webView
        base = WKWebViewJavascriptBridgeBase()
        base.delegate = self
        addScriptMessageHandlers()
    }
    /*将要释放的时候一处所有注册事件
     */
    deinit {
        removeScriptMessageHandlers()
    }
    
    // MARK: - Public Funcs
    public func reset() {
        base.reset()
    }
    /*注册事件
     *handlerName 协议名称
     *handler 响音的函数
     */
    public func register(handlerName: String, handler: @escaping WKWebViewJavascriptBridgeBase.Handler) {
        base.messageHandlers[handlerName] = handler
    }
    /*移除事件
     *handlerName 协议名称
     */
    public func remove(handlerName: String) -> WKWebViewJavascriptBridgeBase.Handler? {
        return base.messageHandlers.removeValue(forKey: handlerName)
    }
    /*发送事件
     *handlerName 协议名称
     *data 传递的数据
     *callback 回调
     */
    public func call(handlerName: String, data: Any? = nil, callback: WKWebViewJavascriptBridgeBase.Callback? = nil) {
        base.send(handlerName: handlerName, data: data, callback: callback)
    }
    
    /* * iOS 核心代码
     * 从WKWebViewJavascriptBridge.fetchQueue事件队列中获取事件
     */
    private func flushMessageQueue() {
        webView?.evaluateJavaScript("WKWebViewJavascriptBridge._fetchQueue();") { (result, error) in
            if error != nil {
                print("WKWebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: \(String(describing: error))")
            }
            //拿到js消息队列中的消息后开始处理
            guard let resultStr = result as? String else { return }
            self.base.flush(messageQueueString: resultStr)
        }
    }
    /*向js注册 名为 nativeHandler 事件,事件处理对象为 LeakAvoider()
     *向js注册 名为 iOS_Native_FlushMessageQueue 事件,事件处理对象为 LeakAvoider()
     */
    private func addScriptMessageHandlers() {
        webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: nativeHandler)
        webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: iOS_Native_FlushMessageQueue)
    }
    /*向js移除 名为 nativeHandler 事件协议
     *向js移除 名为 iOS_Native_FlushMessageQueue 事件协议
     */
    private func removeScriptMessageHandlers() {
        webView?.configuration.userContentController.removeScriptMessageHandler(forName: nativeHandler)
        webView?.configuration.userContentController.removeScriptMessageHandler(forName: iOS_Native_FlushMessageQueue)
    }
}
/*
 *原生调用js的对调
 */
extension WKWebViewJavascriptBridge: WKWebViewJavascriptBridgeBaseDelegate {
    //向webView里面注入javascript
    func evaluateJavascript(javascript: String) {
        webView?.evaluateJavaScript(javascript, completionHandler: nil)
    }
}
/*
 *LeakAvoider 对象的代理方法
 */
extension WKWebViewJavascriptBridge: WKScriptMessageHandler {
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        /*
         * 当js向iOS发送第一个消息的时候表示web页面已经加载到对应的script标签来,这个时候可以WKWebViewJavascriptBridgeJS代码注入
         */
        if message.name == nativeHandler {
            base.injectJavascriptFile()
        }
        /*
         * js通知iOS端消息队列更新了,快去干活
         */
        if message.name == iOS_Native_FlushMessageQueue {
            flushMessageQueue()
        }
    }
}
/* *
 * LeakAvoider  js调用原生方法的代理类
 */
class LeakAvoider: NSObject {
    weak var delegate: WKScriptMessageHandler?
    init(delegate: WKScriptMessageHandler) {
        super.init()
        self.delegate = delegate
    }
}
/*
 * 收到js发给原生的消息,交给delegate处理消息
 */
extension LeakAvoider: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        delegate?.userContentController(userContentController, didReceive: message)
    }
}

在这里面我把每一个方法所做的功能都写上了注释
首先找到初始化方法

  /*初始化
     *webView 监听的 webView
     */
    public init(webView: WKWebView) {
        super.init()
        self.webView = webView
        base = WKWebViewJavascriptBridgeBase()
        base.delegate = self
        addScriptMessageHandlers()
    }

暂且先不看base = WKWebViewJavascriptBridgeBase()做了什么和base.delegate = self
先来看看这个addScriptMessageHandlers()方法做了什么

    private func addScriptMessageHandlers() {
        webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: nativeHandler)
        webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: iOS_Native_FlushMessageQueue)
    }

  1. webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: nativeHandler)监听js调用触发这个协议方法nativeHandler,并发消息的接收者这设置成LeakAvoider对象的代理对象,也就是当前的对象self;
  2. webView?.configuration.userContentController.add(LeakAvoider(delegate: self), name: iOS_Native_FlushMessageQueue)监听js调用触发协议方法iOS_Native_FlushMessageQueue,同时也把消息的接受者设置成LeakAvoider对象的代理对象,也就是当前的对象self。
继续往下看LeakAvoider的代理都干了什么事

当webview收到js发出的消息第一时间收到这个消息的就是LeakAvoider类的实例对象的userContentController方法

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        delegate?.userContentController(userContentController, didReceive: message)
    }

然后LeakAvoider对象把消息传递给了自己的代理对象,也就是上面的初始化LeakAvoider对象时传进来的对象,也就是WKWebviewJavascriptBridge对象,接着又把收到的消息分发了出去

WKWebviewJavascriptBridge的userContentController方法源码:

   public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        /*
         * 当js向iOS发送第一个消息的时候表示web页面已经加载到对应的script标签来,这个时候可以WKWebViewJavascriptBridgeJS代码注入
         */
        if message.name == nativeHandler {
            base.injectJavascriptFile()
        }
        /*
         * js通知iOS端消息队列更新了,快去干活
         */
        if message.name == iOS_Native_FlushMessageQueue {
            flushMessageQueue()
        }
    }

我注释写得很清楚

首先,当收到js触发nativeHandler这个协议时,是需要原生把WKWebViewJavascriptBridgeJS这段js代码注入到js环境中,base.injectJavascriptFile()方法就是把WKWebViewJavascriptBridgeJS.swift文件中的js代码读取处理,并植入js环境中。

其次,当js触发iOS_Native_FlushMessageQueue这个协议时,则表示js环境中的消息队列里有新的事件需要原生处理,然后flushMessageQueue方法就去处理消息队列

    /* * iOS 核心代码
     * 从WKWebViewJavascriptBridge.fetchQueue事件队列中获取事件
     */
    private func flushMessageQueue() {
        webView?.evaluateJavaScript("WKWebViewJavascriptBridge._fetchQueue();") { (result, error) in
            if error != nil {
                print("WKWebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: \(String(describing: error))")
            }
            //拿到js消息队列中的消息后开始处理
            guard let resultStr = result as? String else { return }
            self.base.flush(messageQueueString: resultStr)
        }
    }

通过这个webview通过调用js中的WKWebViewJavascriptBridge._fetchQueue();方法获取到js环境中待原生处理的消息事件,然后把消息的解析过程交给self.base.flus处理消息。

另外就是WKWebViewJavascriptBridge提供外部注册js的监听事件的方法,register函数

    /*注册事件
     *handlerName 协议名称
     *handler 响音的函数
     */
    public func register(handlerName: String, handler: @escaping WKWebViewJavascriptBridgeBase.Handler) {
        base.messageHandlers[handlerName] = handler
    }

iOS原生主动给js环境发送消息,call函数

    /*发送事件
     *handlerName 协议名称
     *data 传递的数据
     *callback 回调
     */
    public func call(handlerName: String, data: Any? = nil, callback: WKWebViewJavascriptBridgeBase.Callback? = nil) {
        base.send(handlerName: handlerName, data: data, callback: callback)
    }

WKWebViewJavascriptBridgeJS.swift 文件解剖

其实这个就是原生想js环境植入一个WKWebViewJavascriptBridgeJS函数并执行这样一个操作,具体函数内部做了什么,下面来看源码:

function() {
    if (WKWebViewJavascriptBridge) {
    return;
    }
    window.WKWebViewJavascriptBridge = {
        /* 外部注册一个方法
        * 注册方式:bridge.registerHandler('protocolName', function(data, responseCallback) {})
        */
        registerHandler: registerHandler,
        //js->iOS会调用的函数
        callHandler: callHandler,
        //消息事件队列
        _fetchQueue: _fetchQueue,
        //iOS->js会调用的函数
        _handleMessageFromiOS: _handleMessageFromiOS
    };
    //发送消息队列
    var sendMessageQueue = [];
    //事件管理者
    var messageHandlers = {};
    //回调的链表
    var responseCallbacks = {};
    //消息编号 自增长
    var uniqueId = 1;
    //外部js注册方法
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    //外部执行事件的函数
    // handlerName 协议名称
    // data 传递的数据
    // responseCallback 原生回调函数
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    //js->iOS
    //js发送消息给iOS-WKWebView函数
    function _doSend(message, responseCallback) {
        if (responseCallback) {
        //通过callbackID来标识每一条消息的唯一性以及,通过callbackID值为key,responseCallback函数为value进行存放,用于后续回调
        var callbackID = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
        responseCallbacks[callbackID] = responseCallback;
        message['callbackID'] = callbackID;
        }
        //把消息添加到队列中
        sendMessageQueue.push(message);
        //核心代码 js发送消息给iOS
        //协议名称 iOS_Native_FlushMessageQueue
        window.webkit.messageHandlers.iOS_Native_FlushMessageQueue.postMessage(null)
    }
    //iOS->js
    //iOS通过调用此函数取得队列中的事件
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }
    //当iOS端callback回调时会走进这个方法
    function _dispatchMessageFromiOS(messageJSON) {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;
        //客户端的回调
        if (message.responseID) {
            responseCallback = responseCallbacks[message.responseID];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseID];
        } else {
            //客户端主动发送消息给js
            if (message.callbackID) {
                var callbackResponseId = message.callbackID;
                responseCallback = function(responseData) {
                    _doSend({ handlerName:message.handlerName, responseID:callbackResponseId, responseData:responseData });
                };
            }
            //取得外部消息处理程序
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WKWebViewJavascriptBridge: WARNING: no handler for message from iOS:", message);
            } else {
                handler(message.data, responseCallback);
            }
        }
    }
    //iOS端发送消息给js第一时间收到消息的方法
    function _handleMessageFromiOS(messageJSON) {
        _dispatchMessageFromiOS(messageJSON);
    }
    //知识点还不足 需大神补充
    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WKWVJBCallbacks;
        delete window.WKWVJBCallbacks;
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i](WKWebViewJavascriptBridge);
        }
    }
}

同样注释都写好了,这个是建立在你对Javascript语法和Javascript对象有一定的了解的基础之上,如果没有这方面的知识,建议还是先去学习一下

WKWebViewJavascriptBridgeBase.swift 文件解剖

源码如下


protocol WKWebViewJavascriptBridgeBaseDelegate: AnyObject {
    func evaluateJavascript(javascript: String)
}

@available(iOS 9.0, *)
public class WKWebViewJavascriptBridgeBase: NSObject {
    //定义一个 Callback 闭包类型
    public typealias Callback = (_ responseData: Any?) -> Void
    //定义一个 Handler 闭包类型
    public typealias Handler = (_ parameters: [String: Any]?, _ callback: Callback?) -> Void
    //定义一个 Message 数据类型
    public typealias Message = [String: Any]
    //发送消息代理对象,因为当前对象是不能给js发送消息的,要依赖于WKWebViewJavascriptBridge中的WKWebView才能给js环境发送消息
    weak var delegate: WKWebViewJavascriptBridgeBaseDelegate?
    //原生环境待发送给js环境的消息队列
    var startupMessageQueue = [Message]()
    //原生发送消息给js时,用来标识和存储js执行完成后执行的原生需要执行的回调闭包
    var responseCallbacks = [String: Callback]()
    //原生用来存储监听js触发某一个协议的时的闭包回调
    var messageHandlers = [String: Handler]()
    //原生主动发送消息给js的消息唯一标识符,自增!!!
    var uniqueId = 0
    //初始化
    func reset() {
        startupMessageQueue = [Message]()
        responseCallbacks = [String: Callback]()
        uniqueId = 0
    }
    //原生制动发送消息给js,消息通过callbackID来做标识,把回调的闭包存在responseCallbacks对象中,当js执行完功能后回传一个消息中也带着这个callbackID,通过这个callbackID标识取出相对应的外部调用的回调,并执行
    func send(handlerName: String, data: Any?, callback: Callback?) {
        var message = [String: Any]()
        message["handlerName"] = handlerName
        
        if data != nil {
            message["data"] = data
        }
        
        if callback != nil {
            uniqueId += 1
            let callbackID = "native_iOS_cb_\(uniqueId)"
            responseCallbacks[callbackID] = callback
            message["callbackID"] = callbackID
        }
        queue(message: message)
    }
    //从js 的 _fetchQueue 中获取到的事件 数据的交换是使用 json 数据格式
    func flush(messageQueueString: String) {
        //数据解析
        guard let messages = deserialize(messageJSON: messageQueueString) else {
            log(messageQueueString)
            return
        }
        for message in messages {
            log(message)
            //当消息中没有 responseID 这个字段的时候,说明是由原生主动发送消息给js的,这个消息是js的回调,把消息发到给外部处理的闭包然后移除这个消息监听
            if let responseID = message["responseID"] as? String {
                guard let callback = responseCallbacks[responseID] else { continue }
                callback(message["responseData"])
                responseCallbacks.removeValue(forKey: responseID)
                //如果是js 主动发消息给原生,则这个消息ID是由js那边定义,从消息中取出responseID字段
            } else {
                //这里的callback 是原生执行完成功能之后要回调给js的消息
                var callback: Callback?
                //取出消息中的callbackID
                if let callbackID = message["callbackID"] {
                    callback = { (_ responseData: Any?) -> Void in
                        let msg = ["responseID": callbackID, "responseData": responseData ?? NSNull()] as Message
                        self.queue(message: msg)
                    }
                } else {
                    callback = { (_ responseData: Any?) -> Void in
                        // no logic
                    }
                }
                //从messageHandlers获取到handlerName所对应的回调并执行,当执行完成之后通过调用callback把结果回传给js环境
                guard let handlerName = message["handlerName"] as? String else { return }
                guard let handler = messageHandlers[handlerName] else {
                    log("NoHandlerException, No handler for message from JS: \(message)")
                    return
                }
                handler(message["data"] as? [String : Any], callback)
            }
        }
    }
    // 向js对象中注入 WKWebViewJavascriptBridgeJS
    func injectJavascriptFile() {
        let js = WKWebViewJavascriptBridgeJS
        delegate?.evaluateJavascript(javascript: js)
    }
    // 把消息添加到main队列中执行
    private func queue(message: Message) {
        if startupMessageQueue.isEmpty {
            dispatch(message: message)
        } else {
            startupMessageQueue.append(message)
        }
    }
    // 数据解析
    private func dispatch(message: Message) {
        guard var messageJSON = serialize(message: message, pretty: false) else { return }
        
        messageJSON = messageJSON.replacingOccurrences(of: "\\", with: "\\\\")
        messageJSON = messageJSON.replacingOccurrences(of: "\"", with: "\\\"")
        messageJSON = messageJSON.replacingOccurrences(of: "\'", with: "\\\'")
        messageJSON = messageJSON.replacingOccurrences(of: "\n", with: "\\n")
        messageJSON = messageJSON.replacingOccurrences(of: "\r", with: "\\r")
        messageJSON = messageJSON.replacingOccurrences(of: "\u{000C}", with: "\\f")
        messageJSON = messageJSON.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
        messageJSON = messageJSON.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
        
        let javascriptCommand = "WKWebViewJavascriptBridge._handleMessageFromiOS('\(messageJSON)');"
        if Thread.current.isMainThread {
            delegate?.evaluateJavascript(javascript: javascriptCommand)
        } else {
            DispatchQueue.main.async {
                self.delegate?.evaluateJavascript(javascript: javascriptCommand)
            }
        }
    }
    // 数据序列化
    private func serialize(message: Message, pretty: Bool) -> String? {
        var result: String?
        do {
            let data = try JSONSerialization.data(withJSONObject: message, options: pretty ? .prettyPrinted : JSONSerialization.WritingOptions(rawValue: 0))
            result = String(data: data, encoding: .utf8)
        } catch let error {
            log(error)
        }
        return result
    }
    // 数据反序列化
    private func deserialize(messageJSON: String) -> [Message]? {
        var result = [Message]()
        guard let data = messageJSON.data(using: .utf8) else { return nil }
        do {
            result = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [WKWebViewJavascriptBridgeBase.Message]
        } catch let error {
            log(error)
        }
        return result
    }
    // 打印数据
    private func log<T>(_ message: T, file: String = #file, function: String = #function, line: Int = #line) {
        #if DEBUG
            let fileName = (file as NSString).lastPathComponent
            print("\(fileName):\(line) \(function) | \(message)")
        #endif
    }
}

看这篇文章的前提是你已经会使用WKWebViewJavascriptBridge,而想进一MJExtension JSON数据转模型,MJExtension工程量比较大,估计得一两个星期才能全面看完

上一篇 下一篇

猜你喜欢

热点阅读