# 一大波 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转换成ES5ts-loader: 将TypeScript转换成JavaScriptawesome-typescript-loader: 将TypeScript转换成JavaScript,性能优于ts-loadersass-loader:将SCSS/SASS代码转换成CSScss-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-loadermocha-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内置的插件,模块热替换,也被称为HMRspeed-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会生出不同的chunkhashContenthash:根据文件内容为指标来定义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-pluginuglifyjs-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 linkResolveLoader
# 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的节点进行变换操作生产新的ASTTaro就是利用babel完成的小程序语法转换
生成:以新的
AST为基础生成代码
如果想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目
# 参考资料
# 最后
文中若有不准确或错误的地方,欢迎指出,有兴趣可以的关注下Github~