在新部署SPA

时间:2017-06-17 05:28:43

标签: reactjs webpack single-page-application webpack-2 create-react-app

我有一个单页应用,我在每条路线上进行代码分割。当我部署新版本的应用时,如果用户仍然打开该页面并访问之前未访问过的路由,则用户通常会收到错误。

如果应用程序启用了服务工作程序,则还会出现这种情况。当用户在新部署后访问页面时,服务工作者将从缓存中提供服务。然后,如果用户尝试访问不在其缓存中的页面,他们将获得块加载失败。

目前我在我的应用程序中禁用了代码拆分以避免这种情况,但我一直很好奇处理这个问题的最佳方法是什么。我已经考虑过在用户完成加载初始页面后预加载所有其他路由,我相信这可能会解决路由上代码拆分的问题。但是,假设我想对组件进行代码拆分,那么这意味着我必须尝试确定何时以及如何预加载所有这些组件。

所以我想知道人们如何处理单页应用程序的这个问题?谢谢! (我目前正在使用create-react-app)

4 个答案:

答案 0 :(得分:6)

我更喜欢让用户刷新而不是自动刷新(这避免了无限刷新循环错误的可能性)。
以下策略对于React应用程序来说效果很好,代码是按路线拆分的:

策略

  1. 将index.html设置为从不缓存。这样可以确保请求您的初始资产的主文件始终是新文件(并且通常不大,因此不进行缓存就不会成为问题)。参见MDN Cache Control

  2. 对块使用一致的块哈希。这样可以确保只有更改的块才会具有不同的哈希。 (请参见下面的webpack.config.js代码段)

  3. 在部署时不要使CDN的缓存无效,因此在部署新版本时,旧版本不会丢失其块。

  4. 在路线之间导航时检查应用程序版本,以通知用户它们是否在旧版本上运行并请求刷新。

  5. 最后,以防万一发生ChunkLoadError :添加一个错误边界。 (请参见下面的错误边界)

webpack.config.js(Webpack v4)中的代码段

来自Uday Hiwarale

optimization: {
  moduleIds: 'hashed',
  splitChunks: {
      cacheGroups: {
          default: false,
          vendors: false,
          // vendor chunk
          vendor: {
              name: 'vendor',
              // async + async chunks
              chunks: 'all',
              // import file path containing node_modules
              test: /node_modules/,
              priority: 20
          },
      }
  }

错误边界

React Docs for Error Boundary

import React, { Component } from 'react'

export default class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.    
    return { hasError: true, error };
  }
  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service    
    console.error('Error Boundary Caught:', error, errorInfo);
  }
render() {
    const {error, hasError} = this.state
    if (hasError) {
      // You can render any custom fallback UI      
      return <div>
      <div>
        {error.name === 'ChunkLoadError' ?
          <div>
            This application has been updated, please refresh your browser to see the latest content.
          </div>
          :
          <div>
            An error has occurred, please refresh and try again.
          </div>}
      </div>
      </div>
    }
    return this.props.children;
  }
}

注意:请确保在内部导航事件中清除错误(例如,如果您使用react-router),否则错误边界将持续通过内部导航,并且只会在实际导航或页面刷新时消失

答案 1 :(得分:1)

create-react-app中的问题是脚本标记所引用的块不存在,因此在我们的index.html中引发了错误。这是我们得到的错误。

Uncaught SyntaxError: Unexpected token <    9.70df465.chunk.js:1 

让它工作的方法是在index.html中所有脚本标签上方添加一个window.onerror函数。

<script>
  window.onerror = function (message, source, lineno, colno, error) {
    if (error && error.name === 'SyntaxError') {
      window.location.reload(true);
    }
  };
</script>

我希望有一个更好的方法,但是这是我能想到的最好的方法,并认为这是一个非常安全的解决方案,因为create-react-app将不会编译或生成任何语法错误,这应该是唯一的方法出现语法错误的情况。

答案 2 :(得分:1)

我正在使用AsyncComponent HOC延迟加载块,并且遇到了相同的问题。 我要做的工作是确定错误并进行一次硬重新加载。

.catch(error => {
        if (error.toString().indexOf('ChunkLoadError') > -1) {
          console.log('[ChunkLoadError] Reloading due to error');
          window.location.reload(true);
        }
      });

完整的HOC文件如下:

export default class Async extends React.Component {
  componentWillMount = () => {
    this.cancelUpdate = false;
    this.props.load
      .then(c => {
        this.C = c;
        if (!this.cancelUpdate) {
          this.forceUpdate();
        }
      })
      .catch(error => {
        if (error.toString().indexOf('ChunkLoadError') > -1) {
          console.log('[ChunkLoadError] Reloading due to error');
          window.location.reload(true);
        }
      });
  };

  componentWillUnmount = () => {
    this.cancelUpdate = true;
  };

  render = () => {
    const props = this.props;
    return this.C ? (
      this.C.default ? (
        <this.C.default {...props} />
      ) : (
        <this.C {...props} />
      )
    ) : null;
  };
}

答案 3 :(得分:0)

尽管非常简单,但我们还是用一个难看的方法解决了这个问题。目前可能暂时还可以,但可能会对某人有所帮助。

我们创建了一个AsyncComponent来加载块(即路由组件)。当该组件加载一个块并接收到错误时,我们只需执行一次简单的页面重新加载即可更新index.html及其对主块的引用。难看的原因是,根据页面的外观或加载方式,用户可能会在刷新之前看到空白页面的短暂闪烁。可能会造成刺激,但这也许是因为我们不希望SPA自发刷新。

App.js

// import the component for the route just like you would when
// doing async components
const ChunkedRoute = asyncComponent(() => import('components/ChunkedRoute'))

// use it in the route just like you normally would
<Route path="/async/loaded/route" component={ChunkedRoute} />

asyncComponent.js

import React, { Component } from 'react'

const asyncComponent = importComponent => {
  return class extends Component {
    state = {
      component: null,
    }

    componentDidMount() {
      importComponent()
        .then(cmp => {
          this.setState({ component: cmp.default })
        })
        .catch(() => {
          // if there was an error, just refresh the page
          window.location.reload(true)
        })
    }

    render() {
      const C = this.state.component
      return C ? <C {...this.props} /> : null
    }
  }
}

export default asyncComponent