如何在WebKit中使用JavaScriptCore

作者: 庄msia | 来源:发表于2018-12-01 01:00 被阅读2次

    这里先要道个歉。其实有点标题党了

    众所周知,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啦,目前我没遇到这种情况所以没动力深入研究解决办法😂,或许可以拆分成多个函数去进行不同的回调?但判断太多了不好写了

    又或者是,前端负责维护一张方法名表,动态获取这张方法名表后去解析动态生成,但这样又跟注册有点像了我又不是很喜欢。。。。

    总之目前用在我负责的项目的话这样说足够的,但通用性不强,说不定哪天心血来潮会根据这个思路写一个通用的库

    相关文章

      网友评论

        本文标题:如何在WebKit中使用JavaScriptCore

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