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

今天在对接一个网页时加载网页总是碰到
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 的解决方法并不可行

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 处理一个请求时,首先会进入

 - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 

询问是否允许加载此次请求,以返回的 BOOL 值为准。

如果我们默认不实现此代理方法,系统会自动判断是否可以处理。如:是否是合法的 URL、是否是请求系统定制的一些 API,例如 tel:// 等等

而当我们不实现

- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error

的回调时,即便出错了也不会有任何表现

言归正传:
出现这个错误的原因就是 WebView 加载了其实它无法处理的请求(URL)。导致进入了 “错误回调”。而“错误回调” RN 官方已经帮我们实现了其回调,并且帮我们加载了一个错误视图在上面。

如下是 iOS 代码:

- (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 代码

...
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请求时,会进入 “错误回调”

 - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 

并且 RN 官方代码中,也实现了这个方法,但是里面对URL的校验只有一行代码

BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];

也就是说,只要 scheme 不等于 RCTJSNavigationScheme 那么都是允许加载的,

这样就相当于几乎不设防,那么无论合法或者不合法的 URL 都会允许加载。

嗯,这么是不合理的。

找到这么一个暴力但是挺实用的方法

 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 放在一起校验,当做返回值

return !isJSNavigation  && (如上校验)

如此便可以解决多数的拦截不成功问题了。也就不会出现我们碰到的这个问题了

解决方法二

对不合法的请求进行拦截

当然 React Native 中的 WebView 也是存在这个回调的。

RN 可以通过设置 onShouldStartLoadWithRequest 这个 WebView 初始化参数进行拦截。其返回值同样是一个 BOOL 值。

如此我们就可以在 RN 中进行 URL 拦截了,而不必修改 react-native 中的代码了。

----------- ************* ------------

但是事实并没有这么简单,即便我们设置了这个拦截,在真实的网络环境中,如果存在不合法的URL,还是会出现错误页面。

我们都已经设置了拦截,为什么还是会出现错的视图呢?

经过实践和源码分析:

当 iOS 中webView 回调

 - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 

这个方法的时候,其实会去执行RN webView onShouldStartLoadWithRequest 的方法的,如果其回调了 NO,直接返回 NO。否则返回了

return !isJSNavigation;

但我们都知道 RN 是单开了一个线程,那么回调就是异步的,为了实现同步的效果,所以 iOS WebView 中进行了线程锁。

将当前线程锁定 250ms,250ms 后查看 RN 的回调结果,当然如果 RN 没有回调,默认值是 YES,允许此次请求。


// 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 中支持我们设定在加载出错的情况的下,自定义的错误视图

/**
     * 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 错误视图展示。

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 屏幕中间会出现错误信息了和为什么错误信息样式如此完美(丑)。

正题:

其实进入到

- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error

就会发消息给 RN,然后 RN 开始渲染 renderError。

请大家记住这是一个很重要的点,后面会用到。暂且记为 “重点一”

----------**********-------

下面切换一下重点。

请看如下代码

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 行代码开始


    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);
    }

更改为

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 则使用自定义的,没有则使用默认的。

解决方法四(相对完美)

当然我们都不希望更改源码。那就只能找到合适的时机,合适的地方来做合适的更改达到想要的效果

通过仔细观察代码,发现如下代码给我们留下了一线生机

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,其实他们都是很简单了

 webView: {
    backgroundColor: '#ffffff',
  },
  container: {
    flex: 1,
  },

总结起来就是

style: {
    backgroundColor: '#ffffff',
    flex: 1,
}

故:

<WebView nativeConfig={
                        {
                            props: {
                                backgroundColor: '#ffffff',
                                flex: 1,
                            }
                        }
                    }
        }

此时碰到错误请求

例如:自定义的 URL JS 交互方法 native://saveImage

或者跳转到没有安装的APP alipays:// 时

均不会对当前的 webView 造成影响

当然此时是否需要展示错误信息,完全在你的手里,设定自定义的 renderError 则使用自定义的,没有则使用默认的。

后感

这种问题算是 RN 中的一点小瑕疵吧,也算是帮助(提醒、迫使)我们去看一些源码,深入理解工作原理。

加油!!!

欢迎加入QQ群: 722600238

在这里可以讨论、帮助你解决你遇到的问题

另外我的个人博客也已经上线,以后文章或先更新个人博客

onety的博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350