谈谈你对webpack的理解
webpack主要是用来解决模块化
打包问题的。
什么是模块
将某一个复杂的项目按照某种规则或者规范划分为多个文件,每个文件就是一个模块。模块内部是数据是私有的。
模块化实现历程
- 通过
script
标签引入js文件 - 在前者的基础上,使用
命名空间
的方式,每个模块只暴露一个对象。 - 在前者的基础上,使用立即执行函数
早期模块化的方式中,每个能实现某些功能js文件被设计为一个单独的模块,然后通过script标签
引入
1 | <script src="module-a.js"></script> |
这种方式的缺点很明显,被引入后,模块中的变量都成为全局变量
,存在变量污染
问题,而且模块之间没有依赖关系
。
随后,就出现了命名空间
方式,规定每个模块只
暴露一个全局对象,然后模块的内容都挂载到这个对象中。
1 | //moduleA.js |
这样在很大程度上解决了全局变量污染
的问题,但是没有解决依赖混乱
的问题,而且不安全,模块内部的数据可以被随意修改
。
后来又选择用立即执行函数
为模块添加私有空间
, 解决了内部数据可以被随意修改的问题。
1 | //moduleA.js |
支持传入参数,能在一定程度上解决模块依赖问题,但是必须注意引入模块的先后顺序
,否则就会出现undefined
的问题。
理想的解决方式是,在页面中通过script标签引入一个JS入口文件
,其余用到的模块可以通过代码控制
,按需加载进来。
除了模块加载的问题以外,还需要规定模块化的规范,如今流行的则是CommonJS
、ES Modules
,关于二者的详细介绍参考本博客内的
我们上述讨论的模块化的范围只限于js
文件,后来html,css等文件也可以被模块化,这就需要借助webpack
。
模块化的好处
解决了全局变量污染的问题
提高了代码的可维护性与复用性
使得项目中文件的依赖关系明确,支持按需加载。
什么是webpack
用于现代JavaScript
应用程序的静态模块打包工具。
webpack的构建流程
初始化阶段
合并配置文件
和shell语句
[Shell 语句指的是在命令行界面(CLI)或脚本中使用的指令]中的配置参数,得到最终的配置对象options
完成上述步骤之后,创建,并根据options对象初始化Compiler
对象,该对象掌控者webpack
生命周期,不执行具体的任务,只是进行一些调度工作
。
初始化插件(执行 new MyPlugin()
并调用插件的 apply
方法,注册回调函数)
1 | class Compiler extends Tapable { |
简单来说就做了这些事
- 得到options配置对象
- 创建,初始化compiler对象
- 初始化插件(plugins)
编译阶段
Compiler
初始化完成后会调用Compiler
的run
方法来真正启动webpack
编译构建流程,主要流程如下:
compile
开始编译- 从入口文件开始,使用配置的
loader
转换文件,构建模块,并分析模块的依赖关系,递归构建模块。 build-module
构建模块seal
封装构建结果emit
把各个chunk输出到结果文件
compile 编译
执行了run
方法后,首先会触发compile
,主要是构建一个Compilation
对象
该对象是编译阶段的主要执行者,主要会依次执行下述流程:执行模块创建
、依赖收集
、分块、打包等主要任务。
make 编译模块
当创建了上述的compilation
对象后,就开始从Entry
入口文件开始读取,主要执行_addModuleChain()
函数,如下:
1 | _addModuleChain(context, dependency, onModule, callback) { |
build module 完成模块编译
对应上述代码中的this.buildModule()
这里主要调用配置的loaders
,将我们的模块转成标准的JS
模块
模块构建完成后,开始分析模块的依赖关系,对应上述代码中的 this.processModuleDependencies
。
从配置的入口模块开始,分析其 AST
,当遇到require
等导入其它模块的语句时,便将其加入到依赖的模块列表,如果有需要,则递归构建依赖的模块。
webpack是先使用loader
处理入口文件
,构建好模块后,进行依赖分析
,发现其他模块,然后才对其他模块递归使用loader构建模块,再进行依赖分析,以此类推,就能构建完所有模块,并构建好模块依赖图。
打包并输出
seal 输出资源
seal
方法主要任务是生成chunks
,对chunks
进行一系列的优化操作
,并生成要输出的代码
根据入口文件和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
(如果只有一个入口文件,一般只有一个chunk),再把每个 Chunk
转换成一个单独的文件。
chunk
webpack
中的 chunk
,可以理解为配置在 entry
(入口文件) 中的模块,或者是动态引入的模块。
每个 chunk 可以包含一个或多个模块
,并且可以每个chunk可以被单独加载
。
入口点(Entry Points):每个入口点都会创建一个初始 chunk。
动态导入(Dynamic Imports):
import()
动态加载模块时,会创建异步 chunks,它们是按需加载
的。代码分割(Code Splitting):开发者可以通过配置让 Webpack 根据某些规则
自动分割代码
到不同的 chunks 中。
emit 输出完成
在确定好输出内容后,根据配置确定输出的路径
和文件名
1 | output: { |
在 Compiler
开始生成文件前,钩子 compiler.hooks.emit
会被执行,这是我们修改最终文件的最后一个机会
从而webpack
整个打包过程则结束了
小结

说说webpack中常见的Loader?解决了什么问题?
是什么
loader
本质是一个函数,用于对文件
的源代码
进行转换,使之变为webpack可用的模块
,在 import
或加载
模块时预处理文件
webpack
做的事情,仅仅是分析出各种模块的依赖关系
,然后形成资源列表,最终打包生成到指定的文件中。如下图所示:

在webpack
内部中,任何文件
都是模块
,不仅仅只是js
文件,这得益于loader扩大了模块化的范围
默认情况下,在遇到import
或者require
加载模块的时候,webpack
只支持对js
和 json
文件打包
像css
、sass
、png
等这些类型的文件的时候,webpack
则无能为力,这时候就需要配置对应的loader
进行文件内容的解析
配置方式
推荐在配置文件中配置,rules
是一个数组,意味着我们可以给多种文件配置loader
,每一类文件对应一个对象。
use
也是一个数组,这意味着我们可以对任意一种文件使用多个loader
,每个loader
是一个对象的格式,loader
是支持链式调用
的,调用的顺序是从右至左
的。
1 | module.exports = { |
常见的loader
css-loader
分析 css
模块之间的关系,并合成⼀个 css
1 | npm install --save-dev css-loader |
如果只通过css-loader
加载文件,这时候页面代码设置的样式并没有生效
原因在于,css-loader
只是负责将.css
文件进行一个解析,而并不会将解析后的css
插入到页面中
如果我们希望再完成插入style
的操作,那么我们还需要另外一个loader
,就是style-loader
style-loader
把 css-loader
生成的内容,用 style
标签挂载到页面的 head
中,简单来说就是把css-loader生成的css代码内联到html文件中。
1 | npm install --save-dev style-loader |
1 | rules: [ |
less-loader
开发中,我们也常常会使用less
、sass
、stylus
预处理器编写css
样式,使开发效率提高,这里需要使用less-loader
1 | npm install less-loader -D |
1 | rules: [ |
编写loader
在编写 loader
前,我们首先需要了解 loader
的本质
其本质为函数,函数中的 this
作为上下文会被 webpack
填充,指向 webpack
提供的对象,能够获取当前 loader
所需要的各种信息,因此我们不能将 loader
设为一个箭头函数
函数接受一个参数source,为 webpack
传递给 loader
的文件源内容
函数中有异步操作或同步操作,异步操作通过 this.callback
返回,返回值要求为 string
或者 Buffer
代码如下所示:
1 | // 导出一个函数,source为webpack传递给loader的文件源内容 |
1 | module.exports = function(source, inputSourceMap) { |
一般在编写loader
的过程中,保持功能单一,避免做多种功能
如less
文件转换成 css
文件也不是一步到位,而是 less-loader
、css-loader
、style-loader
几个 loader
的链式调用才能完成转换
说说webpack中常见的Plugin?解决了什么问题?
是什么
Plugin
(Plug-in)是一种计算机应用程序,它和主应用程序
互相交互,以提供特定的功能
是一种遵循一定规范的应用程序接口编写出来的程序,只能运行在程序规定的系统下,因为其需要调用原纯净系统
提供的函数库
或者数据
webpack
中的plugin
也是如此,plugin
赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 webpack
的不同阶段(钩子 / 生命周期),贯穿了webpack
整个编译周期
主要用来解决loader
无法解决的其他事情,本质是一个具有apply
方法的js对象
(区别于vue的插件本质是一个具有install
方法的对象),这个方法会被compiler
对象调用。webpack构建过程中会广播
很多事件,plugin可以监听
自己感兴趣的事件,从而改变最后的打包结果。
配置方式
这里讲述文件的配置方式,一般情况,通过配置文件导出对象中plugins
属性传入new
实例对象。如下所示:
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装 |
特性
插件本质是一个类,插件实例其本质是一个具有apply
方法javascript
对象
apply方法被调用的时候会传入compiler
对象
1 | const pluginName = 'ConsoleLogOnBuildWebpackPlugin'; |
tap
方法是用来注册一个函数,当某个特定的钩子被触发时,这个函数就会被执行。你可以把它看作是一种订阅模式,你的插件“订阅”了特定事件,并提供了一个回调函数,在该事件发生时执行。
compiler hook
的 tap
方法的第一个参数,应是驼峰式命名的插件名称
关于整个编译生命周期钩子
(hooks),有如下:
- entry-option :初始化 option
- run:会在 Webpack 开始编译之前触发
- compile: 真正开始的编译,在创建 compilation 对象之前
- compilation :生成好了 compilation 对象
- make:从 entry 开始递归分析依赖,准备对每个模块进行 build
- after-compile: 编译 build 过程结束
- emit :在将内存中 assets 内容写到磁盘文件夹之前
- after-emit :在将内存中 assets 内容写到磁盘文件夹之后
- done: 完成所有的编译过程
- failed: 编译失败的时候
常见的plugin
html-webpack-plugin:
在打包结束后,自动生成⼀个
html
文件,并自动引入
打包后的js,css文件(自动注入到head标签中)。1
2
3
4
5
6
7
8
9
10const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: "My App",
filename: "app.html",
template: "./src/html/index.html"
})
]
};mini-css-extract-plugin:
提取
CSS
代码到一个单独的文件中,通常用来代替style-loader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/,
use: [MiniCssExtractPlugin.loader,'css-loader','sass-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'//放置提取出的css代码的css文件的文件名
})
]
}
更多的常见plugin可参考:webpack基础 | 三叶的博客
编写plugin
由于webpack
基于发布订阅模式,在整个编译周期
中会广播
出许多事件,插件通过监听感兴趣的事件,并调用webpack提供的api
,就可以在特定的阶段执行自己的插件任务。
在之前也了解过,webpack
编译会创建两个核心对象:
compiler
:包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin,和 webpack 整个生命周期相关的钩子compilation
:作为 plugin内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的 Compilation 将被创建
如果自己要实现plugin
,也需要遵循一定的规范:
- 插件必须是一个
函数
或者是一个包含apply
方法的对象,这样才能访问compiler
实例 - 传给每个插件的
compiler
和compilation
对象都是同一个引用,因此不建议修改 - 异步的事件需要在插件处理完任务时,调用回调函数通知
Webpack
进入下一个流程,不然会卡住
实现plugin
的模板如下:
1 | class MyPlugin { |
emit
钩子:是一个特殊的钩子,它在 Webpack 准备好要输出所有资源文件到磁盘之前触发,允许插件作者在这个关键时刻介入处理或修改即将输出的内容。
tap
方法:用来注册一个回调函数到特定的 Webpack 钩子上,使得当这个钩子被触发时,能够执行你的自定义逻辑。
说说Loader和Plugin的区别?
前面两节我们有提到Loader
与Plugin
对应的概念,先来回顾下
loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事,
比如提取css代码到一个单独的文件。
从整个运行时机上来看,如下图所示:

可以看到,两者在运行时机
上的区别:
- loader 运行在打包文件之前
- plugins 在
整个编译周期
都起作用
在Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack
提供的 API
改变输出结果
对于loader
,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将A.scss
或A.less
转变为B.css
,单纯的文件转换过程。
webpack类似的工具还有哪些?区别?
模块化是一种处理复杂系统分解为更好的可管理模块的方式
每个模块完成一个特定的子功能,所有的模块按某种方法组装起来,成为一个整体(bundle
)
在前端领域中,并非只有webpack
这一款优秀的模块打包工具,还有其他类似的工具,例如Rollup
、Parcel
、snowpack
,以及最近风头无两的Vite
这里没有提及gulp
、grunt
是因为它们只是定义为构建工具
,不能类比,关于gulp
的介绍可参考hexo博客搭建的一些思考 | 三叶的博客
Rollup

Rollup
是一款 ES Modules
打包器,从作用上来看,Rollup
与 Webpack
非常类似。不过相比于 Webpack
,Rollup
要小巧的多
现在很多我们熟知的库都都使用它进行打包,比如:Vue
、React
和three.js
等
举个例子:
1 | // ./src/messages.js |
1 | // ./src/logger.js |
1 | // ./src/index.js |
然后通过rollup
进行打包,把index.js
文件和它依赖的模块打包成一个chunk
,结果如下:
1 | const log = msg => { |
可以看到,代码非常简洁,完成不像webpack
那样存在大量引导代码和模块函数
并且error
方法由于没有被使用,输出的结果中并无error
方法,可以看到,rollup
默认使用Tree-shaking
优化输出结果
因此,可以看到Rollup
的优点:
- 打包后的代码更简洁、打包效率更高
- 默认支持 Tree-shaking
但缺点也十分明显,不能处理其他类型的资源文件
和 CommonJS
模块,又或是编译 ES
新特性,这些额外的需求 ,Rollup
需要使用插件去完成。
综合来看,rollup
并不适合开发应用,因为需要使用第三方模块,而目前第三方模块大多数使用CommonJs
方式导出成员,并且rollup
不支持HMR
,使开发效率降低(所以vite只在生产打包的时候使用rollup)
但是在用于打包JavaScript
库时,rollup
比 webpack
更有优势,因为其打包出来的代码更小、速度更快,其存在的缺点可以忽略。
Parcel

Parcel ,是一款完全零配置
的前端打包器,它提供了 “傻瓜式” 的使用体验,只需了解简单的命令,就能构建前端应用程序。
Parcel
跟 Webpack
一样都支持以任意类型文件
作为打包入口,但建议使用HTML
文件作为入口
1 | <!-- ./src/index.html --> |
main.js文件通过ES Moudle
方法导入其他模块成员
1 | // ./src/logger.js |
1 | // ./src/main.js |
运行之后,使用命令打包
1 | npx parcel src/index.html |
执行命令后,Parcel
不仅打包了应用,同时也启动了一个开发服务器
,跟webpack Dev Server
一样
跟webpack
类似,也支持模块热替换(HMR)
,但用法更简单
同时,Parcel
有个十分好用的功能:支持自动安装依赖
,像webpack
开发阶段突然需要安装某个第三方依赖,必然会终止dev server
然后安装再启动。而Parcel
则免了这繁琐的工作流程。
同时,Parcel
能够零配置加载其他类型的资源文件,无须像webpack
那样配置对应的loader
由于打包过程是多进程
同时工作,构建速度会比Webpack
快,输出文件也会被压缩
,并且样式代码也会被单独提取到单个文件
中
可以感受到,Parcel
给开发者一种很大的自由度,只管去实现业务代码,其他事情用Parcel
解决
Snowpack

Snowpack,是一种闪电般快速
的前端构建工具,专为现代Web
设计,较复杂的打包工具(如Webpack
或Parcel
)的替代方案,利用JavaScript
的本机模块系统,避免不必要的工作并保持流畅的开发体验。
开发阶段,每次保存单个文件
时,Webpack
和Parcel
都需要重新构建
和重新打包
应用程序的整个bundle
,这个过程包括:重新解析依赖关系,重新优化和压缩,重新生成资源文件。而Snowpack
为你的应用程序每个文件
构建一次,就可以永久缓存,文件更改时,Snowpack
只会重新构建该单个文件
。
Vite

vite ,是一种新型前端构建工具,能够显著提升前端开发体验
它主要由两部分组成:
- 一个开发服务器,它基于原生ES 模块提供了丰富的内建功能,如速度快到惊人的模块热更新HMR
- 一套
构建指令
,它使用Rollup
打包你的代码,并且它是预配置的,可以输出用于生产环境的优化过的静态资源
其作用类似webpack
+ webpack-dev-server
,其特点如下:
- 快速的冷启动
- 即时的模块热更新
- 真正的
按需编译
vite
会直接启动开发服务器,不需要进行打包操作,因此启动速度非常快
利用现代浏览器支持ES Module
的特性,当浏览器请求某个模块
的时候,再根据需要对模块的内容进行编译,这种方式大大缩短了编译时间。
原理图如下所示:

在热模块HMR
方面,当修改一个模块的时候,仅需让浏览器重新请求该模块即可,无须像webpack
那样需要把该模块的相关依赖模块全部编译一次,效率更高。
简述
Webpack
相比上述的模块化工具,webpack
大而全,很多常用的功能做到开箱即用。有两大最核心的特点:一切皆模块和按需加载
与其他构建工具相比,有如下优势:
- 智能解析:对 CommonJS 、 AMD 、ES6 的语法做了兼容
- 万物模块:对 js、css、图片等资源文件都支持打包,不过需要通过配置loader来实现
- 开箱即用:HMR、Tree-shaking等功能
- 代码分割:可以将
代码切割
成不同的 chunk,实现按需加载,降低了初始化时间 - 插件系统,具有强大的 Plugin 接口,具有更好的灵活性和扩展性
- 易于调试:支持 SourceUrls 和 SourceMaps
- 快速运行:webpack 使用异步 IO 并具有多级缓存,这使得 webpack 很快且在增量编译上更加快
- 生态环境好:社区更丰富,出现的问题更容易解决
简述vite和webpack的区别
我们只讨论最主要的区别:核心构建机制
Vite:
开发阶段基于浏览器原生 ESM 模块,按需编译请求的模块,无需全量打包。直接启动开发服务器,冷启动速度极快(毫秒级)。生产环境则使用Rollup
进行打包,优化 Tree-Shaking 和代码体积。优势:避免全量编译,启动和热更新效率高,适合现代浏览器优先的项目。关键点:
- 基于现代浏览器对esm模块的支持
- 按需编译模块
- 立即启动开发服务器,启动速度非常快
- 某个模块被修改只需重新请求该模块即可
Webpack:
始终采用 全量打包 机制,递归分析所有依赖并生成 Bundle 文件,启动和热更新速度随项目规模增大显著变慢。生产构建依赖自身插件体系,成熟但复杂度高。优势:兼容性强,支持老旧浏览器和复杂代码分割需求。关键点:
全量打包,开发服务器启动速度较慢
某个模块被修改并保存后,Webpack 重新编译被修改的模块,和直接依赖这个模块的模块,而非整个项目(不再是全量打包)
1
A → B → C(修改了 C)
当文件
C
被修改并保存时:- Webpack 检测到
C
的变化。 - 重新编译
C
及其父模块B
(若B
的代码依赖C
的逻辑)。 - 不重新编译无关模块(如
A
)。 - 通过 HMR 将更新后的模块推送到浏览器,局部更新界面。
- Webpack 检测到
浏览器兼容性更好,不需要浏览器对esm模块的支持
说说如何借助webpack来优化前端性能?
背景
随着前端的项目逐渐扩大,必然会带来的一个问题就是性能
尤其在大型复杂的项目中,前端业务可能因为一个小小的数据依赖,导致整个页面卡顿甚至奔溃
一般项目在完成后,会通过webpack
进行打包,利用webpack
对前端项目性能优化是一个十分重要的环节
如何优化
通过webpack
优化前端的手段有:
- JS,CSS,Html代码压缩
- 文件大小压缩
- 图片压缩
- Tree Shaking
- 代码分离
- 内联 chunk
js代码压缩
terser
是一个JavaScript
的解释、绞肉机、压缩机的工具集,可以帮助我们压缩、丑化我们的代码,让bundle
更小
在production
模式下,webpack
默认就是使用 TerserPlugin
来处理我们的代码的。如果想要自定义配置它,配置方法如下:
1 | const TerserPlugin = require('terser-webpack-plugin') |
压缩css代码
CSS
压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等
CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin
1 | npm install css-minimizer-webpack-plugin -D |
1 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') |
HTML代码压缩
使用HtmlWebpackPlugin
插件来生成HTML
的模板时候,通过配置属性minify
进行html
优化
关于HtmlWebpackPlugin
插件的详细使用方法,可参考webpack基础 | 三叶的博客。
1 | module.exports = { |
设置了minify
,实际会使用另一个插件html-minifier-terser
文件大小压缩
前面介绍的都是代码压缩
,是指对源代码
进行处理,以减小其体积而不改变其功能。
代码压缩通常涉及以下几种操作:
- 移除空白字符:包括空格、制表符、换行符等。
- 缩短变量名和函数名:将长的
标识符
替换为短的名字,比如从myVariableName
变成a
。 - 移除注释:在生产环境中,注释是没有必要的,所以会被删除。
- 简化语句:例如,合并多个
var
声明或者将一些表达式简化。
文件大小压缩
则是指使用算法
对文件内容进行编码
,从而生成一个更小的表示形式,有时可能会导致文件类型的改变(如压缩成.zip或.rar档案)
常见的文件压缩方法有:
- 无损压缩:如ZIP、Gzip、Brotli等,可以完全还原原始文件的内容。这些压缩方法适用于所有类型的文件,并且特别适合于文本文件,因为文本文件中往往存在很多重复模式,容易被压缩算法利用。
- 有损压缩:如JPEG图片压缩,视频编码等,通过去除一些人类视觉或听觉不易察觉的信息来减小文件大小,但不能完全恢复原始文件。
在网络传输中,服务器常常会在发送响应之前使用Gzip
或Brotli
等压缩算法对整个响应体(包含HTML、JS、CSS等)进行压缩,以减少传输的数据量。当客户端接收到这个压缩后的数据后,会自动解压并处理
。
1 | npm install compression-webpack-plugin -D |
1 | const CompressionPlugin = require('compression-webpack-plugin'); |
图片压缩
一般来说在打包之后,一些图片文件的大小,是远远要比 js
或者 css
文件要来的大,所以图片压缩较为重要
image-webpack-loader
,这是一个专门用来压缩图片的加载器。它不会影响文件的存储位置或名称,而是专注于减少图像文件的大小。
TreeShaking
Tree Shaking
是一个术语,在计算机中表示消除死代码(一般指的是js代码),基于ES Module
的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)
在 Webpack5 中,Tree Shaking 在生产环境下默认启动
,这就意味着不需要配置usedExports
,同时还会自动启用代码压缩
。
如果想在开发环境
启动 Tree Shaking,需要配置 optimization.usedExports
为 true,启动标记功能;
1 | module.exports = { |
usedExports
用于在 Webpack 编译过程中启动标记功能
,使用之后,没被用上的变量/函数(包括未导入的函数/变量和导入后未使用的函数/变量),在webpack
打包中会被加上unused harmony export
注释,当生成产物时,被标记的变量/函数对应的导出语句
会被删除。
当然,仅仅删除未被使用的变量/函数的导出语句
是不够的,若 Webpack 配置启用了代码压缩工具,如 Terser
插件,那么在打包的最后它还会删除所有引用被标记内容
的代码语句,这些语句一般称作 Dead Code
。可以说,真正执行 Tree Shaking 操作的是 Terser 插件。
如下面sum
函数没被用到,webpack
打包会添加注释,terser
在优化时,则将该函数连同引用该函数的代码删除掉。
要注意的是,上述注释只有在开发打包下,开启usedExports,不开启代码压缩,才能看到。

但是,并不是所有 Dead Code 都会被 Terser 删除。
1 | // src/math.js |
我们添加一条console.log
语句,它打印了调用 squre
函数的返回结果,index.js
保留原样。按照我们之前的设想,打包后会删除与 square
函数相关的代码语句,即 square
函数的声明语句
、打印语句都会被删除。
然而实际上,打包后的math.js
模块中,square
函数的痕迹被完全清除,但是打印语句仍然被保留。这是因为,这条语句存在副作用。
副作用(side effect) 的定义是,在导入时
会执行特殊行为的代码(不是export,而是比如调用函数之类的代码)。例如 polyfill,它影响全局作用域,因而存在副作用。
显然,以上示例的 console.log()
语句存在副作用。Terser 在执行 Tree Shaking 时,会保留它认为存在副作用
的代码,而不是将其删除,即便这个代码是Dead code。
作为开发者,如果你非常清楚某条语句会被判别为有副作用
,但其实是无害的(删除后无影响),应该被删除,可以使用 /*#__PURE__*/
注释,来向 terser 传递信息,表明这条语句是纯的,没有副作用,terser 可以放心将它删除:
1 | // src/math.js |
然后在打包结果中就不会有console.log
语句
sideEffects
sideEffects
用于告知webpack compiler
哪些模块是有副作用(区别于pure注释
的代码层面),
"sideEffects"
是 package.json
的一个字段,默认值为 true
,即认为所有模块都可能是有副作用的。如果你非常清楚你的 package 是纯粹的,不包含副作用,那么可以简单地将该属性标记为 false
,来告知 webpack 整个包都是没有副作用的,可以安全地删除所有未被使用的代码
(Dead Code),执行比较激进的tree-shaking
;如果你的 package 中有些模块确实有一些副作用,可以改为提供一个数组:
1 | "sideEffects":[ |
更多内容参考:Webpack 5 实践:你不知道的 Tree Shaking本篇文章从 什么是 Tree Shaking、如何使用 T - 掘金
在本博客内的《前端面试-vue》一文中也有对tree-shaking的部分介绍。
总结一下关键点:
tree-shaking的作用是用来删除未使用的js代码,减小最终代码的体积
tree-shaking基于esm的静态导入,即在编译的时候就能确定哪些模块的具体哪些功能被使用
生产模式打包,会默认开启tree-shaking,开发模式打包,需要手动开启tree-shaking,即设置usedExports属性为true。
默认情况,tree-shaking会标记模块中未被导入的变量和函数,以及导入了但是没有被使用的变量和函数,然后使用terser压缩js代码的时候,不仅仅会删除这些被标记的的变量和函数,还会删除引用了这些被标记的变量和函数的代码,这些代码被叫做dead code
但是如果某条dead code被认为是有副作用的,则不会被tree-shaking删除,比如
console.log
语句
代码分离
将代码分离到不同的bundle
中,之后我们可以按需加载
,或者并行加载这些文件
默认情况下,所有的JavaScript
代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度
代码分离可以分出更小的bundle
,以及控制资源加载优先级,提供代码的加载性能
这里通过splitChunksPlugin
来实现,该插件webpack
已经默认安装和集成,只需要配置即可
默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial
或者all
1 | module.exports = { |
splitChunks
主要属性有如下:
- Chunks:对同步代码还是异步代码进行处理
- minSize: 拆分包的大小, 至少为minSize,如果包的大小不超过minSize,这个包不会拆分
- maxSize: 将大于maxSize的包,拆分为不小于minSize的包
- minChunks:被引入的次数,默认是1
内联chunk
可以通过InlineChunkHtmlPlugin
插件将一些chunk
的模块内联到html
,如runtime
的代码(对模块进行解析、加载、模块信息相关的代码),代码量并不大,但是必须加载的。
1 | const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin') |
说说webpack的热更新是如何做到的?原理是什么?
是什么
HMR
全称 Hot Module Replacement
,可以理解为模块热替换
,指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用。
例如,我们在应用运行过程中修改了某个模块,通过自动刷新,会导致整个应用的整体刷新,那页面中的状态信息都会丢失
如果使用的是 HMR
,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用。
在webpack
中配置开启热模块也非常的简单,如下代码:
1 | const webpack = require('webpack') |
通过上述这种配置,如果我们修改并保存css
文件,确实能够以不刷新的形式更新到页面中
但是,当我们修改并保存js
文件之后,页面依旧自动刷新了,这里并没有触发热模块
所以,HMR
并不像 Webpack
的其他特性一样可以开箱即用,需要有一些额外的操作
我们需要去指定哪些模块发生更新时进行HRM
,如下代码:
1 | if(module.hot){ |
实现原理

- Webpack Compile:将 JS 源代码编译成 bundle.js
- HMR Server:用来将热更新的文件输出给 HMR Runtime
- Bundle Server:静态资源文件服务器,提供文件访问路径
- HMR Runtime:
socket服务器
,会被注入到浏览器,更新文件的变化 - bundle.js:构建输出的文件
- 在HMR Runtime 和 HMR Server之间建立 websocket,即图上4号线,用于实时更新文件变化
上面图中,可以分成两个阶段:
- 启动阶段为上图 1 - 2 - A - B
在编写未经过webpack
打包的源代码后,Webpack Compile
将源代码和 HMR Runtime
一起编译成 bundle
文件,传输给Bundle Server
静态资源服务器
- 更新阶段为上图 1 - 2 - 3 - 4
当某一个文件或者模块发生变化时,webpack
监听到文件变化对文件重新编译打包,编译生成唯一的hash
值,这个hash
值用来作为下一次热更新的标识
根据变化的内容生成两个补丁文件:manifest
(包含了 hash
和 chundId
,用来说明变化的内容)和chunk.js
模块
由于socket
服务器在HMR Runtime
和 HMR Server
之间建立 websocket
链接,当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash
值,如下图的h
属性,作为下一次热更细的标识
在浏览器接受到这条消息之前,浏览器已经在上一次socket
消息中已经记住了此时的hash
标识,这时候我们会创建一个 ajax
去服务端请求获取到变化内容的 manifest
文件
mainfest
文件包含重新build
生成的hash
值,以及变化的模块,对应上图的c
属性
浏览器根据 manifest
文件获取模块变化的内容,从而触发render
流程,实现局部模块更新
总结
关于webpack
热模块更新的总结如下:
- 通过
webpack-dev-server
创建两个服务器:提供静态资源的服务(express)和Socket服务 - express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
- socket server 是一个 websocket 的长连接,双方可以通信
- 当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk)
- 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)
- 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新。
如何提高webpack的构建速度?
背景
随着我们的项目涉及到页面越来越多,功能和业务代码也会随着越多,相应的 webpack
的构建时间也会越来越久
构建时间与我们日常开发效率密切相关,当我们本地开发启动 devServer
或者 build
的时候,如果时间过长,会大大降低我们的工作效率
所以,优化webpack
构建速度是十分重要的环节
如何优化
常见的提升构建速度的手段有如下:
- 优化 loader 配置
- 合理使用 resolve.extensions
- 优化 resolve.modules
- 优化 resolve.alias
- 使用 DLLPlugin 插件
- 使用 cache-loader
- terser 启动多线程
- 合理使用 sourceMap
优化loader配置
在使用loader
时,可以通过配置include
、exclude
、test
属性来匹配文件
如采用 ES6 的项目为例,在配置 babel-loader
时,可以这样:
1 | module.exports = { |
说白了就是使用loader的时候,尽可能精确的匹配文件。
合理使用 resolve.extensions
在开发中我们会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库, resolve
可以帮助webpack
从每个 require/import
语句中,找到需要引入的,合适的模块代码
解析到未加扩展名的文件时,通过resolve.extensions
,自动给文件添加拓展名,默认情况如下:
1 | module.exports = { |
当我们引入文件的时候,若没有文件后缀名
,则会根据数组内的值依次查找
当我们配置的时候,则不要随便把所有后缀都写在里面,这会调用多次文件的查找,这样就会减慢打包速度
简单的来说就是,后缀自动填充数组的长度不要太长了。
优化 resolve.modules
resolve.modules
用于配置 webpack
去哪些目录下寻找第三方模块
。默认值为['node_modules']
,所以默认会从node_modules
中查找文件。当安装的第三方模块都放在项目根目录下的 ./node_modules
目录下时,所以可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:
1 | module.exports = { |
优化 resolve.alias
alias
给一些常用路径
起一个别名
,特别当我们的项目目录结构比较深的时候,一个文件的路径可能是./../../
的形式
通过配置alias
以减少查找过程,在vue的脚手架中,这是自动配置好的。
1 | module.exports = { |
使用 cache-loader
在一些性能开销较大的 loader
之前添加 cache-loader
,以将结果缓存到磁盘里,显著提升二次构建
速度
保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader
使用此loader
1 | module.exports = { |
terser 启动多线程
使用多进程并行运行来提高构建速度,其实默认就是使用多线程
1 | module.exports = { |
更多优化方式参考:面试官:如何提高webpack的构建速度?
说说webpack proxy工作原理?为什么能解决跨域?
是什么
webpack proxy
,即webpack
提供的代理服务
基本行为就是接收客户端发送的请求后,转发给其他服务器
其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)
想要实现代理首先需要一个中间服务器(代理服务器),webpack
中提供服务器的工具为webpack-dev-server
webpack-dev-server
webpack-dev-server
是 webpack
官方推出的一款开发工具,将自动编译
和自动刷新浏览器
等一系列对开发友好的功能,全部集成在了一起,目的是为了提高开发者日常的开发效率,只适用在开发阶段
关于配置方面,在webpack
配置对象属性中,通过devServer
属性提供,如下:
1 | // ./webpack.config.js |
proxy属性的名称是需要被代理的请求路径前缀
,一般为了辨别都会设置前缀为/api
,值为对应的代理匹配规则,对应如下:
- target:表示的是代理到的目标地址
- pathRewrite:默认情况下,我们的
/api
也会被写入到URL中,如果希望删除,可以使用pathRewrite - secure:默认情况下,不接收转发到https的服务器上,如果希望支持,可以设置为false
- changeOrigin:它表示是否更新
代理请求
的headers
中host
字段,如果这个值为true,则修改代理请求(代理服务器发送给目标服务器的请求)的host从localhost:8080
为api.github.com
工作原理
参考前端面试—vue部分一文中的跨域解决部分。