一次有趣的React H5加载异常

1,771 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

Hello 大家好! 我是前端 无名

背景

我们的H5网页经常嵌套在react-native(RN端)提供的webView中运行的。

有一次,经过几天的日日夜夜,终于把产品小姐姐的需求给做完了,按时进入测试。

某一天,测试小哥哥过来:

你这个网页,第一次进入,正常,点击刷新按钮以后,退出,重新打开H5,一直在loading,必须强杀应用才能正常展示。说着拿出手机给我演示。

我看了整个过程以后,点击刷新以后,再次进入,看到一眼loading,这loading不是我们的啊,再看看,loading是透明的,我们的H5其实已经加载出来了。这个loading是RN端的loading 啊,这个我也不好排查,我不知道RN端loading消失的时机,于是跟着测试小哥哥一起去找RN端。

RN端小哥: 这个是你们前端的问题吧,你看看人家别人的网页都没有问题。

前端我: 这个你们的loading没有消失,能看看是资源加载失败还是什么其他问题吗?。

.....开始撕逼

排查问题

  1. RN端的webView代码

image.png

我们可以看到RN端是直接使用webView的renderLoading去渲染的,loading的消失其实是交个react-native-webview去控制的。

然后查看loadEnd正常调用,然后就让RN端小哥哥修改了loading 处理方式,变为自己主动控制。

RN端webView嵌套结构如下:

  return <View> <WebView /> <LoadingView /></View>

这样以后刚进入默认loading直接渲染,监听到loadEnd事件以后,修改Loading状态,去除loading组件

修改完以后,完美解决,H5每次都可以正常渲染。

事情就这么解决了???...

RN端小哥说,这个不能这么改,很多地方都用到了这个WebView组件,这样改会不会有其他影响,你看别的H5都没有问题。

  1. react-native-webView源码

RN端小哥哥不修改,我们怎么办呢?RN的loading不消失,作为H5的我们怎么解决呢??

哎,愁死了!

看看react-native-webView的源码去,看看rendLoading消失的时机到底是什么时候!

问了下RN小哥哥当前在用的react-native-webView版本,然后开始看代码了。

源码目录:src/WebView.android.tsx

import React from 'react';

import {
  Image,
 ...
} from 'react-native';

...省略

/**
 * Renders a native WebView.
 */
class WebView extends React.Component<AndroidWebViewProps, State> {


  startUrl: string | null = null;

  state: State = {
    viewState: this.props.startInLoadingState ? 'LOADING' : 'IDLE',
    lastErrorEvent: null,
  };

  
  reload = () => {
    this.setState({
      viewState: 'LOADING',
    });
    UIManager.dispatchViewManagerCommand(
      this.getWebViewHandle(),
      this.getCommands().reload,
      undefined
    );
  };


  updateNavigationState = (event: WebViewNavigationEvent) => {
    if (this.props.onNavigationStateChange) {
      this.props.onNavigationStateChange(event.nativeEvent);
    }
  };

  //重点
  onLoadingStart = (event: WebViewNavigationEvent) => {
    const { onLoadStart } = this.props;
    const { nativeEvent: { url } } = event;
    //重点
    this.startUrl = url;
    if (onLoadStart) {
      onLoadStart(event);
    }
    this.updateNavigationState(event);
  };

  onLoadingError = (event: WebViewErrorEvent) => {
    event.persist(); // persist this event because we need to store it
    const { onError, onLoadEnd } = this.props;
    if (onError) {
      onError(event);
    } else {
      console.warn('Encountered an error loading page', event.nativeEvent);
    }

    if (onLoadEnd) {
      onLoadEnd(event);
    }
    if (event.isDefaultPrevented()) return;
    //重点
    this.setState({
      lastErrorEvent: event.nativeEvent,
      viewState: 'ERROR',
    });
  };

 

  onLoadingFinish = (event: WebViewNavigationEvent) => {
    const { onLoad, onLoadEnd } = this.props;
    const { nativeEvent: { url } } = event;
    if (onLoad) {
      onLoad(event);
    }
    if (onLoadEnd) {
      onLoadEnd(event);
    }
    //重点
    if (url === this.startUrl) {
      this.setState({
        viewState: 'IDLE',
      });
    }
    this.updateNavigationState(event);
  };


  onLoadingProgress = (event: WebViewProgressEvent) => {
    const { onLoadProgress } = this.props;
    const { nativeEvent: { progress } } = event;
    //重点
    if (progress === 1) {
      this.setState((state) => {
        if (state.viewState === 'LOADING') {
          return { viewState: 'IDLE' };
        }
        return null;
      });
    }
    if (onLoadProgress) {
      onLoadProgress(event);
    }
  };



  render() {
    const {
      onMessage,
      onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp,
      originWhitelist,
      renderError,
      renderLoading,
      source,
      style,
      containerStyle,
      nativeConfig = {},
      ...otherProps
    } = this.props;

    let otherView = null;
    //重点
    if (this.state.viewState === 'LOADING') {
      otherView = (renderLoading || defaultRenderLoading)();
    } else if (this.state.viewState === 'ERROR') {
      const errorEvent = this.state.lastErrorEvent;
      invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
      otherView = (renderError || defaultRenderError)(
        errorEvent.domain,
        errorEvent.code,
        errorEvent.description,
      );
    } else if (this.state.viewState !== 'IDLE') {
      console.error(
        `RNCWebView invalid state encountered: ${this.state.viewState}`,
      );
    }

      ...

    const webView = (
      <NativeWebView
        key="webViewKey"
        {...otherProps}
        messagingEnabled={typeof onMessage === 'function'}
        messagingModuleName={this.messagingModuleName}
        onLoadingError={this.onLoadingError}
        onLoadingFinish={this.onLoadingFinish}
        onLoadingProgress={this.onLoadingProgress}
        onLoadingStart={this.onLoadingStart}
        onHttpError={this.onHttpError}
        onRenderProcessGone={this.onRenderProcessGone}
        onMessage={this.onMessage}
        onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
        ref={this.webViewRef}
        source={resolveAssetSource(source as ImageSourcePropType)}
        style={webViewStyles}
        {...nativeConfig.props}
      />
    );

    return (
      <View style={webViewContainerStyle}>
        {webView}
        {otherView}
      </View>
    );
  }
}

export default WebView;

  1. 分析源码 从源码中我们可以看出renderLoading 的消失主要看viewState的状态值。

我们就重点关注哪些地方修改了viewState状态值。

viewState默认是loading, 在onLoadingError,onLoadingProgress,onLoadingFinish会修改。

并且只有viewState==='IDLE', H5也才能正常渲染。

image.png

image.png

image.png

由于我们看到onLoadEnd每次都会调用(H5成功失败都会调用),那为什么再次刷新loading不消失呢?

原因就处在url===this.startUrl,由于我们采用的是HashRouter,并且懒加载。打开H5的链接是:https:xxxx.a/index.html 这样默认打开我们的home页:https:xxxx.a/index.html#/

这导致我们starturl与url不一致,然后RN的renderLoading不能正常消失。

问题来了?为什么不点击H5网页的刷新按钮正常加载啊?

不点击刷新按钮,每次打开都会调用:onLoadingProgress函数,progress为1,这个时候同样是viewState==='IDLE',正常加载。

点击H5网页刷新按钮,做了哪些操作呢??

image.png

这是只是重新加载网页,网页后面加了一个ticket,不用缓存。 结果window.location.replace以及window.location.href都不行。

只要在H5网页中刷新了,然后退出H5重新打开webView就会一直loading,并且不强杀应用还不行。

排查是刷新以后,onLoadingProgress 中progress不是1,所以修改不了ViewState,。

  1. 解决方案

原因找到了,怎么解决呢,RN小哥哥不修改代码,H5怎么办呢?

解决方案:我们只要保障startUrl和loadEnd后的url一致不就可以了吗。 所以修改默认打开的h5网页地址为home页:https:xxxx.a/index.html#/

后来查看react-native-webview 的 issues 发现,react-native-webview后面升级已经解决这个问题了。

后语

欢迎大家多提意见。项目模板在不断优化,一赞一回!欢迎评论。