theme: channing-cyan
在前端技术发展日新月异的今天,前端工程化已经成了每个前端工作/学习者的必备知识,那说到工程化,不可避免地就会谈到 webpack(虽然市面上已经有了很多成熟好用的打包工具,如rollup、vite,但是目前来看webpack龙头地位依旧不可撼动)但是我们都知道,webpack的内容之多难度之大劝退了很多想要入门的人,但是难归难,又不能不学,至少被问到的时候得能说上几句吧!接下来我就带大家一起来看看webpack有哪些常见的知识点/原理/面试题吧!!!
温馨提示:本文近9000字,建议收藏反复学习!!!
通过本文,你将学到:
- webpack的常见知识点以及对应的原理
- webpack常见面试题
话不多说,接下来让我们一起进入今天的学习吧!!!
1.webpack 是啥?有啥作用
概念:Webpack 是一种用于构建 JavaScript 应用程序的静态模块打包器,它能够以一种相对一致且开放的处理方式,加载应用中的所有资源文件(图片、CSS、视频、字体文件等),并将其合并打包成浏览器兼容的 Web 资源文件。
功能:
- 模块的打包:通过打包整合不同的模块文件保证各模块之间的引用和执行
- 代码编译:通过丰富的
loader
可以将不同格式文件如.sass/.vue/.jsx
转译为浏览器可以执行的文件 - 扩展功能:通过社区丰富的
plugin
可以实现多种强大的功能,例如代码分割、代码混淆、代码压缩、按需加载…..等等
2.常见的 loader 及其作用?
-babel-loader
:将 es6 转译为 es5
- file-loader
:可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存,并在代码中通过URL去引用输出的文件
-url-loader
:和file-loader
功能相似,但是可以通过指定阈值来根据文件大小使用不同的处理方式(小于阈值则返回 base64 格式编码并将文件的 data-url
内联到bundle
中)
-raw-loader
:加载文件原始内容
webpack5 自身内置了
file-loader/ url-loader/ raw-loader
等 loader,所以我们不需要再显示引入 loader 只需要指定对应的 type 即可实现相同的功能 如file-loader
等价于type= "asset/resource"'
-
image-webpack-loader
: 加载并压缩图片资源
-awesome-typescirpt-loader
: 将 typescript 转换为 javaScript 并且性能由于ts-loader
-sass-loader
: 将 SCSS/SASS 代码转换为 CSS
-css-loader
: 加载 CSS 代码 支持模块化、压缩、文件导入等功能特性
-style-loader
: 把 CSS 代码注入到 js 中,通过DOM
操作去加载 CSS 代码
当我们使用类似于
less
或者scss
等预处理器的时候,通常需要多个 loader 的配合使用如test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader']
-
source-map-loader
: 加载额外的Source Map
文件
-eslint-loader
: 通过 ESlint 检查 js 代码
-cache-loader
: 可以在一些开销较大的Loader
之前添加可以将结果缓存到磁盘中,提高构建的效率
-thread-loader
: 多线程打包,加快打包速度
以上便是常用的 loader 以及作用,对 loader 感兴趣的小伙伴之后可以自行了解
3.常见的 plugin 及作用
-define-plugin
: 定义环境变量(webpack4 之后可以通过指定mode:production/development
实现同样效果)
-web-webpack-plugin
:为单页面应用输出 HTML 性能优于html-webpack-plugin
-clean-webpack-plugin
: 每次打包时删除上次打包的产物, 保证打包目录下的文件都是最新的
-webpack-merge
: 用来合并公共配置文件,常用(例如分别配置webpack.common.config.js/ webpack.dev.config.js/webpack.production.config.js
并将其合并)
-ignore-plugin
: 忽略指定的文件,可以加快构建速度
-terser-webpack-plugin
:压缩 ES6 的代码(tree-shaking)
-uglifyjs-webpack-plugin
: 压缩 js 代码
-mini-css-extract-plugin
: 将 CSS 提取为独立文件,支持按需加载
-css-minimize-webpack-plugin
:压缩 CSS 代码
css 文件的压缩需要
mini-css-extract-plugin
和css-minimize-webpack-plugin
的配合使用 即先使用mini-css-extract-plugin
将 css 代码抽离成单独文件,之后使用css-minimize-webpack-plugin
对 css 代码进行压缩
-serviceworker-webpack-plugin
: 为离线应用增加离线缓存功能
-ModuleconcatenationPlugin
: 开启Scope Hositing
用于合并提升作用域, 减小代码体积
-copy-webpack-plugin
: 在构建的时候,复制静态资源到打包目录。
-compression-webpack-plugin
: 生产环境采用gzip
压缩 JS 和 CSS
-ParalleUglifyPlugin
: 多进程并行压缩 js
-webpack-bundle-analyzer
: 可视化 webpack 输出文件大小的根据
-speed-measure-webpack-plugin
: 用于分析各个 loader 和 plugin 的耗时,可用于性能分析
-webpack-dashboard
: 可以更友好地展示打包相关信息
以上便是常见的插件及其作用,接下来讲解我自己使用过的觉得好用的插件/工具
4.有没有用过好用的工具/plugin
-splitChunkPlugin
:用于代码分割
-webpack-merge
: 提取公共配置,用于分别编写不同环境的配置文件( `` )
-HotModuleReplacementPlugin
:支持模块热替换
-ignore-plugin
: 忽略指定文件,可以加快构建速度
-clean-webpack-plugin
: 每次打包时删除上次打包的产物, 保证打包目录下的文件都是最新的
-speed-measure-webpack-plugin
: 分析出 Webpack 打包过程中的 Loader 和 Plugin 的耗时,用于性能分析
-mini-css-extract-plugin
: 将 CSS 代码抽离为独立文件,支持按需加载, 配合 css-minimize-webpack-plugin
使用
-terser-webpack-plugin
: 实现更精细的代码压缩功能
-SourceMapDevtoolPlugin
:精细度配置SourceMap
, 不能和devtool
选项同时使用
-UnusedWebpackPlugin
: 反向查找项目中没被用到的文件,日常工作经常用到,可在重构或者性能分析时使用
-webpack-dashboard
: webpack-dashboard 是一个命令行可视化工具,能够在编译过程中实时展示编译进度、模块分布、产物信息等相关信息,性能分析时很有用。
- Webpack Analysis
:Webpack Analysis 是 webpack 官方提供的可视化分析工具。
-BundleAnalyzerPlugin
:性能分析插件,可以在运行后查看是否包含重复模块/不必要模块等
以上便是我日常学习中经常用到的一些工具/插件,有用过其他好用的工具/插件的同学欢迎在评论区推荐讨论!
5.loader 和 plugin 有啥区别?
Loader:
Loader 本质上是一个函数,负责代码的转译,即对接收到的内容进行转换后将转换后的结果返回
配置 Loader 通过在 modules.rules
中以数组的形式配置
Plugin:
Plugin 本质上是一个带有apply(compiler)
的函数,基于tapable这个事件流框架来监听webpack构建/打包过程中发布的 hooks 来通过自定义的逻辑和功能来改变输出结果。
Plugin 通过plugins
以数组的形式配置
总结:
Loader主要负责将代码转译为webpack 可以处理的 JavaScript 代码,而 Plugin 更多的是负责通过接入webpack 构建过程来影响构建过程以及产物的输出,Loader的职责相对比较单一简单,而Plugin更为丰富多样
6.如何保证众多 Loader 按照想要的顺序执行?
可以通过enforce
来强制控制 Loader 的执行顺序 (pre
表示在所有正常的 loader 执行之前执行,post
则表示在之后执行)
Loader 的执行有以下两个阶段:
- Pitching 阶段: loader 上的 pitch 方法,按照
后置(post)、行内(inline)、普通(normal)、前置(pre)
的顺序调用。更多详细信息,请查看 Pitching Loader。- Normal 阶段: loader 上的 常规方法,按照
前置(pre)、普通(normal)、行内(inline)、后置(post)
的顺序调用。模块源码的转换, 发生在这个阶段。
7.如何编写 Loader
这也是面试官喜欢问的问题,而我们大部分人别说写了,可能连用都没咋用过,所以问这个问题更多是为了了解你对 loader 这方面的知识掌握程度,毕竟只有当你足够了解 loader 的知识,你才有自己编写 loader 的可能,那接下来我们就按照从 loader 的特性来分析如何编写 loader 这个思路来讲解
loader 支持链式调用,上一个 loader 的执行结果会作为下一个 loader 的入参。
- 根据这个特性,我们知道我们的loader想要有返回值,并且这个返回值必须是标准的 JavaScript 字符串或者
AST
代码结构,这样才能保证下一个loader的正常调用。
- 根据这个特性,我们知道我们的loader想要有返回值,并且这个返回值必须是标准的 JavaScript 字符串或者
loader 的主要职责就是将代码转译为webpack可以理解的 js 代码。
- 根据这个特性,loader 内部一般需要通过
return / this.callback
来返回转换后的结果
- 根据这个特性,loader 内部一般需要通过
单个 loader 一把只负责单一的功能。
- 根据这个特性,我们的 loader 应该符合单一职责的原则,尽量别让单个 loader 执行太多职责
善于利用开发工具
- loader-utils: loader-utils 是一个非常重要的 Loader 开发辅助工具,为开发中提供了诸如读取配置、
requestString
的序列化和反序列化、getOptions/getCurrentRequest/parseQuery
等核心接口….等等功能,对于 loader 的开发十分有用 - schema–utils:schema-utils是用于校验用户传入 loader 的参数配置的校验工具,也是在开发中十分有用
- loader-utils: loader-utils 是一个非常重要的 Loader 开发辅助工具,为开发中提供了诸如读取配置、
loader 是无状态的
- 根据此特性,我们不应该在 loader 保存状态
webpack 默认缓存 loader 的执行结果
- webpack会默认缓存loader 的执行结果直到资源/所依赖的资源发生变化 如果想要 loader 不缓存 可以通过
this.cacheble
显式声明不做缓存
- webpack会默认缓存loader 的执行结果直到资源/所依赖的资源发生变化 如果想要 loader 不缓存 可以通过
Loader 接收三个参数
source
: 资源输入 对于第一个执行的 loader 为资源文件的内容 后续执行的 loader 则为前一个 loader 的执行结果 也可能是字符串 或者是代码的AST
结构sourceMap
: 可选参数 代码的sourcemap
结构data
: 可选参数 其他需要在Loader
链中传递的信息
正确上报 loader 的异常信息
- 一般尽量使用
logger.error
减少对用户的干扰 - 对于需要明确警示用户的错误 优先使用
this.emitError
- 对于已经严重到不能继续往下编译的错误 使用
callback
- 一般尽量使用
loader 函数中的
this
由 webpack 提供 并且指向了loader-runtime
的loaderContext
对象- 可以通过
this
来获取 loader 需要的各种信息 Loader Context提供了许多实用的接口,我们不仅可以通过这些接口获取需要的信息,还可以通过这些接口改变 webpack 的运行状态(相当于产生 Side Effect)
- 可以通过
loader 由
pitch
和normal
两个阶段- 根据此特性,我们可以在
pitch
阶段预处理一些操作
- 根据此特性,我们可以在
webpack 会按照
use
定义的顺序从前往后执行Pitch
Loader 从后往前执行Normal
Loader
我们可以将一些预处理的逻辑放在Pitch
中
- 正确处理日志 使用
Loader Context``的getLogger接口(支持verbose/log/info/warn/error
五种级别的日志 用户可以通过infrastructureLogging.level 配置项筛选不同日志内容 ) - 充分调试你编写的loader
- 创建出webpack实例 并运行laoder
- 获取loader执行结果 对比、分析判断是否符合预期
- 判断执行过程中是否出错
以上便是开发 loader 需要的知识以及常规步骤,相信答出这些内容后,面试官变不会再为难你了!说完如何开发loader,接下来就趁热打铁讲解一下如何开发plugin
8.如何编写 Plugin
上面我们已经讲完了如何编写loader,接下来我们还是按照分析 plugin 特性来讲解如何开发 plugin,一起来看看吧!由于 plugin 比 loader 复杂不少,这里我们需要一些前置知识来作为支撑!
我们都知道Plugin是通过监听 webpack 构建过程中发布的 hooks 来实施对应的操作从而影响更改构建逻辑以及生成的产物,而在这个过程中compiler
和compilation
可以说是最核心的两个对象了,其中compiler
可以暴露了整个构建流程的 200+个hooks
,而compilation
则暴露了更细粒度的hooks
。
compiler
对象是一个全局单例,代表了 webpack 从开启到关闭的整个生命周期,负责启动编译和监听文件,而compilation
是每次构建过程的上下文对象,包含当次构建所需要的信息
每次热更新和重新编译都会创建一个新的
compilation
对象,compilation
对象只代表当次编译
我们都知道插件是通过监听 webpack 构建过程中发布的hooks
从而在特定阶段去执行特定功能来达到改变构建逻辑以及产物的目的,而这些都离不开tapable (一个专门用于处理各种发布订阅的库 有同步异步、熔断、循环、瀑布流等钩子),关于了解tapable的使用,这里推荐这篇文章:Webpack tapable 使用研究 - 掘金 (juejin.cn)。
讲完 plugin 的前置知识,接下来就让我们正式开始学习如何开发插件
插件是通过监听 webpack 发布的 hooks 来工作的
- 根据这个特性,我们的 plugin 一定是一个函数或者一个包含
apply()
的对象,这样才可以监听compiler
对象
- 根据这个特性,我们的 plugin 一定是一个函数或者一个包含
传递给插件的
compiler
和compilation
都是同一个引用- 根据此特性,我们知道我们的插件是会影响到其他插件的,所以我们在编写插件的时候应该分析会对其他插件造成啥影响
基于tapable来完成对
hooks
的复杂的订阅以及响应- 编译过程的特定节点会分发特定钩子,插件可以通过这些钩子来执行对应的操作
- 通过 tapable的回调机制以参数形式传递上下文信息
- 可以通过上下文的众多接口来影响构建流程
监听一些具有特定意义的
hook
来影响构建compiler.hooks.compilation
:webpack刚启动完并创建compilation
对象后触发compiler.hooks.make
:webpack开始构建时触发compiler.hooks.done
:webpack 完成编译时触发,此时可以通过stats
对象得知编译过程中的各种信息
善于使用开发工具
- 使用
schema-utils
用于校验参数(关于schema-utils
的使用方法读者可以自行查阅)
- 使用
正确处理插件日志信息以及插件信息
- 使用
stats
汇总插件的统计数据 - 使用
ProgressPlugin
插件的reportProgress
接口上报执行进度 - 通过
compilation.getLogger
获取分级日志管理器 - 使用
compilation.errors/warining
处理异常信息(eslint-webpack-plugin的做法)
- 使用
测试插件
- 通过分析
compilation.error/warn
数组来判断 webpack 是否运行成功 - 分析构建产物判断插件功能是否符合预期
- 通过分析
以上便是如何编写 plugin 所需的知识和常规流程,建议可以阅读一些插件例如eslint-webpack-plugin / DefinePlugin
等插件的源码来更深入地学习插件开发的知识和流程
9.什么是文件指纹?文件指纹有什么作用?怎么用?
概念:文件指纹是指文件打包后的一连串后缀
作用:
- 版本管理: 在发布版本时,通过文件指纹来区分 修改的文件 和 未修改的文件。
- 使用缓存: 浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件。
种类:
Hash
:和整个项目的构建相关,只要项目有修改(compilation
实例改变),Hash
就会更新Contenthash
:和文件的内容有关,只有内容发生改变时才会修改Chunkhash
:和webpack构架的 chunk 有关 不同的entry会构建出不同的chunk (不同ChunkHash
之间的变化互不影响)
如何使用:
- JS 文件:使用
Chunkhash
- CSS 文件:使用
Contenthash
- 图片等静态资源: 使用
hash
生产环境的output为了区分版本变动,通过
Contenthash
来达到清理缓存及时更新的效果,而开发环境中为了加快构建效率,一般不引入Contenthash
10.Babel 的原理
babel 可以将代码转译为想要的目标代码,并且对目标环境不支持的api 自动 polyfill
。而babel实现这些功能的流程是 解析(parse)-转换(transfrom)-生产(generator)
,接下来我们就看看每个流程都做了啥工作
解析
:根据代码生成对应的AST
结构- 进行代码分析,将代码分割成token流(语法单元数组),再根据token流生成对应的
AST
- 进行代码分析,将代码分割成token流(语法单元数组),再根据token流生成对应的
转换
:遍历AST
节点并生成新的AST
节点生成
:根据新的AST
生成目标代码
11.文件监听的原理
开启文件监听后,webpack 会轮询访问文件的最后修改时间,当发现文件修改时间发生变化后,会先缓存起来等到aggregateTimeout
再统一执行
开启文件监听方式:可以在构建时带上--watch
参数或者设置watch:true
,而watchOptions
则可以对监听的细节进行定制
1 | watch: true, |
12.什么是 Source map?如何使用
source map是将编译打包后的代码映射回源码
可以通过devtool配置项来设置,还可以通过SourceMapDevToolPlugin
实现更加精细粒度的控制
devtool配置项和
SourceMapDevToolPlugin
不能同时使用,因为devtool选项已经内置了这些插件,如果同时使用相当于应用了两次插件
devtool | performance | production | quality | comment |
---|---|---|---|---|
(none) | build: fastest rebuild: fastest | yes | bundle | Recommended choice for production builds with maximum performance. |
eval |
build: fast rebuild: fastest | no | generated | Recommended choice for development builds with maximum performance. |
eval-cheap-source-map |
build: ok rebuild: fast | no | transformed | Tradeoff choice for development builds. |
eval-cheap-module-source-map |
build: slow rebuild: fast | no | original lines | Tradeoff choice for development builds. |
eval-source-map |
build: slowest rebuild: ok | no | original | Recommended choice for development builds with high quality SourceMaps. |
cheap-source-map |
build: ok rebuild: slow | no | transformed | |
cheap-module-source-map |
build: slow rebuild: slow | no | original lines | |
source-map |
build: slowest rebuild: slowest | yes | original | Recommended choice for production builds with high quality SourceMaps. |
inline-cheap-source-map |
build: ok rebuild: slow | no | transformed | |
inline-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | |
inline-source-map |
build: slowest rebuild: slowest | no | original | Possible choice when publishing a single file |
eval-nosources-cheap-source-map |
build: ok rebuild: fast | no | transformed | source code not included |
eval-nosources-cheap-module-source-map |
build: slow rebuild: fast | no | original lines | source code not included |
eval-nosources-source-map |
build: slowest rebuild: ok | no | original | source code not included |
inline-nosources-cheap-source-map |
build: ok rebuild: slow | no | transformed | source code not included |
inline-nosources-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | source code not included |
inline-nosources-source-map |
build: slowest rebuild: slowest | no | original | source code not included |
nosources-cheap-source-map |
build: ok rebuild: slow | no | transformed | source code not included |
nosources-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | source code not included |
nosources-source-map |
build: slowest rebuild: slowest | yes | original | source code not included |
hidden-nosources-cheap-source-map |
build: ok rebuild: slow | no | transformed | no reference, source code not included |
hidden-nosources-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | no reference, source code not included |
hidden-nosources-source-map |
build: slowest rebuild: slowest | yes | original | no reference, source code not included |
hidden-cheap-source-map |
build: ok rebuild: slow | no | transformed | no reference |
hidden-cheap-module-source-map |
build: slow rebuild: slow | no | original lines | no reference |
hidden-source-map |
build: slowest rebuild: slowest | yes | original | no reference. Possible choice when using SourceMap only for error reporting purposes. |
这么多的选择,那么我们应该如何使用呢,根据我的实践,我觉得比较好的设置应该是下面这样
- 开发环境:
cheap-eval-source-map
,生产这种source map
速度最快,并且由于开发环境下没有代码压缩,所以不会影响断点调试 - 生产环境:
hidden-source-map
,由于进行了代码压缩,所以并不会占用多大的体积
避免在生产中使用
inline-
和eval-
因为它们会增加 bundle 体积大小 并且降低整体性能
13.HMR(热更新)的原理
这个可以说是webpack的最高频考点之一了,同时也是webpack的难点,也是webpack的核心功能之一!接下来我将带大家先学习如何使用HMR再逐步分析HMR的原理。
如何开启 HMR:
通过设置devServer: {hot: true}
开启 开启后便可以在发生改变后局部刷新改变的部分
原理:
- 使用
webpack-dev-server(WDS)
托管静态资源 同时以Runtime
方式注入HMR客户端代码 - 浏览器加载页面后 与WDS建立
WebSocket
连接 - webpack 监听到文件变化后 增量构建发生变更的模块 并通过WebSocket发送
hash
事件 - 浏览器接收到
hash
事件后 请求manifest
资源文件 确认增量变更范围 - 浏览器加载发生变更的增量模块
- webpack运行时触发变更模块的
module.hot.accept
回调 执行代码变更逻辑 done
:构建完成,更新变化
总结就是webpack将静态资源托管在 WDS 上,而 WDS 又和浏览器通过 webSocket
建立联系,而当webpack监听到文件变化时,就会向浏览器推送更新并携带新的hash
与之前的hash
进行对比,浏览器接收到hash
事件后变化加载变更的增量模块并触发变更模块的 module.hot.accept
回调执行变更逻辑。
(图片来自与范文杰大佬的Webpack 原理系列十:HMR 原理全解析 - 掘金 (juejin.cn),这篇写的很不错,对 HMR 感兴趣的小伙伴推荐阅读)
14.Tree shaking 的原理
相信大家对于Tree shaking都不陌生吧,那你知道如何使用Tree shaking吗?你知道Tree shaking的原理吗?接下来就让我们一一学习
什么是 Tree shaking?
Tree-Shaking 是一种基于 ES Module 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾其它模块使用,并将其删除,以此实现打包产物的优化。
使用 Tree shaking
使用 Tree shaking 的三个必要条件
使用 ESM 规范编写模块代码
配置
optimization.usedExports
为true
启动标记功能启动代码优化功能 可以通过如下方法实现
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
对于使用了
babel-loader
loader 或者根据对代码进行转译的时候,注意应该关闭对于导入/导出语句的转译,因为这会影响到后续的 tree shaking 比如应该将babel-loader
的babel-preset-env
的modules
配置为false
必要条件:
所有导入导出语句只能在模块顶层 且导入导出的模块名必须为字符串常量 不能动态导入的(ESM 模块之间的依赖关系是高度确定的 与运行状态无关 编译工具只需要对 ESM 模块做静态分析就可以从代码字面量中推断出哪些模块值没被使用)
Tree shaking 的原理
Tree shaking 的工作流程可以分为
1.标记哪些导出值没有被使用; 2. 使用Terser将这些没用到的导出语句删除
标记的流程如下:
- make阶段:收集模块导出变量并记录到模块依赖关系图中
- seal阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
- 构建阶段:利用Terser将没有被用到的导出语句删除
开发中如何利用 Tree shaking?
- 避免无作用的重复赋值
- 使用
#pure
标记函数无副作用(这种做法在开源项目的源码中经常出现,如pinia、reactive….等) - 禁止转译 导入/导出语句(使用了
babel-loader
需要将babel-preset-env
的modules
配置为false
) - 使用支持 Tree shaking的包
- 优化导出值的粒度
1 | //正确做法 |
关于 tree shaking 的原理,这里同样推荐范文杰大佬写的原理系列九:Tree-Shaking 实现原理
15.如何用 webpack 来优化项目的性能
说到优化,我们应该想到可以分为开发环境和生产环境的不同优化。
开发环境:开发环境我们需要的是更快的构建速度、模块热替换、更加友好的Source map
通过
cache: { type: 'systemfile'}
开启缓存构建可以加快二次构建的效率通过模块热替换可以做到局部更新变化,提高开发效率
根据设置
devtool: cheap-eval-source-map
在保证构建效率的同时又能进行代码调试使用Thread-loader以多进程的方式运行资源加载逻辑
通过 stats 来分析性能做优化
生产环境:生产环境我们需要的是更小的体积,更稳定又快的性能
- 压缩代码:使用
UglifyJsPlugin
和ParallelUglifyPlugin
来压缩代码 利用cssnano(css-loader? minimize)
来压缩 css - 利用CDN:可以使用CDN来加快对静态资源的访问,提高用户的使用体验
Tree Shaking
: 删除没用到的代码- 提取公共第三方库: 使用
SplitChunksPlugin
插件来进行公共模块抽取 - 使用TerserWebpackPlugin多进程执行代码压缩、uglify 功能
- 压缩代码:使用
16.webpack 构建流程是什么?
相信经过上面的那么多讲解,大家对于 webpack 应该有一个不错的认识,接下来就让我们化零为整,从整体来看看 webpack 整个构建流程究竟发生了什么?话不多说,赶紧开始!
构建流程
- 初始化参数: 从配置文件和
Shell
语句中读取与合并并计算出最终的参数 - 开始编译: 用上一步得到的初始化参数初始化
Complier
对象,加载所有配置的插件,执行compiler
对象的run
方法开始编译流程 - 确定入口: 根据
entry
找出入口文件 - 编译模块:从入口文件开始,根据配置的
loader
对模块进行转译,如果该模块还有依赖的模块,则递归对这些模块进行翻译,通过递归上述操作直到对所有模块都进行转译 - 完成模块编译: 在经过
Loader
翻译完所有模块后,得到了每个模块转译后的内容以及模块之间的依赖关系图(ModuleGraph) - 输出资源: 根据入口和模块之间的依赖关系 生成一个个包含多个模块的
Chunk
, 再把每个Chunk
转换成一个单独的文件加入到输出列表中 - 输出完成: 根据输出项的配置,将文件内容写到文件系统
流程简化:
初始化阶段: 合并计算配置参数,创建Compiler
、Compilation
等基础对象,并初始化Plugin,并最终根据entry
配置,找到所有入口模块
构建模块: 从entry
开始,调用loader
转译对应的模块,调用 Acorn
将代码转换为AST
结构, 遍历AST
从中 构建出完整的模块依赖关系图(递归操作)
生成阶段: 根据entry
配置,根据模块生成一个个chunk
对象,之后转译Chunk
代码并封装为Asset
, 最后写出到文件系统
单次构建过程自上而下按顺序执行 如果启动了
watch
则构建完成后不会退出 webpack 进程 而是持续监听文件内容 发生变化时回到构建阶段重新执行构建
从资源转换角度看
compiler.make
阶段entry
文件以dependence
对象形式加入compilation
的依赖列表 ,dependence
对象记录了entry
的相关信息- 根据
dependency
创建 对应的module
对象,之后读入module
对应的文件内容, 调用loader-runner
对内容做转化, 转化结果若有对其他依赖则继续读入依赖资源, 重复此过程直到所有的依赖均被转换为module
compilation.seal
阶段- 遍历
module
集合, 根据entry
配置以及引入资源的方式, 将module
分配到不同的Chunk
Chunk
之间最终形成ChunkGraph
结构- 遍历
ChunkGraph
调用compilation.emitAssets
方法标记chunk
的输出规则, 及转换为assets
集合
- 遍历
compiler.emitAssets
阶段- 将
assets
写入文件系统
- 将
总结:
以上便是webpack整个构建流程的学习,虽然一眼看过去很多也很复杂,但是相信经过重复学习,我们一定可以熟练掌握的,这是我们深入学习webpack的必经之路
17.你知道哪些优化 webpack 构建的手段或者知识?
梳理完webpack的整体构建流程后,接下来让我们看看日常工作中如何优化webpack的配置来提高我们的性能!!!
使用高版本的 webpack 和node
多进程构建:使用thread-loader(HappyPack不维护了,这里不推荐)
使用Tree shaking 删除多余模块导出
- 配置
optimization.usedExports
为true
启动标记功能 - 启动代码优化功能 可以通过如下方法实现
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
- 配置
使用Scope Hoisting合并模块
- Scope Hoisting用于 将符合条件的多个模块合并到同一个函数空间 中,从而减少产物体积,优化性能。
- 开启方法:
mode = 'production'
开启生产模式- 使用
optimization.concatenateModules
配置项 - 使用
ModuleConcatenationPlugin
插件
开启模块热替换
- 可以通过
devServer:{hot:true}
或者 使用HotModuleReplacementPlugin
开启模块热替换 - 忽略部分很少变化的大文件如node_modules提高构建效率
- 可以通过
监控产物体积
- 监控产物体积可以帮助我们分析项目的性能,避免项目体积过大带来的资源消耗
- 通过 performance 配置项来自定义各种阈值或参数
业内认为一般情况下应该保证关键路径的资源体积始终小于 170kb,如果超过这个大小,可能就需要考虑优化来减小体积
缩小文件的搜索范围
优化 loader 配置:可以通过
test/ include / exclude
来指定文件的 loader 命中的文件范围,可以通过指定include
来使 loader 只处理那些需要被处理的模块优化
resolves.modules
配置:用于指定webpack去哪些路径下寻找第三方模块- 例如当所有第三方模块都放在 node_modules时 可以配置
resolve: {modules: path.resolve(__dirname, 'node_modules')}
- 例如当所有第三方模块都放在 node_modules时 可以配置
优化 resolve.mainFilelds 配置:用于配置第三方模块使用哪个入口文件 -为了减少搜索范围,可以使用
resolve: {mainFields: ['main']}
-如果想优先使用 ESModule 版本的话,设置resolve: {mainFields: ['jsnext:main', 'main']}
配置
resolve.alias
:resolve.alias
通过别名将原导入路径映射成一个新的导入路径配置
resolve.extensions
: 引入文件时省略数组内的后缀名配置
resolve.noParse
: 省略对指定文件的处理,如(JQuery等大型库)可以提高构建性能(被忽略的文件不能包含导入语句如require / import / define
)
设置环境
- 设置
mode: production/development
可以开启对应的优化
- 设置
代码压缩
- 使用
terser-webpack-plugin
压缩 ES6 代码 - 使用
ParalleUglifyPlugin
多进程压缩代码 - 使用
css-minimize-webpack-plugin
对 css 代码进行压缩 - 使用
html-minimizer-webpack-plugin
压缩 html 代码
- 使用
使用CDN加速
- 将静态资源存储在 CDN上可以加快对静态资源的访问速度,减少流量消耗
- 通过
output.publicPath
设置 JavaScript 文件地址 - 通过
WebPlugin.stylePublicPath
设置 CSS 文件的地址 - 通过
css-loader.publicPath
设置被 CSS 导入的资源的地址
- 通过
- 将静态资源存储在 CDN上可以加快对静态资源的访问速度,减少流量消耗
为不同的环境配置对应的配置文件
- 使用
webpack-merge
分别书写development/ production / test环境下的配置文件
- 使用
使用缓存构建
- 配置
cache: {type: 'systemfile'}
开启构建缓存,可以大幅提高二次构建的速度
- 配置
使用 DllPlugin:使用DllPlugin进行分包,使用
DllReferencePlugin
引用mainfext.json
, 通过将一些很少变动的代码先打包成静态资源,避免重复编译来提高构建性能提取公共代码
- 使用
splitChunkPlugin
提取公共代码,减少代码体积 (webpack3 通过CommonsChunkPlugin
)
- 使用
动态 Polyfill:使用
polyfill-service
只返回给用户需要的polyfill使用可视化工具来分析性能
- 使用UnusedWebpackPlugin分析未被使用到的文件
- 使用Webpack Dashboard 以命令行的形式输出编译过程的各种信息
- 使用Webpack Bundle Analyzer分析重复的模块或者没被用到的模块
- 使用
--json=stats,json
将构建过程中的信息都输出到指定文件 - 使用Webpack Analysis:官方提供的可视化分析工具
以上便是我们开发中可以使用的用来优化 webpack 的配置和手段,已经可以满足大部分情况了,如果评论区有同学有其他好的建议,可以在评论区分享讨论!
总结:
好了,不知不觉已经写了接近 9000 个字了,笔者近期学习webpack,写这篇的目的也是为了复习和牢固自己的知识,同时帮助有需要的同学,webpack的学习是一个漫长困难的过程,希望我们可以坚持下来,文章中若有出现错误,请各位在评论区指出一起学习讨论,笔者后续将更新其他关于webpack和vite的文章,感兴趣的同学可以点个关注一起学习!!!
ps:创作不易,请点赞支持哈哈哈!!!
引用文章/ 资源: