7

我们的电子商务平台已经投入生产,但我们遇到了奇怪的 ChunkLoadError。此错误随机发生,不可复制。当我们试图打开失败的文件时,它就在那里并且可以正常加载。

如果用户得到这个错误,他会得到白屏(逻辑上),但刷新后一切都很好。

我们正在 React(最新)、Express(最新)上运行 SSR 电子商务

我们的 webpack / razzle 配置


const path = require('path');
const autoprefixer = require('autoprefixer');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const LoadablePlugin = require('@loadable/webpack-plugin');

const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserPlugin = require('terser-webpack-plugin');
// const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');

module.exports = {
  modify: (baseConfig, env, webpack) => {
    const { target, dev } = env;
    const appConfig = { ...baseConfig };
    // Setup SCSS
    if (target === 'web') {
      const filename = path.resolve(__dirname, 'build');

      const cssLoader = {
        loader: 'css-loader',
        options: {
          minimize: !dev,
          sourceMap: false,
          importLoaders: 1
        }
      };

      const postCSSLoader = {
        loader: 'postcss-loader',
        options: {
          ident: 'postcss',
          sourceMap: false,
          plugins: () => [
            autoprefixer({
              browsers: [
                '>1%',
                'last 4 versions',
                'Firefox ESR',
                'not ie < 9' // React doesn't support IE8 anyway
              ]
            })
          ]
        }
      };

      const sassLoader = {
        loader: 'sass-loader',
        options: {
          minimize: !dev,
          sourceMap: false,
          importLoaders: 1
        }
      };

      if (dev) {
        appConfig.output.filename = 'static/js/[name].js';
        appConfig.module.rules.push({
          test: /\.scss$/,
          use: ['style-loader', cssLoader, postCSSLoader, sassLoader]
        });
      } else {
        appConfig.output.filename = 'static/js/[name].[chunkhash:8].js';

        // For production, extract CSS
        appConfig.module.rules.push({
          test: /\.scss$/,
          use: [MiniCssExtractPlugin.loader, cssLoader, postCSSLoader, sassLoader]
        });

        appConfig.plugins.push(
          new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
          new webpack.IgnorePlugin(/moment/, /react-kronos/),
          new webpack.optimize.OccurrenceOrderPlugin(),
          // new webpack.optimize.LimitChunkCountPlugin({maxChunks: 50}),
          new CompressionPlugin()
          ,new BundleAnalyzerPlugin({
            analyzerMode: 'static',
            generateStatsFile: true,
            openAnalyzer: false
          })
          // ,new DuplicatePackageCheckerPlugin()
        );
      }

      // optimization
      appConfig.optimization = {
        ...baseConfig.optimization,
        minimize: !dev,
        minimizer: [new TerserPlugin({
          parallel: true,
        })],
        splitChunks: {
          chunks: 'initial',
          minSize: 30000,
          // minRemainingSize: 0,
          maxSize: 0,
          minChunks: 1,
          maxAsyncRequests: 6,
          maxInitialRequests: 4,
          automaticNameDelimiter: '~',
          automaticNameMaxLength: 30,
          cacheGroups: {
            defaultVendors: {
              test: /[\\/]node_modules[\\/]/,
              priority: -10
            },
            default: {
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true
            }
          }
        },
        moduleIds: 'total-size', //added in future deterministic
        chunkIds: 'total-size', //added
        mangleWasmImports: !dev, //added
        removeAvailableModules: !dev, //added
        mergeDuplicateChunks: !dev, //added
        flagIncludedChunks: !dev,
        occurrenceOrder: false,
        usedExports: !dev,
        // namedModules: true,
        // namedChunks: true,
        runtimeChunk: 'single'
        // runtimeChunk: {
        //   name: entrypoint => `runtimechunk~${entrypoint.name}`
        // }
      };

      appConfig.plugins.push(
        new LoadablePlugin({
          outputAsset: false,
          writeToDisk: { filename }
        }),
        new LodashModuleReplacementPlugin({
          collections: true,
          cloning: true,
          deburring: true,
          // coercions: true,
          flattening: true,
          paths: true,
          // placeholders: true
          shorthands: true
          // caching: true
        })
      );
    } else {
      appConfig.module.rules.push({
        test: /\.(scss)$/,
        use: ['ignore-loader']
      });
    }
    return appConfig;
  },
  modifyBabelOptions: mainBabelOptions => {
    return {
      ...mainBabelOptions,
      ...{ plugins: [].concat(mainBabelOptions.plugins ? mainBabelOptions.plugins : [], ['lodash']) }
    };
  }
};

多个错误

这是随机挑选的痕迹

(error: https://www.freshbox.sk/static/js/common-blocks-functional-userButton.3074d9ca.chunk.js)
  at m.e(webpack/bootstrap:170:18)
  at importAsync(./src/common/blocks/header/HeaderVariant2.jsx:35:9)
  at requireAsync(./src/common/blocks/header/HeaderVariant2.jsx:34:28)
  at loadAsync(./node_modules/@loadable/component/dist/loadable.esm.js:217:31)
  at componentDidMount(./node_modules/@loadable/component/dist/loadable.esm.js:147:16)
  at Ji(./node_modules/react-dom/cjs/react-dom.production.min.js:212:132)
  at b(./node_modules/react-dom/cjs/react-dom.production.min.js:255:229)
  at If(./node_modules/scheduler/cjs/scheduler.production.min.js:19:467)
  at cg(./node_modules/react-dom/cjs/react-dom.production.min.js:122:325)
  at Jj(./node_modules/react-dom/cjs/react-dom.production.min.js:248:370)
  at yj(./node_modules/react-dom/cjs/react-dom.production.min.js:239:376)
  at Ig(./node_modules/react-dom/cjs/react-dom.production.min.js:230:137)
  at bk(./node_modules/react-dom/cjs/react-dom.production.min.js:281:43)
  at a(./node_modules/react-dom/cjs/react-dom.production.min.js:284:301)
  at Nj(./node_modules/react-dom/cjs/react-dom.production.min.js:240:120)
  at ik(./node_modules/react-dom/cjs/react-dom.production.min.js:284:287)
  at hydrate(./node_modules/react-dom/cjs/react-dom.production.min.js:290:206)
  at done(./src/client/index.js:81:3)
  at checkReadyState(./node_modules/@loadable/component/dist/loadable.esm.js:428:11)
  at E/</n.push(./node_modules/@loadable/component/dist/loadable.esm.js:435:7)
  at ? (/static/js/common-components-category-listing-_default-LayoutSwitcher.869947cb.chunk.js:1:75)```
4

1 回答 1

13

如果为了优化加载而将代码拆分为块,则索引文件通常包含根据当前 webpack 构建的所有块的名称和路径。对代码进行更改并再次构建可以使用已编辑的代码重命名块。然而,浏览器首先加载索引以及某些所需的块,然后按需获取其余块,以使整个代码分割优化工作。

我怀疑在这种情况下,浏览器加载索引后会进行生产部署,并且某些块会被下一次构建的更改重命名。这导致陈旧索引中某些块的地址无效。之后,当用户导航到网站的该部分时,当浏览器中的旧索引尝试加载那些不存在的块时,它会抛出Loading chunk XY failed. 刷新应该更新索引并解决问题。

解决此问题的一种方法是使用服务人员。它的工作方式如下:

  • 浏览器加载索引和特定所需的块以呈现请求的部分。
  • 当用户查看/使用加载的部分时,Service Worker 会在后台安装并将所有剩余的块加载到内存中,以便在请求时可以离线提供所有块。基本上是在浏览器内存中创建服务器当前状态的副本。即使在服务器更新后,这不仅可以保留文件,还可以大大缩短其他块的加载时间。
  • 当有一个新的生产部署时,安装的 service worker 可以最初为本地缓存的文件提供服务,以防止任何崩溃。
  • Service Worker 可以在后台更新并在用户下次访问站点时提供新版本的站点,或者显示新版本可用的消息并要求用户重新加载。

对服务工作者使用 stale-while-revalidate 或 cache-first 逻辑应该在这里工作。希望有帮助。

于 2020-03-16T09:29:34.187 回答