这里先要道个歉。其实有点标题党了
众所周知,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年还得注册才能用。。。自己写一个方便的又不难
我是怎么做的呢
首先我们要确定一下目标:
- web端可以直接调用bridge的方法
- 安卓那边可以很容易就实现,所以不能依赖前端有额外的注入,不然他们就得增加额外的维护工作,越多的维护内容意味着更容易的出错,这是我们应该避免的
- 基于上面那一条,这个额外的工作应该是自动生成的
- 我写代码的必要要求:低侵入性
综上所诉:
- JavaScriptCore可以很方便的完成,只要能解决怎么注入
- 避免前端差别对待只要iOS本地进行注入就行
- 自动完成可以交给runtime生成注入的js代码
- 这个尽量,必要时用黑魔法也是能接受的(记得写好测试代码)
*以下代码均使用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都知道的坑:
- 如果js调用的方法叫getData,那么原生对应的方法名得叫[get:Data:],如果有三个参数就可以是[get:Da:ta:],swift的话可以给变量取别名是没问题的
- 这里字典最好用NSDictionary,其实感觉用[AnyHash: AnyHash]应该也是能行的,但我嫌不好看
- 识别不了非JavaScriptCore支持的类型
- 虽然传block(闭包)也是可以的,但实际上我这种做法传这个就没什么意义了。因为不是WebKit在调用JavaScriptCore,具体会在下面流程看到
- 基于上一点,这个方法都需要一个返回值,这个没任何要求只要是NSObject的子类都行,因为下面的协议需要是@objc的
- 返回类型需要能转字典和转JSON,这里为了方便使用了HandyJSON实现
- 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啦,目前我没遇到这种情况所以没动力深入研究解决办法😂,或许可以拆分成多个函数去进行不同的回调?但判断太多了不好写了
又或者是,前端负责维护一张方法名表,动态获取这张方法名表后去解析动态生成,但这样又跟注册有点像了我又不是很喜欢。。。。
网友评论