前面几篇我们处理了 JS、CSS、Less、Sass、图片和字体,但别忘了还有一个关键角色——HTML。它既是页面入口,也是各种资源的引用入口。如果每次都手动建一个 index.html、再手动把打包出来的 bundle.js、CSS 文件一个个 script、link 进去,那每次产物改名(比如加了 [contenthash])都要跟着改,维护成本极高。
本文介绍如何让 webpack 自动生成并注入 HTML。
一、手动维护 HTML 的痛点
先看看「手动流」长什么样:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>demo</title>
<link rel="stylesheet" href="../dist/css/main.a1b2c3d4.css" />
</head>
<body>
<div id="app"></div>
<script src="../dist/js/runtime.c9d0e1f2.js"></script>
<script src="../dist/js/vendors.e5f6a7b8.js"></script>
<script src="../dist/js/main.11223344.js"></script>
</body>
</html>
问题一目了然:
- 产物带 hash,每次构建文件名都变,HTML 必须同步修改。
- 多入口 / 代码分割时,
script数量不固定,手工维护极易出错。 - 依赖顺序(runtime、vendors、main)必须正确,否则运行时报错。
- 开发环境和生产环境产物不同,得维护两份。
html-webpack-plugin 正是为解决这些问题而生:它以模板为基础,自动生成 HTML,并把当次构建产生的所有 JS、CSS 按正确顺序注入进去。
二、基本使用
1. 安装
npm install html-webpack-plugin --save-dev
2. 配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html', // 以它为模板
}),
],
};
模板里不需要再写任何 script / link:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>webpack demo</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
执行 npm run build 后,dist/index.html 会自动带上正确路径的引用:
<script defer src="/js/main.11223344.js"></script>
注意默认注入的脚本带 defer,会等 HTML 解析完再执行,因此脚本放在 <head> 也没关系。
三、常用配置项
new HtmlWebpackPlugin({
template: './public/index.html', // 模板路径
filename: 'index.html', // 输出文件名,可带目录
title: '我的应用', // 传给模板的标题
inject: true, // 'head' | 'body' | false,默认 'head'
favicon: './public/favicon.ico', // 自动注入 favicon
minify: isProduction && { // 生产环境压缩 HTML
collapseWhitespace: true,
removeComments: true,
},
})
其中 title 等参数可以在模板中通过模板语法读取(默认支持 EJS 风格):
<title><%= htmlWebpackPlugin.options.title %></title>
四、多页面应用(MPA)
多入口时,为每个页面各 new 一个 HtmlWebpackPlugin,并用 chunks 指定该页面要引入哪些入口的产物:
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './public/app.html',
filename: 'app.html',
chunks: ['app'],
}),
new HtmlWebpackPlugin({
template: './public/admin.html',
filename: 'admin.html',
chunks: ['admin'],
}),
],
};
如果不写 chunks,每个 HTML 都会注入全部入口的脚本——这通常不是我们想要的,所以多页面场景务必显式指定。excludeChunks 则用于排除某些入口。
五、处理 HTML 模板内部的资源
默认情况下,模板里写的 <img src="./logo.png"> 不会被 webpack 处理,因为 HTML 是被当作纯文本读取的。要让 HTML 中的资源也走打包流程(压缩、hash 等),需要 html-loader:
npm install html-loader --save-dev
module.exports = {
module: {
rules: [
{
test: /\.html$/i,
use: 'html-loader',
},
],
},
};
这样模板里的 <img src="../assets/logo.png" /> 会被解析,交给对应的 asset module 处理,最终替换成正确的产物路径。
注意:
html-loader处理的是「模板内容」,html-webpack-plugin处理的是「生成 HTML 页面」,两者可以配合使用,职责不同。
六、拷贝纯静态文件
有些文件不需要经过任何处理,只是原样拷贝到产物目录(如 robots.txt、CNAME、已压缩好的第三方库)。这类需求用 copy-webpack-plugin:
npm install copy-webpack-plugin --save-dev
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'public/robots.txt', to: 'robots.txt' },
{ from: 'public/static', to: 'static' },
],
}),
],
};
区分原则很简单:需要参与构建(改路径、加 hash、压缩)的用 loader / plugin;原样拷贝的用 copy-webpack-plugin。
七、完整配置示例
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProduction ? 'production' : 'development',
entry: {
app: './src/app.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true,
publicPath: '/',
},
module: {
rules: [
{ test: /\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
{ test: /\.html$/i, use: 'html-loader' },
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
generator: { filename: 'images/[hash][ext][query]' },
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: 'webpack demo',
favicon: './public/favicon.ico',
inject: 'head',
minify: isProduction && {
collapseWhitespace: true,
removeComments: true,
},
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
],
};
小结
处理 HTML 资源的核心工具是 html-webpack-plugin:
| 需求 | 方案 |
|---|---|
| 自动生成 HTML 并注入产物 | html-webpack-plugin |
| 设置标题、favicon、压缩 | title / favicon / minify 选项 |
| 多页面分别注入不同入口 | 多次 new + chunks |
| 让模板里的图片走打包 | html-loader |
| 原样拷贝静态文件 | copy-webpack-plugin |
有了它,HTML 不再是需要手工同步的「易碎品」,而是构建流程中自动产出的一环。