多个入口文件
目前要输出的 entry 路径是 我们硬编码的(tsc_outputs/src/screen/membership/pages/index/index.js
),实际开发中我们希望可以通过一定的规则扫描目录,自动生成 entry 数组,同时编译生成多个 html 文件。
通过 HtmlWebpackPlugin,实现区分入口生成多个页面。
新建 rule 页面入口
路径为:src/screen/membership/pages/rule/index.tsx
,修改 webpack.config.js 来支持多个 entry:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const resolve = (dir) => path.join(__dirname, '..', dir);
/**
* @return {import('webpack').Configuration}
*/
const createConfig = () => {
return {
mode: 'development',
entry: {
'screen-membership-pages-index': resolve(
'tsc_outputs/src/screen/membership/pages/index/index.js'
),
'screen-membership-pages-rule': resolve(
'tsc_outputs/src/screen/membership/pages/rule/index.js'
),
},
output: {
path: resolve('dist'),
filename: '[name]-index.js',
clean: true,
},
plugins: [new HtmlWebpackPlugin()],
};
};
module.exports = createConfig;
- entry 从字符串变成了对象,key 为 entry 的名称,value 为入口文件路径。
- filename 变成了
[name]-index.js
,这里的[name]
会被 webpack 自动替换成 entry 的名称,假如还是名称 index.js,那么 webpack 会报错提示:Error: Conflict: Multiple chunks emit assets to the same filename index.js
,因为同目录下重名文件会覆盖。
执行一下试试:npm run dev
会发现在 dist 里生成了一个 index.html 文件,里面把这两个 entry 的 js 文件都引入了。可我们想生成两个 html 文件,各自引入各自的 js 文件。
支持生成多个 html 文件
修改 webpack.config.js 如下:
...
plugins: [
new HtmlWebpackPlugin({
chunks: ['screen-membership-pages-index'],
filename: 'screen/membership/pages/index/index.html',
}),
new HtmlWebpackPlugin({
chunks: ['screen-membership-pages-rule'],
filename: 'screen/membership/pages/rule/index.html',
}),
],
...
在 plugins 中使用了两个 HtmlWebpackPlugin 实例,分别指定各自的 js 文件以及 html 名称。
- chunks 属性,可以指定要引入的 js 文件,值就是 webpack 的 entry 中的名字,如果不指定则会引入所有的 js 文件。
- filename 属性,可以指定生成的 html 的文件名,如果不指定,则会在 webpack 的 output->path 目录下生成一个 index.html。
再执行下试试: npm run dev
,会看到 dist 目录会生成如下:
dist
├── screen
│ └── membership
│ └── pages
│ ├── index
│ │ └── index.html
│ └── rule
│ └── index.html
├── screen-membership-pages-index-index.js
└── screen-membership-pages-rule-index.js
本地起一个 web 服务,就可以通过浏览器打开看到生成的页面了。
- tsc 只管编译,将 tsx、jsx 等代码编译为 js 文件
- webpack 只管打包,通过分析 entry 入口文件和它的依赖,将其打包成一个或多个 js 文件,这里的多个是针对代码中一些”懒加载“的 js 而言的。
- html-webpack-plugin 只管生成 html 文件,传给它需要整合的 js 和 css(后面再说),它会自动将其引入到 html 页面中,甚至 html 文件也可以 用模版来生成个性化的 html。
复制待处理资源
由于我们将代码编译到 tsc_outputs 目录下,对应的资源也要复制过去,否则代码中引入的资源就会查找不到。 在 package.json 中新加个复制资源的脚本,需要引入一个 copyfiles 库。
安装 copyfiles
npm i copyfiles -D
添加复制资源脚本,并在编译前执行:
"scripts": {
"tsc": "rm -rf tsc_outputs && tsc",
"copy-assets": "copyfiles -e './node_modules/**/*' -e './tsc_outputs/**/*' -e './dist/**/*' -e './**/*.{ts,tsx}' './**/*.*' ./tsc_outputs",
"dev": "npm run tsc && npm run copy-assets && webpack build -c scripts/webpack.config.js",
"build": "",
"test": "echo 'no test'"
},
使用 html 模版
分别在src/screen/membership/pages/index
和src/screen/membership/pages/rule
下创建 index.html 文件,并提前设置好 root 节点:
index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
</head>
<body>
<div id="root"></div>
</body>
</html>
index.tsx 代码中也修改
const div = document.body.appendChild(document.createElement('div'));
render(<App />, div);
为:
render(<App />, document.getElementById('root'));
现在的目录结构是:
src
└── screen
└── membership
└── pages
├── index
│ ├── index.html
│ └── index.tsx
└── rule
├── index.html
└── index.tsx
这里在每个独立的页面入口都放置了一个 index.html 文件,添加这个文件有两个目的:
- 针对每个页面可以在 html 中设置个性化的信息,比如 title,meta 等。
- 根据 html 文件可以方便扫描文件目录生成 entry 入口,即:找到 index.html 文件,就对应把同目录的 index.js 作为一个独立 entry 入口。而 html 文件则作为模版传给 HtmlWebpackPlugin。
修改 webpack.config.js 以支持 html 模版:
...
plugins: [
new HtmlWebpackPlugin({
chunks: ['screen-membership-pages-index'],
filename: 'screen/membership/pages/index/index.html',
template: resolve('tsc_outputs/src/screen/membership/pages/index/index.html'),
}),
new HtmlWebpackPlugin({
chunks: ['screen-membership-pages-rule'],
filename: 'screen/membership/pages/rule/index.html',
template: resolve('tsc_outputs/src/screen/membership/pages/rule/index.html'),
}),
...
可以尝试把各个模版里添加 title、meta 等信息,然后编译执行npm run dev
,看看会发生什么。
github 上有个很好的示例,展示了 webpack 的 entry 与 htmlWebpackPlugin 的 chunks 间的关系:
扫描生成入口
安装查找文件神器:
npm i glob @types/glob -D
scripts 下添加获取入口文件的脚本 getScreens.js:
const glob = require('glob');
const fs = require('fs');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
/**
* 根据入口生成screens列表
*/
const getScreens = (cwd) => {
/** @type Array<{entryJs:string, entryHtml:string}> */
const screens = [];
const files = glob('**/index.html', { cwd, sync: true }); //, (err, files) => {
files.forEach((file) => {
const indexJs = file.replace(/index\.html$/, 'index.js');
const indexJsPath = path.join(cwd, indexJs);
if (!fs.existsSync(indexJsPath)) return;
screens.push({
entryJs: indexJsPath,
entryHtml: path.join(cwd, file),
});
});
return screens;
};
/**
* 根据screens列表生成Webpack的entryObjet和HtmlWebpackPlugin实例数组
* @param {Array<{entryJs:string, entryHtml:string}>} screens
* @return {{entryData:import('webpack').EntryObject, htmlEntries:Array<import('html-webpack-plugin')>}}
*/
const createEntries = (screens) => {
/** @type {import('webpack').EntryObject} */
const entryData = {};
/** @type {Array<import('html-webpack-plugin')>} */
const htmlEntries = [];
screens.forEach((screen) => {
const screenNameIndex = screen.entryJs.indexOf('screen/');
const entryFileNameBaseDist = screen.entryJs
.substring(screenNameIndex)
.replace(/\.js$/, '');
entryData[entryFileNameBaseDist] = {
import: [screen.entryJs],
};
htmlEntries.push(
new HtmlWebpackPlugin({
chunks: [entryFileNameBaseDist],
filename: `${entryFileNameBaseDist}.html`,
template: screen.entryHtml,
})
);
});
return { entryData, htmlEntries };
};
module.exports = {
getScreens,
createEntries,
};
有点长,不过整体的思路是查找 tsc_outputs 下的 index.html 文件,并找到与它同目录的 index.js 文件,js 文件作为 entry 入口给 webpack 打包,index.html 文件作为模板文件给 HtmlWebpackPlugin 生成 html 文件。
对应再修改下 webapck.config.js:
const path = require('path');
const { getScreens, createEntries } = require('./getScreens');
const resolve = (dir) => path.join(__dirname, '..', dir);
/**
* @return {import('webpack').Configuration}
*/
const createConfig = () => {
const screens = getScreens(resolve('tsc_outputs'));
const { entryData, htmlEntries } = createEntries(screens);
return {
mode: 'development',
entry: entryData,
output: {
path: resolve('dist'),
filename: '[name].js',
clean: true,
},
plugins: [...htmlEntries],
};
};
module.exports = createConfig;