上一篇文章中,我们讲解了如何使用 webpack 打包一个简单的应用,期间浅尝辄止地介绍了打包过程中会遇到的一些基本概念。
不过,先要订正上一篇文章里的有一个优化点。webpack 从 5.20.0+ 开始,增加了 output.clean 的支持,这样就不需要再额外引入 CleanWebpackPlugin 了。
module.exports = {
//...
output: {
clean: true, // Clean the output directory before emit.
},
};
diff 状况如下:
在此之后呢,我们就要去一一深入webpack 中的核心概念和需要掌握的重要技能点了。
当然,首当其冲要讲的就是 打包输入(Entry) 和 打包输出(Output) 了。顾名思义,Entry 用来指定打包的入口文件,而 Output 则是用于制定打包产物的输出位置以及输出形式。
在上一篇文章中,我们就简单使用了下它们:
// webpack.dev.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
},
mode: 'development',
};
- entry 的值是一个字符串,指定打包的入口文件是 src/index.js,而
- output.filename 则是表明将打包结果输出到 dist/bundle.js
下面就要详细介绍。
打包入口
entry 的可不只支持字符串类型,还有对象、数组甚至是函数。
多样的 entry 类型
entry 为字符串的场景,我们已经介绍过,就不多赘述了。
// webpack.prod.config.js
module.exports = {
mode: 'production',
entry: './src/index.js',
}
以上的配置,只是定义了一个打包入口,只有一个打包输出。
npm run build
> webpack-demo@1.0.0 build
> webpack --config webpack.prod.config.js
asset index.html 234 bytes [emitted]
asset main.js 84 bytes [emitted] [minimized] (name: main)
orphan modules 59 bytes [orphan] 1 module
./src/index.js + 1 modules 146 bytes [built] [code generated]
webpack 5.94.0 compiled successfully in 305 ms
输出文件 dist/mian.js。
如果要定义多个打包入口(比如对于多页应用),就要使用 entry 的对象形式。
module.exports = {
mode: 'production',
entry: {
index: './src/index.js',
lib: './src/lib.js',
},
};
// src/lib.js
window.Util = {
escapeHtml(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
},
}
以上,我们将 entry 定义成包含了 2 个属性 index、lib 的对象。
index、lib 这两个属性名有一个专有名词来描述,即「chunk name」。chunk 的字面意思是「代码块」,在打包领域,我们将从入口文件开始、由入口文件及其内部依赖所组成的依赖树理解成是一个 chunk,从 chunk 得到的打包产物成为 bundle。
chunk、entry 以及 bundle 三者的关系如下:
默认情况下,打包的输出文件名由 chunk name 决定的。删除 dist/
目录,再次执行打包:
npm run build
> webpack-demo@1.0.0 build
> webpack --config webpack.prod.config.js
asset lib.js 140 bytes [emitted] [minimized] (name: lib)
asset index.js 84 bytes [emitted] [minimized] (name: index)
orphan modules 59 bytes [orphan] 1 module
./src/index.js + 1 modules 146 bytes [built] [code generated]
./src/lib.js 174 bytes [built] [code generated]
webpack 5.94.0 compiled successfully in 225 ms
./src/index.js 和 ./src/lib.js 最终会输出成 dist/index.js 和 dist/lib.js 2 个文件(在不指定 output 的情况下)。
如果项目只有一个入口文件,那么他的 chunk nanme 就是 main。也就是说:
module.exports = {
entry: './src/index.js',
}
等同于
module.exports = {
entry: {
main: './src/index.js'
},
}
正因为如此,默认单入口打包的输出文件才会是 dist/main.js。
除了对象,entry 还能设置数组:
npm install babel-polyfill
module.exports = {
mode: 'production',
entry: ['babel-polyfill', './src/index.js'] ,
};
当 entry 是一个数组时,数组的最后一个元素才是实际的入口文件,而前面的资源是用来合并到入口文件里去的。
也就是说:
module.exports = {
mode: 'production',
entry: ['babel-polyfill', './src/index.js'] ,
};
的写法,等同于
// webpack.dev.config.js
module.exports = {
mode: 'production',
entry: './src/index.js',
};
// src/index.js
import 'babel-polyfill';
效果类似:
最后,entry 甚至可以是一个函数,函数的返回类型可以是字符串、对象或数组,其含义与单独将 entry 定义成字符串、对象或数是一样的。
// 返回一个字符串型的入口
module.exports = {
entry: () => './src/index.js',
};
// 返回一个对象型的入口
module.exports = {
entry: () => ({
index: ['babel-polyfill', './src/index.js'],
lib: './src/lib.js',
}),
};
使用函数的一个优点,就是可以在里面添加一些判断逻辑,动态定义入口文件。
// 返回一个字符串类型的入口
module.exports = {
entry: () => './src/index.js',
};
// 返回一个对象类型的入口
module.exports = {
entry: () => ({
index: ['babel-polyfill', './src/index.js'],
lib: './src/lib.js',
}),
};
另外,webpack 还支持返回 Promise 来支持异步操作。
module.exports = {
entry: () => new Promise((resolve) => {
// 模拟异步操作
setTimeout(() => {
resolve('./src/index.js');
}, 1000);
}),
};
以上配置,在运行 npm run build 之后,需要等待 1 秒钟之后才会开始打包。
context
webpack 的入口文件的路径可不是只由 entry 决定的,还依赖 context 设置。
以下的配置:
module.exports = {
entry: './src/index.js',
}
其实等同于
module.exports = {
context: path.join(process.cwd(), '.')
entry: './src/index.js',
}
实际上,context 指代打包入口的路径前缀,是可选的,默认指向项目当前的工作目录(current working directory)。这在多入口打包时,能让 entry 的编写更加简洁。
举例来说:以下两种配置效果一样,入口都是 <项目根路径>/src/scripts/index.js。
// 案例一
module.exports = {
context: path.join(__dirname, './src'),
entry: './scripts/index.js',
};
// 案例二
module.exports = {
context: path.join(__dirname, './src/scripts'),
entry: './index.js',
};
打包效果:
当然,因为没有 src/scripts/index.js 文件打包报错了,不过验证了 context 的作用。
单页/多页应用案例
webpack 多 chunk name 的 entry 设置,让我们既可以打包单页应用(SPA),也可以打包多页应用。 案例如下:
单页应用:
module.exports = {
entry: './src/app.js',
};
多页应用:
module.exports = {
entry: {
pageA: './src/pageA.js',
pageB: './src/pageB.js',
pageC: './src/pageC.js',
},
};
对多页应用来苏红,每个页面(HTML)对应一个独立的 bundle。
当然,针对入口打包我们还可以做一些优化,就是将一些公共模块(vendor,比如 react、react-dom)从业务代码中抽离出来,这样打包出来的文件尺寸就会比较小(只包含业务代码)。
webpack 4 之后,我们可以通过
optimization.splitChunks
来优化,后续文章中会介绍,本篇就不再赘述了。
打包输出
讲完打包入口,再来说打包输出,与打包输出相关的配置集中在 output 对象中。
filename
之前关于 output 的应用比较简单:
module.exports = {
output: {
filename: 'bundle.js',
},
};
以上,filename 用于指定输出文件名,默认输出的目录是 dist(由 path 属性决定)。
module.exports = {
output: {
filename: 'bundle.js',
// 输出目录默认是当前项目下的 dist 目录
path: path.join(process.cwd(), 'dist'),
},
};
当然,filename 还可以是一个相对路径:
module.exports = {
output: {
filename: './js/main.js',
},
};
执行打包,文件被输出到 dist/js/main.js。
一如既往的,如果打包文件的父级目录不存在,webpack 会自动创建。
输出占位符
更加方便的是,对于多入口配置,filename 中还可以使用类似 [placeholder]
形式动态生成文件名。以下面为例:
// webpack.prod.config.js
module.exports = {
mode: 'production',
entry: {
pageA: './src/pageA.js',
pageB: './src/pageB.js',
},
output: {
filename: '[name].js',
clean: true
},
// pageA.js
document.getElementById("app").innerHTML = `Page A`
// pageB.js
document.getElementById("app").innerHTML = `Page B`
在打包输出时,上面配置的 filename 中的 [name]
会被替换为 chunk name,因此最后项目中实际生成的资源是 pageA.js 与 pageB.js。
除了 [name]
,其他还支持的占位符还包括:
在实际项目中,我们使用比较多的是 [name]
,它与 chunk 是一一对应的关系,并且可读性较高。如果要控制客户端缓存,最好还要加上 [chunkhash]
。
module.exports = {
entry: {
pageA: './src/pageA.js',
pageB: './src/pageB.js',
},
output: {
filename: '[name]@[chunkhash].js',
},
plugins: [
new HtmlWebpackPlugin({
filename: 'pageA.html',
chunks: ['pagea'],
template: './index.html'
}),
new HtmlWebpackPlugin({
filename: 'pageB.html',
chunks: ['pageB'],
template: './index.html'
}),
]
};
执行打包,结果类似下面这样:
output: {
filename: '[name]@[chunkhash:5].js',
},
当然,还可以通过 [chunkhash:5] 方式缩短 hash 的长度到 5 位:
path
output.path 用来指定打包文件的输出目录,它的值必须是一个绝对路径。其默认值是当前的工作目录下的 dist/ 目录。
module.exports = {
entry: './src/app.js',
output: {
filename: 'bundle.js',
path: path.join(process.cwd(), 'dist'),
},
};
默认 dist/ 目录的配置是从 webpack 4 起使用的,之前默认都是项目根目录。
ouput.path 类似 entry.context,不过一个是指定输出目录,一个是指定入口文件目录。除非我们需要更改它,否则不必单独配置。
与webpack-dev-server配合使用
为了避免开发环境和生产环境产生不一致而造成开发者的疑惑,我们可以将 webpack-dev-server 的 static 与 webpack 中的 output.path 保持一致。
module.exports = {
entry: './src/app.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist') ,
},
devServer: {
static: '/dist/',
port: 3000,
},
};
这样,静态资源的服务地址与打包文件的输出目录是一样的,理解上就不容易出现问题。
publicPath
publicPath 是 output 下重要性仅次于 filaname 的一个属性。你的项目可能不需要设置 path,但基本上是要设置 publicPath 的。
publicPath 名字上很容易让人跟 path 混淆。不过,弄清他们的作用后,就不难区分了。
- path:这个已经介绍过了,是用来指定打包文件的输出目录的,默认是当前项目下的 dist/。
- publicPath:则用于指定打包出来的文件内部引用的资源的路径前缀。比如打包文件里动态引入的 CSS、JavaScript,或是图片、svg 等这类文件。
也就是说,path 控制的是打包时的输出行为;publicPath 则是控制打包后、运行时的资源引入行为。
我们以下面的配置为例:
module.exports = {
mode: 'production',
entry: './src/app.js',
output: {
// publicPath: 'xxx',
clean: true
},
// 为了支持解析图片,后续文章会将,暂时略过
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
type: 'asset/resource'
},
]
},
}
// app.js
import img from '../assets/images/avatar.jpg'
document.getElementById("app").innerHTML = `
<h1>Hello Vanilla!</h1>
<figure>
<img src="${img}" alt="My Avatar" />
<figcaption>My Avatar</figcaption>
</figure>
`
引入了项目根目录下 assets 下的一张图片。下面开始测试,
publicPath 可以设置 3 种不同类型的值:
- HTML 相关
当设置的 publicPath 为空或是以 ./
、../
开头的相对路径时,我们来看看效果。
output: {
publicPath: '',
clean: true
}
打包输出:
main.js 中输出代码:
依次类似,当 publicPath 分别位 ./
或 ../
时,main.js 中输出代码如下:
可以看见,图片资源输出位置不变(一致位于 dist/ 目录下),而引用的图片地址就是"publicPath + 文件名"。
也就是说,publicPath 为空或是以 ./
、../
开头的相对路径时时,被请求的资源是相对于 HTML 所在路径进行加载的。
举例来说,假设当前 HTML 地址为 https://example.com/app/index.html
,我们要异步加载的资源文件叫 0.chunk.js。
那么不同取值下的 publicPath 对应的资源文件加载路径分别是:
publicPath: "" // 实际路径 https://example.com/app/0.chunk.js
publicPath: "./js/" // 实际路径 https://example.com/app/js/0.chunk.js
publicPath: "../assets/" // 实际路径 https://example.com/aseets/0.chunk.js
注意,/js/ 最后一个
/
是必需的,否则输出会是类似 /js1058d90f4a32a506ab53.jpg 的错误地址。下同。
- Host 相关
同理,如果 publicPath 的值是以“/”开头时。以"/"、"/js/" 为例,输出结果如下:
那么 publicPath 则是以当前页面的 host name 为基础路径的。
举例来说,假设当前 HTML 地址为 https://example.com/app/index.html
,我们要异步加载的资源文件叫 0.chunk.js。
publicPath: "/" // 实际路径https://example.com/0.chunk.js
publicPath: "/js/" // 实际路径https://example.com/js/0.chunk.js
publicPath: "/dist/" // 实际路径https://example.com/dist/0.chunk.js
- CDN 相关
当然,实际项目里考虑到性能,打包出来的静态资源通常会部署在单独的 CDN 上,与网页脚本的部署域名分开。这个时候,publicPath 的值就是一个以具体协议或相对协议开头的域名地址。
以"cdn.com/"、"//cdn.co…" 为例,输出结果如下:
举例来说,假设当前 HTML 地址为 https://example.com/app/index.html
,我们要异步加载的资源文件叫 0.chunk.js。
publicPath: "http://cdn.com/" // 实际路径 http://cdn.com/0.chunk.js
publicPath: "https://cdn.com/" // 实际路径 https://cdn.com/0.chunk.js
publicPath: "//cdn.com/assets/" // 实际路径 //cdn.com/assets/0.chunk.js
简单来说,当为 publicPath 设置了值时,最终打包出来的结果地址就是"publicPath + 文件名" 。
当然,当你不设置 publicPath 时,其默认值为 'auto',它会根据当前加载资源的脚本位置(e.currentScript
API)来计算,确保资源被正确加载。
总结
本文讲解了webpack 中 2 个最基础的核心概念:打包输入(Entry) 和 打包输出(Output)。Entry 用来指定打包的入口文件,而 Output 则是用于制定打包产物的输出位置以及输出形式。
希望本文讲解对你的工作能够有所帮助。感谢阅读,再见。