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

2018-11-21 11:06:18 浏览数 (1)

今天在对接一个网页时加载网页总是碰到 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,被苹果因为电池的原因降速了)的原因。

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

所以第二种方法就是

  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 中的一点小瑕疵吧,也算是帮助(提醒、迫使)我们去看一些源码,深入理解工作原理。

加油!!!

0 人点赞