【问题标题】:Code splitting causes chunks to fail to load after new deployment for SPA代码拆分导致新部署 SPA 后无法加载块
【发布时间】:2017-11-19 22:22:05
【问题描述】:

我有一个单页应用程序,我在每条路线上拆分代码。当我部署我的应用程序的新版本时,如果用户仍然打开页面并访问他们以前未访问过的路线,用户通常会收到错误消息。

如果应用启用了服务工作者,也可能发生这种情况。当用户在新部署后访问页面时,Service Worker 将从缓存中提供服务。然后,如果用户尝试访问不在其缓存中的页面,他们将获得块加载失败。

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

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

【问题讨论】:

  • 您找到在本地重现此内容的方法了吗?
  • 我没有尝试在本地复制它,但我认为尝试复制的一种选择(注意:我没有对此进行测试,因此它可能不起作用)是创建 2 个不同的构建,它们具有不同的块。服务第一个加载它在浏览器上。停留在该页面上时,停止提供第一个构建并提供第二个构建。现在尝试导航到另一个页面。
  • 这就是我最后所做的!我构建了 2 个不同的 docker 镜像(甚至可能不需要 docker),它们包含不同版本的代码。在浏览应用程序时切换应用程序并在您的下一个“块加载”(为我导航到不同的 URL)时发生此错误。

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


【解决方案1】:

我更喜欢让用户刷新而不是自动刷新(这样可以防止出现无限刷新循环错误的可能性)。
以下策略适用于 React 应用程序,路由上的代码拆分:

策略

  1. 将您的 index.html 设置为从不缓存。这可确保请求您的初始资产的主文件始终是最新的(通常它并不大,因此不缓存它应该不是问题)。见MDN Cache Control

  2. 对你的块使用一致的块散列。这确保了只有发生变化的块才会有不同的哈希值。 (见下面的 webpack.config.js sn-p)

  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),否则错误边界将持续到内部导航之后,并且只会在真正的导航或页面刷新时消失.

【讨论】:

  • 请记住,除非发生“真正的”页面更改或刷新,否则 ErrorBoundary 将通过导航操作保持错误。
  • 很好的解决方案乔丹。打算研究类似的事情。
  • @Jordan 感谢您的详尽回答!我对 (4) 的一个问题是如何检查路线之间的版本? (我们目前存储用户当前使用的版本。您如何验证最新版本是什么?)
  • @KennethTruong 我怀疑有比我想出的更好的方法来做到这一点;但是我所做的是在后端创建一个端点,该端点将返回预期的前端版本(然后比较它是否已过时),并且当用户启动导航时我会针对该端点发出请求。无论如何,此步骤实际上只对长期存在的选项卡是必需的,因此可能只在活动时间超过指定持续时间的选项卡上发出该请求,以防止 每个 用户在 每个导航。
【解决方案2】:

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

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

更新

我们解决这个问题的方法是让我们的应用程序成为一个渐进式网络应用程序,这样我们就可以利用服务工作者。

将 create react 应用程序转换为 PWA 很容易。 CRA Docs on PWA

然后,为了确保用户始终使用最新版本的 service worker,我们确保每当有更新的 worker 等待时,我们都会告诉它 SKIP_WAITING,这意味着下次刷新浏览器时,他们将获得最多最新代码。

import { Component } from 'react';
import * as serviceWorker from './serviceWorker';

class ServiceWorkerProvider extends Component {
  componentDidMount() {
    serviceWorker.register({ onUpdate: this.onUpdate });
  }

  onUpdate = (registration) => {
    if (registration.waiting) {
      registration.waiting.postMessage({ type: 'SKIP_WAITING' });
    }
  }

  render() {
    return null;
  }
}

export default ServiceWorkerProvider;

Bellow 是我们尝试的第一件事,但确实遇到了一些无限循环

我让它工作的方法是在 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 不会编译或构建任何语法错误,这应该是唯一的出现语法错误的情况。

【讨论】:

  • 下面的解决方案现在有效吗?我目前有 PWA,但仍然出现错误
【解决方案3】:

我们用一个略显丑陋但非常简单的解决方案解决了这个问题。目前可能是暂时的,但可能会对某人有所帮助。

我们创建了一个 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

【讨论】:

  • 这个我没试过,但是你不能递归运行componentDidMount函数直到组件正确导入而不是刷新页面吗?
  • @BrandonKeithBiggs,问题在于 index.html 中指定的块 ID 不再有效,因此 componentDidMount 将无限运行,因为该组件将永远不会再存在(它过去存在)。刷新页面会导致 index.html 更新,使用新的块 ID,一切都会正常工作。
【解决方案4】:

我正在使用 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;
  };
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-04
    • 2015-11-24
    • 2016-12-28
    • 1970-01-01
    • 1970-01-01
    • 2019-07-06
    相关资源
    最近更新 更多