环境
截止写文时(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]后发现有问题,一直卡在编译中,也不报错,于是记录一下解决过程。
思路
- 最初认为是 Webpack 本身的问题,就先参考了 Webpack 4 官方文档[3],发现没用。
- 然后想到是不是和 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 ),后续会标注
- paths.js 不变
- entry 不变
- output 不变
- 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
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:
复制一份已有的配置,添加 chunks
、filename
字段,因目前项目只使用 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/index
和 src/test
文件夹,确保文件夹下都有入口文件 index.tsx
,后续会扫这个文件。
2. 在 paths.js 中添加扫描函数,并导出
调整 paths.js,在 module.exports
前添加下列扫描函数:
/**
* 扫描函数
*/
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 前添加
代码语言: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.js
、build.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