Webpack面试题
本文最后更新于 2025年10月31日 中午
Webpack 是什么
Webpack 是一种用于构建 JavaScript 应用程序的静态模块打包器,它能够以一种相对一致且开放的处理方式,加载应用中的所有资源文件(图片、CSS、视频、字体文件等),并将其合并打包成浏览器兼容的 Web 资源文件。
- 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序
- 编译兼容。通过 Webpack 的 Loader 机制,不仅仅可以对代码做 polyfill,还可以编译转换诸如
.less、.vue、.jsx这类在浏览器无法识别的格式文件,帮助开发者使用新特性和新语法,提高开发效率 - 能力扩展。通过 Webpack 的 Plugin 机制,在实现模块化打包和编译兼容的基础上,可以进一步实现如按需加载、代码压缩等系列功能,进一步提高自动化程度,工程效率以及打包输出的质量
Loader 和 Plugin
功能不同:
- Loader 是函数,在该函数中对接收到的内容进行转换返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作
- Plugin 是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果
用法不同:
- Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性
- Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 示例,参数通过构造函数传入
常见 Loader
参考
官网
文件
- val-loader:将代码作为模块执行,并将其导出为 JS 代码
- ref-loader:用于手动建立文件之间的依赖关系
JSON
- cson-loader:加载并转换 CSON 文件
语法转换
- babel-loader:把 ES6 转换成 ES5
- buble-loader:使用 Bublé 加载 ES2015+ 代码并将其转换为 ES5
- traceur-loader:使用 Traceur 加载 ES2015+ 代码并将其转换为 ES5
- ts-loader:像加载 JavaScript 一样加载 TypeScript 2.0+
- coffee-loader:像加载 JavaScript 一样加载 CoffeeScript
- fengari-loader:使用 fengari 加载 Lua 代码
- elm-webpack-loader:像加载 JavaScript 一样加载 Elm
模板
- html-loader:将 HTML 导出为字符串,需要传入静态资源的引用路径
- pug-loader:加载 Pug 和 Jade 模板并返回一个函数
- markdown-loader:将 Markdown 编译为 HTML
- react-markdown-loader:使用 markdown-parse 解析器将 Markdown 编译为 React 组件
- posthtml-loader:使用 PostHTML 加载并转换 HTML
- handlebars-loader:将 Handlebars 文件编译为 HTML
- markup-inline-loader:将 SVG/MathML 文件内嵌到 HTML,对字体或动画用 SVG 非常实用
- twig-loader:编译 Twig 模板并返回一个函数
- remark-loader:通过 remark 加载 markdown,且支持解析内容中的图片
样式
- style-loader:将模块导出的内容作为样式并添加到 DOM 中,通过 DOM 操作去加载 CSS
- css-loader:加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
- less-loader:加载并编译 LESS 文件
- sass-loader:加载并编译 SASS/SCSS 文件
- postcss-loader:使用 PostCSS 加载并转换 CSS/SSS 文件
- stylus-loader:加载并编译 Stylus 文件
框架
- vue-loader:加载并编译 Vue 组件
- angular2-template-loader:加载并编译 Angular 组件
第三方
- awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
- eslint-loader:通过 ESLint 检查 JavaScript 代码
- tslint-loader:通过 TSLint 检查 TypeScript 代码
- mocha-loader:加载 Mocha 测试用例的代码
- coverjs-loader:计算测试的覆盖率
- i18n-loader: 国际化
- cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里
总结
- babel-loader:把 ES6 转换成 ES5
- html-loader:将 HTML 导出为字符串,需要传入静态资源的引用路径
- eslint-loader:通过 ESLint 检查 JavaScript 代码
- tslint-loader:通过 TSLint 检查 TypeScript 代码
- i18n-loader: 国际化
- style-loader:将模块导出的内容作为样式并添加到 DOM 中,通过 DOM 操作去加载 CSS
- css-loader:加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
- less-loader:加载并编译 LESS 文件
- sass-loader:加载并编译 SASS/SCSS 文件
常见 Plugin
参考
- https://juejin.cn/post/7138203576098095112#heading-2
- https://juejin.cn/post/6844904094281236487#heading-1
官网
- define-plugin:定义环境变量
- ignore-plugin:忽略部分文件
- html-Webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
- web-Webpack-plugin:可方便地为单页应用输出 HTML,比 html-Webpack-plugin 好用
- uglifyjs-Webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
- terser-Webpack-plugin: 支持压缩 ES6 (Webpack4)
- Webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
- mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载
- serviceworker-Webpack-plugin:为网页应用增加离线缓存功能
- clean-Webpack-plugin: 目录清理
- ModuleConcatenationPlugin: 开启 Scope Hoisting
- speed-measure-Webpack-plugin: 可以看到整个打包耗时、每个 Loader 和 Plugin 执行耗时
- Webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)
总结
- splitChunkPlugin: 用于代码分割
- webpack-merge: 提取公共配置,用于分别编写不同环境的配置文件
- speed-measure-webpack-plugin: 分析打包过程中的 Loader 和 Plugin 的耗时,用于性能分析
- terser-webpack-plugin: 实现更精细的代码压缩功能
- UnusedWebpackPlugin: 反向查找项目中没被用到的文件,可在重构或者性能分析时使用
- Webpack Analysis: 官方提供的可视化分析工具
- BundleAnalyzerPlugin: 性能分析插件,可以在运行后查看是否包含重复模块/不必要模块等
- webpack-dashboard: 一个命令行可视化工具,能在编译过程中实时展示编译进度、模块分布、产物信息等相关信息,性能分析时很有用
如何编写 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
如何编写 Plugin
Webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。API 可以去 官网查阅。
- compiler 暴露了和 Webpack 整个生命周期相关的钩子
- compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
- 插件需要在其原型上绑定 apply 方法,才能访问 compiler 实例
- 传给每个插件的 compiler 和 compilation 对象都是同一个引用,若在一个插件中修改它们身上的属性,会影响后面的插件
- 找出合适的事件点去完成想要的功能
- emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
- watch-run 当依赖的文件发生变化时会触发
- 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住
Loader 写过一个简单的删除 console.log,另一个是进阶版的 Banner Loader,可以在文件开头自动加上版权信息以及构建时间。Plugin 写过一个统计构建耗时的插件,在 compile 钩子记录开始时间,在 done 钩子输出总耗时。通过这类插件,可以直观帮助团队优化构建速度。我对 Loader 的理解是“专注文件内容转换”,对 Plugin 的理解是“基于生命周期扩展构建能力”。
Tree shaking 原理
Tree Shaking 是一个利用 ES6 模块静态结构特性来去除生产环境下不必要代码的优化过程。原理:
- 当 Webpack 分析代码时,它会标记出所有的 import 语句和 export 语句
- 当 Webpack 确定某个模块没有被导入时,它会在生成的 bundle 中排除这个模块的代码
- Webpack 还会进行递归的标记清理,以确保所有未使用的依赖项都不会出现在最终的 bundle 中
确保使用的是 ES6 模块语法(即 import 和 export),因为只有这样才能让 Tree Shaking 发挥作用。为了启用 Tree Shaking,需要在配置文件中添加如下设置:
1 | |
Babel 原理
Babel 可以将代码转译为想要的目标代码,并且对目标环境不支持的 api 自动 polyfill。实现这些功能流程是解析-转换-生产三步:
- 解析:根据代码生成对应的 AST 结构
- 词法分析:将代码(字符串)分割为 token 流,即语法单元成的数组
- 语法分析:分析 token 流(上面生成的数组)并生成 AST
- 转换:遍历 AST 节点并生成新的 AST 节点
- Taro 就是利用 babel 完成的小程序语法转换
- 生成:根据新的 AST 生成目标代码
文件监听原理
文件监听是在发现源码发生变化时,自动重新构建出新的输出文件。开启监听模式,有两种方式:
- 启动 Webpack 命令时,带上
--watch参数 - 在配置 Webpack.config.js 中设置
watch: true
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。缺点:每次需要手动刷新浏览器。配置如:
1 | |
热更新原理
Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。这个机制可以做到不用人为手动刷新浏览器,就可以将旧的模块变更为新的模块。通过设置 devServer: {hot: true} 开启热更新,开启后便可以在发生改变后局部刷新改变的部分。
HMR 的核心是客户端从服务端拉取更新后的文件,准确来说是 chunk diff(chunk 需要更新部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源变化时,webpack-dev-server(WDS)会向浏览器推送更新,并带上构建时的 hash 让客户端与上一次资源进行对比。浏览器对比出差异后会向 WDS 发 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该 chunk 的增量更新。
后续(拿到增量更新之后如何处理、哪些状态该保留、哪些又需要更新)由 HotModulePlugin 完成,提供了相关 API 以供开发者针对自身场景进行处理, react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。
什么是文件指纹
文件指纹是指文件打包后的一连串后缀。作用:
- 版本管理: 在发布版本时,通过文件指纹来区分修改的文件和未修改的文件
- 使用缓存: 浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件
什么是 Source map
Source Map 是一种文件,它建立了构建后的代码与源代码之间的映射关系。通常开发阶段开启用来调试代码,帮助找到代码问题所在。在配置文件的 devtool 中指定 devtool: 'source-map' 开启。
Code Splitting
Code Splitting 代码分割,是一种优化技术。它允许将一个大的 chunk 拆分成多个小的 chunk,从而实现按需加载,减少初始加载时间,并提高应用程序性能。通过 optimization.splitChunks 配置项来开启代码分割。
构建流程
主要包括以下几个步骤:
- 初始化参数。启动构建,解析读取 Webpack 配置里的 webpack.config.js 参数和命令行参数,生成配置对象并实例化 compiler
- 开始编译。使用初始化 compiler 对象开始,注册所有配置的插件,插件监听构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译
- 确定入口。从配置的 entry 入口,开始解析文件构建 AST 语法树,找出依赖,递归下去
- 编译模块。递归中根据文件类型和 loader 配置,调用所有配置的 loader 对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译。经过第四步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
- 输出资源。根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 输出完成。确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
这个流程是一个串行的过程,Webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会广播事件,插件只需要监听好它所关心的事件,就能加入到这条 Webpack 机制中,去改变 Webpack 的运作,使得整个系统扩展性良好。
提高构建速度
- 利用缓存:利用 Webpack 的持久缓存功能,避免重复构建没有变化的代码
- 使用多进程/多线程构建:使用 thread-loader、happypack 等插件可将过程分为多个进程或线程
- 使用 DllPlugin:可以将第三方库预先打包成单独的文件,减少构建时间
- 使用 HardSourceWebpackPlugin:可以缓存中间文件,加速后续构建过程
- 移除不必要的插件:移除不必要的插件和配置,避免不必要的复杂性和性能开销
减少构建体积
- 代码分割(Code Splitting):将应用程序的代码划分为多个代码块,按需加载
- 使用 Tree Shaking:配置 Tree Shaking 机制,去除未使用的代码,减小生成的文件体积
- 压缩代码:使用工具如 UglifyJS 或 Terser 来压缩 JavaScript 代码
- 使用生产模式:在 Webpack 中使用生产模式,通过设置
mode: 'production'来启用优化 - 使用压缩工具:使用现代的压缩工具,如 Brotli 和 Gzip,来对静态资源进行压缩
- 利用 CDN 加速:将静态资源路径修改为 CDN 上的路径,减少图片、字体等静态资源等打包