Create React App v3 + Webpack v4 多页应用配置

2020-11-11 10:03:22 浏览数 (1)

环境

截止写文时(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)[1]「Webpack」配置React多个页面同时打包和调试[2]后发现有问题,一直卡在编译中,也不报错,于是记录一下解决过程。

思路

  1. 最初认为是 Webpack 本身的问题,就先参考了 Webpack 4 官方文档[3],发现没用。
  2. 然后想到是不是和 create-react-app 有关,于是使用了关键词 createreactapp multiple entry webpack4 doesn't work 进行搜索后,根据 Create React App V2 - Multiple entry points[4] 中给出的解决方案解决了。

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

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

只需调整第五步:ManifestPlugin 调整

解决方案

把原先遍历 entrypoints.main 数组

代码语言:javascript复制
const entrypointFiles = entrypoints.main.filter(
  (fileName) => !fileName.endsWith(".map")
);

改为遍历 entrypoints 对象,即可

代码语言:javascript复制
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

代码语言:javascript复制
# 卸载旧版 create-react-app
npm uninstall -g create-react-app

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

0. yarn eject

代码语言:javascript复制
yarn eject

1. 调整 paths.js

添加新的入口 appTestJs

代码语言:javascript复制
module.exports = {
  ...,
  appTestJs: resolveModule(resolveApp, "src/test"),
}

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

2. 修改 webpack.config.js 的 entry

搜索:entry:

将原数组形式单入口:

改为对象形式多入口:

代码语言:javascript复制
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

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

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

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

4. 修改 webpack.config.js 的 plugins

搜索 plugins:

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

原:

改:

完整配置

代码语言:javascript复制
// 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 数组

代码语言:javascript复制
const entrypointFiles = entrypoints.main.filter(
  (fileName) => !fileName.endsWith(".map")
);

改为遍历 entrypoints 对象,即可

代码语言:javascript复制
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[5]

大致如下

代码语言:javascript复制
historyApiFallback: {
  disableDotRule: true,
  verbose: true,
  rewrites: [
    { from: /^/test/, to: '/test.html' },
  ]
},

验证

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

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

复盘

版本、时效性

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

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

错误日志

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

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

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

HtmlWebpackPlugin 和 ManifestPlugin

简单 mark 一下这两个插件。

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

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

不足:配置很麻烦

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

优化

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

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

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

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

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

代码语言:javascript复制
/**
 * 扫描函数
 */
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

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

在 module.exports 前添加

代码语言:javascript复制
// 生成 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 中调用上述函数:

代码语言:javascript复制
// 生成 entry、plugins 配置
const multiEntryConfig = setupMultiEntryConfig(webpackEnv);

4. 调整 entry 和 plugins 配置

entry: multiEntryConfig.entry

...multiEntryConfig.plugins,

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

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

原:

改为:

验证

yarn start 一下,ok 的。

然后加一个新入口,

再重新运行一下 yarn start

Done~

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

参考资料

[1]

React-CRA 多页面配置(npm run eject): https://segmentfault.com/a/1190000016960824

[2]

「Webpack」配置React多个页面同时打包和调试: https://zhuanlan.zhihu.com/p/31908335

[3]

Webpack 4 官方文档: https://v4.webpack.js.org/concepts/#entry

[4]

Create React App V2 - Multiple entry points: https://stackoverflow.com/questions/55308657/create-react-app-v2-multiple-entry-points

[5]

Multiple html pages with create-react-app app: https://sapandiwakar.in/multiple-html-pages-with-create-react-app-app/

[6]

HtmlWebpackPlugin: https://www.webpackjs.com/plugins/html-webpack-plugin/

[7]

Webpack Manifest Plugin: https://github.com/danethurber/webpack-manifest-plugin

0 人点赞