今天在对接一个网页时加载网页总是碰到 Error loading page Domain: WebKitErrorDomain Error Code: 101 The URL can't be shown (无法显示的URL)这样的错误,当然WebView屏幕中间也出现了这样错误的提示和内容。
本以为是个小错误,其实并不简单。
谷歌了一下,网上也有各种解决方法
如:https://github.com/facebook/react-native/issues/9037 中 @lacker 的解决方法并不可行
代码语言:javascript复制renderError={ (e) => {
if (e === 'WebKitErrorDomain') {
return
}
}}
可以在评论区看到,并没有解决问题 于是没办法中的办法就是把 React Native 中 WebView 的代码撸了一遍 找到了 4 种解决办法,这里与大家分享,没进坑的同学直接跳过去,进坑的同学希望看到后对你有帮助
前缀引导
WebView 正如其名,就是用来加载网页(html),我们可以将网页链接(URL),网页内容(字符串),二进制流等交给 WebView 来显示我们制作的网页。
当然系统 API 也会给我们暴漏各种接口、回调供我们处理各种情况。
例如:
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType 当 WebView 将要处理一个新的请求时,询问是否允许此次请求
- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error 当 WebView 加载出现异常的时候,会进入此回调,供我们处理错误。
- 等等
出现此种错误的情况与原因
出现错误的原因
当 WebView 处理一个请求时,首先会进入
代码语言:javascript复制 - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
询问是否允许加载此次请求,以返回的 BOOL 值为准。
如果我们默认不实现此代理方法,系统会自动判断是否可以处理。如:是否是合法的 URL、是否是请求系统定制的一些 API,例如 tel:// 等等
而当我们不实现
代码语言:javascript复制- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
的回调时,即便出错了也不会有任何表现
言归正传: 出现这个错误的原因就是 WebView 加载了其实它无法处理的请求(URL)。导致进入了 “错误回调”。而“错误回调” RN 官方已经帮我们实现了其回调,并且帮我们加载了一个错误视图在上面。
如下是 iOS 代码:
代码语言:javascript复制- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if (_onLoadingError) {
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
// a new URL in the WebView before the previous one came back. We can just
// ignore these since they aren't real errors.
// [http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os](http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os)
return;
}
if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
// Error code 102 "Frame load interrupted" is raised by the UIWebView if
// its delegate returns FALSE from webView:shouldStartLoadWithRequest:navigationType
// when the URL is from an http redirect. This is a common pattern when
// implementing OAuth with a WebView.
return;
}
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"domain": error.domain,
@"code": @(error.code),
@"description": error.localizedDescription,
}];
_onLoadingError(event);
}
}
如下是 重点的部分 JS 代码
代码语言:javascript复制...
otherView = (this.props.renderError || defaultRenderError)(
errorEvent.domain,
errorEvent.code,
errorEvent.description
);
...
...
return (
<View style={styles.container}>
{webView}
{otherView}
</View>
);
...
从代码中可以看到,当webView 加载中出现一个错误时,会自动添加一个错误视图到 WebView 的视图正上方。也就是我们当前所碰到的错误的情况。
出现错误的情况
一般来说出现此情况的有如下几种原因:
- 不合法的URL
- 非 http/https 开头的URL
- URL含有不合法字符(需要用 URL 编码进行编码)
- URL 格式不正确
- 不合法的系统API
- 例如:tel:// 写成了 tell://
- 不合法的APP跳转
- 未在 LSApplicationQueriesSchemes 添加的第三方APP跳转
- 未安装的APP
- 例如跳转到 支付宝 alipays://
- 自定义的通过 URL 与 js 交互的URL(其实这么做是很巧妙的)
- 例如: 自定义 native://save_image 保存图片
- native://dismiss 当前页面消失
等等。
解决方法
解决方法 一
正如前面所说,当存在不合法的URL请求时,会进入 “错误回调”
代码语言:javascript复制 - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
并且 RN 官方代码中,也实现了这个方法,但是里面对URL的校验只有一行代码
代码语言:javascript复制BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
也就是说,只要 scheme 不等于 RCTJSNavigationScheme 那么都是允许加载的,
这样就相当于几乎不设防,那么无论合法或者不合法的 URL 都会允许加载。
嗯,这么是不合理的。
找到这么一个暴力但是挺实用的方法
代码语言:javascript复制 if (![request.URL.scheme isEqual:@"http"] &&
![request.URL.scheme isEqual:@"https"] &&
![request.URL.scheme isEqual:@"about:blank"]) {
if ([[UIApplication sharedApplication]canOpenURL:request.URL]) {
[[UIApplication sharedApplication]openURL:request.URL];
}
return NO;
}
return YES;
将此校验和 RN 的 isJSNavigation 放在一起校验,当做返回值
代码语言:javascript复制return !isJSNavigation && (如上校验)
如此便可以解决多数的拦截不成功问题了。也就不会出现我们碰到的这个问题了
解决方法二
对不合法的请求进行拦截
当然 React Native 中的 WebView 也是存在这个回调的。
RN 可以通过设置 onShouldStartLoadWithRequest 这个 WebView 初始化参数进行拦截。其返回值同样是一个 BOOL 值。
如此我们就可以在 RN 中进行 URL 拦截了,而不必修改 react-native 中的代码了。
----------- ************* ------------
但是事实并没有这么简单,即便我们设置了这个拦截,在真实的网络环境中,如果存在不合法的URL,还是会出现错误页面。
我们都已经设置了拦截,为什么还是会出现错的视图呢?
经过实践和源码分析:
当 iOS 中webView 回调
代码语言:javascript复制 - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
这个方法的时候,其实会去执行RN webView onShouldStartLoadWithRequest 的方法的,如果其回调了 NO,直接返回 NO。否则返回了
代码语言:javascript复制return !isJSNavigation;
但我们都知道 RN 是单开了一个线程,那么回调就是异步的,为了实现同步的效果,所以 iOS WebView 中进行了线程锁。
将当前线程锁定 250ms,250ms 后查看 RN 的回调结果,当然如果 RN 没有回调,默认值是 YES,允许此次请求。
代码语言:javascript复制// Block the main thread for a maximum of 250ms until the JS thread returns
if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) {
BOOL returnValue = _shouldStartLoad;
[_shouldStartLoadLock unlock];
_shouldStartLoadLock = nil;
return returnValue;
} else {
RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES");
return YES;
}
在实际的测试中,可以发现 0.25S 的时间貌似并不够回调(1.包内置在APP中,并不是通过本地服务调试 2.为了测试,onShouldStartLoadWithRequest 只有一行代码 return false)。
仍然会进入 RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES"); 的警告中
在如此的测试中其时间明显不过,当然也可能是因为我的手机是 iPhone5s(升级到了 11.1.0,被苹果因为电池的原因降速了)的原因。
但事实就是,其时间着实不够。
所以第二种方法就是
- 在 RN webView 中 onShouldStartLoadWithRequest 进行拦截,
- 增加线程锁锁定时间,具体时间,可以根据不同机型进行测试。例如:500ms(当然如此会导致,无论加载哪个请求,都至少会延迟 500ms 页面渲染)
- 目前测试更改为 350ms ,没有再出现时间不够问题
image.png
解决方法三
前言: RN WebView 中支持我们设定在加载出错的情况的下,自定义的错误视图
代码语言:javascript复制/**
* Function that returns a view to show if there's an error.
*/
renderError: PropTypes.func, // view to show if there's an error
当出现错误的情况下,可以添加一个错误视图到 WebView 的上层。
当然,如果此参数不被赋值,RN 内部有 defaultRenderError 错误视图展示。
代码语言:javascript复制var defaultRenderError = (errorDomain, errorCode, errorDesc) => (
<View style={styles.errorContainer}>
<Text style={styles.errorTextTitle}>
Error loading page
</Text>
<Text style={styles.errorText}>
{'Domain: ' errorDomain}
</Text>
<Text style={styles.errorText}>
{'Error Code: ' errorCode}
</Text>
<Text style={styles.errorText}>
{'Description: ' errorDesc}
</Text>
</View>
);
到这里,就很清晰的知道为什么加载出错 WebView 屏幕中间会出现错误信息了和为什么错误信息样式如此完美(丑)。
正题:
其实进入到
代码语言:javascript复制- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
就会发消息给 RN,然后 RN 开始渲染 renderError。
请大家记住这是一个很重要的点,后面会用到。暂且记为 “重点一”
----------**********-------
下面切换一下重点。
请看如下代码
代码语言:javascript复制var webViewStyles = [styles.container, styles.webView, this.props.style];
if (this.state.viewState === WebViewState.LOADING ||
this.state.viewState === WebViewState.ERROR) {
// if we're in either LOADING or ERROR states, don't show the webView
webViewStyles.push(styles.hidden);
}
出自 WebView.ios.js 442 行
从代码上可以看到,只要 webView 出现任何错误,那么 webView 将会被隐藏。。
o my gold!!!
为什么加载出错的情况下,我的 webView 被隐藏了呢?????
并且 this.props.style 是先于 webViewStyles.push(styles.hidden); 添加到 webViewStyles 中的。也就是说 外部的 this.props.style 对 webView 的显示与隐藏无任何作用。
只要 webView 被隐藏了,那么一切等于 0。
在加上上述 “重点一”,那么,那么,无能为力。
此时也就证明了 https://github.com/facebook/react-native/issues/9037 中 @lacker 的解决方法并不可行
这一点,可能 RN 官方为我们考虑的太多了,出现了一点瑕疵。
另:iOS 苹果官方的 WebView 在遇到加载错误的情况下,也不会隐藏 UIWebView 的。
->>>>>>>> 可能出错的只是我的这个页面中很小的一个小功能,没有这个功能也无所谓,最起码主体界面不应该收到影响。 ->>>>>>>> 如果真的出错了,完全可以通过状态外部隐藏,或者顶层加上错误遮罩,但是不能组件内部隐藏,如此外部是无法控制的
到这里诞生了我们的第三个解决方法
那就是修改 WebView.ios.js 代码,当出现错误的情况下,我们不希望 webView 被隐藏掉,如果真的希望隐藏,我们可以通过 style 来隐藏
那么就是将 441 行代码开始
代码语言:javascript复制 var webViewStyles = [styles.container, styles.webView, this.props.style];
if (this.state.viewState === WebViewState.LOADING ||
this.state.viewState === WebViewState.ERROR) {
// if we're in either LOADING or ERROR states, don't show the webView
webViewStyles.push(styles.hidden);
}
更改为
代码语言:javascript复制var webViewStyles = [styles.container, styles.webView, this.props.style];
if (this.state.viewState === WebViewState.LOADING) {
// if we're in either LOADING states, don't show the webView
webViewStyles.push(styles.hidden);
}
错误情况下,我们不希望 webView 被强制隐藏掉。
可以通过 <WebView style={{}}/> 来控制显示隐藏
当然此时是否需要展示错误信息,完全在你的手里,设定自定义的 renderError 则使用自定义的,没有则使用默认的。
解决方法四(相对完美)
当然我们都不希望更改源码。那就只能找到合适的时机,合适的地方来做合适的更改达到想要的效果
通过仔细观察代码,发现如下代码给我们留下了一线生机
代码语言:javascript复制var webView =
<NativeWebView
ref={RCT_WEBVIEW_REF}
key="webViewKey"
style={webViewStyles}
source={resolveAssetSource(source)}
injectedJavaScript={this.props.injectedJavaScript}
bounces={this.props.bounces}
scrollEnabled={this.props.scrollEnabled}
decelerationRate={decelerationRate}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
onLoadingStart={this._onLoadingStart}
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
messagingEnabled={messagingEnabled}
onMessage={this._onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={this.props.scalesPageToFit}
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
mediaPlaybackRequiresUserAction={this.props.mediaPlaybackRequiresUserAction}
dataDetectorTypes={this.props.dataDetectorTypes}
{...nativeConfig.props}
/>;
当看到 {...nativeConfig.props} 的时候,送了一口气,只要有动态的地方,就有我们可以利用的地方
我们可以 通过 nativeConfig.props 来更改 style,将 style 属性重写掉。
并且代价也不大
var webViewStyles = [styles.container, styles.webView, this.props.style];
是默认的 style,其实他们都是很简单了
代码语言:javascript复制 webView: {
backgroundColor: '#ffffff',
},
container: {
flex: 1,
},
总结起来就是
代码语言:javascript复制style: {
backgroundColor: '#ffffff',
flex: 1,
}
故:
代码语言:javascript复制<WebView nativeConfig={
{
props: {
backgroundColor: '#ffffff',
flex: 1,
}
}
}
}
此时碰到错误请求
例如:自定义的 URL JS 交互方法 native://saveImage
或者跳转到没有安装的APP alipays:// 时
均不会对当前的 webView 造成影响
当然此时是否需要展示错误信息,完全在你的手里,设定自定义的 renderError 则使用自定义的,没有则使用默认的。
后感
这种问题算是 RN 中的一点小瑕疵吧,也算是帮助(提醒、迫使)我们去看一些源码,深入理解工作原理。
加油!!!