苹果在iOS 8中推出了 WKWebView,这是一个高性能的 web 框架,相较于 UIWebView来说,有巨大提升。本文将针对 WKWebView 进行简单介绍,然后介绍下如何和 JS 进行愉快的交互。还望各位大佬不吝赐教。

本文分为两大部分

  1. WKWebView 简单介绍
  2. 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 上运行

 
使用 WKWebView 和 UIWebView 打开 京东 网易 新浪 三个网站所耗费的时长
 
使用 WKWebView 和 UIWebView 打开 京东 网易 新浪 三个网站所耗费的内存

在内存测试中发现,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;
}
  1. 初始化一个 WKWebView,我们需要传一个 WKWebViewConfiguration对象,来对 WKWebView 进行配置。
  2. 构造一个请求。
  3. 加载这个请求。

只需要这三步,我们就可以使用一个高性能的 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,

  1. 使用 WKUserScript
  2. 直接调用 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,我们也是要通过它来进行,而你所需要做的,只需要三步,需要三步,三步。

  1. 向 JS 注入一个字符串
[_webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeMethod"];

我们向 JS 注入了一个方法,叫做 nativeMethod

  1. JS 调用 Native
window.webkit.messageHandlers.nativeMethod.postMessage(value);

一句话调用,我们就可以在 Native 中接收到 value

  1. 接收 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  

说好的不受限制呢

 
15088520633631.jpg

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。

注意上边代码的注释,我们来一步一步看,发生了什么。

  1. 把值传给 Native。
  2. Native 接受到之后,调用 JS 的 nativeCallBack 方法。
  3. 接收到 Native 调用之后,channel 的 port1 把 Native 的值转出去。
  4. channel 的 port2 接收到 port1 发送的值之后,在 prot2 的 onmessage 方法中接收。
  5. 执行 Promise 的 then,并把 data 传过去。
  6. then 接收到调用,执行里边的代码。

那到底能不能执行呢,我们运行一下试试

 
 

哈哈哈,果然和我们预料的一样,我只想说一句,

作者:XcodeMen
链接:https://www.jianshu.com/p/c9ceb6a824e2
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

WK 与 JS 的那些事的更多相关文章

  1. JS闭包那些事

    关于闭包,我曾经一直觉得它很讨厌,因为它一直让我很难搞,不过有句话怎么说来着,叫做你越想要一个东西,就要装作看不起它的样子.所以,抱着这个态度,我终于掳获了闭包. 首先来认识一下什么是闭包,闭包,一共 ...

  2. Js的那些事

    先说说  var array = new Array(10); 和 var array = Array.apply(null, {length:10});这两个有啥区别,乍一看两个都是生成长度是10的 ...

  3. js弹窗那些事

    <!doctype html> <html> <head> <meta charset="utf-8"> <meta name ...

  4. JS 比较日期相隔都少天&& 比较两个日期大小&&指定日期往前后推指定天数

    //这些天常接触到有关于js操作日期事 就小结了一下,希望对你有帮助 function conversionDate(a,b){ var start =a.split('-'); var end = ...

  5. 一个Java程序猿眼中的前后端分离以及Vue.js入门

    松哥的书里边,其实有涉及到 Vue,但是并没有详细说过,原因很简单,Vue 的资料都是中文的,把 Vue.js 官网的资料从头到尾浏览一遍该懂的基本就懂了,个人感觉这个是最好的 Vue.js 学习资料 ...

  6. JS&Swift相互交互

    加载本地HTML文件       x         override func loadView() {    super.loadView()    let conf = WKWebViewCon ...

  7. [web建站] 极客WEB大前端专家级开发工程师培训视频教程

    极客WEB大前端专家级开发工程师培训视频教程  教程下载地址: http://www.fu83.cn/thread-355-1-1.html 课程目录:1.走进前端工程师的世界HTML51.HTML5 ...

  8. UEditor编辑器并不难

    1.背景:        其实学习UEditor本该在这之前就应该学习整合到自己的项目中的了,第一次接触UEditor是在暑假期间,当时做东西在师兄的代码中发现了这东西,心想:卧槽,竟然可以这样整合别 ...

  9. Salesforce 快速查看被引入Package的组件

    在 Salesforce Package 生成一个新版本的时候,由于经常需要去检查有哪些新的组件将要被引入 Package 中,这个在有众多组件的情况下检查起来会有点眼花缭乱,为了方便,就想着用 JS ...

随机推荐

  1. kd-tree 小结

    核心思想 是一种分割 \(k\) 维数据空间的数据结构 一维情况下就是平衡树,以 \(key\) 为标准判断插入左儿子还是右儿子 \(kdtree\) 就是平衡树在多维空间的扩展 因为有多维,我们按不 ...

  2. mac隐藏和显示隐藏文件

    显示:defaults write com.apple.finder AppleShowAllFiles -bool true隐藏:defaults write com.apple.finder Ap ...

  3. php注册

    <?php var_dump($_GET);//打印出对象的数据类型//链接数据库$link = @mysql_connect('localhost','root','root');#选择数据库 ...

  4. 核心API

    1.ProcessEngine ProcessEngine是Activiti中最核心的类,其他的类都是由他而来.Activiti流程引擎的配置文件是名为 activiti.cfg.xml 的XML文件 ...

  5. Oracle中的索引详解(转载)

    一. ROWID的概念 存储了row在数据文件中的具体位置:64位 编码的数据,A-Z, a-z, 0-9, +, 和 /, row在数据块中的存储方式 SELECT ROWID, last_name ...

  6. arcgis 地理国情建库软件已完成

    arcgis 地理国情软件已完成: 1.创建1:25000(或则其他比例尺)国家2000坐标系接合表 2.按照地理国情普查数据库标准,创建标准数据库 3.外业调查工作底图制作 4.矢量和影像数据批量裁 ...

  7. sprintf详解

    原文:http://www.cnblogs.com/wqlblogger/archive/2007/01/09/615525.html 转摘声明:选自<CSDN 社区电子杂志——C/C++杂志& ...

  8. (转) 在PHP中使用全局变量

    简介 即使开发一个新的大型PHP程序,你也不可避免的要使用到全局数据,因为有些数据是需要用到你的代码的不同部分的.一些常见的全局数据有:程序设定类.数据库连接类.用户资料等等.有很多方法能够使这些数据 ...

  9. C语言数组指针(指向数组的指针)

    注意:数组指针的定义,与指针数组的区别 转载:http://c.biancheng.net/cpp/biancheng/view/162.html 指向多维数组元素的指针变量 ① 指向数组元素的指针变 ...

  10. Subversion FAQ(常见问题解答)

    转自:http://subversion.apache.org/faq.zh.html 常见问题: 为什么会有这样一个项目? 为了接管CVS的用户基础.确切的说,我们写了一个新的版本控制系统,它和CV ...