WK 与 JS 的那些事

作者 : 开心源码 本文共9222个字,预计阅读时间需要24分钟 发布时间: 2022-05-11 共120人阅读

我们的小马童鞋又发功了。最近打算将UIWebView替换成WKWebView,所以原来的Hybrid层需要动动土,小马小试牛刀。当然遇到了少量问题,看看他是怎样一步步处理的吧。

苹果在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,看看这两个究竟有什么区别

WKWebViewUIWebView
内存占使用大 且有内存泄漏
加载速度
与 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 需要支持 结构化克隆算法 。很遗憾,这个算法目前不支持传递 FunctionError,它只支持一下几种类型

对象类型注意
所有的原始类型除了symbols
Boolean对象
String对象
Date
RegExplastIndex 字段不会被保留。
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 接收到调使用,执行里边的代码。

那究竟可以不可以执行呢,我们运行一下试试

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

总结

上边啰嗦了这么多,其实很简单,利使用 MessageChannel 端口转发功可以来处理作使用域改变的问题,JS 不使用传递方法给 Native,Native 直接调使用一个统一的全局方法就行。交互简单方便。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » WK 与 JS 的那些事

发表回复