WKWebview 和 UIWebview的区别以及通信
属性
UIWebView | WKWebView |
---|---|
UIScrollView *scrollView | UIScrollView *scrollView |
WKWebViewConfiguration *configuration | |
id <UIWebViewDelegate> delegate | id <WKUIDelegate> UIDelegate |
id <WKNavigationDelegate> navigationDelegate | |
WKBackForwardList *backForwardList | |
NSString *customUserAgent |
加载
UIWebView | WKWebView |
---|---|
- (void)loadRequest:(NSURLRequest *)request | - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request |
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL | - (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL |
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0)) | |
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL | - (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0)) |
- (void)stopLoading | - (void)stopLoading |
- (void)reload | - (nullable WKNavigation *)reload |
- (nullable WKNavigation *)reloadFromOrigin | |
NSURLRequest *request | |
NSString *title | |
NSURL *URL | |
double estimatedProgress | |
BOOL hasOnlySecureContent |
记录
UIWebView | WKWebView |
---|---|
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item | |
-(void)goBack | - (nullable WKNavigation *)goBack |
-(void)goForward | - (nullable WKNavigation *)goForward |
BOOL canGoBack | BOOL canGoBack |
BOOL canGoForward | BOOL canGoForward |
BOOL loading | BOOL loading |
执行JS
UIWebView | WKWebView |
---|---|
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script | - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler |
手势
UIWebView | WKWebView |
---|---|
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point | |
BOOL keyboardDisplayRequiresUserAction | |
BOOL scalesPageToFit | |
BOOL allowsBackForwardNavigationGestures | |
BOOL allowsMagnification | |
CGFloat magnification |
分页
只有 UIWebView
有分页的属性
- UIWebPaginationMode paginationMode
- UIWebPaginationBreakingMode paginationBreakingMode
- CGFloat pageLength
- CGFloat gapBetweenPages
- NSUInteger pageCount
JS 交互
主要对比 UIWebView
与 JS
、WKWebView
与 JS
的交互实现方式
UIWebView <--> JS
UIWebView
与 JS
的交互主要借助 JSBridge
。两端通过实现相同的 bridge
逻辑,流程比较多。比如实现:
- 调用方注册
registerHandler:_
- 调用另一端的方法入口:
callHandler:_
- 处理来自Native的调用:
_handleMessageFromNative:
- 处理来自 JS 的调用:JS 发送调用数据给 Native 是通过改变
iframe
的src
messagingIframe.src = json
,触发 web 的 loadRequest,然后进入 Native 的代理方法- _shouldStartLoadWithRequest:
。Native 在这个发法中解析出 json 数据
WKWebView <--> JS
WKWebView
与 JS
交互相对来说,比较简单。
Native 调用 JS 两种方式:
- 借助
WKUserScript
- 直接使用
evaluateJavaScript:_ completionHandler:_
使用 WKUserScript
改变 web 的背景色:
// initWithSource: 传入一段 js
// injectionTime: js 注入时机。两个时机 document 加载前 或 加载后
// forMainFrameOnly: 作用于所有的 frame 还是 main frame
WKUserScript *script = [[WKUserScript alloc] initWithSource:@"document.body.style.background = \"#777\";" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *content = [WKUserContentController new];
[content addUserScript:script];
WKWebViewConfiguration *config = [WKWebViewConfiguration new];
config.userContentController = content;
使用 evaluateJavaScript:_ completionHandler:_
:
[self.webView evaluateJavaScript:@"document.body.style.background = \"#777\";" completionHandler:^(id _Nullable, NSError * _Nullable error) {
//处理 js 回调
}];
JS 调用 Native
初始化 WKWebView
的时候,它有个 configuration
参数,上面使用 WKUserScript
我们已经初始化这个对象。
- 我们依然借助这个属性,向 JS 注入一个方法
callJSMethod
:
[self.wkweb.configuration.userContentController addScriptMessageHandler:self name:@"callNativeMethod"];
- JS 调用这个方法:
window.webkit.messageHandlers.callNativeMethod.postMessage(value);
- Native 收到该方法的调用:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSString *name = message.name; //上面我们注入到 js 的名字
id param = message.body; // js 传过来的参数
if ([name isKindOfClass:[NSString class]]) {
if ([name isEqualToString:@"callNativeMethod"]) {
//TODO 处理该方法的调用
}
}
}
问题
postMessage问题一
之前调试 Native 与 JS 通信的时候,遇到了 web 调用 postMessage(_) 后,Native 收不到的问题。然后解决了,并在 Stack Overflow 上回答了这个问题
postMessage问题二
有一种场景,JS 调用 Native 后,要处理 Native 的回调。web端做法如下:
const pageInfo = {
pageUrl: "http://www.taobao.com/",
};
document.getElementById("click").onclick = function (value) {
pageInfo.callBack = function () {
console.log("native call back");
}
window.webkit.messageHandlers.callNativeMethod.postMessage(pageInfo);
};
我们给 pageInfo
增加一个 callback 属性,用来处理来自 Native 的回调。运行调试,结果报错:
DataCloneError(DOM Exception 25): The object can not be cloned.
这是因为当调用 postMessage(message) 时,里面的 message 需要支持 结构化克隆算法(也就是copy js 中复杂参数的算法)。遗憾的是 Function
和 Error
都不支持。
换种方式,把 Function
转换为 String
:
document.getElementById("click").onclick = function (value) {
pageInfo.callBack = function () {
console.log("native call back");
}.toString();
window.webkit.messageHandlers.callNativeMethod.postMessage(pageInfo);
};
Native 处理 JS 调用后,回调给 JS:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSString *name = message.name; //上面我们注入到 js 的名字
NSDictionary *dict = @{@"location": @"xxx"}; // 构造回传 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) {
}];
}
运行调试,成功。
作用域问题
我们增加一个局部变量 number1
。收到 Native 回调后,输出这个临时变量的值
document.getElementById("click").onclick = function (value) {
var arg1 = 10;
pageInfo.callBack = function () {
console.log("native call back");
console.log(arg1);
}.toString();
window.webkit.messageHandlers.callNativeMethod.postMessage(pageInfo);
};
运行调试,结果报错:
ReferenceError:Can't find variable: arg1
这是因为我们把一个 function
转换成 字符串之后,传给 Native,Native 在执行的时候,他的作用域已经变了,变成了 window
,这个时候,window
下没有 arg1,所以报错找不到该变量。
解决方法是,使用 MessageChannel
Channel Messaging API的Channel Messaging接口允许我们创建一个新的消息通道,并通过它的两个
MessagePort
属性发送数据
借助于 MessageChannel
我们调整消息传递的流程如下:
- 封装一个方法
_postMessage
用来调用 native 方法,同时返回一个promise
用来接收回调数据 - 在
_postMessage
方法中 我们为window
增加一个函数回调nativeCallBack
- 然后调用 native 方法
callNativeMethod
- native 将处理结果,通过
nativeCallBack
返回给window
- 在
window
的函数回调中,我们通过channel.port1.postMessage(返回的数据)
把返回的数据传递给channel
- 在方法
_postMessage
返回的promise
里面,通过channel.port2.onMessage
来接收port1
传递过来的值,然后resolve(返回的值)
- 在外部 调用
_postMessage
的地方,执行promise
的then
,就可以把数据传递给调用方了。 - 这样,调用方的局部变量不需要依赖
window
,而且还能收到window
回调的数据
// 调用方
document.getElementById("li1").onclick = function () {
const arg1 = 10;
_postMessage(person, 'nativeMethod').then((val) => {
// 7.
console.log(val);
console.log(arg1);
})
};
// 1.
function _postMessage(val, name){
var channel = new MessageChannel(); // 创建一个 MessageChannel
// 2.
window.nativeCallBack = function(nativeValue) {
// 5.
channel.port1.postMessage(nativeValue)
};
// 3.
window.webkit.messageHandlers[name].postMessage(val);
return new Promise((resolve, reject) => {
channel.port2.onmessage = function(e){
// 6.
var data = e.data;
resolve(data);
channel = null;
window.nativeCallBack = null;
}
})
}
在 native 端,处理回调:
NSString *jsonString = json格式的回调数据;
// 4.
[_webView evaluateJavaScript:[NSString stringWithFormat:@"%@(%@)", @"nativeCallBack", jsonString] completionHandler:^(id _Nullable jsData, NSError * _Nullable error) {
}];
再次运行调试,成功打印了临时变量 arg1.