WKWebView 的简单用法
基本用法
创建
1 | - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration; |
比UIWebView
多了个configuration
,这个配置可以设置很多东西。具体查看WKWebViewConfiguration.h
,可以配置js是否支持,画中画是否开启等,这里主要讲两个比较常用的属性:
websiteDataStore
WKWebView
拥有自己的私有存储,它的一些缓存等数据都存在websiteDataStore
中。
1 | @property (nonatomic, strong) WKWebsiteDataStore *websiteDataStore; |
具体增删改查就可以通过WKWebsiteDataStore.h
中提供的方法,这里不多说,一般用的时候比较少,真的要清除缓存,简单粗暴的方法是删除沙盒目录中的Cache文件夹。
userContentController
这个属性很重要,js与oc交互,以及注入js都会用到。
1 | @property (nonatomic, strong) WKUserContentController *userContentController; |
查看WKUserContentController
的头文件,你会发现它有如下几个方法:
1 | @interface WKUserContentController : NSObject <NSCoding> |
基本创建
1 | WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; |
动态注入js
通过给userContentController
添加WKUserScript
,可以实现动态注入js。比如我先注入一个脚本,给每个页面添加一个Cookie
1 | //注入一个Cookie |
然后再注入一个脚本,每当页面加载,就会alert当前页面cookie:
1 | //创建脚本 |
注入的js source可以是任何js字符串,也可以js文件。比如你本地可能就会有一个native_functions.js
,你可以通过以下的方式添加:
1 | NSString *jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"native_functions" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil]; |
加载
1 | - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request; |
比如加载主 bundle 中的一个html需要使用loadRequest:
方法。loadReqest
这种方式会把当前load的这个html文件的路径作为baseURL,以此寻找其他资源。
1 | [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"]]]]; |
loadHTMLString:baseURL:
直接加载 html 字符串。baseURL
是 html 中资源的路径:
1 | NSString *html = @"...."; |
代理
UIWebView
的代理协议拆成了一个跳转的协议和一个关于UI的协议:
1 | @protocol WKNavigationDelegate; //类似于UIWebView的加载成功、失败、是否允许跳转等 |
WKNavigationDelegate
常用方法:
1 | //下面这2个方法共同对应了UIWebView的 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType; |
示例:控制哪些站点可以被访问
1 | func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { |
WKUIDelegate
当 JS 中调用 alert 等方法的时候,会来到这几个代理方法中,进行自定义操作。可以使用这个方式来进行 JS 向 Native 传递参数执行方法。
1 | /* 警告 */ |
新属性
1 | @property (nullable, nonatomic, readonly, copy) NSString *title; //页面的title,终于可以直接获取了 |
这些属性都很有用,而且支持KVO,所以我们可以通过KVO观察这些值的变化,以便于我们做出最友好的交互。
OC JS 交互
OC -> JS
1 | //执行一段js,并将结果返回,如果出错,error则不为空 |
示例,比如我想获取页面中的title
,除了直接self.webView.title
外,还可以通过这个方法:
1 | [self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) { |
JS -> OC
URL 拦截
通过自定义Scheme,在链接激活时,拦截该URL,拿到参数,调用OC方法:
1 | // 在HTML中写上A标签直接填写假请求地址 |
WKWebView 拦截:1
2
3
4
5
6
7
8
9
10
11
12
13- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//1 根据url,判断是否是所需要的拦截的调用 判断协议/域名
if (是){
//2 取出路径,确认要发起的native调用的指令是什么
//3 取出参数,拿到JS传过来的数据
//4 根据指令调用对应的native方法,传递数据
//确认拦截,拒绝WebView继续发起请求
decisionHandler(WKNavigationActionPolicyCancel);
}else{
decisionHandler(WKNavigationActionPolicyAllow);
}
return YES;
}
假跳转的缺点:
- 丢失消息。在同一个运行逻辑内快速的连续发送两个通信请求,那么后面的消息将收不到。
- URL长度限制。
scriptMessageHandler
在OC中添加一个scriptMessageHandler,则会在all frames
中添加一个js的function: window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
。那么当我在OC中通过如下的方法添加了一个handler,如
1 | //配置对象注入 |
这里不能直接传入 self,会被
WKUserContentController
强引用,而不能销毁。所以需要创建一个代理类对象,类似于处理 NSTimer。
当我在js中调用下面的方法时:
1 | //准备要传给native的数据,包括指令,数据,回调等 |
注意,
postMessage
方法要求必须要有一个参数,即使是一个空对象,也要写成postMessage({})
,否则 native 无法收到消息。
在OC中将会收到WKScriptMessageHandler
的回调
1 | -(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ |
我们可以把js的function转换为字符串,再传递给OC。再让 OC 通过
evaluateJavaScript:completionHandler:
调用,将结果传出。就实现了 js->oc->js->oc 的回调
记得在适当的地方调用 removeScriptMessageHandler
:
1 | - (void)dealloc { |
WKUIDelegate
前面说到,WKUIDelegate
协议中的几个弹窗方法,可以用来实现 JS2OC 的通信。前面几种 JS2OC 的通信的 callback 都需要 OC自己调用 OC2JS 的异步方法返回。而使用 WKUIDelegate
可以同步返回结果。
JS 端调用
1 | var data = { |
客户端拦截:
1 | - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler{ |
这里 prompt
和 defaultText
就是 JS 端传来的数据。通过 completionHandler()
就可以同步将值返回了。js 端调用接收:
1 | let result = window.prompt(somePrompt, someDefaultText) |
JavaScriptCore
JavaScriptCore是苹果Safari浏览器的JavaScript引擎
OC 调用 JS
OC 调用 JS 可以直接使用 evaluteScript
方法:
1 |
|
JSContext
- JSContext 是JS代码的执行环境。JSContext 为JS代码的执行提供了上下文环境,通过jSCore执行的JS代码都得通过JSContext来执行。
- JSContext对应于一个 JS 中的全局对象。JSContext对应着一个全局对象,相当于浏览器中的window对象,JSContext中有一个GlobalObject属性,实际上JS代码都是在这个GlobalObject上执行的,但是为了容易理解,可以把JSContext等价于全局对象。我们甚至可以直接在 JSContext 上拿 GlobalObject 的属性。
1 | JSValue *value = [context evaluateScript:@"var a = 1+2*3;"]; |
JSValue
- JSValue 是对 JS 值的包装。JS中的值拿到OC中是不能直接用的,需要包装一下
- JSValue存在于JSContext中。JSValue是不能独立存在的,它必须存在于某一个JSContext中。
- JSValue对其对应的JS值和其所属的JSContext对象都是强引用的关系。因为jSValue需要这两个东西来执行JS代码,所以JSValue会一直持有着它们。
示例:
js 代码:
1 | // 计算阶乘 |
oc 调用:
1 | NSString *factorialScript = [self loadJSFromBundle]; |
从 Bundle 中拿到 js 代码执行,从 context 中拿到方法,然后通过 callWithArguments
执行,执行后得到 JSValue 的值。
JS 调用 OC
两种方式可以让 OC 暴露方法给 JS:
- Block:可以将 OC 中的单个方法暴露给 JS 调用。
- JSExport协议:可以将OC的中某个对象直接暴露给JS使用
Block
JSCore会自动将这个Block包装成一个JS方法:
1 | context[@"makeNSColor"] = ^(NSDictorary * colors){ |
js 端就可以直接调用这个 makeNSColor
方法了。运行得到一个 NSColor 对象,在 js 中表现为一个 object。
如果我们在 js 端再对这个方法封装一下:
1 | var colorForWord = function (word) { |
在 oc 端调用的示意图如下:
OC Caller
去调用这个colorForWrod
函数,因为colorForWrod
函数接收的是一个String
类型那个参数word,OC Caller
传过去的是一个NSString
类型的参数,JSCore转换成对应的String
类型。然后colorForWrod
函数继续向下调用,就像上面说的,知道其拿到返回的wrapper Object
,它将wrapper Object
返回给调用它的OC Caller
,JSCore又会在这时候把wrapper Object
转成JSValue类型,最后再OC中通过对JSValue调用对应的转换方法,即可拿到里面包装的值,这里我们调用- toObject
方法,最后会得到一个NSColor
对象,即从最开始那个暴露给JS的Block中返回的对象。
注意点
- 不要在Block中直接使用JSValue。
- 不要在Block中直接使用JSContext。
Block会强引用它里面用到的外部变量,如果直接在Block中使用JSValue的话,那么这个JSvalue就会被这个Block强引用,而每个JSValue都是强引用着它所属的那个JSContext的,这是前面说过的,而这个Block又是注入到这个Context中,所以这个Block会被context强引用,这样会造成循环引用,导致内存泄露。不能直接使用JSContext的原因同理。所以建议把JSValue当做参数传到Block中,而不是直接在Block内部使用,这样Block就不会强引用JSValue了。
针对第二点,可以使用[JSContext currentContext]
方法来获取当前的Context。
JSExport 协议
通过JSExport 协议可以很方便的将OC中的对象暴露给JS使用,且在JS中用起来就和JS对象一样。
声明一个自定义的协议并继承自JSExport协议。然后当你把实现这个自定义协议的对象暴露给JS时,JS就能像使用原生对象一样使用OC对象了:
参考
iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够