WK 与 JS 的那些事
苹果在iOS 8中推出了 WKWebView
,这是一个高性能的 web 框架,相较于 UIWebView
来说,有巨大提升。本文将针对 WKWebView 进行简单介绍,然后介绍下如何和 JS 进行愉快的交互。还望各位大佬不吝赐教。
本文分为两大部分
- WKWebView 简单介绍
- JS 交互
1 WKWebView
就目前移动开发趋势来说,很多 APP 都会嵌套一些 H5 的应用。H5 有一些 Native 无法比拟的优势,例如:更新快,不用发版,随时上线等等。然而在 iOS 中, UIWebView 是及其难用的。随着 iOS 8 的推出,Apple 重构了 UIWebView,于是 WKWebView 横空出世。
1.1 WKWebView VS UIWebView
根据官方文档,我们来简单对比一下 UIWebView 和 WKWebView,看看这两个到底有什么区别
WKWebView | UIWebView | |
---|---|---|
内存占用 | 小 | 大 且有内存泄漏 |
加载速度 | 快 | 慢 |
与 JS 交互 | 易 | 难 (可与 JSCore 配合) |
帧率 | 60FPS | 掉帧 |
从文档来看,二者区别还是很明显的,但到底区别有多大的,我们用数据说话。打开京东,网易,新浪这三个网站,从打开时间和占用内存上来比较一下,看谁能胜出。该测试在 2015款 MBP 上打开,使用 Xcode 9 GM 版,在 iPhone 8 Plus 上运行
在内存测试中发现,UIWebView 占用内存很不稳定,在打开新浪的网站时,最高内存能飙升到 200m 后来慢慢回落到 160m 左右,但会上下波动。但 WKWebView 上就没有这个问题。通过上述对比,不难看出,WKWebVeiw 要优于 UIWebView。
1.2 如何使用 WKWebView
得益于苹果 API 的高度封装,我们使用 WKWebView 及其简单
- (WKWebView *)wkWebView {
if (!_wkWebView) {
_wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:[WKWebViewConfiguration new]]; //1.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.jd.com"]]; //2.
[_wkWebView loadRequest:request]; //3.
}
return _wkWebView;
}
- 初始化一个 WKWebView,我们需要传一个
WKWebViewConfiguration
对象,来对 WKWebView 进行配置。 - 构造一个请求。
- 加载这个请求。
只需要这三步,我们就可以使用一个高性能的 web 框架。是不是很赞!!!
关于 WKWebView 如何使用,这里就不做过多的详细介绍了,网上这种文章太多了,大家可以自行翻阅。接下来我们说如何与 JS 交互。
2. JS 交互
WebVeiw 与 JS 交互是一个很古老的问题,如何与 JS 交互是一个 WebVeiw 必须具备的能力,在 UIWebView 时代,我们可以通过拦截 URL 的方式来进行交互,也可以通过 WebViewJavascriptBridge来进行交互,还可以配合 JSCore来进行交互。但是在 WKWebView 时代,由于它是在一个单独的进程中运行,我们无法获取到 JSContext,所以我们无法使用 JSCore 这个强大的框架来进行交互,那我们怎么办呢,且听我一一道来。
2.1 Native 调用 JS
还记的上边说的 WKWebViewConfiguration
么,在这个类里边,有一个属性
@property (nonatomic, strong) WKUserContentController *userContentController;
Native 和 H5 交互基本全靠这个对象, 在 WKWebVeiw 中,我们使用我们有两种方式来调用 JS,
- 使用
WKUserScript
- 直接调用 JS 字符串
2.1.1 使用 WKUserScript
要想使用 WKUserScript,首先,我们要构造一个 WKUserScript 对象,构造方法及其简单,我们使用下边代码来创建一个 WKUserScript 对象。
// source 就是我们要调用的 JS 函数或者我们要执行的 JS 代码
// injectionTime 这个参数我们需要指定一个时间,在什么时候把我们在这段 JS 注入到 WebVeiw 中,它是一个枚举值,WKUserScriptInjectionTimeAtDocumentStart 或者 WKUserScriptInjectionTimeAtDocumentEnd
// MainFrameOnly 因为在 JS 中,一个页面可能有多个 frame,这个参数指定我们的 JS 代码是否只在 mainFrame 中生效
- initWithSource:injectionTime:forMainFrameOnly:
至此,我们已经构建了一个 WKUserScript,然后呢,我们要做的就是要把它添加进来
- addUserScript:
至此使用 WKUserScript 调用 JS 完成。
2.1.2 直接调用 JS 字符串
在 WKWebView 中,我们也可以直接执行 JS 字符串
- (void)evaluateJavaScript: completionHandler:
我们通过调用这个方法来执行 JS 字符串,然后在 completionHandler
中拿到执行这段 JS 代码后的返回值。
至此,Native 调用 JS 完成。是不是简单到害怕
2.2 JS 调用 Native
在 WK 这套框架下,JS 调用 Native 简直简单到丧心病狂。还记的上边那个 WKUserContentController
,我们也是要通过它来进行,而你所需要做的,只需要三步,需要三步,三步。
- 向 JS 注入一个字符串
[_webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeMethod"];
我们向 JS 注入了一个方法,叫做 nativeMethod
- JS 调用 Native
window.webkit.messageHandlers.nativeMethod.postMessage(value);
一句话调用,我们就可以在 Native 中接收到 value
- 接收 JS 调用
上边我们调用 addScriptMessageHandler:name
的时候,我们要遵守 WKScriptMessageHandler
协议,然后实现这个协议。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSString * name = message.name // 就是上边注入到 JS 的哪个名字,在这里是 nativeMethod
id param = message.body // 就是 JS 调用 Native 时,传过来的 value
// TODO: do your stuff
}
完了,Native 调用 JS 就这么简单,是不是丧心病狂,简直简单到不能再简单了。
但是,你以为这么就完了么,上边写的这些东西在网上随便一搜都有一大片,重新再写一遍,貌似意义不是很大啊,怎么也得来点稍微不一样的东西吧。
2.3 JS 调用 Native 后的回调
举一个很常见的例子,假设我们有这么一个需求,我的 JS 要调用 Native 发一个网络请求,Native 执行完了,把请求数据回传给 JS。
很简单的一个需求,来,想想怎么执行。
2.3.1 postMessage 的坑
可能很快就想到了,postMessage 的时候,直接把这个方法传过去不就行了。一开始我也是这么做的。
const person = {
firstName: "John",
lastName: "Doe",
age: 50,
eyeColor: "blue",
};
document.getElementById("li1").onclick = function (nativeValue) {
person.callBack = function () {
console.log("native call");
}
window.webkit.messageHandlers.nativeMethod.postMessage(person);
};
首先构造一个 person,然后我们给 person 增加一个 callBack 属性,然后传进去,运行程序。打开 Safari 选择 开发->模拟器,打开调试界面,然后我们点击查看控制台。
然后你会发现,报错了,为什么呢,这一切都是因为 postMessag 这个方法。
打开 postMessage文档,你会发现,
message
将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化
这个 message 需要支持 结构化克隆算法。很遗憾,这个算法目前不支持传递 Function
和 Error
,它只支持一下几种类型
对象类型 | 注意 |
---|---|
所有的原始类型 | 除了symbols |
Boolean | 对象 |
String | 对象 |
Date | |
RegExp | lastIndex 字段不会被保留。 |
Blob | |
File | |
FileList | |
ArrayBuffer | |
ArrayBufferView | 这基本上意味着所有的 类型化数组 ,比如 Int32Array 等等。 |
ImageData | |
Array | |
Object | 仅包括普通对象 (比如对象字面量 ) |
Map | |
Set |
说好的不受限制呢
2.3.2 function 转为 字符串
那既然它不支持传一个 Function
,那我们就得另辟蹊径了,String
总支持吧,我们把一个方法转为字符串,然后传到 Native,然后 Native 执行这个字符串。貌似可行的,我们来试一下。
JS 代码
document.getElementById("li1").onclick = function () {
person.callBack = function (nativeValue) {
console.log("native call");
}.toString();
window.webkit.messageHandlers.nativeMethod.postMessage(person);
};
OC 代码
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"nativeMethod"]) {
NSLog(@"body:%@, ", message.body);
NSDictionary *dict = @{@"key1": @"value1",
@"key2": @"value2"
}; // 构造回传 js 数据
id data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // 转为 json 字符串
[_webView evaluateJavaScript:[NSString stringWithFormat:@"(%@)(%@)", message.body[@"callBack"], jsonString] completionHandler:^(id _Nullable jsData, NSError * _Nullable error) {
}];
}
}
果然不出我们所料,我们可以直接得到这个 Native 传递给 JS 的值。
但是,这个作用域会不会变化呢,我们在来改一下 JS 代码
document.getElementById("li1").onclick = function () {
var arg1 = 100;
var arg2 = 200;
person.callBack = function (nativeValue) {
console.log(nativeValue);
console.log(arg1 + arg2);
}.toString();
window.webkit.messageHandlers.nativeMethod.postMessage(person);
};
大家猜能不能打印出来 300,我们来试一下。
完蛋,找不到 arg1。。。。
怎么回事呢?
我们把一个 function 转换成 字符串之后,传给 Native,Native 在执行的时候,他的作用域已经变了,变成了 window,这个时候,window 下是没有 arg1 和 arg2 的,所以我们找不到。
如果我们这么做的话,确实是可以实现上述的需求的,但是,这样作用域就改变了,所有的变量都要定义为全局变量,函数要改为全局函数,以遍能够在回调中获取正确的变量。
这确实是一个可行的方法,但有没有更好的方法呢?H5 本来写的好好的,匿名函数写的 6 的飞起,干嘛都要改成全局变量,全局函数,要是这么写,我都不好意思给 H5 提需求让人家改。
我就想,能不能像 UIWebView 一样使用 JSCore,但是使用 JSCore 的话,我们要获取 JSContext,而 WKWebView 是运行在一个单独的进程中,我们是不可能进行应用间的通信的(目前我没发现,如果有的话,还请多多指教)。我就想,要不去扒一扒 WebKit 的源码,看看会有什么发现。
2.3.3 改下源码 ?
然后我就找啊找,终于找到了关键的方法
virtual void didPostMessage(WebKit::WebPageProxy& page, WebKit::WebFrameProxy& frame, const WebKit::SecurityOriginData& securityOriginData, WebCore::SerializedScriptValue& serializedScriptValue)
{
@autoreleasepool {
RetainPtr<WKFrameInfo> frameInfo = wrapper(API::FrameInfo::create(frame, securityOriginData.securityOrigin()));
ASSERT(isUIThread());
static JSContext* context = [[JSContext alloc] init]; //1. 创建一个 JSContext
JSValueRef valueRef = serializedScriptValue.deserialize([context JSGlobalContextRef], 0);
JSValue *value = [JSValue valueWithJSValueRef:valueRef inContext:context];
id body = value.toObject; // 把 JS 的类型转为 OC 类型
auto message = adoptNS([[WKScriptMessage alloc] _initWithBody:body webView:fromWebPageProxy(page) frameInfo:frameInfo.get() name:m_name.get()]); // 构造 message
[m_handler userContentController:m_controller.get() didReceiveScriptMessage:message.get()]; // 调用代理对象,传递 message
}
}
看到这里,我想,能不能把这个 JSContext 漏出来,这样的话,说不定还能想 UIWebView 和 JSCore 一样。但是转念一想,WKWebView 从 iOS 8 就出现了,现在到 iOS 11 了,难道都没想过如何解决回调这个问题么?难道苹果那帮开发都没发现么?怎么办,这不科学啊。
2.3.4 我有一个同学
其实,我们一开始就想错了。一直在想,如何把这个方法传过来,其实纵使能把一个 function 传过来,我们也没有办法去执行,因为我们能执行的只有一个字符串,而这个字符串执行后作用域肯定是会变的。所以,归根到底,这是 H5 的工作,我们做不了,想要支持回调,让 H5 自己去研究。我敢保证,你如果这么去给 H5 说,他追出去三条街,也要把砍你。
我们要先帮 H5 解决这个问题,我们才能去推动 H5 解决这个问题。
然而,我有一个同学,一个做 H5 的同学,@励志成为网红的网黄,在我苦苦思索不能解决的时候,我给他说了我的问题。然后我们就这个问题和看法进行了深入的探讨和交流。在达成了某些不可描述的交易之后,我们终于找到了一种解决办法。
他说,可以用 BroadcastChannel来解决这个问题。
BroadcastChannel API 允许同一原始域和用户代理下的所有窗口,iFrames等进行交互。也就是说,如果用户打开了同一个网站的的两个标签窗口,如果网站内容发生了变化,那么两个窗口会同时得到更新通知。
然后进行了一波研究之后,发现 API 不支持。有兴趣的可以研究这个 API
然后,我们继续进行交易,好在,这次交易,取得了重大成功。
有一天,他在看 Vue
的源码时,发现了这么一个类 MessageChannel,看起来可以解决这个问题。
官方文档上这么说
Channel Messaging API的MessageChannel接口允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据
它有两个端口,port1 和 port2,这两个端口可以互相发消息,可以互相监听,这样的话,我们是不是可以另辟蹊径来解决这个问题呢,我们来看下代码。
JS 代码
document.getElementById("li1").onclick = function () {
const arg1 = 100;
const arg2 = 200;
_postMessage(person, 'nativeMethod').then((val) => {
// 6.
console.log(val);
console.log(arg1 + arg2);
})
};
function _postMessage(val, name){
var channel = new MessageChannel(); // 创建一个 MessageChannel
window.nativeCallBack = function(nativeValue) {
// 3.
channel.port1.postMessage(nativeValue)
};
// 1.
window.webkit.messageHandlers[name].postMessage(val);
return new Promise((resolve, reject) => {
channel.port2.onmessage = function(e){
// 4
var data = e.data;
// 5.
resolve(data);
channel = null;
window.nativeCallBack = null;
}
})
}
我们封装了一个 _postMessage 方法,在这个方法中我们,返回了一个 Promise 对象,其实 JS 调用 Native 是一个异步操作,JS 调用客户端,等待客户端执行完毕,执行完毕后,告诉 JS,JS 在执行接下来的操作。
OC 代码
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"nativeMethod"]) {
NSLog(@"body:%@, ", message.body);
NSDictionary *dict = @{
@"key1": @"value1",
@"key2": @"value2"
}; // 构造回传 js 数据
id data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // 转为 json 字符串
// 2
[_webView evaluateJavaScript:[NSString stringWithFormat:@"%@(%@)", @"nativeCallBack", jsonString] completionHandler:^(id _Nullable jsData, NSError * _Nullable error) {
}];
}
}
在 OC 代码中,我们构造一个 JSON ,然后执行 JS nativeCallBack(jsonString)
,把构造的 JSON 传给 JS。
注意上边代码的注释,我们来一步一步看,发生了什么。
- 把值传给 Native。
- Native 接受到之后,调用 JS 的 nativeCallBack 方法。
- 接收到 Native 调用之后,channel 的 port1 把 Native 的值转出去。
- channel 的 port2 接收到 port1 发送的值之后,在 prot2 的 onmessage 方法中接收。
- 执行 Promise 的 then,并把 data 传过去。
- then 接收到调用,执行里边的代码。
那到底能不能执行呢,我们运行一下试试
哈哈哈,果然和我们预料的一样,我只想说一句,
作者:XcodeMen
链接:https://www.jianshu.com/p/c9ceb6a824e2
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
WK 与 JS 的那些事的更多相关文章
- JS闭包那些事
关于闭包,我曾经一直觉得它很讨厌,因为它一直让我很难搞,不过有句话怎么说来着,叫做你越想要一个东西,就要装作看不起它的样子.所以,抱着这个态度,我终于掳获了闭包. 首先来认识一下什么是闭包,闭包,一共 ...
- Js的那些事
先说说 var array = new Array(10); 和 var array = Array.apply(null, {length:10});这两个有啥区别,乍一看两个都是生成长度是10的 ...
- js弹窗那些事
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name ...
- JS 比较日期相隔都少天&& 比较两个日期大小&&指定日期往前后推指定天数
//这些天常接触到有关于js操作日期事 就小结了一下,希望对你有帮助 function conversionDate(a,b){ var start =a.split('-'); var end = ...
- 一个Java程序猿眼中的前后端分离以及Vue.js入门
松哥的书里边,其实有涉及到 Vue,但是并没有详细说过,原因很简单,Vue 的资料都是中文的,把 Vue.js 官网的资料从头到尾浏览一遍该懂的基本就懂了,个人感觉这个是最好的 Vue.js 学习资料 ...
- JS&Swift相互交互
加载本地HTML文件 x override func loadView() { super.loadView() let conf = WKWebViewCon ...
- [web建站] 极客WEB大前端专家级开发工程师培训视频教程
极客WEB大前端专家级开发工程师培训视频教程 教程下载地址: http://www.fu83.cn/thread-355-1-1.html 课程目录:1.走进前端工程师的世界HTML51.HTML5 ...
- UEditor编辑器并不难
1.背景: 其实学习UEditor本该在这之前就应该学习整合到自己的项目中的了,第一次接触UEditor是在暑假期间,当时做东西在师兄的代码中发现了这东西,心想:卧槽,竟然可以这样整合别 ...
- Salesforce 快速查看被引入Package的组件
在 Salesforce Package 生成一个新版本的时候,由于经常需要去检查有哪些新的组件将要被引入 Package 中,这个在有众多组件的情况下检查起来会有点眼花缭乱,为了方便,就想着用 JS ...
随机推荐
- uni-app 页面配置和跳转(一)转
今天看Dcloud官网更新了个uni-app,据说一套代码三端发布(Android,iOS,微信小程序),果断一试. uni.navigateTo(OBJECT) 保留当前页面,跳转到应用内的某个页面 ...
- SSH连接virtualbox中的虚拟机
SSH连接virtualbox中的虚拟机 SSH 与 Virtualbox 使用virtualbox创建虚拟机进行工作,可以有效地减少本机环境与工作环境之间的相互影响.但Server虚拟机的界面实在太 ...
- 使用idea开发工具,nginx服务部署Extjs6,SpringBoot项目到服务器
编译ExtJs文件 1.输入命令 2.开始编译 3.找到编译后的文件 E:\idea_project\BaiSheng_Model\fin-ui\build\production\Admin 4.将文 ...
- jsp继承rapid库
如果jsp不使用继承方式开发,而是用标准的指令或动作元素去include,实在是太多重复代码. rapid-framework是谷歌的一个项目,可以实现jsp网页的继承,也就是书写模板页. 但是在ma ...
- Java - 谨慎覆盖equals
平时很难遇到需要覆盖equals的情况. 什么时候不需要覆盖equals? 类的每个实例本质上是唯一的,我们不需要用特殊的逻辑值来表述,Object提供的equals方法正好是正确的. 超类已经覆盖了 ...
- Oracle PL/SQL Developer 上传下载Excel
接到需求,Oracle数据库对Excel数据进行上传和下载,百度后没有很全的方案,整理搜到的资料,以备不时之需. 一.下载Oracle数据到Excel中. 下载数据到Excel在MSSql中很简单,直 ...
- HDU 3191 次短路长度和条数
http://www.cnblogs.com/wally/archive/2013/04/16/3024490.html http://blog.csdn.net/me4546/article/det ...
- JRebel&XRebel
介绍==>>>> JRebel&XRebel官网 https://zeroturnaround.com/HotSwap和JRebel原理 http://www.holl ...
- Docker问题集合
1. 安装后启动出现 解决办法: 删除以下文件夹重新启动docker服务即可: 可能原因:(1) 之前docker进程出现错误并保存在keys.json文件中 (2) 删除之前配置了阿里云镜像,生成了 ...
- js权威指南学习笔记(三)语句
1.声明语句 如果用var声明的变量没有初始化,那么这个变量的值会被初始化为undefined. 函数声明语句的语法如下: 4 4 1 console.log(func ...