如何在WebKit中使用JavaScriptCore

這里先要道個歉。其實有點標題黨了

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

那怎么辦呢?

總不能說放棄WebKit用回被蘋果拋棄的UIWebView吧?

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

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

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

我是怎么做的呢

首先我們要確定一下目標:

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

綜上所訴:

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

*以下代碼均使用swift

首先我們按照UIWebView時代的需求,準備一個繼承自JSExport協議的協議:

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

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

這里有幾點用過JSExport都知道的坑:

  1. 如果js調用的方法叫getData,那么原生對應的方法名得叫[get:Data:],如果有三個參數就可以是[get:Da:ta:],swift的話可以給變量取別名是沒問題的
  2. 這里字典最好用NSDictionary,其實感覺用[AnyHash: AnyHash]應該也是能行的,但我嫌不好看
  3. 識別不了非JavaScriptCore支持的類型
  4. 雖然傳block(閉包)也是可以的,但實際上我這種做法傳這個就沒什么意義了。因為不是WebKit在調用JavaScriptCore,具體會在下面流程看到
  5. 基于上一點,這個方法都需要一個返回值,這個沒任何要求只要是NSObject的子類都行,因為下面的協議需要是@objc的
  6. 返回類型需要能轉字典和轉JSON,這里為了方便使用了HandyJSON實現
  7. JSResult的內容是根據需求來的,這個只是作為例子,isNotAFunction和asyncCallback是用來做額外處理的,會在后面解釋為什么有這兩個東西

然后是實現了JSBridgeCallFunction的類

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得知客戶端沒有實現某些type,所以返回了isNotAFunction(這個名字是從JSContext的exceptionHandler里面學來的??)

User也是實現了HandlyJSON所以可以拿簡單轉字典

前面說了是用輸入框進行回調,那么就要去WKWebView處理輸入框的WKUIDelegate方法里進行處理

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") {//這個是沒用的,留著方便調試
                completionHandler("{ status: -1, msg: '無對應方法' }")
            }
        }
        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("")
}

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

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

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

那么重點來了,為了實現傳進來的prompt是類似于JsBridge.getData("USERINFO")的東西,要怎么生成這個注入的js呢,對此我請來了前端的負責人寫了一段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來生成了,最終的生成函數:

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
}

在頁面加載完調用:

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("注入失敗,請檢查JSBridge.generateJSBridgeJs()")
        }
    }
}

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

But,其實這個自動化生成有一些限制:

首先我這里根據項目需求,把js調用的函數寫死為:

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

這樣就需要和前端協商好參數的順序了,如果有回調就需要放到最后一位,像有時候callback是必選的,paraB是可選的話,他們一般的習慣都是把paraB放到最后一位去,反過來這種對他們來說就有點反人類了,但無傷大雅,反正不是我在寫嘿嘿嘿

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

但還有可能js傳了多個function作為參數,那這個就GG啦,目前我沒遇到這種情況所以沒動力深入研究解決辦法??,或許可以拆分成多個函數去進行不同的回調?但判斷太多了不好寫了

又或者是,前端負責維護一張方法名表,動態獲取這張方法名表后去解析動態生成,但這樣又跟注冊有點像了我又不是很喜歡。。。。

總之目前用在我負責的項目的話這樣說足夠的,但通用性不強,說不定哪天心血來潮會根據這個思路寫一個通用的庫

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容

  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數據結構(3).初始化時...
    歐辰_OSR閱讀 29,482評論 8 265
  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,122評論 1 32
  • 鏈接:http://www.lxweimin.com/p/fd61e8f4049e 一、簡介 這部分主要介紹下 W...
    柒黍閱讀 1,858評論 0 4
  • 男:“我們分手吧!”認真的臉,認真的語氣。 女:“別開玩笑了,今天不是愚人節,我們回去吧!”語氣中帶著顫抖。 男:...
    楠得閱讀 552評論 0 0
  • by Lewis Pulsipher 原文在此: 前10條,后11條 我稍稍精簡翻譯了一下,與大家共勉。前輩說的話...
    王兵閱讀 401評論 0 10