# 一大波 Webpack 面试题来啦~
# 0.有哪些常见的 Loader?你用过哪些 Loader?
raw-loader
: 加载文件原始内容(utf-8)file-loader
: 原封不动的把文件输出到一个文件夹中,在代码中通过相对URL
去引用输出的文件 (处理图片和字体)url-loader
: 与file-loader
类似,区别是用户可以设置一个阈值(limit
),大于这个**阈值(limit
)会交给file-loader
直接复制处理,小于阈值(limit
)**时返回文件的base64
形式编码 (处理图片和字体)source-map-loader
: 加载额外的Source Map
文件,以方便断点调试svg-inline-loader
: 将压缩后的SVG
内容注入代码中image-loader
: 加载并且压缩图片文件json-loader
: 加载JSON
文件(默认包含)handlebars-loader
: 将Handlebars
模版编译成函数并返回babel-loader
: 把ES6
转换成ES5
ts-loader
: 将TypeScript
转换成JavaScript
awesome-typescript-loader
: 将TypeScript
转换成JavaScript
,性能优于ts-loader
sass-loader
:将SCSS/SASS
代码转换成CSS
css-loader
: 可以处理import .css
的文件.并将css
代码写入bundle.js
文件中,支持模块化、压缩、文件导入等特性(还需要借助 style-loader 的功能将样式插入到html.head.style
标签中去)style-loader
: 将css-loader
获取到的代码 转交给style-loader
.让style-loader
将css
插入到页面的html.head.style
标签中去postcss-loader
:实际上是一套强大的插件体系,扩展CSS
语法,使用下一代CSS
,可以配合autoprefixer
插件自动补齐CSS3
前缀eslint-loader
:通过ESLint
检查JavaScript
代码tslint-loader
:通过TSLint
检查TypeScript
代码, 用的不多,主要用eslint-loader
mocha-loader
: 加载Mocha
测试(浏览器/NodeJS)用例的代码coverjs-loader
: 使用CoverJS
来计算测试的覆盖率vue-loader
: 解析和转换.vue
单文件组件,提取出其中的逻辑代码script
、样式代码style
、以及HTML
模版template
,再分别把它们交给对应的Loader
去处理i18n-loader
: 国际化cache-loader
: 可以在一些性能开销较大的Loader
之前添加,目的是将结果缓存到磁盘里
更多 Loader 请移步官网
# 1.有哪些常见的 Plugin?你用过哪些 Plugin?
define-plugin
:定义全局环境变量 (Webpack4
之后指定mode
会自动配置)ignore-plugin
:在打包时忽略本地化内容ProvidePlugin
: 这是webpack
内置的插件,可以自动加载模块,而不必到处import
或require
。uglifyjs-webpack-plugin
: 用来缩小(压缩优化)js
文件,至少需要Node v6.9.0
和Webpack v4.0.0
版本。html-webpack-plugin
:简化HTML
文件创建 (依赖于html-loader
)- 为
html
文件中引入的外部资源如script、link
动态添加每次compile
后的hash
,防止引用缓存的外部文件问题 - 可以生成创建
html
入口文件,比如单页面可以生成一个html
文件入口,配置N
个html-webpack-plugin
可以生成N
个页面入口
- 为
copy-webpack-plugin
: 在webpack
中拷贝文件和文件夹web-webpack-plugin
: 可方便地为单页应用输出HTML
,比html-webpack-plugin
好用clean-webpack-plugin
: 在每次生成dist
目录前,先删除本地的dist
文件(不然每次手动删除太麻烦)DllPlugin
: 这是webpack
内置的插件,为了极大减少构建时间,进行分离打包不常更新的依赖包,比如react
,react-dom
就可以打包一次,后面只要不更新依赖,就直接用打包好的dll
文件,不必重复打包ModuleConcatenationPlugin
: 这是webpack
内置的插件,开启Scope Hoisting
(作用域提升)HotModuleReplacementPlugin
: 这是webpack
内置的插件,模块热替换,也被称为HMR
speed-measure-webpack-plugin
: 费时分析,可以看到每个Loader
和Plugin
执行耗时 (整个打包耗时、每个Plugin
和Loader
耗时)webpack-bundle-analyzer
: 可视化Webpack
输出的所有文件的体积 (业务组件、依赖第三方模块
)
更多 Plugin
请移步官网
更多第三方插件,请查看 [awesome-webpack](更多第三方插件,请查看 awesome-webpack 列表。) 列表。
# 2.说一说 Loader 和 Plugin 的区别?
Loader
本质就是一个函数,在该函数中对接收到的文件内容进行转换,返回转换后的结果。
因为 Webpack
只认识 JavaScript
,所以 Loader
就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin
就是插件,基于事件流框架 Tapable
,插件可以扩展 Webpack
的功能,在 Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过 Webpack
提供的 API
改变输出结果。
Loader
在 module.rules
中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object
,内部包含了 test
(类型文件)、loader
、options
(参数)等属性。
Plugin
在 plugins
中单独配置,类型为数组,每一项是一个 Plugin
的实例,参数都通过构造函数传入。
# 3.Webpack 构建流程简单说一下
Webpack
的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数
:从配置文件和Shell
语句中读取与合并参数,得出最终的参数开始编译
:用上一步得到的参数初始化Compiler
对象,加载所有配置的插件,执行对象的run
方法开始执行编译确定入口
:根据配置中的entry
找出所有的入口文件编译模块
:从入口文件出发,调用所有配置的Loader
对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理完成模块编译
:在经过第 4 步使用Loader
翻译完所有模块后,得到了每个模块被编译后的最终内容以及它们之间的依赖关系输出资源
:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会输出完成
:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack
会在特定的时间点广播出特定的事件,插件在监听到特定的事件后会执行特定的逻辑,并且插件可以调用 Webpack
提供的 API
改变 Webpack
的运行结果。
简单说
初始化:启动构建,读取与合并配置参数,加载
Plugin
,实例化Compiler
编译:从
Entry
出发,针对每个Module
串行调用对应的Loader
去翻译文件的内容,再找到该Module
依赖的Module
,递归地进行编译处理输出:将编译后的
Module
组合成Chunk
,将Chunk
转换成文件,输出到文件系统中
对源码感兴趣的可以看看这篇文章 从源码窥探 Webpack4.x 原理
# 4.使用 webpack 开发时,用过哪些可以提高效率的插件?
webpack-dashboard
:为 webpack 在命令行上构建了一个一目了然的仪表盘(dashboard
),其中包括构建过程和状态、日志以及涉及的模块列表webpack-merge
:可以用来提取公共配置(webpack.base.config.js
),减少重复配置代码speed-measure-webpack-plugin
:简称SMP
(费时分析),分析出Webpack
打包过程中Loader
和Plugin
的耗时,有助于找到构建过程中的性能瓶颈。size-plugin
:监控资源体积变化,尽早发现问题HotModuleReplacementPlugin
:模块热替换插件,能够实现修改、添加或删除前端页面中的模块代码,而且是在页面不刷新的前提下webpack-bundle-analyzer
:webpack
打包模块分析工具
# 5.source map 是什么?生产环境怎么用?
source map
是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map 文件只要不打开开发者工具,浏览器是不会加载的
线上环境一般有三种处理方案:
hidden-source-map
:借助第三方错误监控平台Sentry
使用nosources-source-map
:只会显示具体行数以及查看源代码的错误栈。安全性比sourcemap
高sourcemap
:通过nginx
设置将.map
文件只对白名单开放(公司内网)
注意:避免在生产中使用 inline-
和 eval-
,因为它们会增加 bundle
体积大小,并降低整体性能。
# 6.模块打包原理?
Webpack
实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
# 7.文件监听原理?
在发现代码发生变化时,自动重新构建出新的输出文件。
Webpack
开启监听模式,有两种方式:
- 启动
webpack
命令时,在命令行后面带上--watch
参数 - 在配置
webpack.config.js
中设置watch:true
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout
后再执行。
具体的配置如下:
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout: 300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll: 1000
}
};
# 8.说一说 Webpack
的热更新原理?
重要,前方高能!!!
Webpack
的热更新又称热模块替换(Hot Module Replacement
),缩写为 HMR
。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR
的核心就是客户端从服务端拉取更新后的文件,准确的说是 chunk diff
(chunk
需要更新的部分),实际上 WDS
(webpack-dev-server
) 与浏览器之间维护了一个 Websocket
,当本地资源发生变化时,WDS
会向浏览器推送更新,并带上构建时的 hash
,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS
发起 Ajax
请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS
发起 jsonp
请求获取该 chunk 的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModuleReplacementPlugin
来完成,提供了相关 API
以供开发者针对自身场景进行处理,像 react-hot-loader
和 vue-loader
都是借助这个 API
实现 HMR
更多细节的可以参考下这篇文章 → Webpack HMR 原理解析
# 9.如何对 bundle 体积进行监控和分析?
VSCode
中有一个插件 Import Cost
可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer
生成 bundle
的模块组成图表形式,显示所占用的体积大小。
bundlesize
工具包可以进行自动化资源体积监控。
# 10.文件指纹是什么?怎么用?
文件指纹是打包后输出的文件名的后缀。
Hash
:和整个项目的构建相关,只要项目文件有修改,整个项目构建的hash
值就会更改Chunkhash
:和Webpack
打包的chunk
(代码块) 有关,不同的entry
会生出不同的chunkhash
Contenthash
:根据文件内容为指标来定义hash
,文件内容不变,则contenthash
不变
# JS 的文件指纹设置
设置 output
的 filename
,用 chunkhash
。
具体的配置代码如下:
module.exports = {
entry: { app: './scr/app.js', search: './src/search.js' },
output: { filename: '[name][chunkhash:8].js', path: __dirname + '/dist' }
};
# CSS 的文件指纹设置
设置 MiniCssExtractPlugin
的 filename
,使用 contenthash
。
具体的配置代码如下:
module.exports = {
entry: { app: './scr/app.js', search: './src/search.js' },
// chunkhash:8 的意思是根据chunk来生成长度为8的hash值
output: { filename: '[name][chunkhash:8].js', path: __dirname + '/dist' },
// contenthash:8 的意思是根据文件内容来生成长度为8的hash值
plugins: [new MiniCssExtractPlugin({ filename: `[name][contenthash:8].css` })]
};
# 图片的文件指纹设置
设置file-loader
的name
,使用hash
。
占位符名称及含义
ext
资源后缀名name
文件名称path
文件的相对路径folder
文件所在的文件夹contenthash
文件的内容hash
,默认是md5
生成hash
文件内容的hash
,默认是md5
生成emoji
一个随机的指代文件内容的emoj
具体的配置代码如下:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') },
module: {
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: [{ loader: 'file-loader', options: { name: 'img/[name][hash:8].[ext]' } }]
}
]
}
};
# 11.在实际工程中,配置文件上百行乃是常事,如何保证各个 loader 按照预想方式工作?
可以使用 enforce
强制执行 loader
的作用顺序,pre
代表在所有正常 loader
之前执行,post
是所有 loader
之后执行。(inline
官方不推荐使用)
# 12.如何优化 Webpack 的构建速度?
其实这个问题就像能不能说一说「从 URL 输入到页面显示发生了什么」一样,实际上当然是越详细越好咯~
使用
高版本
的Webpack
和Node.js
多进程/多实例构建:
HappyPack
(不维护了)、thread-loader
压缩代码
多进程并行压缩
webpack-paralle-uglify-plugin
uglifyjs-webpack-plugin
开启parallel
参数 (不支持 ES6)terser-webpack-plugin
开启parallel
参数
通过
mini-css-extract-plugin
提取Chunk
中的CSS
代码到单独文件,通过css-loader
的minimize
选项开启cssnano
压缩CSS
。
图片压缩
- 使用基于
Node
库的imagemin
(很多定制选项、可以处理多种图片格式) - 配置
image-webpack-loader
- 使用基于
缩小打包作用域:
exclude/include
(确定loader
规则范围)resolve.modules
指明第三方模块的绝对路径 (减少不必要的查找)resolve.mainFields
只采用main
字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)resolve.extensions
尽可能减少后缀尝试的可能性noParse
对完全不需要解析的库进行忽略 (不去解析但仍会打包到bundle
中,注意被忽略掉的文件里不应该包含import、require、define
等模块化语句)IgnorePlugin
(完全排除模块)- 合理使用
alias
(路径别名,可以让后续引用的地方减少路径的复杂度。)
提取页面公共资源
:基础包分离:
- 使用
html-webpack-externals-plugin
,将基础包通过CDN
引入,不打入bundle
中 - 使用
SplitChunksPlugin
进行(公共脚本、基础包、页面公共文件)分离(Webpack4
内置) ,替代了 CommonsChunkPlugin 插件
- 使用
DLL
:- 使用
DllPlugin
进行分包,使用DllReferencePlugin
(索引链接) 对manifest.json
引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。 HashedModuleIdsPlugin
可以解决模块数字id
问题
- 使用
充分利用缓存提升二次构建速度
:babel-loader
- 开启缓存
terser-webpack-plugin
- 开启缓存使用
cache-loader
或者hard-source-webpack-plugin
Tree shaking
- 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的
bundle
中去掉(只能对ES6 Modlue
生效) 开发中尽可能使用ES6 Module
的模块,提高tree shaking
- 效率禁用
babel-loader
的模块依赖解析,否则Webpack
接收到的就都是转换过的CommonJS
形式的模块,无法进行tree-shaking
- 使用
PurifyCSS
(不在维护) 或者uncss
去除无用CSS
代码purgecss-webpack-plugin
和mini-css-extract-plugin
配合使用(建议)
- 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的
Scope hoisting
- 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。
Scope hoisting
将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突 - 必须是
ES6
的语法,因为有很多第三方库仍采用CommonJS
语法,为了充分发挥Scope hoisting
的作用,需要配置mainFields
对第三方模块优先采用jsnext:main
中指向的ES6
模块化语法
- 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。
动态
Polyfill
- 建议采用
polyfill-service
只给用户返回需要的polyfill
,社区维护。 (部分国内奇葩浏览器UA
可能无法识别,但可以降级返回所需全部polyfill
)
- 建议采用
更多优化可以参考官网-构建性能
# 13.代码分割的本质是什么?有什么意义呢?
代码分割是指,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程。
在 Webpack
构建时,会避免加载已声明要异步加载的代码,异步代码会被单独分离出一个文件,当代码实际调用时被加载至页面。
代码分割的本质其实就是在源代码全部直接上线和打包成唯一脚本 main.bundle.js
这两种极端方案之间的一种更适合实际场景的中间状态。
代码分割技术的核心是异步加载资源,可喜的是,浏览器允许我们这么做,W3C stage 3 规范:whatwg/loader
对其进行了定义:你可以通过 import()
关键字让浏览器在程序执行时异步加载相关资源。
「用可接受的服务器性能压力增加来换取更好的用户体验」。
源代码全部直接上线:虽然过程可控,但是 http 请求多,性能开销大。
打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好
可以参考下这篇文章 → 项目不知道如何做性能优化?不妨试一下代码分割
# 14.是否写过Loader
?简单描述一下编写Loader
的思路?
Loader
支持链式调用,所以开发上需要严格遵循单一职责,每个 Loader
只负责自己需要负责的事情。
Loader
的 API
可以去官网查阅
Loader
是运行在Node.js
中,我们可以调用任意Node.js
自带的 API 或者安装第三方模块进行调用Webpack
传给Loader
的原内容都是UTF-8
格式编码的字符串,当某些场景下Loader
处理二进制文件时,需要通过exports.raw = true
告诉Webpack
该Loader
是否需要二进制数据- 尽可能的异步化
Loader
,如果计算量很小,同步也可以 Loader
是无状态的,我们不应该在Loader
中保留状态- 使用
loader-utils
和schema-utils
为我们提供的实用工具 - 加载本地
Loader
方法Npm link
ResolveLoader
# 15.是否写过 Plugin?简单描述一下编写 Plugin 的思路?
webpack
在运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在特定的阶段植入想要添加的自定义功能。Webpack
的 Tapable
事件流机制保证了插件的有序性,使得整个系统扩展性良好。
Plugin 的 API 可以去官网查阅
compiler
(全局只有一个) 暴露了和Webpack
整个生命周期相关的钩子compilation
(每次编译过程生成的实例) 暴露了与模块和依赖有关的粒度更小的事件钩子插件需要在其原型上绑定
apply
方法,才能访问compiler
实例传给每个插件的
compiler
和compilation
对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件找出合适的事件点去完成想要的功能
emit
事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit
事件是修改Webpack
输出资源的最后时机)watch-run
当依赖的文件发生变化时会触发
异步的事件需要在插件处理完任务时调用回调函数通知
Webpack
进入下一个流程,不然会卡住
# 16.聊一聊 Babel 原理吧
大多数JavaScript Parser
遵循 estree
规范,Babel
最初基于 acorn
项目(轻量级现代 JavaScript
解析器) Babel
大概分为三大部分:
解析:将代码转换成
AST
- 词法分析:将代码(字符串)分割为
token
(令牌,不是验证功能的 token)流,即语法单元成的数组 - 语法分析:分析
token
流(上面生成的数组), 并生成AST
- 词法分析:将代码(字符串)分割为
转换:访问
AST
的节点进行变换操作生产新的AST
Taro
就是利用babel
完成的小程序语法转换
生成:以新的
AST
为基础生成代码
如果想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目
# 参考资料
# 最后
文中若有不准确或错误的地方,欢迎指出,有兴趣可以的关注下Github~