前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React Native iOS 剖析 WebView && 解决 Error loading page Domain: WebKitErrorDomain Error Code: 101 The U

React Native iOS 剖析 WebView && 解决 Error loading page Domain: WebKitErrorDomain Error Code: 101 The U

作者头像
onety码生
发布2018-11-21 11:06:18
3.9K0
发布2018-11-21 11:06:18
举报
文章被收录于专栏:码生码生

今天在对接一个网页时加载网页总是碰到 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(其实这么做是很巧妙的)

等等。

解决方法

解决方法 一

正如前面所说,当存在不合法的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,被苹果因为电池的原因降速了)的原因。

但事实就是,其时间着实不够。

所以第二种方法就是

  1. 在 RN webView 中 onShouldStartLoadWithRequest 进行拦截,
  2. 增加线程锁锁定时间,具体时间,可以根据不同机型进行测试。例如:500ms(当然如此会导致,无论加载哪个请求,都至少会延迟 500ms 页面渲染)
  3. 目前测试更改为 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 中的一点小瑕疵吧,也算是帮助(提醒、迫使)我们去看一些源码,深入理解工作原理。

加油!!!

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.01.13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前缀引导
  • 出现此种错误的情况与原因
    • 出现错误的原因
      • 出现错误的情况
      • 解决方法
        • 解决方法 一
          • 解决方法二
            • 解决方法三
              • 解决方法四(相对完美)
              • 后感
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档