环境
截止写文时(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 多个页面同时打包和调试后发现有问题,一直卡在编译中,也不报错,于是记录一下解决过程。
思路
-
最初认为是 Webpack 本身的问题,就先参考了 Webpack 4 官方文档,发现没用。
-
然后想到是不是和 create-react-app 有关,于是使用了关键词 createreactapp multiple entry webpack4 doesn't work
进行搜索后,根据 Create React App V2 - Multiple entry points 中给出的解决方案解决了。
先前两篇文章中的前几个步骤不用调整,当然由于 webpack 版本不同,需要做一些相应调整(例如:只有 webpack.config.js 没有 dev 和 prod.js ),后续会标注
- paths.js 不变
- entry 不变
- output 不变
- 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
1. 调整 paths.js
添加新的入口 appTestJs
1
2
3
4
5
|
module.exports = {
...,
appTestJs: resolveModule(resolveApp, "src/test"),
}
|
添加对应的入口文件 src/Test.tsx
2. 修改 webpack.config.js 的 entry
搜索:entry:
将原数组形式单入口:
改为对象形式多入口:
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
最终项目跑通后,打包效果如图
访问 http://localhost:3000/test.html
访问 http://localhost:3000/index.html
4. 修改 webpack.config.js 的 plugins
搜索 plugins:
复制一份已有的配置,添加 chunks
、filename
字段,因目前项目只使用 paths.appHtml 作为模板,所以 template
字段不需要修改。
原:
改:
完整配置
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
访问 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
ManifestPlugin
该插件用来生成 asset manifest 资产清单。参考Webpack Manifest Plugin
不足:配置很麻烦
显然每一次添加新页面都手动维护一堆配置信息不优雅,如果网页多了就需要重复 1、2、3、4 步骤,很不方便,期望优化成无需修改配置
的模式。
优化
参考了前文提到的「Webpack」配置 React 多个页面同时打包和调试,主要思路就是利用 nodejs 操作文件的能力,fs.readdirSync
来扫描入口文件夹,自动生成相应的配置文件。
1. 改造入口文件目录结构
在 src
目录下分别建立 src/index
和 src/test
文件夹,确保文件夹下都有入口文件 index.tsx
,后续会扫这个文件。
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
,注释或删除无用的 appIndexJs
和 appTestJs
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 中调用上述函数:
1
2
|
// 生成 entry、plugins 配置
const multiEntryConfig = setupMultiEntryConfig(webpackEnv);
|
4. 调整 entry 和 plugins 配置
entry: multiEntryConfig.entry
...multiEntryConfig.plugins,
5. 在 start.js & build.js 中调整 checkRequiredFiles 检查函数
此时如果直接运行 yarn start
会报错,全局搜一下 appIndexJs
会发现在 start.js
、build.js
中的 checkRequiredFiles
函数里有相关的校验逻辑,需要调整一下:
原:
改为:
验证
先 yarn start
一下,ok 的。
然后加一个新入口,
再重新运行一下 yarn start
,
Done~
后记
补充一下考虑使用多页应用的场景:
好处是可以提升首屏渲染速度。
注意:公共的包需要单独放在 vendor 里缓存
感谢阅读到这里~ 也感谢分享相关资料的大佬们~