如何在WebKit中使用JavaScriptCore

這里先要道個(gè)歉。其實(shí)有點(diǎn)標(biāo)題黨了

眾所周知,WKWebView由于采用了異步處理js的方式,間接砍掉了UIWebView的documentView.webView.mainFrame.javaScriptContext屬性,也就不能很方便的使用javaScriptCore讓js調(diào)用原生方法,最近我在負(fù)責(zé)這類(lèi)工作,其中一個(gè)要求就是要能實(shí)現(xiàn)web端直接使用jsBridge.getData(),jsBridge.openNative()的形式進(jìn)行調(diào)用。

那怎么辦呢?

總不能說(shuō)放棄WebKit用回被蘋(píng)果拋棄的UIWebView吧?

總不能跟他們說(shuō):對(duì)不起我做不了吧(雖然我真的很想這樣說(shuō)??

在不算特別難的情況下,查找了一下目前iOS主流的jsBrideg方案(這里不客氣的說(shuō)一句在座的各位都是垃圾),沒(méi)有一個(gè)是符合邏輯學(xué)的,像什么WebViewJavascriptBridge,dsBridge等等都是同一類(lèi)東西,即需要web注冊(cè)啦,調(diào)用只能用bridge.call(“方法名”)啦等等等等

雖說(shuō)如此但我還是從dsBridge中找到了比較好的處理回調(diào)的方式:利用輸入框來(lái)回調(diào),除此之外真的沒(méi)什么有用的了,真心不建議使用這些第三方,太麻煩了根本不像是有夢(mèng)想的人寫(xiě)出來(lái)的東西,都2018年還得注冊(cè)才能用。。。自己寫(xiě)一個(gè)方便的又不難

我是怎么做的呢

首先我們要確定一下目標(biāo):

  1. web端可以直接調(diào)用bridge的方法
  2. 安卓那邊可以很容易就實(shí)現(xiàn),所以不能依賴前端有額外的注入,不然他們就得增加額外的維護(hù)工作,越多的維護(hù)內(nèi)容意味著更容易的出錯(cuò),這是我們應(yīng)該避免的
  3. 基于上面那一條,這個(gè)額外的工作應(yīng)該是自動(dòng)生成的
  4. 我寫(xiě)代碼的必要要求:低侵入性

綜上所訴:

  1. JavaScriptCore可以很方便的完成,只要能解決怎么注入
  2. 避免前端差別對(duì)待只要iOS本地進(jìn)行注入就行
  3. 自動(dòng)完成可以交給runtime生成注入的js代碼
  4. 這個(gè)盡量,必要時(shí)用黑魔法也是能接受的(記得寫(xiě)好測(cè)試代碼)

*以下代碼均使用swift

首先我們按照UIWebView時(shí)代的需求,準(zhǔn)備一個(gè)繼承自JSExport協(xié)議的協(xié)議:

final class JSResult: NSObject, HandyJSON {
    var status: Int = 0
    var msg: String?
    var data: [String: Any] = [:]
    func isNotAFunction() -> JSResult{
        status = -1
        msg = "無(wú)對(duì)應(yīng)方法"
        return self
    }
    var asyncCallback: ((JSResult)->Void)?
}

@objc protocol JSBridgeCallFunction: JSExport {
    ///從 APP 獲取數(shù)據(jù)
    func get(_ type: String, Data extraParams: NSDictionary) -> JSResult
}

這里有幾點(diǎn)用過(guò)JSExport都知道的坑:

  1. 如果js調(diào)用的方法叫g(shù)etData,那么原生對(duì)應(yīng)的方法名得叫[get:Data:],如果有三個(gè)參數(shù)就可以是[get:Da:ta:],swift的話可以給變量取別名是沒(méi)問(wèn)題的
  2. 這里字典最好用NSDictionary,其實(shí)感覺(jué)用[AnyHash: AnyHash]應(yīng)該也是能行的,但我嫌不好看
  3. 識(shí)別不了非JavaScriptCore支持的類(lèi)型
  4. 雖然傳block(閉包)也是可以的,但實(shí)際上我這種做法傳這個(gè)就沒(méi)什么意義了。因?yàn)椴皇荳ebKit在調(diào)用JavaScriptCore,具體會(huì)在下面流程看到
  5. 基于上一點(diǎn),這個(gè)方法都需要一個(gè)返回值,這個(gè)沒(méi)任何要求只要是NSObject的子類(lèi)都行,因?yàn)橄旅娴膮f(xié)議需要是@objc的
  6. 返回類(lèi)型需要能轉(zhuǎn)字典和轉(zhuǎn)JSON,這里為了方便使用了HandyJSON實(shí)現(xiàn)
  7. JSResult的內(nèi)容是根據(jù)需求來(lái)的,這個(gè)只是作為例子,isNotAFunction和asyncCallback是用來(lái)做額外處理的,會(huì)在后面解釋為什么有這兩個(gè)東西

然后是實(shí)現(xiàn)了JSBridgeCallFunction的類(lèi)

class JSBridge: NSObject, JSBridgeCallFunction {
    func get(_ type: String, Data extraParams: NSDictionary) -> JSResult {
        let result = JSResult()
        guard let type = GetDataType(rawValue: type) else { return result.isNotAFunction() }
        switch type {
        case .USERINFO:
            if let data = User.current.toJSON() {
                result.data = data
            }
        }
        
        return result
    }
}

extension JSBridge {
    enum GetDataType: String {
        ///獲取用戶信息
        case USERINFO
    }
}

這里為了方便js得知客戶端沒(méi)有實(shí)現(xiàn)某些type,所以返回了isNotAFunction(這個(gè)名字是從JSContext的exceptionHandler里面學(xué)來(lái)的??)

User也是實(shí)現(xiàn)了HandlyJSON所以可以拿簡(jiǎn)單轉(zhuǎn)字典

前面說(shuō)了是用輸入框進(jìn)行回調(diào),那么就要去WKWebView處理輸入框的WKUIDelegate方法里進(jìn)行處理

func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
    if let context = JSContext() {
        context.setObject(JSBridge(), forKeyedSubscript: "JSBridge" as NSString)
        context.exceptionHandler = { context, value in
            if let valueStr = value?.toString(), valueStr.contains("is not a function") {//這個(gè)是沒(méi)用的,留著方便調(diào)試
                completionHandler("{ status: -1, msg: '無(wú)對(duì)應(yīng)方法' }")
            }
        }
        if let result = context.evaluateScript(prompt)?.toObject() as? JSResult {
            if result.asyncCallback != nil {
                result.asyncCallback = { result in
                    completionHandler(result.toJSONString())
                }
            } else {
                completionHandler(result.toJSONString())
            }
            return
        }
    }
    
    completionHandler("")
}

感覺(jué)蘋(píng)果也是基本放棄這個(gè)庫(kù)了。好多地方都不是很方便接入swift(包括初始化居然是optional的。。。)

這里我解釋一下,prompt傳進(jìn)來(lái)的是類(lèi)似于JsBridge.getData("USERINFO")的東西,然后直接交給JSContext去映射原生方法

asyncCallback是用來(lái)處理異步的,上面這個(gè)處理的邏輯其實(shí)是很微妙的,如果js那邊調(diào)用的時(shí)候其實(shí)是用一個(gè)異步回調(diào)的話,那么到了上面這段代碼的時(shí)候其實(shí)是把異步轉(zhuǎn)成了同步,那么真正遇到原生里面需要異步處理的時(shí)候就會(huì)出問(wèn)題(比如要登陸,登陸結(jié)束才能回調(diào)js)所以我設(shè)計(jì)就是如果需要處理原生異步的話,返回的result對(duì)象的asyncCallback就不會(huì)為空,上面代碼判斷不為空就重新賦值這個(gè)閉包,然后在真正處理結(jié)束的地方才會(huì)調(diào)用result.asyncCallback?()

那么重點(diǎn)來(lái)了,為了實(shí)現(xiàn)傳進(jìn)來(lái)的prompt是類(lèi)似于JsBridge.getData("USERINFO")的東西,要怎么生成這個(gè)注入的js呢,對(duì)此我請(qǐng)來(lái)了前端的負(fù)責(zé)人寫(xiě)了一段js:

!(function () {
    function _objToJson (obj) {
        var str = '';
        try {
            str = JSON.stringify(obj);
        } catch (e) {}
        return str;
    }
    function _jsonToObj (str) {
        var obj = {};
        try {
            obj = JSON.parse(str);
        } catch (e) {}
        return obj;
    }
    function _toQuery (method, type, params) {
        var str = params
            ? 'JSBridge.' + method + '("' + type + '",' + _objToJson(params) + ')'
            : 'JSBridge.' + method + '("' + type + '")';
        return str;
    }
    function _getData(type, extraParams, callback) {
        var query = _toQuery('getData', type, extraParams);
        var result = prompt(query);
        if (callback && typeof callback === 'function') {
            callback(result);
        }
        return result;
    }
    var JSBridge = window.JSBridge = {
        getData: _getData
    };
    var doc = document;
    var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('JSBridgeReady');
    readyEvent.bridge = JSBridge;
    doc.dispatchEvent(readyEvent);
})();

然后我把這段js分割成兩段:

static private let jsPrefix =
"""
!(function () {
    function _objToJson (obj) {
        var str = '';
        try {
        str = JSON.stringify(obj);
        } catch (e) {}
        return str;
    }
    function _jsonToObj (str) {
        var obj = {};
        try {
        obj = JSON.parse(str);
        } catch (e) {}
        return obj;
    }
    function _toQuery (method, type, params) {
        var str = params
            ? 'JSBridge.' + method + '("' + type + '",' + _objToJson(params) + ')'
            : 'JSBridge.' + method + '("' + type + '")';
        return str;
    }
    var doc = document;
    var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('JSBridgeReady');
    readyEvent.bridge = JSBridge;
    doc.dispatchEvent(readyEvent);

"""
static private let jsSufix = "})();"

中間的部分就用runtime來(lái)生成了,最終的生成函數(shù):

static func generateJSBridgeJs() -> String {
    var result = "var JSBridge = window.JSBridge = {"
    var functions = ""
    var count: UInt32 = 0
    let methodList = protocol_copyMethodDescriptionList(JSBridgeCallFunction.self, true, true, &count)
    for index in 0..<Int(count) {
        if let method = methodList?[index], let selector = method.name {
            
            let methodName = NSStringFromSelector(selector).replacingOccurrences(of: ":", with: "")
            result += "\(methodName): _\(methodName),"
            
            functions +=
            """
            
                function _\(methodName) (paraA, paraB, callback) {
                    var query = _toQuery('\(methodName)', paraA, paraB);
                    var result = prompt(query);
                    if (callback && typeof callback === 'function') {
                        callback(result);
                    }
                    return result;
                }
            
            """
        }
        
    }
    result += "};"
    return jsPrefix + result + functions + jsSufix
}

在頁(yè)面加載完調(diào)用:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.evaluateJavaScript(JSBridge.generateJSBridgeJs()) { (result, error) in
        guard let result = result as? Bool, result, error == nil else {
            fatalError("注入失敗,請(qǐng)檢查JSBridge.generateJSBridgeJs()")
        }
    }
}

江江!搞定,至此不管后端怎么加方法,只要這邊JSBridgeCallFunction里添加新的方法就行了,完全不需要修改任何地方

But,其實(shí)這個(gè)自動(dòng)化生成有一些限制:

首先我這里根據(jù)項(xiàng)目需求,把js調(diào)用的函數(shù)寫(xiě)死為:

function _\(methodName) (paraA, paraB, callback)

這樣就需要和前端協(xié)商好參數(shù)的順序了,如果有回調(diào)就需要放到最后一位,像有時(shí)候callback是必選的,paraB是可選的話,他們一般的習(xí)慣都是把paraB放到最后一位去,反過(guò)來(lái)這種對(duì)他們來(lái)說(shuō)就有點(diǎn)反人類(lèi)了,但無(wú)傷大雅,反正不是我在寫(xiě)嘿嘿嘿

實(shí)際情況下可能會(huì)有更多的參數(shù),但這個(gè)其實(shí)也很有辦法解決:假設(shè)只有一個(gè)異步回調(diào),那么在前面獲取的方法有多少個(gè)參數(shù),生成多少個(gè)para就行,然后_toQuery改成傳數(shù)組

但還有可能js傳了多個(gè)function作為參數(shù),那這個(gè)就GG啦,目前我沒(méi)遇到這種情況所以沒(méi)動(dòng)力深入研究解決辦法??,或許可以拆分成多個(gè)函數(shù)去進(jìn)行不同的回調(diào)?但判斷太多了不好寫(xiě)了

又或者是,前端負(fù)責(zé)維護(hù)一張方法名表,動(dòng)態(tài)獲取這張方法名表后去解析動(dòng)態(tài)生成,但這樣又跟注冊(cè)有點(diǎn)像了我又不是很喜歡。。。。

總之目前用在我負(fù)責(zé)的項(xiàng)目的話這樣說(shuō)足夠的,但通用性不強(qiáng),說(shuō)不定哪天心血來(lái)潮會(huì)根據(jù)這個(gè)思路寫(xiě)一個(gè)通用的庫(kù)

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

相關(guān)閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 30,224評(píng)論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類(lèi)型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,639評(píng)論 1 32
  • 鏈接:http://www.itdecent.cn/p/fd61e8f4049e 一、簡(jiǎn)介 這部分主要介紹下 W...
    柒黍閱讀 1,982評(píng)論 0 4
  • 男:“我們分手吧!”認(rèn)真的臉,認(rèn)真的語(yǔ)氣。 女:“別開(kāi)玩笑了,今天不是愚人節(jié),我們回去吧!”語(yǔ)氣中帶著顫抖。 男:...
    楠得閱讀 611評(píng)論 0 0
  • by Lewis Pulsipher 原文在此: 前10條,后11條 我稍稍精簡(jiǎn)翻譯了一下,與大家共勉。前輩說(shuō)的話...
    王兵閱讀 435評(píng)論 0 10

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