一、webpack 构建发展简史
1 为什么需要构建工具
- 转换 ES6 语法
上图:ES6 module 主浏览器的支持情况
- 转换 JSX
- CSS 前缀补全/预处理器(less、sass)
- 压缩混淆
- 图片压缩
2 前端构建演变之路
webpack 的优势: 1.社区生态丰富 2.配置灵活、插件化扩展:官方提供的 loader、plugins 能满足日常开发,且质量高 3.官方更新迭代速度快
3 基础用法
3.1 基本概念
- entry(入口)
入口文件,指示 webpack 应该使用哪些模块来作为其构建内部依赖图的开始。
默认是./src/index.js,可以通过配置 webpack.config.js 进行设置。
// 单入口, entry 是一个字符串
module.exports = {
entry: './path/public/index.html',
}
// 多入口,entry 是一个对象
module.exports = {
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js',
},
}
- output(输出) 用来告诉 webpack 如何将编译后的文件输出到磁盘。
// 单入口,
module.exports = {
entry: './path/public/index.html',
output: {
filename: 'bundle.js',
path: __dirname + '/dist',
},
}
// 多入口
module.exports = {
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js',
},
output: {
// 通过占位符来保证文件名称的唯一性
filename: '[name].js',
path: __dirname + '/dist',
},
}
- loader(核心概念)
webpack 开箱即用只支持 JS、JSON 两种文件类型,通过 loader 可以将其他如 vue指令、css、typescript 等文件类型转化为有效的模块,并且可以添加到依赖图中。
本身是一个函数,接受文件源作为参数,返回转换的结果。
常用的loader | 名称 | 描述 | |------|------------| | babel-loader | 转换 ES6、ES7 等 JS 新语法特性 | | css-loader | 支持 css 文件的加载和解析 | | less-loader | 将 less 文件转换为 css 文件 | | ts-loader | 将 TS 转换为 JS | | file-loader | 进行图片、字体等媒体文件的打包 | | row-loader | 将文件以字符串的形式导入 | | thread-loader | 多进程打包 js 和 css |
用法:
module.exports = {
entry: './path/public/index.html',
output: {
filename: 'bundle.js',
path: __dirname + '/dist',
},
module: {
rules: [
{
// test: 指定匹配的规则, use 指定使用哪个loader
test: /.\txt$/, use: 'row-loader',
},
],
},
}
- plugins
主要是用于打包优化、资源管理 以及环境变量的注入。
可以理解为 loader 无法做的的事情,都可以通过 plugins 来完成。如:构建之前删除某些文件目录等操作。
作用于整个构建过程。
| 名称 | 描述 | 补充 |
|---|---|---|
| CommonChunkPlugin | 将chunks相同的模块代码提取到公共 JS | 常用于多页面打包 |
| CleanWebpackPlugin | 清理构建目录 | |
| ExtractTextWebpackPlugin | 将 CSS | |
| CopyWebpackPlugin | 将文件或者文件夹拷贝到构建的输出目录 | |
| HtmlWebpackPlugin | 创建 html 文件去承载输出的 bundle | |
| UglifyjsWebpackPlugin | 压缩 JS | |
| ZipWebpackPlugin | 将打包出的资源生成一个 zip包 |
用法:
module.exports = {
entry: './path/public/index.html',
output: {
filename: 'bundle.js',
path: __dirname + '/dist',
},
module: {
rules: [
{
// test: 指定匹配的规则, use 指定使用哪个loader
test: /.\txt$/, use: 'row-loader',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
})
],
}
- mode | 选项 | 描述 | |------|------------| | development | 设置 process.env.NODE_ENV 的值为 development。将自动开启 NamedChunksPlugin 和 NamedModulesPlugin(webpack5 中均已移除) | | production | 设置 process.env.NODE_DEV 的值为 production。将自动开启FlagDependenceUsagePlugin、FlagIncludedChunksPlugin、 ModuleConncatenationPlugin、NoEmitOnErrorsPlugin、OccerentOrderPlugin、SideEffectsFlagPlugi、TerserPlugin | | none | 不开启任何优化选项 |
module.exports = {
mode: 'development',
}
-
兼容性 webpack 支持所有符合 ES5 标准的浏览器(不支持 IE8 及以下版本)。import() 和 require.ensure() 需要 promise。如果需要支持旧版本浏览器,需要提前加载 polyfill。
-
bundle:打包最终生成的文件
-
chunk:每个chunk是由多个module组成,可以通过代码分割成多个chunk。
-
module:webpack中的模块(js、css、图片等等)
3.2 解析 ES6
借助 babel-loader, 配置文件为 .babellrc
module: {
rules: [
{
test: /\.m?js$/,
// 可以排除某些文件,转译尽可能少的文件,节约时间
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-proposal-object-rest-spread']
}
}
}
]
}
babel 的选项:
cacheDirectory:默认 false,为 true 时,指定的目录将用来缓存 loader 的执行结果,之后 webpack 再次构建,会尝试先读取缓存。cacheIdentifier:默认由 @babel/core 版本号、babel-loader 版本号、.babellrc 文件的内容、BABEL_ENV 的值组成的字符串,用来在 identify 改变后来强制缓存失效。cacheCompression:默认为 true,使用 Gzip 压缩每个 babel transform 的输出。customize:默认为 null, 导出 custom 回调函数的模块路径。 详见官方说明
3.3 文件监听
--watch
// package.json
{
"name": "hello-world",
"script": {
"build": "webpack",
// 监听
"watch": "webpack --watch",
},
}
但是有个缺点: 每次都要手动刷新浏览器
原理:
轮询判断文件的最后编辑时间是否发生变化。某个文件发生变化时,并不会立即告诉监听者,而是先将编译结果缓存起来,等待 aggregateTimeout。
module.exports = {
// 默认为 false,不开启监听
watch: true,
// 只有开启监听时,以下配置才有意义
watchOptions: {
// 默认为空,不监听的文件/文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化后的 300ms 后才会去执行,默认 300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停询问系统指定的文件有没有发生变化实现的,默认每秒询问 1000 次。
poll: 1000,
},
};
3.4 热更新
3.4.1 方式一( webpack-dev-serve)
特点
- 不刷新浏览器
- 不输出文件,没有磁盘的IO,而是放在内存中,所以速度会比 --watch 快
- 通常需要和 HotModuleReplacementPlugin 插件配合使用
// package.json
{
"name": "hello-world",
...
"script": {
"build": "webpack",
// --open 控制每次构建完成自动打开浏览器
"dev": "webpack-dev-serve --open",
},
}
// webpack.config.js
const webpack = require('webpack');
module.exports = {
mode: "development",
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
// 开发服务器配置
devServer: {
// 告诉服务器内容的来源。仅在需要提供静态文件时才进行配置
contentBase: './dist',
// 开启热更新
hot: true,
},
}
3.4.2 方式二(webpack-dev-middleware)
webpack-dev-middleware 是一个封装器,可以将 webpack 处理过的文件发送到一个 server, webpack-dev-server 在内部使用了它。 特点
- 会将 webpack 输出的文件传输给服务器
- 适用于灵活的定制场景 通常都是配合 express、koa 使用。 参考
3.4.3 关于 hot module replacement
hot module replacement 会在程序运行过程中,替换、添加或删除模块,而无需重新加载整个页面,主要方式如下:
1. 保留在完全重新加载页面期间丢失的应用程序状态
2. 只更新变更的内容
3. 在源码中修改 css/js 时,会立即在浏览器中更新,相当于在浏览器的 devtools 直接更改样式。
从应用程序方面看,其原理如下
- 应用程序要求 HMR runtime 检查更新
- HRM runtime 异步地下载更新,然后通知应用程序
- 应用程序要求 HRM runtime 更新
- HRM runtime 同步更新
从 compiler 中看,compiler 将发出 “update” 进行更新
- 更新后的 manifest
- 一个或多个 updated chunk(JavaScript)
词汇解析:
runtime:主要是指浏览器在运行过程中,webpack 用来连接模块化应用程序所需代码,包括连接模块的加载解析逻辑、已加载到浏览器中的连接逻辑、尚未加载模块的延迟加载逻辑。
manifest:包含所有模块执行、解析及映射等详细要点信息的数据集合。当完成打包,发送到浏览器时,runtime 会根据 manifest 来解析和加载模块。
详见官方说明
3.4.5 热更新的原理
-
过程1:启动阶段(图中标记1、2、A、B)
文件系统通过 webpack compile 将代码打包,然后传输给 bundle server,然后浏览器就能访问到我们写的代码了。 -
过程2:文件更新(图中标记1、2、3、4、5)
代码变化后,依然交给 compiler 进行编译,然后通知 HMR server,HMR server 就能知道哪些模块发生了变化,发生变化后会通知 HMR runtime,并通过 json 文件(manifest)进行传输,于是 HMR runntime 就会更新代码。
3.4 文件指纹
打包后输出的文件名称后缀。
作用:版本管理(发布的时候,修改的文件才需要发不上去)
- hash:和整个文件有关,一旦项目中有文件变更,整个项目构建的 hash 值就会更改
- Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值,通常用在 js 文件
- Contenthash:根据文件内容来定义 hash,内容变则 contenthash 变,通常用在 css 文件中
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './path/public/index.html',
output: {
// js 文件 8 位长的 chunkhash
filename: '[name]_[chunkhash:8].js',
path: __dirname + '/dist',
},
module: {
rules: [
{
test: /.(png|jpg|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
// 图片或字体 8 位 hash
name: '[name]_[hash:8].[ext]',
},
},
],
},
{
test: /.css$/,
use: [
MiniCssExtractPlugin.loader,
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8]',
}),
],
}
mini-css-extract-plugin 插件不可以和 style-loader 一起使用。因为 style-loader 是将 css 插入 js 中,而 mini-css-extract-plugin 是输出一个单独的文件。
3.5 代码压缩
- html 压缩
- js 压缩
- css 压缩