如何在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阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,020评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32
  • 链接:https://www.jianshu.com/p/fd61e8f4049e 一、简介 这部分主要介绍下 W...
    柒黍阅读 1,757评论 0 4
  • 男:“我们分手吧!”认真的脸,认真的语气。 女:“别开玩笑了,今天不是愚人节,我们回去吧!”语气中带着颤抖。 男:...
    楠得阅读 509评论 0 0
  • by Lewis Pulsipher 原文在此: 前10条,后11条 我稍稍精简翻译了一下,与大家共勉。前辈说的话...
    王兵阅读 378评论 0 10