开源项目:BlackHawk,纯 Swift 开发的基于 WKWebView 的高性能 Cordova 替代:https://github.com/Lucky-Orange/BlackHawk
本篇文章中,我将跟大家一起学习使用 WKWebView 屌炸天的新传值方式,实现从 Javascript 层向 Native 层的传值,并反射出我们想要的对方,执行我们想要的方法。
Javascript 层和 Native 层的定义
基本定义
Javascript 层此处指网页中的 js runtime,就是所有 js 运行的地方,我们将其看做一层 js 虚拟机。而此处的 Native 层指的是 Swift 或者 OC 代码运行的那层,严格意义上来讲,这一层并没有运行时(runtime),他们都是编译型语言,在硬件设备上运行时,用的都是二进制形式,所有变量的字符串名称亦不再存在,只有指针。
苹果的 runtime 技术
为了避免上文中编译型语言的缺点,苹果构造甚至直接开放了 runtime,不仅在编译的时候存储了 名称(类,函数,变量)<=>指针 对应表,还把这些底层 runtime 接口开放给所有开发者使用。以我对 OC 浅薄的了解,似乎 OC 底层的面向对象的实现就是直接用的 runtime。
有了 名称<=>指针 对应表,我们就可以胡作非为啦 HIAHIA~
屌炸天的一句话传值
苹果在 WKWebView 中的 js runtime 里事先注入了一个 window.webkit.messageHandlers.OOXX.postMessage() 方法,我们可以使用这个方法直接向 Native 层传值,异常方便。首先,我们要把一个名为 OOXX 的 ScriptMessageHandler 注册到我们的 wk。
增加 delegate
打开我们的 BuildYourOwnHybridDevelopmentFramework 项目,转到 ViewController 类,给 ViewController 增加一个 delegate:
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler {
... ...
注册 ScriptMessageHandler
修改 wk 的初始化函数为:
let conf = WKWebViewConfiguration()
conf.userContentController.addScriptMessageHandler(self, name: "OOXX")
self.wk = WKWebView(frame: self.view.frame, configuration: conf)
handle js 的传值
我们使用之前的优雅方法注册一个处理 js 传过来的值的函数:
private typealias wkScriptMessageHandler = ViewController
extension wkScriptMessageHandler {
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
print(message.name)
print(message.body.description)
}
}
哦了,让我们看看效果。
检验成果
依旧使用 Safari 的 WebAPP 开发工具,在当前页面执行 js 传值代码:
window.webkit.messageHandlers.OOXX.postMessage("Hello WebKit!")
得到如下结果:
搞定!
实现反射
接下来我们将着手反射出我们需要的对象,并执行指定函数,并把结果返回到 js runtime 中。
传递 js 对象到 Native
这个听着有点玄乎呀,不过这个功能是真实存在的,这其实是我某一天脑洞大开想到的,一看苹果果然已经支持了。所谓传递 js 对象,就是我们说的 JSON(严格意义上讲形式上有一点点区别,js 对象的 键 不用加双引号),这是 js 内建的数据存储结构,这种神奇的简单的结构不仅是最流行的格式化数据传输协议,更是 js 这个现代 lisp 语言强大的基础。
我们直接使用上面的数据传递接口传递 js 对象:
window.webkit.messageHandlers.OOXX.postMessage({className: "Callme", functionName: "maybe"})
解析为 NSDictionary
在 Native 层直接将其转化为 NSDictionary 并打印出 className 和 functionName 的值:
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { if message.name == "OOXX" { if let dic = message.body as? NSDictionary { print(dic["className"]) print(dic["functionName"]) } }}
运行项目,执行 js,检查成果:
搞定!下一步反射出对象并执行指定函数。
反射出对象并执行指定函数
我们使用得到的 className 和 functionName 反射出指定的对象,并执行指定函数:
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { if message.name == "OOXX" { if let dic = message.body as? NSDictionary, className = dic["className"]?.description, functionName = dic["functionName"]?.description { if let cls = NSClassFromString(NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName")!.description + "." + className) as? NSObject.Type{ let obj = cls.init() let functionSelector = Selector(functionName) if obj.respondsToSelector(functionSelector) { obj.performSelector(functionSelector) } else { print("方法未找到!") } } else { print("类未找到!") } } }}
上面的代码中,我们使用了 Swift 1.2 中引入的新特性:if let 多个条件,有效的避免了“鞭尸金字塔”的出现。其实这个改进和 if let 本身都只是编译器 trick,帮我们写一些代码而已,但是这些所谓的语法糖能节省开发者许多时间,让开发者把注意力集中在业务逻辑而不是冗长的错误处理代码上面,提高开发效率。同时 OC 连字符串前面都要加 @,实在是有点蛋疼,大家赶快迁移到 Swift 上来吧~
准备 Callme 类和 maybe 函数
为了省事儿我们直接在 ViewController 底部造一个巨简单的 Callme 类,为了反射方便,我们让其继承自 NSObject:
class Callme: NSObject { func maybe() { print("反射成功!") }}
检验成果
下面就到了见证奇迹的时刻,运行项目,调出 Safari 控制台,测试三种情况:
成功!