代码示例: https://github.com/johnlui/Swift-On-iOS/tree/master/BuildYourOwnHybridDevelopmentFramework/BuildYourOwnHybridDevelopmentFramework
开源项目:BlackHawk,纯 Swift 开发的基于 WKWebView 的高性能 Cordova 替代: https://github.com/Lucky-Orange/BlackHawk
本文中,我们将一起构造出一个完整的 js -> Native -> js 回调+传值的数据通道,并设计出插件协议,最终实现在 js API 层完全兼容 Cordova,让现有 Cordova 项目可以无感知迁移。
Console 插件就是 js 向 Swift 传值最好的实例。Console 插件在 iOS 平台是非常必须的,用以在 Xcode 的调试窗口里显示出 js console.log() 的信息,这个功能在 Android 上是自带的。
我们直接给之前传值时候用的 js 对象里增加一个 data 属性,之后在 swift 里将这个参数发送给 Console 的 swift 代码就完成了数据的传输。不多说了,直接上代码。
我们在项目根目录下新建一个 www 目录,在里面新建 plugins 目录,在里面新建 Console.js,放入以下代码:
console = { log: function (string) { window.webkit.messageHandlers.OOXX.postMessage({className: 'Console', functionName: 'log', data: string}); } }
这些插件的 js 层代码需要我们提前注入到 wk 里面:
1. 把 www 文件夹拖入工程,这样 Xcode 就会把他们当做资源文件打包进 ipa
2. 在 wk 生成之后,手动 evaluateJavaScript Console.js 进 js runtime:
... ... self.runPluginJS(["Console"]) self.view.addSubview(self.wk) } func runPluginJS(names: Array<String>) { for name in names { if let path = NSBundle.mainBundle().pathForResource(name, ofType: "js", inDirectory: "www/plugins") { do { let js = try NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding) self.wk.evaluateJavaScript(js as String, completionHandler: nil) } catch let error as NSError { NSLog(error.debugDescription) } } } }
新建 Console 类:
class Console: NSObject { func log(data: String) { NSLog(data) } }
由于这个方法有参数,所以我们需要修改调用方法,把参数传过去:
let functionSelector = Selector(functionName + ":") if obj.respondsToSelector(functionSelector) { obj.performSelector(functionSelector, withObject: dic["data"]?.description) } else { print("方法未找到!") }
搞定!
上面我们已经实现了 js 向 Native 层的传值和反射,接下来我们要做的就是实现 Native 向 js 层的回调。
分析 Accelerometer 这个插件的调用方式。触发方法为:
navigator.accelerometer.getCurrentAcceleration(accelerometerOnSuccess, accelerometerOnError)
处理函数为:
function accelerometerOnSuccess(acceleration) { alert('Acceleration X: ' + acceleration.x + '/n' + 'Acceleration Y: ' + acceleration.y + '/n' + 'Acceleration Z: ' + acceleration.z + '/n' + 'Timestamp: ' + acceleration.timestamp + '/n'); }; function accelerometerOnError(e) { alert(e); };
很明显,这个插件用到了两个回调函数,分别处理正确和错误的回调,所以我们需要维护一下 js 层的队列,每次在调用之前把回调函数压入队列,把序号传给 Native 层,等 Native 层返回结果时,再拿着这个序号来回调 js 函数。
数据结构也很明显,回调时会向 js 层传一个 js 对象,而这个我们也有处理经验,直接用 NSDictionary 就可以了。
在 www/plugins 文件夹中新建一个 Base.js,用于生成和处理队列:
Queue = []; Task = { id: 0, callback: function(){}, errorCallback: function(){}, init: function(id, callback, errorCallback) { this.id = id; this.callback = callback; this.errorCallback = errorCallback; return this } }; fireTask = function(i, j) { Queue[i].callback(JSON.parse(j)); }; onError = function (i, j) { Queue[i].errorCallback(j); };
注入 Base.js:
self.runPluginJS(["Base", "Console"])
细心的人可能发现了,前面 Console 插件修改了核心反射调用方法,会让没有参数的反射失效。所以现在一个插件系统是必须的了,我们要从根本上解决这个问题,并带来更多功能和安全性上的提升。
新建 Plugin 插件基础类:
class Plugin: NSObject { var wk: WKWebView! var taskId: Int! var data: String? required override init() { } func callback(values: NSDictionary) -> Bool { do { let jsonData = try NSJSONSerialization.dataWithJSONObject(values, options: NSJSONWritingOptions()) if let jsonString = NSString(data: jsonData, encoding: NSUTF8StringEncoding) as? String { let js = "fireTask(/(self.taskId), '/(jsonString)');" self.wk.evaluateJavaScript(js, completionHandler: nil) return true } } catch let error as NSError{ NSLog(error.debugDescription) return false } return false } func errorCallback(errorMessage: String) { let js = "onError(/(self.taskId), '/(errorMessage)');" self.wk.evaluateJavaScript(js, completionHandler: nil) } }
我们修改反射类型的基类,并使用 data 类成员变量来存储 js 传过来的字符串数据:
if let cls = NSClassFromString(NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName")!.description + "." + className) as? Plugin.Type{ let obj = cls.init() obj.wk = self.wk obj.taskId = dic["taskId"]?.integerValue obj.data = dic["data"]?.description let functionSelector = Selector(functionName) if obj.respondsToSelector(functionSelector) { obj.performSelector(functionSelector) } else { print("方法未找到!") } } else { print("类未找到!") }
OK,让我们开始真正的插件的构建。
删掉之前的 Callme 类和 Console 类,新建 Console.swift:
class Console: Plugin { func log() { if let string = self.data { NSLog("OOXX >>> " + string) } } }
js 向 Native 层传值成功!
在 www/plugins 目录下新建 Accelerometer.js:
navigator.accelerometer = { getCurrentAcceleration: function(onSuccess, onError) { Queue.push(Task.init(Queue.length, onSuccess, onError)); window.webkit.messageHandlers.OOXX.postMessage({className: 'Accelerometer', functionName: 'getCurrentAcceleration', taskId: Queue.length - 1}); } }
self.runPluginJS(["Base", "Console", "Accelerometer"])
import CoreMotion class Accelerometer: Plugin { var motionManager: CMMotionManager! var isRunning = false // defaults to 10 msec let kAccelerometerInterval: NSTimeInterval = 10 // g constant: -9.81 m/s^2 let kGravitationalConstant = -9.81 func getCurrentAcceleration() { if motionManager == nil { motionManager = CMMotionManager() } if motionManager.accelerometerAvailable { motionManager.accelerometerUpdateInterval = self.kAccelerometerInterval / 1000 motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue(), withHandler: { (data, error) -> Void in let dic = NSMutableDictionary() dic["x"] = data!.acceleration.x * self.kGravitationalConstant dic["y"] = data!.acceleration.y * self.kGravitationalConstant dic["z"] = data!.acceleration.z * self.kGravitationalConstant dic["timestamp"] = NSDate().timeIntervalSince1970 if self.callback(dic) { self.motionManager.stopAccelerometerUpdates() } }) if !self.isRunning { self.isRunning = true } } else { self.errorCallback("accelerometer not available!") } } }
错误回调正常。出现这种错误是因为模拟器没有加速度传感器,我使用同样方式在真机上进行了测试,得到了如下结果:
成功!
至此,《自己动手打造基于 WKWebView 的混合开发框架》系列文章就全部完成了。在该系列文章中,我跟大家一起完成了一个基于 WKWebView 的简易混合开发框架,将一些 Native 接口暴露给了 js runtime,让 js 有了一些本不具备的强大的功能,在此基础上,我们可以按照我们制定出的插件标准持续不断地加入我们需要的接口,满足我们工作中的需要。
本系列文章基本讲述了 BlackHawk 的原理和搭建过程,大家也可以看出,BlackHawk 是比 Cordova 更低的一层,性质应该是基本反射层。而我们做的 Cordova 兼容层只是 BlackHawk 之上的一个应用而已。如果大家只是为自己的 iOS APP 搭建混合开发环境,完全可以直接使用 BlackHawk 的底层和插件协议,不用遵循 Cordova 现有插件的 API 标准,可以做得更加简单粗暴有效。
BlackHawk 是我在工作中进行技术预研的一项成果,目标是构造出一个标准 Cordova 环境,遵循现有事实标准跨平台。最初我打算直接在 Swift 主项目里引入 OC 的Cordovalib 子项目,发现反射有问题,接着我想用 Swift 封装 Cordova 再给主项目使用,后来发现二层子项目里无法反射。之后没有办法,打算再造 Cordova,从 js API 层面对 Cordova 进行兼容,最终得到了 BlackHawk。
最后某天中午我脑洞大开:既然 Cordova 是开源的,那我就可建立一个 Swift 项目,再把 Cordovalib 里的所有 OC 代码复制过来,采用混合编译的方式运行,再在其 OC 的类之上封装一层不就可以了。按照这个思路成功跑了起来,实现了 Cordova 原生的反射,并实现了完全兼容所有 Cordova 插件并可以直接用 Swift 写 Cordova 插件的目的。最后采用了这个方案,毕竟能减少不少工作量,于是就把 BlackHawk 开源了。
在这个过程过我发现 OC 的 Category(Swift 中的 Extension/扩展)在以 Swift 为基础语言新建的项目中不能运行,这算是一个额外的 tip 吧。在解决这个问题的过程中,我被迫写了几十行 OC 代码,那叫一个蛋疼呀,我都快哭了。奉劝大家还是早日迁移到 Swift 来享受美好吧~对了,这个对 Cordova 的 Swift 封装可能某一天会开源哦~