自己动手打造基于 WKWebView 的混合开发框架(二)——js 向 Native 一句话传值并反射出 Swift 对象执行指定函数
2015-9-2 / 阅读数:39757 / 分类: iOS & Swift
本篇文章中,我将跟大家一起学习使用 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 控制台,测试三种情况:
成功!
WRITTEN BY
程序员,Swift Contributor,正在写《iOS 可视化编程与 Auto Layout》。
评论:
ZhHS
2017-03-07 14:02
请问wkwebview与h5交互,h5只能存在本地吗?我在本地尝试交互成功,挂到服务器,JS编译都通不过,提示undefined is not an object (evaluating 'window.webkit.messageHandlers'),请问这是什么原因呢?(麻烦楼主告知一下,我看楼上也有人问,你没有回答,这种方法是只能跟本地的h5交互吗?)
2017-03-07 14:11
@ZhHS:当然不是呀,我一直远程用,出现这个错误是 js 执行顺序的问题
2017-03-07 14:12
@ZhHS:js 注入要放到 webviewdidfinishload 里面
ZhHS
2017-03-07 14:30
@JohnLui:谢谢博主!
鑫鑫
2016-10-24 15:00
window.webkit.messageHandlers.OOXX.postMessage("Hello WebKit!")这句在web检查器里面老是报错
TypeError: undefined is not an object (evaluating 'window.webkit.messageHandlers')
人人
2016-11-23 16:19
@鑫鑫:我的也报错
Arthur
2016-12-22 17:31
@人人:有解决的办法了么?
一只皮皮虾
2017-05-08 20:43
@鑫鑫:我也遇到这个了,。 同求原因~
时光
2016-10-12 16:11
您好,我想问一下JS的代码只要window.webkit.messageHandlers.OOXX.postMessage({className: "Callme", functionName: "maybe"})
这一集就可以了?不用特别写其他的么?
iOS
2016-05-13 08:34
博主,那个用wkwebview的是女,js端怎么调用oc有返回值的方法?
秦小风
2016-07-13 11:58
@iOS:同问wkwebview 。js端怎么调用oc有返回值的方法
小大
2016-11-04 17:47
@秦小风:同问wkwebview 。js端怎么调用oc有返回值的方法
2016-01-25 23:08
明白了 原来问题出在NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName")!.description 我的app名称和项目的文件夹名称是不一样的 我换成项目根目录的文件名即可 。。
2016-01-25 23:02
不行 加了 nsobject还是找不到类 不知道为啥
2016-01-25 18:29
感谢你的分享 我学会了 并且用了 太感谢了
不过NSObject这个方法在swift2.0里 我调用你这个就不行 去了它 就可以
雨凡
2015-11-05 21:05
我在实际项目中遇到问题了
通过Safari发送的指令都可以被执行,
但是!-->
我以JS函数的形式写到网页中,然后用swift调用-->it dosen't work!
是什么情况啊?
你是不是保留了什么没告诉我
2015-11-06 00:38
@雨凡:生命周期问题吧,swift 代码在 js 函数载入之前就调用了那个函数,当然没反应了
雨凡
2015-11-07 15:21
@JohnLui:我用的是在viewDidLoad里面写了js注入
2015-11-07 15:33
@雨凡:果然。。。js 注入要放到 webviewdidfinishload 里面。
雨凡
2015-11-08 00:35
@JohnLui:
这才是重点吗...
KangKai
2015-10-04 23:46
吕老师我想问一下,把WKWebView的初始化代码放在viewDidLoad和viewDidAppear有神马区别吗?你这里为什么放在viewDidAppear里面?
2015-10-04 23:49
@KangKai:这是一个使用生命周期特性搞的一个省事儿的操作,正式项目中会有些问题,BlackHawk 中已经采用了正确的解决方案。
KangKai
2015-10-05 00:14
@JohnLui:学习了~
2016-01-27 13:02
@KangKai:会导致你打开WKWebView慢至少1秒
iuhux
2015-09-15 00:56
楼主,xcode6.4下运行代码performSelector会报 'performSelector' is unavailable: 'performSelector' methods are unavailable这个错,这个方法已经不能在swift里面用了
2015-09-15 01:01
@iuhux:这个项目的环境是 Xcode 7 。。。。
iuhux
2015-09-15 23:15
@JohnLui:哦,升级到xcode7就没问题了
Se
2015-10-14 11:44
@JohnLui:xcode7 应该是swift2.0了吧。 那1.2的话,如何解决这个问题呢?
双木
2015-09-14 16:06
楼主 请教一下
你那个 “依旧使用 Safari 的 WebAPP 开发工具,在当前页面执行 js 传值代码”
那个图是怎么搞出来的?
2015-09-14 16:10
@双木:看上一篇文章
发表评论:
昵称
邮件地址 (选填)
个人主页 (选填)