目录

create-react-app 3.4.3 + Webpack 4.42 multiple entry 多入口配置

环境

截止写文时(2020 年 09 月 22 日),使用的环境如下

  • create-react-app / react-scripts 3.4.3
  • Webpack 4.42
  • TypeScript

仓库地址:https://github.com/xunge0613/react-multipage-app

背景

移动端 H5 想做一个多页应用项目,react + webpack,参考了这两篇写的很不错的文章 React-CRA 多页面配置(npm run eject)「Webpack」配置 React 多个页面同时打包和调试后发现有问题,一直卡在编译中,也不报错,于是记录一下解决过程。

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/efe763a6-6352-44e0-a930-b2d1d531fbfc.png

思路

  1. 最初认为是 Webpack 本身的问题,就先参考了 Webpack 4 官方文档,发现没用。

  2. 然后想到是不是和 create-react-app 有关,于是使用了关键词 createreactapp multiple entry webpack4 doesn't work 进行搜索后,根据 Create React App V2 - Multiple entry points 中给出的解决方案解决了。

先前两篇文章中的前几个步骤不用调整,当然由于 webpack 版本不同,需要做一些相应调整(例如:只有 webpack.config.js 没有 dev 和 prod.js ),后续会标注

  1. paths.js 不变
  2. entry 不变
  3. output 不变
  4. plugins 不变

只需调整第五步:ManifestPlugin 调整

解决方案

把原先遍历 entrypoints.main 数组

1
2
3
const entrypointFiles = entrypoints.main.filter(
  (fileName) => !fileName.endsWith(".map")
);

改为遍历 entrypoints 对象,即可

1
2
3
4
5
let entrypointFiles = [];
for (let [entryFile, fileName] of Object.entries(entrypoints)) {
  let notMapFiles = fileName.filter((fileName) => !fileName.endsWith(".map"));
  entrypointFiles = entrypointFiles.concat(notMapFiles);
}

原理目测是原先的 entry 是数组 entry: ['xxx'],调整后成了对象, entry: { index: 'xxx', test: 'xxx'}

完整步骤

ps:只新增了入口,暂不新增 html 模板

-1. 安装、运行 create-react-app

1
2
3
4
5
# 卸载旧版 create-react-app
npm uninstall -g create-react-app

# 使用 npx 安装最新版
npx create-react-app react-multipage-app --template typescript

0. yarn eject

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/8413bb4e-7077-4bc5-81ed-3889195f041e.png

1
yarn eject

1. 调整 paths.js

添加新的入口 appTestJs

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/78f70e00-40ee-42d8-a12a-8aafc1bc6789.png

1
2
3
4
5
module.exports = {
  ...,
  appTestJs: resolveModule(resolveApp, "src/test"),
}

添加对应的入口文件 src/Test.tsx

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/28436bfd-e451-441d-a245-e535b733324e.png

2. 修改 webpack.config.js 的 entry

搜索:entry:

将原数组形式单入口:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/b179d42e-64f5-45e5-ad5d-24939403886e.png

改为对象形式多入口:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/72e6805f-93a1-4e62-b5f9-783b8efe202b.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
entry: {
  index: [
    isEnvDevelopment &&
      require.resolve("react-dev-utils/webpackHotDevClient"),
    paths.appIndexJs,
  ].filter(Boolean),
  test: [
    isEnvDevelopment &&
      require.resolve("react-dev-utils/webpackHotDevClient"),
    paths.appTestJs, // 上一步配置的新入口
  ].filter(Boolean),
},

3. 修改 webpack.config.js 的 output

搜索 output:

output 中如图所示,修改 filename,增加图中的 [name] 用于为不同入口,分别生成不同的 bundle

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/1b7ce6a4-aa0e-4073-b39a-ce5a0458f0ba.png

最终项目跑通后,打包效果如图

访问 http://localhost:3000/test.html

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/808e2973-c263-46df-b0a5-85d9bee01102.png

访问 http://localhost:3000/index.html

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/da63f7a0-2e90-467a-bea8-f6f6e9c2bc58.png

4. 修改 webpack.config.js 的 plugins

搜索 plugins:

复制一份已有的配置,添加 chunksfilename 字段,因目前项目只使用 paths.appHtml 作为模板,所以 template 字段不需要修改。

原:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/7c29d944-1604-47e8-973a-49569c69b63d.png

改:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/c165c570-3934-4e85-ae74-78c865afc52b.png

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/87e54ed1-d7c7-46dd-8c3d-6d202c606313.png

完整配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Generates an `index.html` file with the <script> injected.
plugins: [
  new HtmlWebpackPlugin(
    Object.assign(
      {},
      {
        chunks: ["index"],
        inject: true,
        template: paths.appHtml,
        filename: "index.html",
      },
      isEnvProduction
        ? {
            minify: {
              removeComments: true,
              collapseWhitespace: true,
              removeRedundantAttributes: true,
              useShortDoctype: true,
              removeEmptyAttributes: true,
              removeStyleLinkTypeAttributes: true,
              keepClosingSlash: true,
              minifyJS: true,
              minifyCSS: true,
              minifyURLs: true,
            },
          }
        : undefined
    )
  ),
  new HtmlWebpackPlugin(
    Object.assign(
      {},
      {
        chunks: ["test"],
        inject: true,
        template: paths.appHtml,
        filename: "test.html",
      },
      isEnvProduction
        ? {
            minify: {
              removeComments: true,
              collapseWhitespace: true,
              removeRedundantAttributes: true,
              useShortDoctype: true,
              removeEmptyAttributes: true,
              removeStyleLinkTypeAttributes: true,
              keepClosingSlash: true,
              minifyJS: true,
              minifyCSS: true,
              minifyURLs: true,
            },
          }
        : undefined
    )
  ),
];

5. 修改 webpack.config.js 的 plugins

搜索 new ManifestPlugin

把原先遍历 entrypoints.main 数组

1
2
3
const entrypointFiles = entrypoints.main.filter(
  (fileName) => !fileName.endsWith(".map")
);

改为遍历 entrypoints 对象,即可

1
2
3
4
5
let entrypointFiles = [];
for (let [entryFile, fileName] of Object.entries(entrypoints)) {
  let notMapFiles = fileName.filter((fileName) => !fileName.endsWith(".map"));
  entrypointFiles = entrypointFiles.concat(notMapFiles);
}

6. rewrite path 配置

由于上文多次提及,目前项目没有配置多个模板,所以此处没有做任何修改。

对于配置多个模板的同学,可以参考此文文末的解决方案 Multiple html pages with create-react-app app

大致如下

1
2
3
4
5
6
7
historyApiFallback: {
  disableDotRule: true,
  verbose: true,
  rewrites: [
    { from: /^\/test/, to: '/test.html' },
  ]
},

验证

访问 http://localhost:3000/index.html

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/d2eb86b7-d0c9-4c58-bf62-aeed6c6c3b23.png

访问 http://localhost:3000/test.html

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/e4c4284f-4173-41de-9003-b18530c75f1b.png

复盘

版本、时效性

参考网上文章时,需要注意一下文章的时间和依赖库的版本,尤其当有大版本变化时,要慎重,避免花费过多时间在可能错误的方向上;尽可能多花一些时间在时效性较高的资料,从而提升解决问题的概率。

ps:本文之前参考的文章多数是基于 create-react-app v2 的,而实际自己使用的是 CRA v3 版本。

错误日志

另外一个影响解决速度的原因是:没有报错信息

webpack.config.js 中的 ManifestPlugin 插件,generate 方法其实是报错了,但没有抛出。下图简单复现了一下,但加上了 try catch,并打印了一下,所以会有提示信息。

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/a11a9684-c370-4084-a936-02cf2656f6c7.png

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/ac3cfc4c-6661-425b-86d5-019229c5c01b.png

立 flag 后续研究研究有没有好的解决方案,

HtmlWebpackPlugin 和 ManifestPlugin

简单 mark 一下这两个插件。

HtmlWebpackPlugin 该插件用来生成 HTML 文件。参考 HtmlWebpackPlugin

ManifestPlugin 该插件用来生成 asset manifest 资产清单。参考Webpack Manifest Plugin

不足:配置很麻烦

显然每一次添加新页面都手动维护一堆配置信息不优雅,如果网页多了就需要重复 1、2、3、4 步骤,很不方便,期望优化成无需修改配置的模式。

优化

参考了前文提到的「Webpack」配置 React 多个页面同时打包和调试,主要思路就是利用 nodejs 操作文件的能力,fs.readdirSync 来扫描入口文件夹,自动生成相应的配置文件。

1. 改造入口文件目录结构

src 目录下分别建立 src/indexsrc/test 文件夹,确保文件夹下都有入口文件 index.tsx,后续会扫这个文件。

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/f810c046-b000-4f97-8f81-2ec97c58ca4e.png

2. 在 paths.js 中添加扫描函数,并导出

调整 paths.js,在 module.exports 前添加下列扫描函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * 扫描函数
 */
function Scan() {
  const dirs = fs.readdirSync(resolveApp("src/"));
  const map = {};
  dirs.forEach((file) => {
    const state = fs.statSync(resolveApp("src/" + file));
    if (state.isDirectory()) {
      map[file] = resolveApp("src/" + file) + "/index.js";
    }
  });
  return map;
}

const dirsMap = Scan();

调整导出 module.exports,添加 dirsMap,注释或删除无用的 appIndexJsappTestJs

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/9098eea1-9386-472e-b012-590dcf8b197a.png

3. 在 webpack.config.js 中添加生成配置函数

在 module.exports 前添加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 生成 entry、plugins 配置
function setupMultiEntryConfig(webpackEnv) {
  const isEnvDevelopment = webpackEnv === "development";
  const isEnvProduction = webpackEnv === "production";

  const entry = {};
  const plugins = [];
  // key: 'index', 'test', ...
  Object.keys(paths.dirsMap).forEach((key) => {
    // entry 配置
    entry[key] = [
      // Include an alternative client for WebpackDevServer. A client's job is to
      // connect to WebpackDevServer by a socket and get notified about changes.
      // When you save a file, the client will either apply hot updates (in case
      // of CSS changes), or refresh the page (in case of JS changes). When you
      // make a syntax error, this client will display a syntax error overlay.
      // Note: instead of the default WebpackDevServer client, we use a custom one
      // to bring better experience for Create React App users. You can replace
      // the line below with these two lines if you prefer the stock client:
      // require.resolve('webpack-dev-server/client') + '?/',
      // require.resolve('webpack/hot/dev-server'),
      isEnvDevelopment &&
        require.resolve("react-dev-utils/webpackHotDevClient"),
      // Finally, this is your app's code:
      paths.dirsMap[key],
      // We include the app code last so that if there is a runtime error during
      // initialization, it doesn't blow up the WebpackDevServer client, and
      // changing JS code would still trigger a refresh.
    ].filter(Boolean);

    // plugins 配置
    // Generates an `index.html` file with the <script> injected.
    const htmlPlugin = new HtmlWebpackPlugin(
      Object.assign(
        {},
        {
          chunks: [key],
          inject: true,
          template: paths.appHtml,
          filename: `${key}.html`,
        },
        isEnvProduction
          ? {
              minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
              },
            }
          : undefined
      )
    );
    plugins.push(htmlPlugin);
  });
  return { entry, plugins };
}

在 module.exports 中调用上述函数:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/bbb57244-9ca5-4a22-a8b7-4b7e9066e012.png

1
2
// 生成 entry、plugins 配置
const multiEntryConfig = setupMultiEntryConfig(webpackEnv);

4. 调整 entry 和 plugins 配置

entry: multiEntryConfig.entry

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/1a09a485-de58-4e20-ba7c-310433e78025.png

...multiEntryConfig.plugins,

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/c8a3c0b5-1f38-4c16-852a-bbb925d0bcac.png

5. 在 start.js & build.js 中调整 checkRequiredFiles 检查函数

此时如果直接运行 yarn start 会报错,全局搜一下 appIndexJs 会发现在 start.jsbuild.js 中的 checkRequiredFiles 函数里有相关的校验逻辑,需要调整一下:

原:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/ea7139d0-0f04-43e8-b25e-469e4c34d977.png

改为:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/e8de061a-0962-4535-9e01-9316b2435996.png

验证

yarn start 一下,ok 的。

然后加一个新入口,

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/c2f651c9-3b88-4524-9065-05b8f3476ee3.png

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/fff0fd47-273a-4847-a132-01db8aa57c22.png

再重新运行一下 yarn start

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/4fdb5512-75a9-4e09-930c-fa076622c82a.png

Done~

后记

补充一下考虑使用多页应用的场景:

  • H5 活动页,每个活动/页面之间关联不大。

好处是可以提升首屏渲染速度。

注意:公共的包需要单独放在 vendor 里缓存

感谢阅读到这里~ 也感谢分享相关资料的大佬们~