美文网首页swift
WKWebViewJavascriptBridge源码解析

WKWebViewJavascriptBridge源码解析

作者: 朽木自雕也 | 来源:发表于2018-08-19 23:29 被阅读134次

    文件目录结构

    1DA4FDEB-8900-4C4D-BDA9-8B47BDA17D83.png
    • WKWebViewJavascrtptBridge.swift 负责WKWebView的封装,oc<->js的之间通信
    • WKWebViewJavascriptBridgeBase.swift 负责对消息的内部处理以及标识
    • WKWebViewJavascriptBridgeJS.swift 复制iOS原生调用js,和js调用iOS原生的一个接口,可以理解成一个js文件,这里存了一个window.WKWebViewJavascriptBridge对象,这个对象就是提供了用来协调js与iOS原生交互的一个重要枢纽

    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工程量比较大,估计得一两个星期才能全面看完

    相关文章

      网友评论

        本文标题:WKWebViewJavascriptBridge源码解析

        本文链接:https://www.haomeiwen.com/subject/prwzbftx.html