Webpack 从 0 开始配置
前言
这是一篇关于webpack5从 0 开始配置的入门文章。
通过本文你可以获得:
- 了解 webpack 具体的作用
- 了解如何使用 webpack 做打包资源
- 了解如何利用 webpack 提升开发体验
- 能看懂官方文档的配置项
- 能获得一些优化的知识
本文阅读时长大概在 15 分钟左右,如果你能根据例子来配置的话,可能花费的时间更长一些。
我会尽可能用白话跟你解释所有配置项的由来,能解决什么问题。
同时也希望读者能亲自配一下,加深自己的印象。
话不多说,我们开始。
一、基本概念
1.1 Webpack 是什么
Webpack 是一个开源的 JavaScript 模块打包工具,其最核心的功能是解决模块之间的依赖,把前端的各种资源文件(js、css、jpg、png 等等)作为各个模块按照特定的规则和顺序组织在一起。
它根据模块的依赖关系进行静态分析,最终打包生成对应的静态资源。
这个过程就叫作模块打包。
官网上的构建图示能很好地说明Webpack的作用

1.2 JS 中的模块
所谓模块,就是将特定功能的代码分拆成多个代码片段,每个片段实现一种目的,最终通过接口将它们组合在一起,各个模块协同工作,保证程序的正常运转。
在很长一段时间,JavaScript 不像其他程序语言一般,能够使用模块化进行开发。因为这门语言诞生时,仅仅作为轻量级的脚本语言,为用户提供上传表单时的校验功能。
随着业务越来越复杂以及前端技术的发展,引入多个 js 文件到页面中已经逐渐成为常态,此时也暴露出一些问题:
- 需要手动维护 JavaScript 的加载顺序。页面中多个 script 之间通常存在依赖关系,但由于这种依赖关系是隐性的,当 js 文件过多时就容易出现问题。
- 每个 script 标签都意味着请求一次静态资源,过多的请求会拖慢页面的渲染速度。
- 每个 script 标签中,顶级作用域都是全局作用域,如果没有经过处理直接在代码中进行变量或者函数声明,会造成全局作用域的污染。
模块化则一一解决了上述问题:
- 通过导入和导出语句来分析模块间的依赖关系
- 使用工具将全部 js 文件打包成一个或多个文件,减少网络开销
- 多个模块间的作用域相互隔离,彼此之间不会存在命名冲突
对于模块化,社区提出了 AMD、CMD、CommonJS 等方案,ES6 模块标准则将模块化提升到语言层面。但由于以下原因,ES6 标准模块还不能用于实际应用:
- 无法使用 code splitting 和 tree shaking
- 大部分 npm 模块采用 ComminJS 的规则,浏览器不支持其语法
- 浏览器兼容性问题等
于是,我们需要使用模块打包工具来帮助我们完成一系列工作。
模块打包工具的任务就是解决模块间的依赖,使其打包后的结果可以运行在浏览器上。
1.3 打包工具做了什么
使用打包工具的一个好处是 —— 它们可以更好地控制模块的解析方式,允许我们使用裸模块和更多的功能,例如 CSS/HTML 模块等。
打包工具做以下内容:
- 从一个打算放在 HTML 中的
<script type="module">“主”模块开始。(Webpack 默认从src/index.js开始) - 分析它的依赖:它的导入,以及它的导入的导入等。
- 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数(bundler function)替代原生的
import调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。 - 在处理过程中,可能会应用其他转换和优化:
- 删除无法访问的代码。
- 删除未使用的导出(“tree-shaking”)。
- 删除特定于开发的像
console和debugger这样的语句。 - 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
- 压缩生成的文件(删除空格,用短的名字替换变量等)。
如果我们使用打包工具,那么脚本会被打包进一个单一文件(或者几个文件),在这些脚本中的 import/export 语句会被替换成特殊的打包函数(bundler function)。因此,最终打包好的脚本中不包含任何 import/export,它也不需要 type="module",我们可以将其放入常规的 <script>:
html1<!-- 假设我们从诸如 Webpack 这类的打包工具中获得了 "bundle.js" 脚本 --> 2<script src="bundle.js"></script>
1.4 Webpack 的优势
-
默认支持多种模块标准,包括 AMD、CommonJS、ES6 模块等。它会帮我们处理好模块间的依赖关系。
-
完备的代码分割(code splitting)解决方案。它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能动态加载。这有助于有效减少资源体积,提升首页渲染速度。
-
Webpack 可以处理各种类型的资源,js、图片、css 等等。
-
Webpack 有庞大的社区支持,插件齐全。
-
webpack 可以提高前端开发的体验,
webpack-dev-server具备整套的协同开发功能,提高前端开发、调试的效率
1.5 Webpack 五个核心概念
-
entry:入口。指定打包工具从哪个文件开始构建内部依赖图,并以此为起点打包 -
output:输出。指定打包好后的 bundles 资源最终输出到哪个地方,输出名字是什么 -
loader:加载器。让 webpack 能够处理非 js 文件的翻译、打包工作。(例如 less、image 等静态资源) -
plugin:插件。让 webpack 能够处理打包优化、压缩、生成模板等功能性任务。 -
mode:模式。development 模式、production 模式、none。能够设置process.env.NODE_ENV的值,并且根据环境不同自动开启一些插件。
1.6 小结
通过上面的介绍,我们能够大概知道 webpack 的概念、作用。
下面我们进入实战环节,在实战环节,我们需要做一些准备工作。
二、准备工作
2.1 初始化项目
bash1mkdir webpack-demo-1 2cd webpack-demo-1 3// 初始化 4yarn init -y 5// 安装webpack和cli工具 6yarn add webpack webpack-cli --dev 7// 查看版本 8npx webpack -v
为了测试,我们首先在根目录下创建src目录,并创建三个文件(index.html在根目录下)
bash1index.html 2src 3├── index.css 4└── index.js 5
分别在里面添加内容:
index.css
css1.div { 2 color: red; 3 font-size: 16px; 4}
index.html
html1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title>first webpack app</title> 8 </head> 9 <body></body> 10 <script src="./dist/bundle.js"></script> 11</html>
index.js
js1function createElement() { 2 const div = document.createElement('div') 3 div.innerHTML = 'hello world' 4 div.classList.add('div') 5 document.body.append(div) 6} 7createElement()
2.2 打包第一个 js 文件
初始化好后,我们可以看到index.html上的<script src="./dist/bundle.js"></script>,目前我们的项目并没有创建dist这个目录,bundle.js也是不存在的。
我们期望能够用webpack打包index.js的内容来生成dist目录和bundle.js文件
此时在命令行上输入:
bash1npx webpack --entry=./src/index.js --output-filename=bundle.js --mode=development
npx webpack是在本地用npx启动webpack的意思
--entry 参数是寻找当前目录下的src/index.js文件
--output-filename 是指定输出的文件名
--mode指的是开发模式
此时,根目录下应该会多了dist目录,下面有一个bundle.js的文件,它就是打包后的index.js文件。
如果此时用http-server或者vscode的live server插件打开本地项目,你会发现chrome浏览器屏幕前输出现hello world字样,代表打包成功了。
由于目前是从 0 开始配置,您需要自行下载 http-server 或者 vscode 的 live-server 插件来查看打包后的项目页面。
简单总结,我们刚才的操作是:
Webpack以entry指定的入口文件src/index.js为入口点查找模块依赖,此时没有其他依赖。于是通过output输出成bundle.js。
最后的参数 mode 指的是打包模式,一共有三种:development、production、none 三种模式。它会自动添加适合于当前模式的一系列配置,减少了人为的工作量。
2.3 配置 script
由于使用 cli 的方式会增加很多指令参数,不容易维护,所以我们需要在package.json中添加脚本,这样就不需要输入那么长的指令了。
在package.json中添加命令:
json1 ... 2 "scripts": { 3 "build": "webpack --output-filename=bundle.js --mode=development" 4 }, 5 ...
上面的脚本省略掉了entry的配置。
这是因为webpack默认是从工程根目录的src目录下的index.js作为入口文件,打包好后的文件自动放在dist目录。所以我们可以按照默认目录配置来简化我们的命令行。
此时通过yarn build也可以打包。
2.4 配置文件
Webpack 提供大量的命令行参数,可以帮助我们满足各种场景的需求。
上面的例子我们已经看到了,我们可以定制入口文件和输出的文件名和指定模式等。
这些命令行参数可以使用下面的命令获取
bash1npx webpack –h
命令中添加更多的参数仅适用于配置较少的项目,如果配置比较多,我们就需要专门的配置文件。
Webpack 每次打包时都会读取该配置文件,这样就不必在命令行中添加太多参数了,方便后期修改维护。
默认的配置文件为webpack.config.js,也可以通过命令行参数--config指定配置文件。
json1 "scripts": { 2 "build": "webpack --config build.config.js", 3 },
这里就按照之前的命令行参数,在根目录下创建webpack.config.js,并且加入配置项:
javascript1// webpack.config.js 2module.exports = { 3 entry: './src/index.js', 4 output: { filename: 'bundle.js' }, 5 mode: 'development', 6}
output.filename还是跟先前一样,但是如果要配置output.path——最终资源输出路径则需要绝对路径。
默认的配置输出路径相当于
javascript1output: { 2 path: path.join(__dirname, 'dist') 3}
由于是默认配置,所以 webpack.config.js 中省略了
output.path的配置
写好配置项后,我们就可以去除package.json中配置的打包参数了。
json1 "scripts": { 2 "build": "webpack" 3 },
执行yarn build后,webpack会预读webpack.config.js中的配置,再进行打包。
当构建后,请使用编辑器打开并在 chrome 上查看结果。
2.5 小结
准备工作主要是熟悉 webpack-cli 的使用以及 webpack 脚本、webpack 配置文件的默认项等。
我们还用 webpack 打包了一个 js 文件,这甚至不需要任何配置,因为 webpack 自身就具备打包 js 的能力。
但是它默认不具备打包其他静态资源(css、sass、file 文件等)的能力,所以我们需要给它配置loader,让它可以”翻译“这些静态资源。
在翻译的同时,我们还能够使用plugin让 webpack 帮助我们做一些额外的工作,例如生成模板文件、压缩、优化等等。
下面进入配置loader、plugin的环节。
三、利用 loader、plugin 打包资源
3.1 打包 css
我们在准备工作中虽然写了 css,但是实际打包后并没有 css 的效果,这是因为 webpack 在分析依赖时,并没有找到 css 的引入语句。
我们可以在index.js上引入 css
js1import './index.css'
打包除 js 文件外的资源需要用到loader,我们先安装两个 loader:
bash1yarn add style-loader css-loader --dev
style-loader用于将<style>标签插入到 html 中,css-loader是用于识别import './index.css'语句并打包 css 文件。
根据目前的官方网站,配置如下:
javascript1// webpack.config.js 2module.exports = { 3 ... 4 module: { 5 rules: [ 6 { 7 test: /\.css$/i, 8 use: ['style-loader', 'css-loader'],//注意顺序,webpack从右到左读取loader 9 }, 10 ], 11 }, 12};
webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这个示例中,所有以 .css 结尾的文件,都将被提供给 style-loader 和 css-loader。
模块 loader 可以链式调用。链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。
上面的顺序是先执行css-loader再执行style-loader。
配置完后,请用yarn build构建并在chrome上查看结果,以下不再提示。
3.2 打包 less
我们先在src目录下增加一个style.less的文件,内容如下:
css1@width: 100px; 2@height: 100px; 3div { 4 background-color: aqua; 5 width: @width; 6 height: @height; 7 border: 1px solid red; 8 user-select: none; 9}
然后在index.js中引入less:
css1import './style.less';
接着安装 less 和 less-loader
bash1yarn add less less-loader --dev
配置:
js1// webpack.config.js 2module.exports = { 3 ... 4 module: { 5 rules: [ 6 ... 7 { 8 test: /\.less$/i, 9 use: ['style-loader', 'css-loader', 'less-loader'], 10 }, 11 ], 12 }, 13};
3.3 postcss
postcss 是利用 JavaScript 转换样式的工具。我们可以用它配合autoprefixer来给 css 添加更多兼容性的前缀代码以支持更多浏览器平台。
首先我们在当前的 index.css 上加一句 css 代码:
diff1// index.css 2.div { 3 color: red; 4 font-size: 16px; 5 /* 为了查看postcss-loader有没有效果 */ 6+ user-select: none; 7}
bash1yarn add postcss autoprefixer postcss-loader --dev
然后在 webpack 中配置规则:
javascript1 module: { 2 rules: [ 3 { 4 test: /\.css$/i, 5 use: [ 6 'style-loader', 7 'css-loader', 8 { 9 loader: 'postcss-loader', 10 options: { 11 postcssOptions: { 12 plugins: [require('autoprefixer')], 13 }, 14 }, 15 }, 16 ], 17 }, 18 ], 19 },
经过打包后,查看打包后的页面,会发现已经被添加了兼容性的前缀代码

postcss 中还有很多有关于 css 加载所需要的插件,都集成到postcss-preset-env的插件中了,比如能够让浏览器支持#12345678这样的八位数颜色以及autoprefixer支持的功能。
我们也可以直接使用这个预设的插件,这样就相当于用了很多个类似autoprefixer这样的小loader。
使用方法:
bash1yarn add postcss-preset-env --dev
直接配置在options.postcssOptions.plugins中即可,这里就替换掉上面的require(autoprefixer),因为postcss-preset-env已经拥有它的功能了。
javascript1 postcssOptions: { 2 plugins: ['postcss-preset-env'], 3 }
专用的 postcss 配置
我们可以使用 less、css 等来书写 css,而 postcss 则需要体现到所有 css 上,因此我们需要给所有 css 预编译工具配置 postcss-loader,但这就会增加大量重复的配置代码。
为了解决这个问题,我们可以使用默认的postcss.config.js来给 postcss 做共同的配置。
在根目录下创建postcss.config.js,内容如下:
js1// postcss.config.js 2module.exports = { 3 plugins: ['postcss-preset-env'], 4}
然后同步修改webpack.config.js配置
js1// webpack.config.js 2module.exports = { 3... 4 module: { 5 rules: [ 6 { 7 test: /\.css$/i, 8 use: ['style-loader', 'css-loader', 'postcss-loader'], 9 }, 10 { 11 test: /\.less$/i, 12 use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'], 13 }, 14 ], 15 }, 16};
设置好后,webpack 打包代码时,会从use的最后一项开始(比如less-loader)往前执行loader,当执行到postcss-loader时,会读取postcss.config.js的配置,最后执行css-loader和style-loader。
3.4 importLoader
css-loader 能够支持类似于@import xxx.css之类的 css 引入。
例如我们在index.css中用这种方法引入的test.css
js1@import './test.css'; 2.div { 3 color: red; 4 font-size: 16px; 5 /* 为了查看postcss-loader有没有效果 */ 6 user-select: none; 7}
test.css内容如下:
css1body { 2 background-color: antiquewhite; 3 min-height: 100vh; 4 user-select: none; 5}
但是根据目前配置,当css-loader识别到这个代码时,postcss-loader已经加载过了,就会导致test.css无法获得postcss的支持,所以需要修改css-loader的配置。
js1 module: { 2 rules: [ 3 { 4 test: /\.css$/i, 5 use: [ 6 'style-loader', 7 { 8 loader: 'css-loader', 9 options: { 10 importLoaders: 1, // 期望往回加载的位数,1代表往回1位-也就是postcss-loader加载 11 }, 12 }, 13 'postcss-loader', 14 ], //注意顺序,webpack从右到左读取loader 15 }, 16 { 17 test: /\.less$/i, 18 use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'], 19 }, 20 ], 21 },
当css-loader读取到新的 css 时(此时可能这段新的 css 没有被 postcss-loader 处理过),配置了options.importLoaders属性后,会重新往回找options.importLoaders位,再依次往后重新 loader 一遍。
上面的例子是往回 1 位找postcss-loader。如果此时postcss-loader前还有other-loader,我们又希望它能够加载,那么可以填 2。
3.5 file-loader 打包图片
实际开发中,我们会将图片等资源放到一个叫assets的目录下。现在我们在src目录下创建assets目录,然后随便放一张图片进去。
随后在index.js中加入以下代码
js1import animal from './assets/animal.jpg' 2// 或 const animal= require('./assets/animal.jpg') 3 4function createImg() { 5 const img = new Image() 6 img.src = animal 7 document.body.append(img) 8} 9createImg()
然后下载
bash1yarn add file-loader --dev
配置:
js1module.exports = { 2 module: { 3 rules: [ 4 { 5 test: /\.css$/i, 6 use: [ 7 'style-loader', 8 { 9 loader: 'css-loader', 10 options: { 11 importLoaders: 1, // 期望往回加载的位数,1代表往回1位-也就是postcss-loader加载 12 esModule: false, // 这里需要关闭esModule 13 }, 14 }, 15 'postcss-loader', 16 ], //注意顺序,webpack从右到左读取loader 17 }, 18 // ...省略其他loader 19 { 20 test: /\.(png|jpe?g|gif)$/i, 21 use: [ 22 { 23 loader: 'file-loader', 24 options: { 25 esModule: false, //不转为 esModule 26 name: '[name].[hash:6].[ext]', //按照name+6位hash+扩展名的规则来命名 27 outputPath: 'image', //输出目录 28 }, 29 }, 30 ], 31 }, 32 ], 33 }, 34}
如果不配置options.esModule,则需要使用require().default或者仅使用import xx from 'xxx'语句。
除此之外,还需要在css-loader处关闭 esModule,这是因为类似于下面的代码,会被替换成require语法,替换成require语法后,需要用.default才能正常访问,这是不符合正常开发习惯的。
css1// index.css 2.div { 3 background: url('./assets/animal.jpg'); /*会被替换成require('./assets/animal.jpg') */ 4}
请在 index.css 上加入上面的代码,然后删除 css-loader 处的
esModule:false代码测试一下。
3.6 url-loader 打包图片
url-loader包含了file-loader的功能,此外它还可以将图片等资源打包成 base64 的形式,这样打包后的dist目录下就不会有对应的静态资源了,资源会转化成 base64 代码储存在打包后的bundle.js中。
好处是减少了静态资源的请求,坏处是静态资源越大,页面显示出来所需要的时间就越长。
我们开发时,一般都将体积小于 10kb 或者 20kb 的转成 base64。
请在assets目录下分别放置一张 20kb 以上和一张 20kb 以下的图片,然后在index.js中引入比较大的图片,在index.css中引入比较小的图片
js1// index.js 2import dp from './assets/dp.png' // 这是比较大的图片 3function createImg() { 4 const img = new Image() 5 img.src = dp 6 document.body.append(img) 7}
css1// index.css 2.div { 3 background: url('./assets/animal.jpg'); /* 这是比较小的图片 */ 4}
然后下载
bash1yarn add url-loader --dev
将原来的file-loader配置修改成以下:
js1 { 2 test: /\.(png|jpe?g|gif)$/i, 3 use: [ 4 { 5 loader: 'url-loader', 6 options: { 7 esModule: false, //不转为 esModule 8 name: '[name].[hash:6].[ext]', //按照name+6位hash+扩展名的规则来命名 9 outputPath: 'image', //输出目录 10 limit: 20 * 1024, // 限制20kb以下才打包成base64 11 }, 12 }, 13 ], 14 },
请删除原来的dist目录,然后重新打包,你会看到新生成的dist目录只有一个大的图片被打包了
这是因为我们的配置是将 20kb 以下的图片转成 base64 的代码,而 20kb 以上的依然打包到image目录下。
3.7 asset 模块打包静态资源
webpack5 内置了 asset 模块,它包含了file-loader和url-loader这两个旧模块的功能。
如果我们希望 asset 模块将所有静态资源以相同的命名规则打包到相同的目录下,则可以在output.assetModuleFilename中配置。
js1 output: { 2 ... 3 assetModuleFilename: 'asset/[name].[hash:6][ext]', //asset模块全局配置 4 },
由于静态资源的种类较多,包含图片、文件、字体等,所以一般不用全局配置。
如果我们希望有file-loader的功能,可以使用asset/resource
js1 module: { 2 rules: [ 3 ... 4 { 5 test: /\.(png|jpe?g|gif|svg)$/i, 6 type: 'asset/resource', 7 generator: { 8 filename: 'assets/[name].[hash:6][ext]', 9 }, 10 }, 11 ], 12 }
如果希望有url-loader的功能,可以使用asset/inline
js1 module: { 2 rules: [ 3 ... 4 { 5 test: /\.(png|jpe?g|gif|svg)$/i, 6 type: 'asset/inline', 7 }, 8 ], 9 }
如果希望混用,则直接使用asset
js1 module: { 2 rules: [ 3 ... 4 { 5 test: /\.(png|jpe?g|gif|svg)$/i, 6 type: 'asset', 7 generator: { 8 filename: 'assets/[name].[hash:6][ext]', //输出规则 9 }, 10 parser: { 11 dataUrlCondition: { 12 maxSize: 20 * 1024, // 小于20kb则解析成dataUrl 13 }, 14 }, 15 }, 16 ], 17 }
我在这里直接使用asset混用模式的配置来替换掉上面的url-loader模块。
请删除原来的 dist 目录再重新打包试一试。由于目前没配 plugin,所以只能手动清除原有的 dist 目录。
3.8 使用 plugin 在打包前删除 dist
注意:webpack5 内置了此功能。
在 webpack.config.js 中设置成
output.clean为true即可diff1 module.exports = { 2 ... 3 output: { 4 filename: '[name].bundle.js', 5 path: path.resolve(__dirname, 'dist'), 6+ clean: true, 7 }, 8 };
以下为旧方法:
——分割线——
webpack 在打包时并不会删除原有的dist目录,而是在其基础上替换内容,所以我们需要手动删除dist目录,下面介绍打包时自动帮我们先删除dist目录的plugin。
bash1yarn add clean-webpack-plugin --dev
使用:
js1const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 2const path = require('path'); 3module.exports = { 4 entry: './src/index.js', 5 output: { 6 path: path.join(__dirname, 'dist'), 7 ... 8 }, 9 mode: 'development', 10 module: { 11 ... 12 }, 13 plugins: [new CleanWebpackPlugin()],//插件会被当成类来用 14};
这里有一点需要注意:我们需要手动设置上output.path。
clean-webpack-plugin在执行时,会读取webpack.config.js中的output.path,如果没加上的话,会报错:
bash1clean-webpack-plugin: options.output.path not defined. Plugin disabled...
3.9 打包字体
assets/resource还可以用来打包字体,配置如下:
js1 module: { 2 rules: [ 3 ... 4 { 5 test: /\.(woff|woff2|eot|ttf|otf)$/i, 6 type: 'asset/resource', 7 generator: { 8 filename: 'fonts/[name].[hash:3][ext]', //输出规则 9 }, 10 }, 11 ], 12 }
3.10 html-webpack-plugin
这个插件用于生成index.html。
用它可以指定一个html模板,每次打包后,webpack 会用该模板新生成一个index.html,并且自动引入打包好的资源。
插件内置.ejs文件作为模板,我们也可以手动指定自己写好的模板,下面是使用自己创建的模板的示例:
在根目录的public目录下,创建了一个template.html的模板,内容如下:
html1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <link rel="icon" href="<%=BASE_URL %>favicon.ico" /> 8 <title><%= htmlWebpackPlugin.options.title %></title> 9 </head> 10 <body> 11 <div id="root"></div> 12 </body> 13</html>
下载
bash1yarn add html-webpack-plugin --dev
插件配置:
js1const { DefinePlugin } = require('webpack');//webpack自带的初始化插件 2const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 4module.exports = { 5 entry: './src/index.js', 6 output: { 7 ... 8 }, 9 mode: 'development', 10 module: { 11 rules: [ 12 ... 13 ], 14 }, 15 plugins: [ 16 new CleanWebpackPlugin(), 17 new HtmlWebpackPlugin({ 18 title: 'my APP', 19 template: './public/template.html', //指定index.html模板而不用插件内置的ejs模板 20 }), 21 new DefinePlugin({ 22 BASE_URL: '"./"', // 可以替换模板中的<%=BASE_URL %> 为”./“ 23 }), 24 ], 25};
执行yarn build后,请用 chrome 打开dist/index.html,会看到这个模板已经将打包好的资源都引入过来了。
这样就节省了我们手动引入资源到index.html下的繁琐环节了。
到这为止,我们就可以将根目录下的index.html删除掉了。
3.11 babel 的使用
为了让浏览器平台直接使用JSX、TS、es6等代码,在打包前,可以用 babel 插件转换一下语法。
下载
bash1yarn add @babel/core --dev // babel核心,需要跟各种插件配合 2yarn add @babel/preset-env --dev // babel预设的插件集合 3yarn add babel-loader --dev // babel的webpack loader
在项目src目录下,创建es6.js,内容如下:
js1export const a = () => { 2 const arr = [1, 2, 3] 3 const arr2 = [...arr] 4 const [a, ...rest] = arr2 5 console.log(a) 6 console.log(rest) 7}
然后在index.js中,引入并执行:
js1import { a } from './es6' 2a()
跟 postcss 一样,babel 中包含大量的插件,比较好的实践是我们单独在根目录下创建babel.config.js
js1// babel.config.js 2module.exports = { 3 presets: ['@babel/preset-env'], 4}
在webpack.config.js中配置:
js1// webpack.config.js 2 module: { 3 rules: [ 4 { 5 test: /\.js$/i, 6 use: ['babel-loader'], 7 }, 8 ], 9 }
构建后打开bundle.js,仔细翻一下,可以看到我们的代码被转译了:

3.12 copy-webpack-plugin
实际项目中,有时候我们并不希望资源被打包,而仅仅是拷贝到 dist 目录下就行。
比如,开发时会把要打包的静态资源放到src/assets中,一些直接拿来就可以用的静态资源则会放到public目录下,然后build时在public目录下的资源我们希望它们可以不经过loader处理,直接被拷贝至dist目录,这样发布线上后可以直接使用。
这样的需求下,我们可以使用copy-webpack-plugin来帮我们做这件事。
安装:
bash1yarn add copy-webpack-plugin --dev
在根目录下的public内放置一张图片或者其他静态资源,目前我的public目录结构是这样的:
js1. 2├── dp.png 3├── template.html
其中dp.png是我需要直接拷贝到dist目录内的静态资源,template.html是模板。
接着在index.js中把原来的createImage函数修改成这样:
js1function createImg() { 2 const img = new Image() 3 // 注意,这里不再使用require或者import语句,因为不需要经过打包,直接用拷贝到dist内的资源 4 img.src = './dp.png' 5 document.body.append(img) 6}
我们在引用该静态资源时,已经预先知道它会被拷贝到dist目录下了,所以可以直接用./而不是require或import。
实际开发中,我们有时候也会这样用静态资源,很多框架就提供了这样的功能,一些特别大的静态资源(例如地图)等,我们并不需要它们再被打包工具“翻译”一遍,而是直接拷贝到dist目录,在这种情况下,我们就能够直接使用./的形式引入这些静态资源。
接着补充配置
js1 plugins: [ 2 ... 3 new CopyWebpackPlugin({ 4 patterns: [ 5 { 6 from: 'public', 7 globOptions: { 8 ignore: ['**/template.html'], // 忽略public下的template.html 9 }, 10 }, 11 ], 12 }), 13 ],
当前public下的template.html会被html-webpack-plugin拷贝到dist目录下并被读写成index.html模板,所以我们在这里就忽略它。
毫无疑问,如果你的项目中不需要被打包的静态资源特别多,那使用这种拷贝的方式能够大大提高上线前构建的速度。
3.13 编译 TS
支持 TS 语法需要安装typescript和执行tsc --init
bash1yarn add typescript @types/react --dev 2yarn add ts-loader --dev 3tsc --init // 初始化`tsconfig.json`文件
在tsconfig.json文件中开启sourceMap以及开启 jsx 语法
json1"compilerOptions": { 2 "jsx": "react", 3 "sourceMap": true, 4 ... 5}
最后配置
javascript1// webpack.config.js 2module.exports = { 3 ... 4 module: { 5 rules: [ 6 ... 7 { 8 test: /\.tsx?$/i, 9 use: ['ts-loader'], 10 }, 11 ], 12 }, 13};
这里有个小插曲,我们已经在项目中使用了 babel,根据官方文档的一篇介绍,我觉得我们可以使用另一个 loader
Note that if you're already using
babel-loaderto transpile your code, you can use@babel/preset-typescriptand let Babel handle both your JavaScript and TypeScript files instead of using an additional loader. Keep in mind that, contrary tots-loader, the underlying@babel/plugin-transform-typescriptplugin does not perform any type checking.
注意如果你已经使用了
babel-loader去转译你的代码,你可以使用@babel/preset-typesctipt让babel去处理你的jvascript以及typesctipt代码来替代其他额外的 loader。请记住,与 ts-loader 不同,底层的 @babel/plugin-transform-typescript 插件不执行任何类型检查。
这种情况下需要下载@babel/preset-typescript
yarn add @babel/preset-typescript --dev
修改配置:
diff1// webpack.config.js 2module.exports = { 3 ... 4 module: { 5 rules: [ 6 ... 7 { 8 test: /\.tsx?$/i, 9- use: ['ts-loader'], 10+ use: ['babel-loader'], 11 }, 12 ], 13 }, 14};
添加babel的预设
diff1// babel.config.js 2module.exports = { 3 presets: [ 4 '@babel/preset-env', 5 '@babel/preset-react', 6+ '@babel/preset-typescript', 7 ], 8 plugins: [['react-refresh/babel']], 9};
3.14 打包 CSV、TSV 和 XML
webpack 内置支持 json,但如果项目要导入 csv、tsv、xml 等资源,需要另外安装 loader
bash1yarn add csv-loader xml-loader --dev
js1// webpack.config.js 2module.exports = { 3 ... 4 module: { 5 rules: [ 6 ... 7 { 8 test: /\.(csv|tsv)$/i, 9 use: ['csv-loader'], 10 }, 11 { 12 test: /\.xml$/i, 13 use: ['xml-loader'], 14 }, 15 ], 16 }, 17};
3.15 区分生产、打包环境
如何区分生产、打包环境其实官网上已经有详细的介绍了,简单来说,就是将webpack.config.js分成三个文件
diff1 webpack-demo 2 |- package.json 3 |- package-lock.json 4- |- webpack.config.js 5+ |- webpack.common.js 6+ |- webpack.dev.js 7+ |- webpack.prod.js
其中 dev.js 是存放开发环境要用的配置,prod.js 存放生产环境要用的配置,common.js 则存放两者都要用的配置。
比如,common.js 会放置入口、出口、必须的插件、必须的 loader 等
webpack.common.js
js1const path = require('path') 2const HtmlWebpackPlugin = require('html-webpack-plugin') 3 4module.exports = { 5 entry: { 6 app: './src/index.js', 7 }, 8 plugins: [ 9 new HtmlWebpackPlugin({ 10 title: 'Production', 11 }), 12 ], 13 output: { 14 filename: '[name].bundle.js', 15 path: path.resolve(__dirname, 'dist'), 16 clean: true, 17 }, 18}
dev.js则存放开发时要用的 devServer 之类的配置,mode是一定要写的(也可以通过脚本命令传递模式)
js1const { merge } = require('webpack-merge') 2const common = require('./webpack.common.js') 3 4module.exports = merge(common, { 5 mode: 'development', 6 devtool: 'inline-source-map', 7 devServer: { 8 static: './dist', 9 }, 10})
prod.js则存放生产环境要用的插件之类的
js1const { merge } = require('webpack-merge') 2const common = require('./webpack.common.js') 3 4module.exports = merge(common, { 5 mode: 'production', 6})
其中dev.js和prod.js是通过 webpack 提供的 merge 函数对 common.js 的配置进行合并。
最后在 package.json 中设置脚本命令对应不同的config.js即可
js1 "scripts": { 2 "build": "webpack --config ./webpack.prod.js", 3 "dev": "webpack serve --config ./webpack.dev.js" 4 },
区分生产、开发环境的难点在于需要根据环境理清每个配置项、插件、loader。
3.16 小结
在这个环节,我们利用webpack替我们打包了一些资源,由于篇幅原因,这里没办法具体讲到所有静态资源。
落实到实际开发也是如此。我们没办法考虑到所有因素,只能在遇到实际问题时再对症下药,去寻找网上提供的方案,再配置webpack帮助我们解决这些问题。
好在很多优秀的模板,例如next.js,umi.js等,得益于这些集成型前端框架的作者,或由于其高超的技术水平,或脱胎于大量实际场景,我们获得了配套的、成熟的 webpack 配置,开箱即用。
我们可以参考它们的配置来做细小的优化或者更多的扩展。
webpack除了拥有几乎所有类型的静态资源的打包能力,对前端开发来说,更重要的是它带来的不一样的开发体验。这对于从远古时期一路走来的前端 er 来说,是革命性的创举,同时也真正让前端拥有模块化开发的感受。
下面介绍 webpack 配套的开发工具。
四、开发工具
4.1 webpack-dev-server
实际开发中,我们都会用到webpack-dev-server这样的静态服务器协助开发,它的好处是能够自动监控文件的修改,而且不用打包就能够直接预览效果。
当前我们的方案是每次修改后执行 yarn build 打包然后用
live-server预览打包后的项目页面。它有以下缺点:
- 每次修改后都需要重新将所有的源码编译打包一次
- 每次编译成功后都需要进行文件读写,性能开销大
- 不能实现局部刷新
而 webpack 的webpack-dev-server能够提供热更新、无需打包即可预览的功能,完美解决上述的问题。
首先我们在 package.json 的script里创建脚本命令:
javascript1 "scripts": { 2 ... 3 "dev": "webpack serve" 4 },
然后安装
bash1yarn add -D webpack-dev-server
修改配置文件,告知 dev server,从什么位置查找文件:
diff1 module.exports = { 2 ... 3+ devServer: { 4+ static: './dist', 5+ }, 6 ... 7 };
以上配置告知 webpack-dev-server,将 dist 目录下的文件 serve 到 localhost:8080 下。
serve,将资源作为 server 的可访问文件
接着使用命令
bash1yarn dev
webpack-dev-server就启动好了
bash1<i> [webpack-dev-server] Project is running at: 2<i> [webpack-dev-server] Loopback: http://localhost:8080/ 3<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.199.229:8080/
接着我们进入http://localhost:8080/就可以预览效果。
此时可以删除dist目录,并且任意修改各个地方的源代码,修改后保存,webpack-dev-server会监听到文件有修改,于是会实时重新加载。
Webpack-dev-server默认会查找webpack.config.js。如果你的
webpack配置文件不是默认的webpack.config.js,假设这里叫wb.config.js那么 package.json 需要这么配javascript1"scripts": { 2 "build": "webpack --config wb.config.js", 3 "dev": "webpack serve --config wb.config.js" 4},
webpack-dev-server的热更新功能主要是将数据保存在缓存当中,每次启动后,都去缓存中更新数据,这样的好处是提高开发效率,减少文件读写,提升静态服务器的性能。
想要获得热更新功能,我们需要手动开启 HMR。
4.2 HMR 功能
HMR 是 hot module replacement 的简写,翻译过来就是模块热替换。
它允许在运行时更新所有类型的模块,而无需完全刷新。
当前的开发模式是组件化开发,当我们修改其中一个组件时,启动 HMR 功能则会让浏览器只对局部发生源代码改变的组件进行更新展示,不需要全部刷新一遍。
webpack5已经内置了这样的功能,我们只需要开启它就行
diff1 module.exports = { 2 ... 3 devServer: { 4 static: './dist', 5+ hot: true, 6 }, 7 ... 8 };
然后我们需要在入口文件(我这里是src/index.js)内写上需要热更新模块的代码
diff1// src/index.js 2 3/* ...省略其他代码...*/ 4+if (module.hot) { 5+ module.hot.accept(['./es6.js'], function () { 6+ console.log('Accepting the updated printMe module!'); 7+ }); 8+}
在上面的代码中,数组的每个项是需要热更新文件的路径。当这些文件的源码修改后,就会触发局部更新,然后执行callback。
当前我的项目中es6.js文件修改了任何代码都只会局部刷新,并且热替换完成后会打印Accepting the updated printMe module!
4.3 打包 React 组件 jsx
打包 jsx 的代码依然需要用到 babel,我们需要安装以下插件(其中章节【babel 的使用】已经用了前三个 ):
bash1yarn add @babel/core --dev // babel核心,需要跟各种插件配合 2yarn add @babel/preset-env --dev // babel预设的插件集合 3yarn add babel-loader --dev // babel的webpack loader 4yarn add @babel/preset-react --dev
接着安装react
bash1yarn add react react-dom
然后在index.js中添加如下代码
javascript1import App from './App.jsx' 2import React, { StrictMode } from 'react' 3import ReactDOM from 'react-dom' 4 5const rootElement = document.getElementById('root') 6ReactDOM.render( 7 <StrictMode> 8 <App /> 9 </StrictMode>, 10 rootElement, 11)
接着新建App.jsx,内容如下:
jsx1import React from 'react' 2 3const App = () => { 4 return <div>hello world</div> 5} 6 7export default App
下面修改配置:
diff1// webpack.config.js 2 module: { 3 rules: [ 4 ... 5- { 6- test: /\.js$/i, 7- use: ['babel-loader'], 8- }, 9+ { 10+ test: /\.jsx?$/i, 11+ use: ['babel-loader'], 12+ }, 13 ], 14 }
最后在 babel.config.js 中添加@babel/preset-react即可
javascript1module.exports = { 2 presets: ['@babel/preset-env', '@babel/preset-react'], 3}
4.4 React 组件热更新
下面实现 React 组件热更新的功能
安装
javascript1yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh --dev
在webpack.config.js中添加插件
diff1const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 2 3module.exports = { 4 entry: './src/index.js', 5 output: { 6 ... 7 }, 8 mode: 'development', 9 module: { 10 rules: [ 11 ... 12 ], 13 }, 14 plugins: [ 15 new CleanWebpackPlugin(), 16 new HtmlWebpackPlugin({ 17 title: 'my APP', 18 template: './public/template.html', //指定index.html模板而不用插件内置的ejs模板 19 }), 20 new DefinePlugin({ 21 BASE_URL: '"./"', // 可以替换模板中的<%=BASE_URL %> 为”./“ 22 }), 23+ new ReactRefreshWebpackPlugin(), 24 ], 25};
当前是启动babel插件来loaderReact 组件的,我们还需要到babel.config.js中添加插件
diff1// babel.config.js 2module.exports = { 3 presets: ['@babel/preset-env', '@babel/preset-react'], 4+ plugins: [['react-refresh/babel']], 5};
下面测试一下:
在src目录下新建一个Hello.jsx,内容如下:
jsx1import React from 'react' 2 3const Hello = () => { 4 return ( 5 <section> 6 <input type="text" /> 7 </section> 8 ) 9} 10 11export default Hello
然后到App.jsx中使用这个组件
diff1import React from 'react'; 2+ import Hello from './Hello.jsx'; 3 4const App = () => { 5 return ( 6 <div> 7 hello231 8+ <Hello /> 9 </div> 10 ); 11}; 12 13export default App;
打开localhost:8080,然后在输入框中输入任意内容:

为了测试组件是否支持热更新,我们在App组件中任意修改内容,这里就把hello231修改成hello i am qiuyanxi。
按照我们的设想,App 组件的修改不会影响到Hello组件,所以原来在输入框中输入的内容依然会存在。
保存后,查看一下页面,成功了

查看chrome控制台提示
bash1[HMR] Updated modules: 2[HMR] - ./src/App.jsx 3[HMR] App is up to date.
4.5 Vue 组件热更新
Vue组件需要用Vue-loader加载
bash1yarn add vue-loader --dev
相对于 React 来说,Vue 的配置简单很多。Vue 组件默认支持 HMR 功能,因此我们不需要额外配置。
diff1// webpack.config.js 2const VueLoaderPlugin = require('vue-loader/lib/plugin'); 3module.exports={ 4 ... 5 module: { 6 rules: [ 7 ... 8+ { 9+ test: /\.vue$/i, 10+ use: ['vue-loader'], 11+ }, 12 ], 13 } 14 plugins: [ 15 ... 16+ new VueLoaderPlugin(), 17 ], 18}
4.6 output.publicPath
在打包时,output.publicPath属性影响打包后的index.html内部的引用路径。
当不设置或者设置成空字符串时,打包后的资源会通过origin+/+output.filename来获取资源
举个例子:
当前我的设置如下:
javascript1module.exports = { 2 output: { 3 filename: 'bundle.js', 4 path: path.join(__dirname, 'dist'), 5 publicPath: '', // 也可以不设置,默认为空字符串 6 }, 7}
那么打包后的脚本被这样引用
html1<script defer src="bundle.js"></script>
当我们开启http-server去访问本地http://127.0.0.0时,会自动访问到http://127.0.0.0/bundle.js
浏览器会自动帮我们加上/。
为了保险起见,我们将output.publicPath设置成/
diff1module.exports={ 2 output: { 3 filename: 'bundle.js', 4 path: path.join(__dirname, 'dist'), 5+ publicPath: '/', 6 }, 7}
这样打包后的脚本引用方式就变成这样
html1<script defer src="/bundle.js"></script>
相当于我们手动加上了/。
原来版本不知道是 webpack 的 bug 还是浏览器的 bug,加上/后可能导致 build 后的资源无法在本地被访问,在笔者写这篇博客时,已经没有这个问题了。——本地静态服务器用的是 http-server
4.7 devServer 常用配置
devServer.hot:开启 HMR 时,我们将其设置成了 true,但是更好的做法是设置成only,因为在构建失败时不刷新页面作为回退。举个例子,当你在某个组件上写错代码时,设置成only不会自动重新刷新全部的组件。
devServer.open:设置成true后,启动 server 服务时,自动打开浏览器
devServer.port:设置端口号
devServer.historyApiFallback:使用 HTML5 History API 时,可能必须提供 index.html 页面来代替任何 404 响应。
js1 devServer: { 2 static: './dist', 3 hot: 'only', // 构建失败时不刷新页面作为回退 4 open: true, // 自动打开浏览器 5 port: 8888, // 端口号 6 compress: true, // 自动压缩 7 historyApiFallback: true, 8 },
比较值得说的是devServer.historyApiFallback,下面我们创建目录src/components,再在里面新建几个两个 React 组件,如下:
bash1components 2├── About.jsx 3└── Home.jsx
内容如下:
jsx1// Home.jsx 2import React from 'react' 3 4const Home = () => { 5 return <div>Home</div> 6} 7 8export default Home
jsx1// About.jsx 2import React from 'react' 3 4const About = () => { 5 return <div>about</div> 6} 7 8export default About
再修改App.jsx的源代码
diff1import React from 'react'; 2import Hello from './Hello.jsx'; 3+ import Home from './components/Home.jsx'; 4+ import About from './components/About.jsx'; 5+ import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 6 7const App = () => { 8 return ( 9 <div> 10 hello i am qiuyanxi 11 <Hello /> 12+ <BrowserRouter> 13+ <Link to='/home'>Home</Link> 14+ <br /> 15+ <Link to='/about'>About</Link> 16+ <Routes> 17+ <Route path='/home' element={<Home />} /> 18+ <Route path='about' element={<About />} /> 19+ </Routes> 20+ </BrowserRouter> 21 </div> 22 ); 23}; 24 25export default App;
现在我们进入页面http://localhost:8888/about,刷新一下。
如果没有配置historyApiFallback,则会提示 404。因为此时类似于向后端请求接口,而/about是前端路由,所以报 404 了。
如果配置了historyApiFallback,则会出现正确的页面。
4.8 proxy 设置
在我们开发过程中,请求后端接口时,经常遇到跨域问题。
此时就需要使用代理转发一下请求。
配置如下:
diff1 devServer: { 2 ... 3+ proxy: { 4+ '/api': { 5+ target: 'https://api.github.com', 6+ pathRewrite: { '^/api': '' }, // 把/api重写为空 7+ changeOrigin: true, // 修改主机来源,一般情况下不需要设置 8+ }, 9+ }, 10 },
举个例子,现在有个现成的 github 接口:
js1https://api.github.com/users
我在本地通过设置好上面的代理后,再通过axios访问
js1axios.get('/api/users').then((res) => { 2 console.log('res.data:', res.data) 3})
如果没有写pathRewrite,就将请求转发到了https://api.github.com/api/users上。
如果不希望传递/api,则需要通过pathRewrite重写路径。重写后转发到https://api.github.com/users
默认情况下,代理时会保留主机头的来源。
https://api.github.com/users这个接口通过判断来源来响应数据,我们可以通过设置changeOrigin: true来绕过 github 的判断。一般开发时并不需要这样设置
4.9 resolve 解析规则
webpack 内部有自己的一套解析规则,我们也可以通过resolve设置来修改它。
常见的有
-
alias:设置别名js1 alias: { 2 '@': path.resolve(__dirname, 'src'), 3 },设置以上的别名后,可以通过
import xxx from '@/xx'来引入根目录下src目录的 xx 文件。 -
enforceExtension:是否允许导入时有扩展名。 -
extensions:尝试按顺序解析后缀名。如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀。举个例子,当前配置如下:
js1 resolve: { 2 extensions: ['.js', '.jsx', '.tsx'], 3 },当我修改如下代码后:
diff1- import Home from './components/Home.jsx 2// 修改成 3+ import Home from './components/HomeWebpack会按照extensions属性自动加上后缀名。如果加上后缀名后依然没有找到文件,就会报错。 -
mainFiles:解析目录时要使用的文件名。假设当前有个
components目录,我在某个地方这样import:js1import xx from './components'如果此时
resolve设置为mainFiles: ['index'],则会引入components目录下的index文件。
4.10 source map
顾名思义,source map 是源代码映射。
如果没有配置source map,那浏览器的报错信息显示将会是打包后的对应位置
比如我在index.js内写这样一段代码
js1console.log(abc)
在没有声明的情况下,浏览器会报错

此时定位的代码错误显示是在打包后的bundle.js中。这对于程序员来说基本无用。
程序员希望得到的信息是:在源代码中的哪一个文件哪一行出问题了。
于是source map出场了,它可以定位到源代码中的信息。
在webpack.config.js中添加配置:
diff1module.exports = { 2 ... 3 mode: 'development', 4+ devtool: 'source-map', 5}
此时yarn dev后就可以看到源代码中的错误信息了

它的原理是,在打包后,生成一份map映射文件,它能够体现bundle.js和源代码的映射关系。
你可以通过yarn build后看到它。
五、如何优化
HTTP 优化有两个大方向:
- 减少请求次数
- 减少单次请求花费的时间
一个网站可能有非常多的静态资源、JS 文件,如果能够减少静态资源的体积,合并某些静态资源(比如很多个 css 样式表),那么就能够对 HTTP 进行优化。
对资源的压缩和合并,我们可以利用打包工具来完成。目前打包工具主流还是使用 webpack,通过合理优化 webpack 的配置,能够让资源的压缩与合并达到理想的效果。
优化 webpack 的思路也是两个方向:
- 更快(提高构建速度,减少花费时间)
- 更小(减少打包体积)
——如何优化打包速度——
打包速度影响到的是开发过程中的热更新速度以及上线前的构建速度。
通过以下方式我们可以优化打包速度:
mode 属性
webpack 内部对production或者development有做优化,所以针对开发和生产环境我们需要配置不同的mode。
resolve 配置
通过resolve解析规则,我们可以手动控制webpack的查找规则,除了对开发友好外,相当于显式告诉webpack利用resolve中的配置规则查找文件,合理的配置会提高webpack查找文件的效率。
alias 设置别名
通过alisa设置别名可以让webpack通过规则项从上到下查找文件,而不是递归查找。
javascript1 alias: { 2 '@': path.resolve(process.cwd(), 'src'), 3 },
通过上面的别名设置,除了让我们开发时可以通过import xx from '@/xxx' 引用src目录下的内容以外,还对webpack的查找规则非常友好——webpack知道可以src目录从上到下查找文件,而不是通过相对路径递归向上查找文件。
extensions 高频扩展名前置
通过设置extensions可以在引入时不写扩展名。
javascript1 resolve: { 2 extensions: ['.js', '.jsx', '.tsx'], 3 },
webpack 会从前到后遍历extensions属性来匹配是否有对应扩展名的文件,一些高频的后缀放在前面可以提高 webpack 搜索的速度
modules告诉 webpack 解析模块时应该搜索的目录
javascript1const path = require('path') 2 3module.exports = { 4 //... 5 resolve: { 6 modules: [path.resolve(__dirname, 'src'), 'node_modules'], 7 }, 8}
上面的代码将告诉webpack搜索src目录和node_modules目录,src目录优先搜索。
这样有助于加快搜索时间
cache 属性
javascript1module.exports = { 2 //... 3 cache: { 4 type: 'filesystem', 5 }, 6}
通过设置 cache 属性为文件系统缓存生成的 webpack 模块和 chunk,来改善构建速度。
thread-loader
在耗时的操作中使用此 loader 可以生成独立的 worker 池。每个 worker 都是一个独立的 node.js 进程。
相当于开启了多进程来处理耗时慢的loader,这样就达到了多loader同时处理的效果。
下面是官方文档的示例:
javascript1module.exports = { 2 module: { 3 rules: [ 4 { 5 test: /\.js$/, 6 include: path.resolve('src'), 7 use: [ 8 'thread-loader', 9 // 耗时的 loader (例如 babel-loader) 10 ], 11 }, 12 ], 13 }, 14}
指定 include 或 exclude
最常见的方式是通过include或exclude属性来帮我们避免不必要的转译,以 babel-loader 为例
javascript1module: { 2 rules: [ 3 { 4 test: /\.js$/, 5 exclude: /(node_modules|bower_components)/, 6 use: { 7 loader: 'babel-loader', 8 options: { 9 presets: ['@babel/preset-env'], 10 }, 11 }, 12 }, 13 ] 14}
上面的属性表示 babel 无需对 node_modules 文件夹或者 bower_components 文件夹做任何处理。
开启 loader 的 cache 功能
有些 loader 支持缓存功能,比如说 babel-loader 可以配置cacheDirectory属性,这个功能开启后能够用指定目录来缓存 loader 的执行结果。这样当 webpack 再次构建时,能够读取缓存来避免每次执行时,可能产生的高消耗的编译过程。
- DLLPlugin ❌
DLLPlugin 在 webpack5 中已经不用了,害我配了半天。
- happypack ❌
这个功能跟 thread-loader 差不多,都是开启多进程,webpack5 已经不需要了。
——如何缩小打包体积——
缩小打包体积的思路有利用一些 plugin 来缩小代码量,或者利用 webpack 的 Tree-shaking 功能来删除没用过的代码。
mode 属性
使用 mode 为 "production" 的配置项以启用更多优化项,包括压缩代码与 tree shaking。
开启压缩
通过optimization属性开启压缩功能。告知 webpack 使用 TerserPlugin 或其它在 optimization.minimizer定义的插件压缩 bundle。
javascript1module.exports = { 2 //... 3 optimization: { 4 minimize: true, 5 }, 6}
压缩 JS
使用 webpack5 开箱即用的插件TerserWebpackPlugin来压缩 JavaScript,只需要将插件添加到 webpack配置文件中即可。
javascript1const TerserPlugin = require('terser-webpack-plugin') 2 3module.exports = { 4 optimization: { 5 minimize: true, //记得开启压缩功能 6 minimizer: [new TerserPlugin()], 7 }, 8}
压缩 css
Optimize CSS Assets Webpack Plugin
使用方式参照官方文档
javascript1var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 2module.exports = { 3module: { 4... 5}, 6plugins: [ 7 new OptimizeCssAssetsPlugin({ 8 assetNameRegExp: /\.optimize\.css$/g, 9 cssProcessor: require('cssnano'), 10 cssProcessorPluginOptions: { 11 preset: ['default', { discardComments: { removeAll: true } }], 12 }, 13 canPrint: true 14 }) 15] 16};❌ 这个插件已经过时了
webpack5 推荐使用 CssMinimizerWebpackPlugin 插件,它在 source maps 和 assets 中使用查询字符串会更加准确,支持缓存和并发模式下运行。
安装
javascript1$ npm install css-minimizer-webpack-plugin --save-dev
webpack.config.js
javascript1const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') 3 4module.exports = { 5 module: { 6 rules: [ 7 { 8 test: /.s?css$/, 9 use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 10 }, 11 ], 12 }, 13 optimization: { 14 minimizer: [ 15 // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释 16 // `...`, 17 new CssMinimizerPlugin(), 18 ], 19 }, 20 plugins: [new MiniCssExtractPlugin()], 21}
Tree Shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。
webpack内置这个功能,只需要通过mode:"production"来开启就行。
最后
webpack 生态太庞大了,而且变更非常快。
不单单是生态、插件、配置项的变化,包括优化方案变化都非常快。
本篇博客作为入门,只能提供一个构思,尽可能结合实际开发来解释webpack对我们的作用。
如果你详细看完了本篇博客,就可以去啃一啃webpack官方文档的配置说明了。
最后建议大家 ⭐️使用 vite⭐️,手动狗头
新年快乐,我们下期再见 👋🏻👋🏻