- WKWebView 自有方式解决js-oc互调时,发现在js调用oc并需要oc回调js时有问题,请看目录二、WKWebView【自有方式js回调】解决互调问题。
- 最近前端小哥哥反馈说H5页面里头部悬停功能不流畅,页面滑动时计时器停止等等问题,说是IOS UIWebView的问题,建议改用WKWebView。
目录索引
一、【WebViewJavascriptBridge方式】解决互调问题
二、WKWebView【自有方式js回调】解决互调问题
三、js中alert失效,及解决alert问题带来的潜在crash风险
四、禁止长按、选择
五、Scheme唤起第三方APP,NSURLErrorDomain Code=-1002 "unsupported URL"
六、关于WKWebView 类似UIWebView.scalesPageToFit自适应说明
七、刷新 reload 与 新增方法reloadFromOrigin
八、ios9闪退 -[xxxVC retain]: message sent to deallocated instance
九、头部悬停不随下拉刷新移动
十、设置userAgent
有关UIWebView与WKwebView的区别和性能,有更好的文章已做说明,这里主要记录使用WKWebViews使用时遇见的问题,已经与WebViewJavascriptBridge进行互调时的问题:
Reference error: can't find variable:WebViewJavascriptBridge
使用webView避免不了的就是原生与页面(OC与JS的交互)的互调,WKWebView中【自有方式】下文会介绍(注意【回调问题】);而我们的工程里一直使用的是【WebViewJavascriptBridge方式】来解决互调问题的,并且将JSBridge文件放在服务端,在页面中直接引用。来到GitHub上看了WebViewJavascriptBridge,下载了最新的SDK和Demo,测试WKWeb成功。接下来就是整合到工程中了。
一、【WebViewJavascriptBridge方式】解决互调问题
查看了WKWebView文档,了解了基本的使用,直接上代码
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
configuration.preferences = preferences;
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
_webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_webView.navigationDelegate = self;// 可无
_webView.UIDelegate = self;// 可无
// jsBridge
[WKWebViewJavascriptBridge enableLogging];
self.bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.webView];
[self.bridge setWebViewDelegate:self];// 为什么可无
[self.bridge registerHandler:@"callActionFromJsBridge" handler:^(id data, WVJBResponseCallback responseCallback) {
}];
SDK - WKWebViewJavascriptBridge文件中的下列方法添加return,否则会crash。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;// TODO_WKWeb 开发者添加return;
}
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
JSBridge.js文件
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
})
var JSBridge = {
call: function(host, method, params, callBack) {
bridge.callHandler('callActionFromJsBridge', {
'host': host,
'method': method,
'params': params
},
callBack);
}
};
window.JSBridge = JSBridge;
页面使用
JSBridge.call('bridge', 'getClientInfo', {},
function(res) {
if (res.code == '0') {
resolve(res);
} else {
reject(res);
}
});
当在工程中pod最新SDK后,APP中【部分】H5页面不能正常显示和使用,控制台显示如下错误:
Reference error: can't find variable:WebViewJavascriptBridge
前端小哥哥看了页面,alert(variable:WebViewJavascriptBridge),但在WKWebView中都不弹框显示了(后面会介绍,需要使用到WKUIDelegate里的方法)后表示很懵逼,因为比较忙,没空查看是否是因为正常页面与非正常页面的实现方式不同等导致的,就去敲代码了。。。
我这边调查了解到:WKWeb的注入js方法
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
是异步的,而UIWebView 的注入js方法
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
是同步的。结合APP中H5页面部分页面有上述错误,大部分页面正常,猜测是这个原因导致的,仅猜测而已。
在测试服务器上多次尝试后,综合考虑,决定在移动端直接注入JSBridge,而前端页面保留以前引入的js文件或直接注释掉两种情况下,测试页面都正常了。【注意】WKUserScriptInjectionTime枚举要使用WKUserScriptInjectionTimeAtDocumentStart。
同时注意注入的js语句是否有语法错误,将会导致注入是否有效。
// 本地注入JSBridge
NSString *jspath = [[NSBundle mainBundle]pathForResource:@"JSBridge.txt" ofType:nil];
NSString *source = [NSString stringWithContentsOfFile:jspath encoding:NSUTF8StringEncoding error:nil];
WKUserScript *userScript = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[configuration.userContentController addUserScript:userScript];
二、WKWebView【自有方式js回调】解决互调问题
- JS 调 OC: 在OC中用 addScriptMessageHandler 添加消息,同时实现WKScriptMessageHandler协议中方法。JS中使用window.webkit.messageHandlers.YourFunName.postMessage() 发送消息,注意要保持YourFunName的一致。
【回调问题】:测试中发现body中的数据不支持回调callBack,只支持常规数据类型如字典。因为功能的历史原因,js调用OC后需要通过回调来回传需要的数据,而在遇见上述问题时,尝试了这种方式,发现js中虽然传入了callBack,但OC中只接收到常规数据,没有callBack。
【回调问题方案一】:在js中将回调方法转换为字符串后传递给OC端,OC端使用转换后的回调方法字符串注入js方法,测试发现,不能完全适配线上的方式,注入js后报错:发生JavaScript异常。如:Error Domain=WKErrorDomain Code=4 "发生JavaScript异常" UserInfo={WKJavaScriptExceptionLineNumber=2, WKJavaScriptExceptionMessage=TypeError: undefined is not an object (evaluating ), WKJavaScriptExceptionColumnNumber=48, WKJavaScriptExceptionSourceURL=xxxURL, NSLocalizedDescription=发生JavaScript异常}
【回调问题方案二】:看过WVJB源码的小伙伴应该知道,在库文件WebViewJavascriptBridge_JS.m中,js调用OC时的方法回调是保存回调方法在本地字典中,以callbackId为key。oc回调时通过方法名和callbackId综合查找到js的回调方法并处理。利用这个思路,解决了问题,并兼容线上使用WVJB的项目,不用修改方法调用方式,只需要引入一段新的js替换WVJB的js即可。
[configuration.userContentController addScriptMessageHandler:self name:@"YourFunName"];
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSDictionary *bodyParam = (NSDictionary*)message.body;
CallBackHandler callBack = ^(id responseData){
NSString *jsonString = [NSString dicToJsonStr:responseData];
// 方案一
NSString *callBackFuncStr = bodyParam[@"callBackFuncStr"];
NSString *jsCallBack = [NSString stringWithFormat:@"((%@)(%@));", callBackFuncStr, jsonString];// 注入js后报错:发生JavaScript异常。
// 方案二
NSString *funcUniqueID = bodyParam[@"funcUniqueID"];
NSString *jsCallBack = [NSString stringWithFormat:@"((ma_sendCallBackFunc)('%@',%@));", funcUniqueID, jsonString];
[self.wkWebView evaluateJavaScript:jsCallBack completionHandler:^(id _Nullable result, NSError * _Nullable error) {
if (error) {
NSLog(@"执行回调 error is %@", error);
}
}];
};
// 内部匹配方法选择器,执行对应的方法
[BridgeImpl actionForJsBridge:self.parentVC params:bodyParam callBack:callBack];
}
window.webkit.messageHandlers.callActionFromJsBridgeTest.postMessage({ 'host': host, 'method': method, 'params': params })
- OC 调 JS:直接在OC中使用上文提到的evaluateJavaScript方法即可,注意,异步。
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
self.navigationItem.title = obj;
}];
回调问题方案二: 引入一段新的js替换WVJB的js
// js端保存回调方法并返回方法索引ID,原生端根据方法索引ID回调js
// 1-回调方法存储器和索引ID
var responseCallBackFuncDict = {};
var responseCallBackFuncUniqueID = 1;
// 2-js调用原生时,添加回调方法到回调方法存储器,并返回索引ID
function ma_addToCallBackFuncDictWith(responseCallBackFunc){
if(!responseCallBackFunc) return 'unValid_funcUniqueID';
var funcUniqueID = 'ma_'+(responseCallBackFuncUniqueID++)+'_'+new Date().getTime();
responseCallBackFuncDict[funcUniqueID] = responseCallBackFunc;
return funcUniqueID;
}
// 3-原生接受js调用并处理相关操作后,发送回调给js,js根据索引ID寻找回调方法来处理数据,然后移除js的回调方法
function ma_sendCallBackFunc(funcUniqueID,responseData){
if(!funcUniqueID) return;
responseCallBackFunc = responseCallBackFuncDict[funcUniqueID];
if(!responseCallBackFunc) return;
responseCallBackFunc(responseData);
delete responseCallBackFuncDict[funcUniqueID];
}
// 保持WVJB一致的方法调用
var JSBridge = {
call:function(host, method, params, callBack) {
var funcUniqueID = ma_addToCallBackFuncDictWith(callBack);
window.webkit.messageHandlers.callActionFromJsBridge.postMessage({
'host': host,
'method': method,
'params': params,
'funcUniqueID': funcUniqueID
});
}
};
window.JSBridgeVar = JSBridge;
// 保持WVJB一致的方法调用 end
三、js中alert失效,及解决alert问题带来的潜在crash风险
对应关系
alert runJavaScriptAlertPanelWithMessage
prompt runJavaScriptTextInputPanelWithPrompt
confirm runJavaScriptConfirmPanelWithMessage
关于js中alert/prompt/confirm失效问题,实现WKUIDelegate协议中的三个方法即可,如下:
#pragma mark - WKUIDelegate
-(void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"温馨提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}])];
[self presentViewController:alertController animated:YES completion:nil];
}
-(void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"温馨提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler(NO);
}])];
[alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler(YES);
}])];
[self presentViewController:alertController animated:YES completion:nil];
}
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {
NSString *hostString = webView.URL.host;
NSString *sender = [NSString stringWithFormat:@"提示:%@", hostString];
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:sender preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.text = defaultText;
}];
[alertController addAction:[UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSString *input = ((UITextField *)alertController.textFields.firstObject).text;
completionHandler(input);
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler(nil);
}]];
[self presentViewController:alertController animated:YES completion:^{}];
}
#pragma mark WKUIDelegate end
注意,添加上述方法后,页面alert可能会导致crash,错误如下
Completion handler passed to -[xxxVC webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called
从 crash 可以看出是 WKWebView 回调函数: runJavaScriptAlertPanelWithMessage中completionHandler 没有被调用导致的。
review代码,在弹窗操作回调里明明调用了completionHandler。为什么呢???
于是猜测:
- WKWebView退出的时候,js刚好执行了alert, alert 框可能弹不出来,completionHandler 没有被执行,导致 crash;
- WKWebView 一打开,js就执行alert,此时由于 WKWebView 所在的 UIViewController 出现(push或present)的动画尚未完成,alert 框可能弹不出来,completionHandler 最后没有被执行,导致 crash。
四、禁止长按、选择
// "document.documentElement.style.webkitTouchCallout='none';" // 禁止长按
// "document.documentElement.style.webkitUserSelect='none';" // 禁止选择
// 方式-1
source = @"document.documentElement.style.webkitTouchCallout='none';document.documentElement.style.webkitUserSelect='none';";
userScript = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
[configuration.userContentController addUserScript:userScript];
// 方式-2
[webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil];
[webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:nil];
// 方式-3
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
[webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil];
[webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none';" completionHandler:nil];
}
五、Scheme唤起第三方APP,NSURLErrorDomain Code=-1002 "unsupported URL"
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSString *urlStr = navigationAction.request.URL.absoluteString;
BOOL check = ![urlStr hasPrefix:@"http://"] && ![urlStr hasPrefix:@"https://"] && ![urlStr hasPrefix:@"about:"] && ![urlStr hasPrefix:@"jsbridge:"];
if ( check ) {
[[GotoManager manager] openThirdWithScheme:urlStr callBack:^(BOOL isOpen) {
}];
}
decisionHandler(WKNavigationActionPolicyAllow);//允许跳转
}
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
NSString *urlStr = error.userInfo[NSURLErrorFailingURLStringErrorKey];
BOOL check = ![urlStr hasPrefix:@"http://"] && ![urlStr hasPrefix:@"https://"] && ![urlStr hasPrefix:@"about:"] && ![urlStr hasPrefix:@"jsbridge:"];
if ( check ) {
[[GotoManager manager] openThirdWithScheme:urlStr callBack:^(BOOL isOpen) {
}];
}
}
六、关于WKWebView 类似UIWebView.scalesPageToFit自适应说明
通过设置UIWebView属性scalesPageToFit为YES可以解决页面在不同设备上的自适应问题,但是WKWebView没有类似的相关属性。据说使用如下两种js语句注入可以解决,经过测试无效,【反而不注入,页面自适应了】。
// 自适应
// @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
// @"document.getElementsByName(\"viewport\")[0].content = \"width=self.webView.frame.size.width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\"";
七、刷新 reload 与 新增方法reloadFromOrigin
reload方法与UIWebview的比较,不同的是有返回值;另外增加了函数reloadFromOrigin。
reloadFromOrigin会比较网络数据是否有变化,没有变化则使用缓存,否则从新请求。适当的时候建议使用reloadFromOrigin。
八、ios9闪退 -[xxxVC retain]: message sent to deallocated instance
已知在iOS9.1和iOS9.3上,打开WKWebView页面关闭时,app闪退。根据错误日志,猜测与代理引用有关。review代码发现WKWeb的UIDelegate和navigationDelegate在WebViewJavascriptBridge和当前控制器的dealloc中都设置为nil了,但是自己设置的WKWebView.scrollView.delegate = self没有设置为nil。在控制器的dealloc中WKWebView.scrollView.delegate = nil后,测试成功。
九、头部悬停不随下拉刷新移动
页面下拉刷新异常如下图:
image.png
前端css代码添加第二个main.header{},覆盖以前样式:
#main .header {
background: linear-gradient(270deg,#ff2e2e 0,#ff0f52 100%);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 30;
}
#main .header {
position: -webkit-sticky;
position: sticky;
}
十、设置userAgent
1.目前适用UIWebView和WKWebView
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSString *newUserAgent = [NSString stringWithFormat:@"%@ APP_USER/APPName(ios_1.0.0) ", userAgent?:@""];
NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:newUserAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dic];
[[NSUserDefaults standardUserDefaults] synchronize];
2.WKWebView
WKWebView *wkWeb = [WKWebView new];
[wkWeb evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable userAgent, NSError * _Nullable error) {
NSString *addAgent = @" APP_USER/APPName(ios_1.0.0) ";
if ( ![userAgent hasSuffix:addAgent] ) {
NSString *newUserAgent = [userAgent stringByAppendingString:addAgent];
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newUserAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
[[NSUserDefaults standardUserDefaults] synchronize];
// 在网上找到的没有下面这句话,结果只是更改了本地的UserAgent,没修改网页的.
// 导致一直有问题,好低级的错误,这个函数是9.0之后才出现的,在这之前,
// 把这段代码放在WKWebView的alloc之前才会有效
if (@available(iOS 9, *)) {
[wkWeb setCustomUserAgent:newUserAgent];
}
else {
[WKWebView setValue:newUserAgent forKey:@"applicationNameForUserAgent"];//KVC设置
}
}
}];
- 与前端合作,jsBridge方式,让前端调用原生iOS方法,或者iOS调用前端的方法都可以。
网友评论