mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-11-16 14:13:47 +08:00
fix img
This commit is contained in:
@@ -169,7 +169,7 @@ function hide_canvas() {
|
||||
<p>通常,一个中高级前端工程师,除了要完成业务功能开发目标外,还要对所开发项目的效率、性能、质量等工程化维度去制定和实施技术优化目标,其中以提升效率为目标的优化技术和工具就属于<strong>效率工程化</strong>的范畴。</p>
|
||||
<p>对于公司而言,团队效率可以直接带来人工投入产出比的提升,因此效率提升通常会被作为技术层面的一个重点优化方向。而在面试中,对效率工程化的理解程度和实践中的优化产出情况,也是衡量前端工程师能力高低的常见标准。</p>
|
||||
<p>例如,在拉勾网搜索前端相关职位,可以看到中高级以上的前端工程师岗位需求中大都会要求熟练掌握 webpack 构建工具、具备开发效率实践经验等。只有具备这方面的能力,你才能应对和优化复杂项目,保证团队高效产出。</p>
|
||||
<p><img src="assets/Ciqc1F8w63CAOEM-AATM8EeT1gM854.png" alt="image" /></p>
|
||||
<p><img src="assets/Ciqc1F8w63CAOEM-AATM8EeT1gM854.png" alt="png" /></p>
|
||||
<p>拉勾网搜索“前端效率工程”的岗位情况</p>
|
||||
<p>然而,大部分时间都投身在业务开发中的前端同学,在效率工程化方面经常面临很多困扰:</p>
|
||||
<ul>
|
||||
|
||||
@@ -180,7 +180,7 @@ function hide_canvas() {
|
||||
<p>那么下面我们先来谈谈脚手架工具究竟是什么。</p>
|
||||
<h3>什么是脚手架</h3>
|
||||
<p>说到<strong>脚手架(Scaffold)</strong> 这个词,相信你并不陌生,它原本是建筑工程术语,指为了保证施工过程顺利而搭建的工作平台,它为工人们在各层施工提供了基础的功能保障。</p>
|
||||
<p><img src="assets/Ciqc1F8w7KGAc5KTAFjMHp-GUzQ575.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/Ciqc1F8w7KGAc5KTAFjMHp-GUzQ575.png" alt="png" /></p>
|
||||
<p>而在<strong>软件开发领域</strong>,脚手架是指通过各种工具来生成项目基础代码的技术。通过脚手架工具生成后的代码,通常已包含了项目开发流程中所需的<strong>工作目录内的通用基础设施</strong>,使开发者可以方便地将注意力集中到业务开发本身。</p>
|
||||
<p>那么对于日常的前端开发流程来说,项目内究竟有哪些部分属于通用基础设施呢?让我们从项目创建的流程说起。对于一个前端项目来说,一般在进入开发之前我们需要做的准备有:</p>
|
||||
<ol>
|
||||
@@ -216,20 +216,20 @@ README.md 8) 默认文档文件
|
||||
</code></pre>
|
||||
<p>而通过脚手架工具,我们就能免去人工处理上的环节,轻松地搭建起项目的初始环境,直接进入到业务开发中。接下来我们就先来看一下前端领域的几个典型脚手架工具,了解这几个脚手架所代表的不同设计理念,接着我们会重点分析两个代表性脚手架工具包内的技术细节,以便在工作中更能得心应手地使用和优化。</p>
|
||||
<h3>三种代表性的前端脚手架工具</h3>
|
||||
<p><img src="assets/CgqCHl8xA46AOLMIAABL15AXwak581.png" alt="7.png" /></p>
|
||||
<p><img src="assets/CgqCHl8xA46AOLMIAABL15AXwak581.png" alt="png" /></p>
|
||||
<h4>Yeoman</h4>
|
||||
<p><img src="assets/Ciqc1F8xA0KAKf0uAABJG0oh-Qs463.png" alt="6.png" /></p>
|
||||
<p><img src="assets/Ciqc1F8xA0KAKf0uAABJG0oh-Qs463.png" alt="png" /></p>
|
||||
<p>[图:logo-yeoman]</p>
|
||||
<p><a href="https://yeoman.io/">Yeoman</a> 是前端领域内较早出现的脚手架工具,它由 Google I/O 在 2012 年首次发布。Yeoman 提供了基于特定生成器(Generator)来创建项目基础代码的功能。时至今日,在它的网站中能找到超过 5600 个不同技术栈的代码生成器。</p>
|
||||
<p>作为早期出现在前端领域的脚手架工具,它没有限定具体的开发技术栈,提供了足够的开放性和自由度,但也因此缺乏某一技术栈的深度集成和技术生态。随着前端技术栈的日趋复杂化,人们更倾向于选择那些以具体技术栈为根本的脚手架工具,而 Yeoman 则更多用于一些开发流程里特定片段代码的生成。</p>
|
||||
<h4>Create-React-App</h4>
|
||||
<p><img src="assets/CgqCHl8xAqOAAmQFAAAlZny__YI029.png" alt="4.png" /></p>
|
||||
<p><img src="assets/CgqCHl8xAqOAAmQFAAAlZny__YI029.png" alt="png" /></p>
|
||||
<p>[图:logo-create-react-app]</p>
|
||||
<p><a href="https://create-react-app.dev/">Create React App</a>(后简称 CRA )是 Facebook 官方提供的 React 开发工具集。它包含了 create-react-app 和 react-scripts 两个基础包。其中 create-react-app 用于选择脚手架创建项目,而 react-scripts 则作为所创建项目中的运行时依赖包,提供了封装后的项目启动、编译、测试等基础工具。</p>
|
||||
<p>正如官方网站中所说的,CRA 带来的最大的改变,是将一个项目开发运行时的各种配置细节完全封装在了一个 react-scripts 依赖包中,这大大降低了开发者,尤其是对 webpack 等构建工具不太熟悉的开发者上手开发项目的学习成本,也降低了开发者自行管理各配置依赖包的版本所需的额外测试成本。</p>
|
||||
<p>但事情总有两面性,这种近乎黑盒的封装在初期带来便利的同时,也为后期的用户自定义优化带来了困难。虽然官方也提供了 eject 选项来将全部配置注入回项目,但大部分情况下,为了少量优化需求而放弃官方提供的各依赖包稳定升级的便利性,也仍不是一个好的选择。在这种矛盾之下,在保持原有特性的情况下提供自定义配置能力的工具 <a href="https://github.com/timarney/react-app-rewired/">react-rewired</a> 和 <a href="https://github.com/arackaf/customize-cra">customize-cra</a> 应运而生。</p>
|
||||
<h4>Vue CLI</h4>
|
||||
<p><img src="assets/CgqCHl8xAyuASVwWAAAcGi1IGPY858.png" alt="5.png" /></p>
|
||||
<p><img src="assets/CgqCHl8xAyuASVwWAAAcGi1IGPY858.png" alt="png" /></p>
|
||||
<p>[图:logo-vue-cli]</p>
|
||||
<p>正如 Create-React-App 在 React 项目开发中的地位, Vue 项目的开发者也有着自己的基础开发工具。Vue CLI 由 Vue.js 官方维护,其定位是 Vue.js 快速开发的完整系统。完整的 Vue CLI 由三部分组成:作为全局命令的 @vue/cli、作为项目内集成工具的 @vue/cli-service、作为功能插件系统的 @vue/cli-plugin-。</p>
|
||||
<p>Vue CLI 工具在设计上吸取了 CRA 工具的教训,在保留了创建项目开箱即用的优点的同时,提供了用于覆盖修改原有配置的自定义构建配置文件和其他工具配置文件。</p>
|
||||
@@ -246,10 +246,10 @@ README.md 8) 默认文档文件
|
||||
<p>还是以上面的 CRA 和 Vue CLI 为例,除了通过脚手架模板生成项目之外,项目内部分别使用 react-scripts 和 vue-cli-service 作为开发流程的集成工具。接下来,我们先来对比下这两个工具在开发与生产环境命令中都使用了哪些配置项,其中一些涉及效率的优化项在后面的课程中还会详细介绍。</p>
|
||||
<h4>webpack loaders</h4>
|
||||
<p>从下面表格中我们可以发现,在一般源文件的处理器使用方面,两个脚手架工具大同小异,对于 babel-loader 都采用了缓存优化,Vue 中还增加了多线程的支持。在样式和其他类型文件的处理上 Vue 默认支持更多的文件类型,相应的,在 CRA 模板下如果需要支持对应文件就需要使用 customize-cra 等工具来添加新处理模块。</p>
|
||||
<p><img src="assets/CgqCHl8w_FmAFzFAAAC4LtmVvTE237.png" alt="1.png" /></p>
|
||||
<p><img src="assets/CgqCHl8w_FmAFzFAAAC4LtmVvTE237.png" alt="png" /></p>
|
||||
<h4>webpack plugins</h4>
|
||||
<p>在与构建核心功能相关的方面(html、env、hot、css extract、fast ts check),两者使用的插件相同,而在其他一些细节功能上各有侧重,例如 React 的 inline chunk 和 Vue 的 preload。</p>
|
||||
<p><img src="assets/CgqCHl8w_GeAFNlqAAFvtG9_RV8768.png" alt="2.png" /></p>
|
||||
<p><img src="assets/CgqCHl8w_GeAFNlqAAFvtG9_RV8768.png" alt="png" /></p>
|
||||
<p>(<a href="https://dutzi.party/react-fast-refresh/">第三方工具</a>)</p>
|
||||
<h4>webpack.optimize</h4>
|
||||
<p>两者在代码优化配置中相同的部分包括:都使用 TerserPlugin 压缩JavaScript, 都使用 splitChunks 做自动分包 (参数不同)。CSS 的压缩分别采用上面表格中的 OptimizeCssAssetsWebpackPlugin 和 OptimizeCssNanoPlugin 。react-scripts 中还开启了 runtimeChunk 以优化缓存。</p>
|
||||
@@ -297,7 +297,7 @@ module.exports = class extends Generator {
|
||||
</code></pre>
|
||||
<p>writing 和 install 是 Yeoman 运行时上下文的两个阶段,在例子中,当我们执行下面的创建项目命令时,依次将生成器中模板目录内的所有文件复制到创建目录下,然后执行安装依赖。</p>
|
||||
<p>在完成生成器的基本功能后,我们就可以通过在生成器目录里 npm link ,将对应生成器包挂载到全局依赖下,然后进入待创建项目的目录中,执行 yo 创建命令即可。 (如需远程安装,则需要先将生成器包发布到 npm 仓库中,支持发布到 @scope/generator-[name] 。)</p>
|
||||
<p><img src="assets/Ciqc1F8w7aaASHMtAABB2xCfKLM444.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/Ciqc1F8w7aaASHMtAABB2xCfKLM444.png" alt="png" /></p>
|
||||
<p>至此,制作 Yeoman 的生成器来定制项目模板的基本功能就完成了。除了基本的复制文件和安装依赖外, Yeoman 还提供了很多实用的功能,例如编写用户交互提示框或合成其他生成器等,可供开发者定制功能体验更完善的脚手架生成器。</p>
|
||||
<h4>为 create-react-app 创建自定义模板</h4>
|
||||
<p>为 create-react-app 准备的自定义模板在模式上较为简单。作为一个最简化的 CRA 模板,模板中包含如下必要文件:</p>
|
||||
|
||||
@@ -220,7 +220,7 @@ package.json
|
||||
}
|
||||
</code></pre>
|
||||
<p>当我们执行 npm run dev:reload,从日志中可以看到本地服务 http://localhost:8080/ 已启动,然后我们在浏览器中输入网址 http://localhost:8080/index.html (也可以在 devServer 的配置中加入 open 和 openPage 来自动打开网页)并打开控制台网络面板,可以看到在加载完页面和页面中引用的 js 文件后,服务还加载了路径前缀名为 /sockjs-node 的 websocket 链接,如下图:</p>
|
||||
<p><img src="assets/Ciqc1F807EuAEAxNAADYSkon5ms681.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/Ciqc1F807EuAEAxNAADYSkon5ms681.png" alt="png" /></p>
|
||||
<p>通过这个 websocket 链接,就可以使打开的网页和本地服务间建立持久化的通信。当源代码发生变更时,我们就可以通过 Socket 通知到网页端,网页端在接到通知后会自动触发页面刷新。</p>
|
||||
<p>到了这里,在使用体验上我们似乎已经达到预期的效果了,但是在以下场景中仍然会遇到阻碍:在开发调试过程中,我们可能会在网页中进行一些操作,例如输入了一些表单数据想要调试错误提示的样式、打开了一个弹窗想要调试其中按钮的位置,然后切换回编辑器,修改样式文件进行保存。可是当我们再次返回网页时却发现,网页刷新后,之前输入的内容与打开的弹窗都消失了,网页又回到了初始化的状态。于是,我们不得不再次重复操作才能确认改动后的效果。对于这个问题,又该如何解决呢?</p>
|
||||
<h4>Hot Module Replacement</h4>
|
||||
@@ -254,12 +254,12 @@ package.json
|
||||
</code></pre>
|
||||
<p>在上面的代码改动中,我们只是在源码部分新增导入了一个简单的 CSS 文件,用于演示热替换的效果。在配置文件中,首先我们在 devServer 配置中新增了 hot:true,其次,新增 module 的配置,使用 style-loader 和 css-loader 来解析导入的 CSS 文件。其中 css-loader 处理的是将导入的 CSS 文件转化为模块供后续 Loader 处理;而 style-loader 则是负责将 CSS 模块的内容在运行时添加到页面的 style 标签中。</p>
|
||||
<p>当我们执行 npm run dev:hmr 命令,可以看到页面控制台的网络面板与上个示例并无区别,而在审查元素面板中可以看到源码中的 CSS 被添加到了页面头部的新增 style 标签中。</p>
|
||||
<p><img src="assets/Ciqc1F807FmAZaa4AAEDscUHKXk227.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/Ciqc1F807FmAZaa4AAEDscUHKXk227.png" alt="png" /></p>
|
||||
<p>而当修改源码中 CSS 的样式后,再回到网页端,我们则会发现这样一些变化:</p>
|
||||
<p>首先在网络面板中,只是新增了两个请求:hot-update.json 和 hot-update.js,而不像上一个立即刷新的示例中那样,会刷新页面重载所有请求。</p>
|
||||
<p><img src="assets/CgqCHl807GGAYoLGAABmQj838Ag823.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/CgqCHl807GGAYoLGAABmQj838Ag823.png" alt="png" /></p>
|
||||
<p>其次,在审查元素面板中我们可以看到,在页面的头部新增了 hot-update.js,并替换了原先 style 标签中的样式内容。</p>
|
||||
<p><img src="assets/CgqCHl807GeAFYunAAEuKSDazpo633.png" alt="Drawing 3.png" /></p>
|
||||
<p><img src="assets/CgqCHl807GeAFYunAAEuKSDazpo633.png" alt="png" /></p>
|
||||
<p>正如我们所见,对于代码中引入的样式文件,可以通过上述设置来开启热替换。但是有同学也许会问,我们为什么不像上一个例子中那样改动 JS 的内容(例如改动显示的文本)来观察热替换的效果呢?原因在于,简单改动 JS 中的显示文本并不能达到热替换的效果。尽管网络端同样新增了 hot-update.json 和 hot-update.js,但紧随其后的是如上一个示例一般的刷新了整个页面。</p>
|
||||
<p>那么,为什么导入的 CSS 能触发模块热替换,而 JS 文件的内容修改就失效了呢?要回答这个问题,我们还得从 webpack 的热更新原理说起。</p>
|
||||
<h3>webpack 中的热更新原理</h3>
|
||||
@@ -269,7 +269,7 @@ package.json
|
||||
<li>instant reload 示例中体现的,浏览器网页端与本地服务器端的 Websocket 通信。</li>
|
||||
<li>hmr 示例中体现的,也即是最核心的,模块解析与替换功能。</li>
|
||||
</ol>
|
||||
<p><img src="assets/Ciqc1F82OZmAFYuKAAC7WNDPQB4766.png" alt="3.png" /></p>
|
||||
<p><img src="assets/Ciqc1F82OZmAFYuKAAC7WNDPQB4766.png" alt="png" /></p>
|
||||
<p>也就是说在这三种技术中,我们可以基于 Node.js 中提供的文件模块 fs.watch 来实现对文件和文件夹的监控,同样也可以使用 sockjs-node 或 socket.io 来实现 Websocket 的通信。而在这里,我们重点来看下第三种, webpack 中的模块解析与替换功能。</p>
|
||||
<h4>webpack 中的打包流程</h4>
|
||||
<p>在讲 webpack 的打包流程之前我们先解释几个 webpack 中的术语:</p>
|
||||
@@ -286,8 +286,8 @@ package.json
|
||||
<li>webpack 通过入口点(<strong>entry point</strong>)递归处理各模块引用关系,最后输出为一个或多个产物包 js(bundle) 文件。</li>
|
||||
<li>每一个入口点都是一个块组(<strong>chunk group</strong>),在不考虑分包的情况下,一个 chunk group 中只有一个 <strong>chunk</strong>,该 chunk 包含递归分析后的所有模块。每一个 chunk 都有对应的一个打包后的输出文件(<strong>asset/bundle</strong>)。</li>
|
||||
</ol>
|
||||
<p><img src="assets/CgqCHl82OaWAMXDLAABdNTOV1dY952.png" alt="4.png" /></p>
|
||||
<p><img src="assets/CgqCHl807JGAPDOBAAJufukFuEU205.png" alt="Drawing 6.png" /></p>
|
||||
<p><img src="assets/CgqCHl82OaWAMXDLAABdNTOV1dY952.png" alt="png" /></p>
|
||||
<p><img src="assets/CgqCHl807JGAPDOBAAJufukFuEU205.png" alt="png" /></p>
|
||||
<p>在上面的 hmr 示例中,从 entry 中的 './src/index1.js' 到打包产物的 dist/main.js,以模块的角度而言,其基本流程是:</p>
|
||||
<ol>
|
||||
<li>唯一 entry 创建一个块组(chunk group), name 为 main,包含了 ./src/index1.js 这一个模块。</li>
|
||||
@@ -297,7 +297,7 @@ package.json
|
||||
<li>依次类推,直到将所有依赖的模块均打入到 chunk 中,最后输出名为 main.js 的产物(我们称为 Asset 或 Bundle)。</li>
|
||||
</ol>
|
||||
<p>上述流程的结果我们可以在预览页面中控制台的 Sources 面板中看到,这里,我们重点看经过 style-loader 处理的 style.css 模块的代码:</p>
|
||||
<p><img src="assets/CgqCHl807JuAERI6AAMPF3IKpN0961.png" alt="Drawing 7.png" /></p>
|
||||
<p><img src="assets/CgqCHl807JuAERI6AAMPF3IKpN0961.png" alt="png" /></p>
|
||||
<h4>style-loader 中的热替换代码</h4>
|
||||
<p>我们简化一下上述控制台中看到的 style-loader 处理后的模块代码,只看其热替换相关的部分。</p>
|
||||
<pre><code>//为了清晰期间,我们将模块名称注释以及与热更新无关的逻辑省略,并将 css 内容模块路径赋值为变量 cssContentPath 以便多处引用,实际代码可从示例运行时中查看
|
||||
|
||||
@@ -167,7 +167,7 @@ function hide_canvas() {
|
||||
<p>那么除了热更新以外,项目的开发环境还有哪些在影响着我们的开发效率呢?在过去的工作中,公司同事就曾问过我一个问题:为什么我的项目在开发环境下每次构建还是很卡?每次保存完代码都要过 1~2 秒才能看到效果,这是怎么回事呢?其实这里面的原因主要是这位同事在开发时选择的 Source Map 设定不对。今天我们就来具体讨论下这个问题。首先,什么是 <strong>Source Map</strong> 呢?</p>
|
||||
<h3>什么是 Source Map</h3>
|
||||
<p>在前端开发过程中,通常我们编写的源代码会经过多重处理(编译、封装、压缩等),最后形成产物代码。于是在浏览器中调试产物代码时,我们往往会发现代码变得面目全非,例如:</p>
|
||||
<p><img src="assets/Ciqc1F85_FmAA4UeAABWNiHqsWQ595.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/Ciqc1F85_FmAA4UeAABWNiHqsWQ595.png" alt="png" /></p>
|
||||
<p>因此,我们需要一种在调试时将产物代码显示回源代码的功能,<strong>source map</strong> 就是实现这一目标的工具。</p>
|
||||
<p>source-map 的基本原理是,在编译处理的过程中,在生成产物代码的同时生成产物代码中被转换的部分与源代码中相应部分的映射关系表。有了这样一张完整的映射表,我们就可以通过 Chrome 控制台中的"Enable Javascript source map"来实现调试时的显示与定位源代码功能。</p>
|
||||
<p>对于同一个源文件,根据不同的目标,可以生成不同效果的 source map。它们在<strong>构建速度</strong>、<strong>质量</strong>(反解代码与源代码的接近程度以及调试时行号列号等辅助信息的对应情况)、<strong>访问方式</strong>(在产物文件中或是单独生成 source map 文件)和<strong>文件大小</strong>等方面各不相同。在开发环境和生产环境下,我们对于 source map 功能的期望也有所不同:</p>
|
||||
@@ -230,7 +230,7 @@ if (options.devtool.includes("source-map")) {
|
||||
<p>通过上面的代码分析,我们了解了不同参数在 Webpack 运行时起到的作用。那么这些不同参数组合下的各种预设对我们的 source map 生成又各自会产生什么样的效果呢?下面我们通过示例来看一下。</p>
|
||||
<h3>不同预设的示例结果对比</h3>
|
||||
<p>下面,以课程示例代码 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/03_develop_environment">03_develop_environment</a> 为例,我们来对比下几种常用预设的差异(为了使时间差异更明显,示例中引入了几个大的类库文件):</p>
|
||||
<p><img src="assets/Ciqc1F87kyiAZvHdAAIGvohk2F4144.png" alt="12.png" /></p>
|
||||
<p><img src="assets/Ciqc1F87kyiAZvHdAAIGvohk2F4144.png" alt="png" /></p>
|
||||
<blockquote>
|
||||
<p>*注1:“/”前后分别表示产物 js 大小和对应 .map 大小。
|
||||
*注2:“/”前后分别表示初次构建时间和开启 watch 模式下 rebuild 时间。对应统计的都是 development 模式下的笔者机器环境下几次构建时间的平均值,只作为相对快慢与量级的比较。</p>
|
||||
@@ -251,19 +251,19 @@ if (options.devtool.includes("source-map")) {
|
||||
<ul>
|
||||
<li>源码且包含列信息</li>
|
||||
</ul>
|
||||
<p><img src="assets/CgqCHl85_KuANSVfAADSE8VO7Qg572.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/CgqCHl85_KuANSVfAADSE8VO7Qg572.png" alt="png" /></p>
|
||||
<ul>
|
||||
<li>源码不包含列信息</li>
|
||||
</ul>
|
||||
<p><img src="assets/Ciqc1F85_LCAMTlgAADhqpZ4v9o628.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/Ciqc1F85_LCAMTlgAADhqpZ4v9o628.png" alt="png" /></p>
|
||||
<ul>
|
||||
<li>Loader转换后代码</li>
|
||||
</ul>
|
||||
<p><img src="assets/CgqCHl85_LqAPrYzAADfmUwS_JE006.png" alt="Drawing 3.png" /></p>
|
||||
<p><img src="assets/CgqCHl85_LqAPrYzAADfmUwS_JE006.png" alt="png" /></p>
|
||||
<ul>
|
||||
<li>生成后的产物代码</li>
|
||||
</ul>
|
||||
<p><img src="assets/CgqCHl85_MGAHhmMAAKGwvDeXIM418.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/CgqCHl85_MGAHhmMAAKGwvDeXIM418.png" alt="png" /></p>
|
||||
<h4>开发环境下 Source Map 推荐预设</h4>
|
||||
<p>在这里我们对开发环境下使用的推荐预设做一个总结(生产环境的预设我们将在之后的构建效率篇中再具体分析):</p>
|
||||
<ul>
|
||||
@@ -295,7 +295,7 @@ if (options.devtool.includes("source-map")) {
|
||||
...
|
||||
</code></pre>
|
||||
<p>在上面的示例中,我们将 devtool 设为 false,而直接使用 EvalSourceMapDevToolPlugin,通过传入 module: true 和 column:false,达到和预设 eval-cheap-module-source-map 一样的质量,同时传入 exclude 参数,排除第三方依赖包的 source map 生成。保存设定后通过运行可以看到,在文件体积减小(尽管开发环境并不关注文件大小)的同时,再次构建的速度相比上面表格中的速度提升了将近一倍,达到了最快一级。</p>
|
||||
<p><img src="assets/CgqCHl85_N2AUkcpAAEqvMKhgVQ549.png" alt="Drawing 5.png" /></p>
|
||||
<p><img src="assets/CgqCHl85_N2AUkcpAAEqvMKhgVQ549.png" alt="png" /></p>
|
||||
<p>类似这样的优化可以帮助我们在一些大型项目中,通过自定义设置来获取比预设更好的开发体验。</p>
|
||||
<h3>总结</h3>
|
||||
<p>在今天这一课时中,我们主要了解了提升开发效率的另一个重要工具——source map 的用途和使用方法。我们分析了 Webpack 中 devtool 的各种参数预设的组合规则、使用效果及其背后的原理。对于开发环境,我们根据一组示例对比分析来了解通常情况下的最佳选择,也知道了如何直接使用插件来达到更细致的优化。</p>
|
||||
|
||||
@@ -253,7 +253,7 @@ faker.fake("{{name.lastName}}, {{name.firstName}} {{name.suffix}}")
|
||||
<li>支持 Swagger 多种接口描述的数据导入与导出。</li>
|
||||
<li>支持部署到内网服务以及自定义插件。</li>
|
||||
</ol>
|
||||
<p><img src="assets/CgqCHl8_hPuAE9TjAALKNihxbeU968.png" alt="image.png" /></p>
|
||||
<p><img src="assets/CgqCHl8_hPuAE9TjAALKNihxbeU968.png" alt="png" /></p>
|
||||
<h4>Apifox</h4>
|
||||
<p><a href="https://www.apifox.cn/">Apifox</a> 是一个桌面应用类的接口管理工具。与 YApi 相比,除了使用方式不同外,其主要特点还包括:</p>
|
||||
<ol>
|
||||
@@ -262,7 +262,7 @@ faker.fake("{{name.lastName}}, {{name.firstName}} {{name.suffix}}")
|
||||
<li>Mock 数据功能方面支持自定义期望,支持自定义占位符规则等。</li>
|
||||
<li>支持生成自动业务代码和接口请求代码,支持自定义代码模板等。</li>
|
||||
</ol>
|
||||
<p><img src="assets/CgqCHl8_hQ6AR4HvAAWFAyE_iQ0413.png" alt="image" /></p>
|
||||
<p><img src="assets/CgqCHl8_hQ6AR4HvAAWFAyE_iQ0413.png" alt="png" /></p>
|
||||
<p>以上两种接口管理工具都包含了提供对应接口的 Mock 服务的能力。相比于单独提供生成 Mock 数据能力的 Mock.js 和 Faker.js,这类工具解决了接口定义与 Mock 数据脱离的问题:</p>
|
||||
<ol>
|
||||
<li>在接口定义阶段,支持后端服务内定义的 Swagger 等 OPEN API 风格的接口定义数据直接导入生成接口文档,也支持在工具界面内填写字段创建,创建时支持设定返回值的 Mock 描述。</li>
|
||||
|
||||
@@ -184,7 +184,7 @@ function hide_canvas() {
|
||||
<li>几个主流的 UI 库的使用情况是: Bootstrap4、 Antd 和 iView 使用 Less, ElementUI 使用 Sass。</li>
|
||||
<li>此外,<a href="https://ashleynolan.co.uk/blog/frontend-tooling-survey-2019-results">2019 年的前端工具调查</a>也显示上面三种工具的使用人数依次递减,即使用人数最多的是 Sass、 其次是 Less、最后是 Stylus。如下图所示:</li>
|
||||
</ul>
|
||||
<p><img src="assets/Ciqc1F9DimmAXh2DAAGk0RtNvg0192.png" alt="image.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9DimmAXh2DAAGk0RtNvg0192.png" alt="png" /></p>
|
||||
<p>在<strong>实现的功能</strong>方面:这三种 CSS 的预处理语言都实现了变量(Variables)、嵌套(Nesting)、混合 (Mixins)、运算(Operators)、父选择器引用(Parent Reference)、扩展(Extend)和大量内建函数(Build-in Functions)。但是与另外两种语言相比,Less 缺少自定义函数的功能(可以使用 Mixins 结合 Guard 实现类似效果),而 Stylus 提供了超过 60 个内建函数,更有利于编写复杂的计算函数。</p>
|
||||
<p>在<strong>语法方面</strong>:<strong>Sass</strong> 支持 .scss 与 .sass 两种文件格式。差异点是 .scss 在语法上更接近 CSS,需要括号、分号等标识符,而 Sass 相比之下,在语法上做了简化,去掉了 CSS 规则的括号分号等 (增加对应标识符会导致报错) 。<strong>Less</strong> 的整体语法更接近 .scss。<strong>Stylus</strong> 则同时支持类似 .sass 的精简语法和普通 CSS 语法。语法细节上也各不相同,示例如下:</p>
|
||||
<pre><code>//scss
|
||||
@@ -232,7 +232,7 @@ html
|
||||
<h4>Snippet</h4>
|
||||
<p><strong>Snippet</strong> 是指开发过程中用户在 IDE 内使用的可复用代码片段,大部分主流的 IDE 中都包含了 Snippet 的功能,就像使用脚手架模板生成一个项目的基础代码那样,开发者可以在 IDE 中通过安装扩展来使用预设的片段,也可以自定义代码片段,并在之后的开发中使用它们。</p>
|
||||
<p>以 VS Code 为例,在扩展商店中搜索 Snippet 可以找到各种语言的代码片段包。例如下图中的<a href="https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets">Javascript(ES6) code snippets</a>,提供了 JavaScript 常用的 import 、console 等语句的缩写。安装后,输入缩写就能快速生成对应语句。</p>
|
||||
<p><img src="assets/CgqCHl9Dip2AHL4kAAOw0HH_-JE819.png" alt="image" /></p>
|
||||
<p><img src="assets/CgqCHl9Dip2AHL4kAAOw0HH_-JE819.png" alt="png" /></p>
|
||||
<p>除了使用扩展包自带的预设片段外,IDE 还提供了用户自定义代码片段的功能。以 VS Code 为例,通过选择菜单中的"Code-首选项-用户片段",即可弹出选择或新增代码片段的弹窗,选择或创建对应 .code-snippets 文件后即可编辑自定义的片段。就像下面示例代码中我们创建了一个简单的生成 TypeScript 接口代码的片段,保存后在项目代码里输入 tif 后再按回车,就能看到对应生成的片段了:</p>
|
||||
<pre><code>//sample.code-snippets
|
||||
{
|
||||
@@ -255,7 +255,7 @@ interface IFName {
|
||||
<p>通过上面演示的自定义功能,我们就可以编写自身开发常用的个性预设片段了。相比使用第三方预设,自定义的预设更灵活也更便于记忆。两者相结合,能够大大提升我们编码的效率。同时,针对实际业务场景定制的自定义片段文件,也可以在团队内共享和共同维护,以提升团队整体的效率。</p>
|
||||
<h4>Emmet</h4>
|
||||
<p><a href="https://emmet.io/">Emmet</a>****(前身为 Zen Coding)是一个面向各种编辑器(几乎所有你见过的前端代码编辑器都支持该插件)的 Web 开发插件,用于高速编写和编辑结构化的代码,例如 Html 、 Xml 、 CSS 等。从下面官方的示例图中可以看到,简单的输入 ! 或 html:5 再输入 tab 键,编辑器中就会自动生成完整的 html5 基本标签结构(完整的缩写规则列表可在<a href="https://github.com/emmetio/emmet/tree/master/snippets">官方配置</a>中查找):</p>
|
||||
<p><img src="assets/CgqCHl9DiriAZmLsAADAvMd_sH0503.gif" alt="emmet-sample.gif" /></p>
|
||||
<p><img src="assets/CgqCHl9DiriAZmLsAADAvMd_sH0503.gif" alt="png" /></p>
|
||||
<p>它的主要功能包括:</p>
|
||||
<ul>
|
||||
<li>缩写代码块:
|
||||
|
||||
@@ -194,7 +194,7 @@ function hide_canvas() {
|
||||
<li>通过制定用于编写的<strong>JSON 语法图式(JSON Schema)</strong>,以及封装能够渲染对应 JSON 语法树的运行时工具集,就可以提升开发效率,降低开发技术要求。</li>
|
||||
</ol>
|
||||
<p>下图中的代码就是组件语法树示例(完整的示例代码参见 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/07_low_code">07_low_code</a>),我们通过编写一个简单的 JSON 语法树以及对应的编译器,来展示低代码开发的模式。</p>
|
||||
<p><img src="assets/CgqCHl9MyGOAKUrZAAFHKI-ma8o592.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/CgqCHl9MyGOAKUrZAAFHKI-ma8o592.png" alt="png" /></p>
|
||||
<h4>编写 JSON 开发的高效性</h4>
|
||||
<p>编写 JSON 语法树开发的高效性体现在:</p>
|
||||
<ol>
|
||||
@@ -213,7 +213,7 @@ function hide_canvas() {
|
||||
<p>针对编写 JSON 过程中的输入效率、记忆成本和可维护性等问题,许多低代码工具进一步提供了可视化操作平台的工作方式。下面再让我们来了解下,这种方式是怎么解决上述问题的。</p>
|
||||
<h3>基于可视化操作平台的低代码开发</h3>
|
||||
<p>可视化的低代码操作平台把编写 JSON 的过程变成了拖拽组件和调试属性配置,如下图所示,这样的交互方式对用户来说更直观友好,开发效率也会更高。</p>
|
||||
<p><img src="assets/CgqCHl9MyHmAdpfQAAdLANM4tuQ134.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/CgqCHl9MyHmAdpfQAAdLANM4tuQ134.png" alt="png" /></p>
|
||||
<h4>可视化操作平台的基本使用方式</h4>
|
||||
<p>绝大部分的可视化操作平台都将界面布局分为三个区域:左侧的<strong>组件选择区</strong>,中部的<strong>预览交互区</strong>以及右侧的<strong>属性编辑区</strong>。这三个区域的排布所对应的,也是用户<strong>生成页面的操作流程</strong>:</p>
|
||||
<ul>
|
||||
|
||||
@@ -262,7 +262,7 @@ function hide_canvas() {
|
||||
<p>以上便是<strong>企业内部无代码开发</strong>的一类应用场景。</p>
|
||||
<p><strong>外部无代码搭建平台</strong></p>
|
||||
<p>另一类面向非开发人员的无代码开发产品,针对的是缺乏开发资源的企业和部门。对于一些常见的小型项目需求,例如招聘页面、报名页面等,它们往往需要借助<strong>外部提供的无代码开发平台</strong>。这类无代码开发平台包括百度 H5、MAKA、易企秀等。</p>
|
||||
<p><img src="assets/Ciqc1F9R0u-ABNY5ABlh1mN5IXo611.png" alt="image" /></p>
|
||||
<p><img src="assets/Ciqc1F9R0u-ABNY5ABlh1mN5IXo611.png" alt="png" /></p>
|
||||
<p>百度 H5 编辑界面</p>
|
||||
<p>这类产品的特点是:</p>
|
||||
<ul>
|
||||
@@ -278,7 +278,7 @@ function hide_canvas() {
|
||||
<ul>
|
||||
<li><strong>更为多样化的应用场景</strong>:同上述面向非开发人员的产品相比,这类产品最主要的功能是提供了描述性的后端的数据与功能模块,因此能够实现的应用场景也更为多样化和通用化。以 iVX 为例,可实现的应用场景从上面的 C 端产品扩展到了 B 端产品,包括:小程序、小游戏、H5、营销活动,BPM、OA、CRM、ERP,企业中台,BI、大屏幕等。</li>
|
||||
</ul>
|
||||
<p><img src="assets/CgqCHl9R0v-ASMv2AAQk8slb6Xc485.png" alt="image" /></p>
|
||||
<p><img src="assets/CgqCHl9R0v-ASMv2AAQk8slb6Xc485.png" alt="png" /></p>
|
||||
<p>iVX 编辑器中后端逻辑描述面板</p>
|
||||
<ul>
|
||||
<li><strong>目标人群的变化</strong>:应用场景扩展对应的是 IDE 功能的复杂化和操作学习成本的增加,于是目标人群也多少有些不同:
|
||||
|
||||
@@ -197,11 +197,11 @@ function hide_canvas() {
|
||||
2012 年,Twitter 发布了名为<a href="https://bower.io/">Bower</a>的<strong>前端</strong>依赖包管理工具。
|
||||
2016 年,Facebook 发布了 npm registry 的兼容客户端<a href="https://blog.npmjs.org/post/151660845210/hello-yarn">Yarn</a>。</p>
|
||||
</blockquote>
|
||||
<p><img src="assets/Ciqc1F9V55yAQ74HAADJqN1JZaM722.png" alt="nodejs-npm-publish-730x340.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9V55yAQ74HAADJqN1JZaM722.png" alt="png" /></p>
|
||||
<p>人们可以把代码包发布到 npm 中</p>
|
||||
<p>2009 年 NodeJS 发布,这对前端开发领域产生了深远的影响。一方面,许多原先基于其他语言开发的工具包如今可以通过 NodeJS 来实现,并通过 npm(Node Package Manager,即 node 包管理器)来安装使用。大量的开发者开始将自己开发的工具包发布到 npm registry 上,<a href="https://blog.npmjs.org/post/156076312840/search-update">包的数量</a>在 2012 年初就达到了 6,000 个,而到 2014 年,数字已经上升到了 50,000 个。</p>
|
||||
<p>另一方面,安装到本地的依赖包在前端项目中如何引用开始受到关注。Twitter 发布的 Bower 旨在解决前端项目中的依赖安装和引用问题,其中一个问题是,在 npm 安装依赖的过程中会引入大量的子包,在早期版本(npm 3 之前)中会产生相同依赖包的大量重复拷贝,这在前端项目中会导致无谓的请求流量损耗。而 Bower 在安装依赖时则可以避免这类问题。然而随着更多模块化打包工具的诞生,它的优势逐渐被其他工具所取代。直到 2017 年,Bower 官方<a href="https://bower.io/blog/2017/how-to-migrate-away-from-bower/">宣布废弃</a>这个项目。</p>
|
||||
<p><img src="assets/CgqCHl9V58mAJ1qqAACUaxrD3X4489.jpg" alt="node_modules_hell.jpg" /></p>
|
||||
<p><img src="assets/CgqCHl9V58mAJ1qqAACUaxrD3X4489.jpg" alt="png" /></p>
|
||||
<p>著名的 node_modules hell(源自 reddit 用户 xaxaxa_trick)</p>
|
||||
<p>npm 的另一个饱受诟病的问题是本地依赖管理算法的复杂性以及随之而来的性能、冗余、冲突等问题。而 2016 年发布的 Yarn 正是为解决这些问题而诞生的。和 npm 相比,Yarn 的主要优点有:</p>
|
||||
<ol>
|
||||
|
||||
@@ -299,7 +299,7 @@ new CommonJsPlugin(options.module).apply(compiler);
|
||||
</ul>
|
||||
<p><strong>优化阶段</strong></p>
|
||||
<p>优化阶段在 seal 函数中共有 12 个主要的处理过程,如下图所示:</p>
|
||||
<p><img src="assets/Ciqc1F9bGtqAJo4uAABnYGwsyYs218.png" alt="image" /></p>
|
||||
<p><img src="assets/Ciqc1F9bGtqAJo4uAABnYGwsyYs218.png" alt="png" /></p>
|
||||
<p>每个过程都暴露了相应的 Hooks,分别如下:</p>
|
||||
<ul>
|
||||
<li>seal、needAdditionalSeal、unseal、afterSeal:分别在 seal 函数的起始和结束的位置触发。</li>
|
||||
@@ -344,7 +344,7 @@ new CommonJsPlugin(options.module).apply(compiler);
|
||||
module.exports = SamplePlugin;
|
||||
</code></pre>
|
||||
<p>执行构建后,可以看到在控制台输出了相应的统计时间结果(这里的时间是从构建起始到各阶段 Hook 触发为止的耗时),如下图所示:</p>
|
||||
<p><img src="assets/Ciqc1F9bGvGAFRmpAAGFrvBhTHE475.png" alt="image" /></p>
|
||||
<p><img src="assets/Ciqc1F9bGvGAFRmpAAGFrvBhTHE475.png" alt="png" /></p>
|
||||
<p>根据这样的输出结果,我们就可以分析项目里各阶段的耗时情况,再进行针对性地优化。这个统计插件将在后面几课的优化实践中运用。</p>
|
||||
<p>除了这类自己编写的统计插件外,Webpack 社区中也有一些较成熟的统计插件,例如<a href="https://github.com/stephencookdev/speed-measure-webpack-plugin">speed-measure-webpack-plugin</a>等,感兴趣的话,你可以进一步了解。</p>
|
||||
<h3>总结</h3>
|
||||
|
||||
@@ -182,16 +182,16 @@ function hide_canvas() {
|
||||
<p>提升编译模块阶段效率的第一个方向就是减少执行编译的模块。显而易见,如果一个项目每次构建都需要编译 1000 个模块,但是通过分析后发现其中有 500 个不需要编译,显而易见,经过优化后,构建效率可以大幅提升。当然,前提是找到原本不需要进行构建的模块,下面我们就来逐一分析。</p>
|
||||
<h4>IgnorePlugin</h4>
|
||||
<p>有的依赖包,除了项目所需的模块内容外,还会附带一些多余的模块。典型的例子是 <a href="https://www.npmjs.com/package/moment">moment</a> 这个包,一般情况下在构建时会自动引入其 locale 目录下的多国语言包,如下面的图片所示:</p>
|
||||
<p><img src="assets/CgqCHl9fIKaAFpvlAAFYNtxZyV0507.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIKaAFpvlAAFYNtxZyV0507.png" alt="png" /></p>
|
||||
<p>但对于大多数情况而言,项目中只需要引入本国语言包即可。而 Webpack 提供的 IgnorePlugin 即可在构建模块时直接剔除那些需要被排除的模块,从而提升构建模块的速度,并减少产物体积,如下面的图片所示。</p>
|
||||
<p><img src="assets/Ciqc1F9fILCATdbnAABZJ_SBA-k160.png" alt="Drawing 1.png" />
|
||||
<img src="assets/CgqCHl9fILaAS4hfAAEWkKJEE7E961.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9fILCATdbnAABZJ_SBA-k160.png" alt="png" />
|
||||
<img src="assets/CgqCHl9fILaAS4hfAAEWkKJEE7E961.png" alt="png" /></p>
|
||||
<p>除了 moment 包以外,其他一些带有国际化模块的依赖包,例如之前介绍 Mock 工具中提到的 Faker.js 等都可以应用这一优化方式。</p>
|
||||
<h4>按需引入类库模块</h4>
|
||||
<p>第二种典型的减少执行模块的方式是按需引入。这种方式一般适用于工具类库性质的依赖包的优化,典型例子是<a href="https://www.npmjs.com/package/lodash"> lodash </a>依赖包。通常在项目里我们只用到了少数几个 lodash 的方法,但是构建时却发现构建时引入了整个依赖包,如下图所示:</p>
|
||||
<p><img src="assets/CgqCHl9fIMWAfBHWAAD0TYKbsl8944.png" alt="Drawing 3.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIMWAfBHWAAD0TYKbsl8944.png" alt="png" /></p>
|
||||
<p>要解决这个问题,效果最佳的方式是在导入声明时只导入依赖包内的特定模块,这样就可以大大减少构建时间,以及产物的体积,如下图所示。</p>
|
||||
<p><img src="assets/CgqCHl9fIMyAfUzpAADukgQoyfw559.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIMyAfUzpAADukgQoyfw559.png" alt="png" /></p>
|
||||
<p>除了在导入时声明特定模块之外,还可以使用 babel-plugin-lodash 或 babel-plugin-import 等插件达到同样的效果。</p>
|
||||
<p>另外,有同学也许会想到 <a href="https://webpack.js.org/guides/tree-shaking/">Tree Shaking</a>,这一特性也能减少产物包的体积,但是这里有两点需要注意:</p>
|
||||
<ol>
|
||||
@@ -200,9 +200,9 @@ function hide_canvas() {
|
||||
</ol>
|
||||
<h4>DllPlugin</h4>
|
||||
<p>DllPlugin 是另一类减少构建模块的方式,它的核心思想是将项目依赖的框架等模块单独构建打包,与普通构建流程区分开。例如,原先一个依赖 React 与 react-dom 的文件,在构建时,会如下图般处理:</p>
|
||||
<p><img src="assets/CgqCHl9fIOSAYnmjAAFH8Ofyt34986.png" alt="Drawing 5.png" />
|
||||
<p><img src="assets/CgqCHl9fIOSAYnmjAAFH8Ofyt34986.png" alt="png" />
|
||||
而在通过 DllPlugin 和 DllReferencePlugin 分别配置后的构建时间就变成如下图所示,由于构建时减少了最耗时的模块,构建效率瞬间提升十倍。</p>
|
||||
<p><img src="assets/CgqCHl9fIPOALYMeAAFQB_4TuTU987.png" alt="Drawing 6.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIPOALYMeAAFQB_4TuTU987.png" alt="png" /></p>
|
||||
<h4>Externals</h4>
|
||||
<p>Webpack 配置中的 externals 和 DllPlugin 解决的是同一类问题:将依赖的框架等模块从构建过程中移除。它们的区别在于:</p>
|
||||
<ol>
|
||||
@@ -212,15 +212,15 @@ function hide_canvas() {
|
||||
<li>在引用依赖包的子模块时,DllPlugin 无须更改,而 externals 则会将子模块打入项目包中。</li>
|
||||
</ol>
|
||||
<p>externals 的示例如下面两张图,可以看到经过 externals 配置后,构建速度有了很大提升。</p>
|
||||
<p><img src="assets/Ciqc1F9fIPiAJx62AAEEeJ5yROI594.png" alt="Drawing 7.png" />
|
||||
<img src="assets/Ciqc1F9fIQSAAB3_AAD6KAV5S6M930.png" alt="Drawing 8.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9fIPiAJx62AAEEeJ5yROI594.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9fIQSAAB3_AAD6KAV5S6M930.png" alt="png" /></p>
|
||||
<h3>提升单个模块构建的速度</h3>
|
||||
<p>提升编译阶段效率的第二个方向,是在保持构建模块数量不变的情况下,提升单个模块构建的速度。具体来说,是通过减少构建单个模块时的一些处理逻辑来提升速度。这个方向的优化主要有以下几种:</p>
|
||||
<h4>include/exclude</h4>
|
||||
<p>Webpack 加载器配置中的 include/exclude,是常用的优化特定模块构建速度的方式之一。</p>
|
||||
<p>include 的用途是只对符合条件的模块使用指定 Loader 进行转换处理。而 exclude 则相反,不对特定条件的模块使用该 Loader(例如不使用 babel-loader 处理 node_modules 中的模块)。如下面两张图片所示。</p>
|
||||
<p><img src="assets/CgqCHl9fIQmAVCu5AAH_1DmTw5Q884.png" alt="Drawing 9.png" />
|
||||
<img src="assets/CgqCHl9fIRmAYw1PAAG8nEHHA1k680.png" alt="Drawing 10.png" />
|
||||
<p><img src="assets/CgqCHl9fIQmAVCu5AAH_1DmTw5Q884.png" alt="png" />
|
||||
<img src="assets/CgqCHl9fIRmAYw1PAAG8nEHHA1k680.png" alt="png" />
|
||||
这里有两点需要注意:</p>
|
||||
<ol>
|
||||
<li>从上面的第二张图中可以看到,jquery 和 lodash 的编译过程仍然花费了数百毫秒,说明通过 include/exclude 排除的模块,并非不进行编译,而是使用 Webpack 默认的 js 模块编译器进行编译(例如推断依赖包的模块类型,加上装饰代码等)。</li>
|
||||
@@ -228,17 +228,17 @@ function hide_canvas() {
|
||||
</ol>
|
||||
<h4>noParse</h4>
|
||||
<p>Webpack 配置中的 module.noParse 则是在上述 include/exclude 的基础上,进一步省略了使用默认 js 模块编译器进行编译的时间,如下面两张图片所示。</p>
|
||||
<p><img src="assets/CgqCHl9fIR-AABfPAAGe7gdO_nc998.png" alt="Drawing 11.png" />
|
||||
<img src="assets/CgqCHl9fIS2ARrYXAAFGpNGygsY433.png" alt="Drawing 12.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIR-AABfPAAGe7gdO_nc998.png" alt="png" />
|
||||
<img src="assets/CgqCHl9fIS2ARrYXAAFGpNGygsY433.png" alt="png" /></p>
|
||||
<h4>Source Map</h4>
|
||||
<p>Source Map 对于构建时间的影响在第三课中已经展开讨论过,这里再稍做总结:对于生产环境的代码构建而言,会根据项目实际情况判断是否开启 Source Map。在开启 Source Map 的情况下,优先选择与源文件分离的类型,例如 "source-map"。有条件也可以配合错误监控系统,将 Source Map 的构建和使用在线下监控后台中进行,以提升普通构建部署流程的速度。</p>
|
||||
<h4>TypeScript 编译优化</h4>
|
||||
<p>Webpack 中编译 TS 有两种方式:使用 ts-loader 或使用 babel-loader。其中,在使用 ts-loader 时,由于 ts-loader 默认在编译前进行类型检查,因此编译时间往往比较慢,如下面的图片所示。</p>
|
||||
<p><img src="assets/Ciqc1F9fITOAXQGlAAEcMk0PqdY814.png" alt="Drawing 13.png" />
|
||||
<p><img src="assets/Ciqc1F9fITOAXQGlAAEcMk0PqdY814.png" alt="png" />
|
||||
通过加上配置项 transpileOnly: true,可以在编译时忽略类型检查,从而大大提升 TS 模块的编译速度,如下面的图片所示。</p>
|
||||
<p><img src="assets/Ciqc1F9fITqAO9uoAAEDJx7jQcA803.png" alt="Drawing 14.png" />
|
||||
<p><img src="assets/Ciqc1F9fITqAO9uoAAEDJx7jQcA803.png" alt="png" />
|
||||
而 babel-loader 则需要单独安装 @babel/preset-typescript 来支持编译 TS(Babel 7 之前的版本则还是需要使用 ts-loader)。babel-loader 的编译效率与上述 ts-loader 优化后的效率相当,如下面的图片所示。</p>
|
||||
<p><img src="assets/CgqCHl9fIUqAGSCpAAD9Llg28C8211.png" alt="Drawing 15.png" />
|
||||
<p><img src="assets/CgqCHl9fIUqAGSCpAAD9Llg28C8211.png" alt="png" />
|
||||
不过单独使用这一功能就丧失了 TS 中重要的类型检查功能,因此在许多脚手架中往往配合 ForkTsCheckerWebpackPlugin 一同使用。</p>
|
||||
<h4>Resolve</h4>
|
||||
<p>Webpack 中的 resolve 配置制定的是在构建时指定查找模块文件的规则,例如:</p>
|
||||
@@ -249,18 +249,18 @@ function hide_canvas() {
|
||||
<li><strong>resolve.symlinks</strong>:指定在查找模块时是否处理软连接。</li>
|
||||
</ul>
|
||||
<p>这些规则在处理每个模块时都会有所应用,因此尽管对小型项目的构建速度来说影响不大,但对于大型的模块众多的项目而言,这些配置的变化就可能产生客观的构建时长区别。例如下面的示例就展示了使用默认配置和增加了大量无效范围后,构建时长的变化情况:</p>
|
||||
<p><img src="assets/CgqCHl9fIU-AGs1fAAErO09KCQg428.png" alt="Drawing 16.png" />
|
||||
<img src="assets/Ciqc1F9fIWCAKxMyAAErVYo_MgQ418.png" alt="Drawing 17.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIU-AGs1fAAErO09KCQg428.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9fIWCAKxMyAAErVYo_MgQ418.png" alt="png" /></p>
|
||||
<h3>并行构建以提升总体效率</h3>
|
||||
<p>第三个编译阶段提效的方向是使用并行的方式来提升构建的效率。并行构建的方案早在 Webpack 2 时代已经出现,随着目前最新稳定版本 Webpack 4 的发布,人们发现在一般项目的开发阶段和小型项目的各构建流程中<a href="https://blog.johnnyreilly.com/2018/12/you-might-not-need-thread-loader.html">已经用不到这种并发的思路</a>了,因为在这些情况下,并发所需要的多进程管理与通信所带来的额外时间成本可能会超过使用工具带来的收益。但是在大中型项目的生产环境构建时,这类工具仍有发挥作用的空间。这里我们介绍两类并行构建的工具: HappyPack 与 thread-loader,以及 parallel-webpack。</p>
|
||||
<h4>HappyPack 与 thread-loader</h4>
|
||||
<p>这两种工具的本质作用相同,都作用于模块编译的 Loader 上,用于在特定 Loader 的编译过程中,以开启多进程的方式加速编译。HappyPack 诞生较早,而 thread-loader 参照它的效果实现了更符合 Webpack 中 Loader 的编写方式。下面就以 thread-loader 为例,来看下应用前后的构建时长对比,如下面的两张图所示。</p>
|
||||
<p><img src="assets/CgqCHl9fIWaAKvjDAAGxNVse3m4379.png" alt="Drawing 18.png" />
|
||||
<img src="assets/Ciqc1F9fIXOAHx6XAAIyabhj3_g078.png" alt="Drawing 19.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIWaAKvjDAAGxNVse3m4379.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9fIXOAHx6XAAIyabhj3_g078.png" alt="png" /></p>
|
||||
<h4>parallel-webpack</h4>
|
||||
<p>并发构建的第二种场景是针对与多配置构建。Webpack 的配置文件可以是一个包含多个子配置对象的数组,在执行这类多配置构建时,默认串行执行,而通过 parallel-webpack,就能实现相关配置的并行处理。从下图的示例中可以看到,通过不同配置的并行构建,构建时长缩短了 30%:</p>
|
||||
<p><img src="assets/CgqCHl9fIXuARXhnAADx6PzQuE0879.png" alt="Drawing 20.png" />
|
||||
<img src="assets/Ciqc1F9fIbCAL6knAAEbXZ1tRpw256.png" alt="Drawing 21.png" /></p>
|
||||
<p><img src="assets/CgqCHl9fIXuARXhnAADx6PzQuE0879.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9fIbCAL6knAAEbXZ1tRpw256.png" alt="png" /></p>
|
||||
<h3>总结</h3>
|
||||
<p>这节课我们整理了 Webpack 构建中编译模块阶段的构建效率优化方案。对于这一阶段的构建效率优化可以分为三个方向:以减少执行构建的模块数量为目的的方向、以提升单个模块构建速度为目的的方向,以及通过并行构建以提升整体构建效率的方向。每个方向都包含了若干解决工具和配置。</p>
|
||||
<p>今天课后的<strong>思考题是</strong>:你的项目中是否都用到了这些解决方案呢?希望你结合课程的内容,和所开发的项目中用到的优化方案进行对比,查漏补缺。如果有这个主题方面其他新的解决方案,也欢迎在留言区讨论分享。</p>
|
||||
|
||||
@@ -190,11 +190,11 @@ compilation.hooks[end].tap(PluginName, () => {
|
||||
...
|
||||
</code></pre>
|
||||
<p>使用后的效果如下图所示:</p>
|
||||
<p><img src="assets/CgqCHl9kV6KAd5qDAACDxSy2vds191.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/CgqCHl9kV6KAd5qDAACDxSy2vds191.png" alt="png" /></p>
|
||||
<p>通过这样的插件,我们可以分析目前项目中的效率瓶颈,从而进一步为选取优化方案及评估方案效果提供依据。</p>
|
||||
<h3>优化阶段效率提升的整体分析</h3>
|
||||
<p>在“第 10 课时|流程分解:Webpack 的完整构建流程”中,我们提到了下面的这张图。如图所示,整个优化阶段可以细分为 12 个子任务,每个任务依次对数据进行一定的处理,并将结果传递给下一任务:</p>
|
||||
<p><img src="assets/CgqCHl9kV6qAUBvfAABnYGwsyYs441.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/CgqCHl9kV6qAUBvfAABnYGwsyYs441.png" alt="png" /></p>
|
||||
<p>因此,这一阶段的优化也可以分为两个不同的方向:</p>
|
||||
<ol>
|
||||
<li>针对某些任务,使用效率更高的工具或配置项,从而<strong>提升当前任务的工作效率</strong>。</li>
|
||||
@@ -208,7 +208,7 @@ compilation.hooks[end].tap(PluginName, () => {
|
||||
<p>Webpack 4 中内置了 <a href="https://www.npmjs.com/package/terser-webpack-plugin">TerserWebpackPlugin</a> 作为默认的 JS 压缩工具,之前的版本则需要在项目配置中单独引入,早期主要使用的是 <a href="https://www.npmjs.com/package/uglifyjs-webpack-plugin">UglifyJSWebpackPlugin</a>。这两个 Webpack 插件内部的压缩功能分别基于 <a href="https://www.npmjs.com/package/terser">Terser</a> 和 <a href="https://github.com/mishoo/UglifyJS2">UglifyJS</a>。</p>
|
||||
<p>从<a href="https://github.com/babel/minify#benchmarks">第三方的测试结果</a>看,两者在压缩效率与质量方面差别不大,但 Terser 整体上略胜一筹。</p>
|
||||
<p>从本节课示例代码的运行结果(npm run build:jscomp)来看,如下面的表格所示,在不带任何优化配置的情况下,3 个测试文件的构建结果都是 Terser 效果更好。</p>
|
||||
<p><img src="assets/Ciqc1F9kbd6AZL4AAAA8akSVxH8499.png" alt="Lark20200918-161929.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9kbd6AZL4AAAA8akSVxH8499.png" alt="png" /></p>
|
||||
<p><strong>Terser 和 UglifyJS 插件中的效率优化</strong></p>
|
||||
<p><a href="https://github.com/terser/terser">Terser</a> 原本是 Fork 自 uglify-es 的项目(Fork 指从开源项目的某一版本分离出来成为独立的项目),其绝大部分的 API 和参数都与 uglify-es 和 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="87f2e0ebeee1feaaedf4c7b4">[email protected]</a> 兼容。因此,两者对应参数的作用与优化方式也基本相同,这里就以 Terser 为例来分析其中的优化方向。</p>
|
||||
<p>在作为 Webpack 插件的 TerserWebpackPlugin 中,对执行效率产生影响的配置主要分为 3 个方面:</p>
|
||||
@@ -238,13 +238,13 @@ function(module,exports){function HelloWorld(){var foo="1234";console.
|
||||
<li><strong>mangle 参数的作用</strong>是对源代码中的变量与函数名称进行压缩,当参数为 false 时,示例代码压缩后的体积从 1.16KB 增加到 1.84KB,对代码压缩的效果影响非常大。</li>
|
||||
</ol>
|
||||
<p>在了解了两个参数对压缩质量的影响之后,我们再来看下它们对效率的影响。以上面表格中的 example-antd 为例,我制作了下面的表格进行对比:</p>
|
||||
<p><img src="assets/Ciqc1F9kbdCALcuwAABCdtCwxuY965.png" alt="Lark20200918-161934.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9kbdCALcuwAABCdtCwxuY965.png" alt="png" /></p>
|
||||
<p>从结果中可以看到,当<strong>compress</strong>参数为 false 时,压缩阶段的效率有明显提升,同时对压缩的质量影响较小。在需要对压缩阶段的效率进行优化的情况下,<strong>可以优先选择设置该参数</strong>。</p>
|
||||
<h4>面向 CSS 的压缩工具</h4>
|
||||
<p>CSS 同样有几种压缩工具可供选择:<a href="https://www.npmjs.com/package/optimize-css-assets-webpack-plugin">OptimizeCSSAssetsPlugin</a>(在 Create-React-App 中使用)、<a href="https://www.npmjs.com/package/@intervolga/optimize-cssnano-plugin">OptimizeCSSNanoPlugin</a>(在 VUE-CLI 中使用),以及<a href="https://www.npmjs.com/package/css-minimizer-webpack-plugin">CSSMinimizerWebpackPlugin</a>(2020 年 Webpack 社区新发布的 CSS 压缩插件)。</p>
|
||||
<p>这三个插件在压缩 CSS 代码功能方面,都默认基于 <a href="https://cssnano.co/">cssnano</a> 实现,因此在压缩质量方面没有什么差别。</p>
|
||||
<p>在压缩效率方面,首先值得一提的是最新发布的 CSSMinimizerWebpackPlugin,它<strong>支持缓存和多进程</strong>,这是另外两个工具不具备的。而在非缓存的普通压缩过程方面,整体上 3 个工具相差不大,不同的参数结果略有不同,如下面的表格所示(下面结果为示例代码中 example-css 的执行构建结果)。</p>
|
||||
<p><img src="assets/CgqCHl9kbb6AI7F5AABRRdbprbU989.png" alt="Lark20200918-161938.png" /></p>
|
||||
<p><img src="assets/CgqCHl9kbb6AI7F5AABRRdbprbU989.png" alt="png" /></p>
|
||||
<blockquote>
|
||||
<p>注:CSSMinimizerWebpackPlugin 中默认开启多进程选项 parallel,但是在测试示例较小的情况下,多进程的通信时间反而可能导致效率的降低。测试中关闭多进程选项后,构建时间明显缩短。</p>
|
||||
</blockquote>
|
||||
@@ -271,13 +271,13 @@ optimization: {
|
||||
...
|
||||
</code></pre>
|
||||
<p>在这个示例中,有两个入口文件引入了相同的依赖包 lodash,在没有额外设置分包的情况下, lodash 被同时打入到两个产物文件中,在后续的压缩代码阶段耗时 1740ms。<strong>而在设置分包规则为 chunks:'all' 的情况下</strong>,通过分离公共依赖到单独的 Chunk,使得在后续压缩代码阶段,只需要压缩一次 lodash 的依赖包代码,从而减少了压缩时长,总耗时为 1036ms。通过下面两张图片也可以看出这样的变化。</p>
|
||||
<p><img src="assets/Ciqc1F9kWAWANNLZAAGM4v1icLA197.png" alt="Drawing 3.png" />
|
||||
<img src="assets/CgqCHl9kWAqAELXZAAG5xisRryc225.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9kWAWANNLZAAGM4v1icLA197.png" alt="png" />
|
||||
<img src="assets/CgqCHl9kWAqAELXZAAG5xisRryc225.png" alt="png" /></p>
|
||||
<p>这里起作用的是 Webpack 4 中内置的 SplitChunksPlugin,该插件在 production 模式下默认启用。其默认的分包规则为 chunks: '<strong>async</strong>',作用是分离动态引入的模块 (import('...')),在处理动态引入的模块时能够自动分离其中的公共依赖。</p>
|
||||
<p>但是对于示例中多入口静态引用相同依赖包的情况,则<strong>不会处理分包。而设置为 chunks: 'all',则能够将所有的依赖情况都进行分包处理,从而减少了重复引入相同模块代码的情况。SplitChunksPlugin 的工作阶段是在optimizeChunks</strong>阶段(Webpack 4 中是在 optimizeChunksAdvanced,在 Webpack 5 中去掉了 basic 和 advanced,合并为 optimizeChunks),而压缩代码是在 optimizeChunkAssets 阶段,从而起到提升后续环节工作效率的作用。</p>
|
||||
<h4>Tree Shaking</h4>
|
||||
<p><a href="https://webpack.js.org/guides/tree-shaking/">Tree Shaking(摇树)</a>是指在构建打包过程中,移除那些引入但未被使用的无效代码(Dead-code elimination)。这种优化手段最早应用于在 Rollup 工具中,而在 Webpack 2 之后的版本中, Webpack 开始内置这一功能。下面我们先来看一下 Tree Shaking 的例子,如下面的表格所示:</p>
|
||||
<p><img src="assets/Ciqc1F9kbaqAUkjGAACmMR1PvL4711.png" alt="Lark20200918-161943.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9kbaqAUkjGAACmMR1PvL4711.png" alt="png" /></p>
|
||||
<p>可以看到,引入不同的依赖包(lodash vs lodash-es)、不同的引入方式,以及是否使用 babel 等,都会对 Tree Shaking 的效果产生影响。下面我们就来分析具体原因。</p>
|
||||
<ol>
|
||||
<li><strong>ES6 模块</strong>: 首先,只有 ES6 类型的模块才能进行Tree Shaking。因为 ES6 模块的依赖关系是确定的,因此可以进行不依赖运行时的<strong>静态分析</strong>,而 CommonJS 类型的模块则不能。因此,CommonJS 类型的模块 lodash,在无论哪种引入方式下都不能实现 Tree Shaking,而需要依赖第三方提供的插件(例如 babel-plugin-lodash 等)才能实现动态删除无效代码。而 ES6 风格的模块 lodash-es,则可以进行 Tree Shaking 优化。</li>
|
||||
|
||||
@@ -176,8 +176,8 @@ function hide_canvas() {
|
||||
<pre><code>./src/example-basic.js
|
||||
import _ from 'lodash'
|
||||
</code></pre>
|
||||
<p><img src="assets/Ciqc1F9kXKSADBZcAAE6sFADuy0456.png" alt="Drawing 0.png" />
|
||||
<img src="assets/CgqCHl9kXKmAUFg3AAErEUA8F2E549.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9kXKSADBZcAAE6sFADuy0456.png" alt="png" />
|
||||
<img src="assets/CgqCHl9kXKmAUFg3AAErEUA8F2E549.png" alt="png" /></p>
|
||||
<p>可以看到,在没有增加任何优化设置的情况下,初次构建时在 optimizeChunkAssets 阶段的耗时是 1000ms 左右,而再次构建时的耗时直接降到了 18ms,几乎可以忽略不计。</p>
|
||||
<p>这里的原因就在于,Webpack 4 内置了压缩插件 TerserWebpackPlugin,且默认开启了<a href="https://webpack.js.org/plugins/terser-webpack-plugin/#cache">缓存</a>参数。在初次构建的压缩代码过程中,就将这一阶段的结果写入了缓存目录(node_modules/.cache/terser-webpack-plugin/)中,当再次构建进行到压缩代码阶段时,即可对比读取已有缓存,如下面的代码所示(相关的代码逻辑在插件的<a href="https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/src/index.js">源代码</a>中可以看到)。</p>
|
||||
<pre><code>terser-webpack-plugin/src/index.js:
|
||||
@@ -210,8 +210,8 @@ if (cache.isEnabled()) {
|
||||
<li><strong>cacheCompression</strong>:默认为 true,将缓存内容压缩为 gz 包以减小缓存目录的体积。在设为 false 的情况下将跳过压缩和解压的过程,从而提升这一阶段的速度。</li>
|
||||
</ul>
|
||||
<p>开启缓存选项前后的构建时长效果如图所示(示例中运行 npm run build:babel),可以看到,由于开启了 Babel 的缓存,再次构建的速度比初次构建时要快了许多。</p>
|
||||
<p><img src="assets/CgqCHl9kXL6AIfhNAAB0Ns1VzFM141.png" alt="Drawing 2.png" />
|
||||
<img src="assets/CgqCHl9kXMOAND8YAACPsQpEG1k472.png" alt="Drawing 3.png" /></p>
|
||||
<p><img src="assets/CgqCHl9kXL6AIfhNAAB0Ns1VzFM141.png" alt="png" />
|
||||
<img src="assets/CgqCHl9kXMOAND8YAACPsQpEG1k472.png" alt="png" /></p>
|
||||
<h4>Cache-loader</h4>
|
||||
<p>在编译过程中利用缓存的第二种方式是使用<a href="https://webpack.js.org/loaders/cache-loader/"> Cache-loader</a>。在使用时,需要将 cache-loader 添加到对构建效率影响较大的 Loader(如 babel-loader 等)之前,如下面的代码所示:</p>
|
||||
<pre><code>./webpack.cache.config.js
|
||||
@@ -227,8 +227,8 @@ module: {
|
||||
...
|
||||
</code></pre>
|
||||
<p>执行两次构建后可以发现,使用 cache-loader 后,比使用 babel-loader 的开启缓存选项后的构建时间更短,如下图所示:</p>
|
||||
<p><img src="assets/CgqCHl9kXOqAGqBaAAB8XJNiH2c187.png" alt="Drawing 4.png" />
|
||||
<img src="assets/Ciqc1F9kXO-Ae1fcAABt0doSQD0218.png" alt="Drawing 5.png" /></p>
|
||||
<p><img src="assets/CgqCHl9kXOqAGqBaAAB8XJNiH2c187.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9kXO-Ae1fcAABt0doSQD0218.png" alt="png" /></p>
|
||||
<p>主要原因是 babel-loader 中的<strong>缓存信息较少</strong>,而 cache-loader 中存储的<strong>Buffer 形式的数据处理效率更高</strong>。下面的示例代码,是 babel-loader 和 cache-loader 入口模块的缓存信息对比:</p>
|
||||
<pre><code>//babel-loader中的缓存数据
|
||||
{"ast":null,"code":"import _ from 'lodash';","map":null,"metadata":{},"sourceType":"module"}
|
||||
@@ -241,8 +241,8 @@ module: {
|
||||
<h4>代码压缩时的缓存优化</h4>
|
||||
<p>在上一课时中曾提到,在代码压缩阶段,对于 JS 的压缩,TerserWebpackPlugin 和 UglifyJSPlugin 都是支持缓存设置的。而对于 CSS 的压缩,目前最新发布的 CSSMinimizerWebpackPlugin 支持且默认开启缓存,其他的插件如 OptimizeCSSAssetsPlugin 和 OptimizeCSSNanoPlugin 目前还不支持使用缓存。</p>
|
||||
<p>TerserWebpackPlugin 插件的效果在本节课的开头部分我们已经演示过了,这里再来看一下 CSSMinimizerWebpackPlugin 的缓存效果对比,如下面的图片所示,开启该插件的缓存后,再次构建的时长降低到了初次构建的 1/4。</p>
|
||||
<p><img src="assets/Ciqc1F9kXQGAWkf8AACuXoea9dE075.png" alt="Drawing 6.png" />
|
||||
<img src="assets/CgqCHl9kXQaAEle1AACx-Pmol-Q677.png" alt="Drawing 7.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9kXQGAWkf8AACuXoea9dE075.png" alt="png" />
|
||||
<img src="assets/CgqCHl9kXQaAEle1AACx-Pmol-Q677.png" alt="png" /></p>
|
||||
<p>以上就是 Webpack 4 中编译与优化打包阶段可用的几种缓存方案。接下来我们再来看下在构建过程中使用缓存的一些注意点。</p>
|
||||
<h3>缓存的失效</h3>
|
||||
<p>尽管上面示例所显示的再次构建时间要比初次构建时间快很多,但前提是两次构建没有任何代码发生变化,也就是说,最佳效果是在缓存完全命中的情况下。而现实中,通常需要重新构建的原因是代码发生了变化。因此<strong>如何最大程度地让缓存命中</strong>,成为我们选择缓存方案后首先要考虑的事情。</p>
|
||||
@@ -253,8 +253,8 @@ module: {
|
||||
<p>编译阶段的执行时间由每个模块的编译时间相加而成。在开启缓存的情况下,代码发生变化的模块将被重新编译,但不影响它所依赖的及依赖它的其他模块,其他模块将继续使用缓存。因此,这一阶段不需要考虑缓存失效扩大化的问题。</p>
|
||||
<h4>优化打包阶段的缓存失效</h4>
|
||||
<p>优化打包阶段的缓存失效问题则需要引起注意。还是以课程开头的 example-basic 为例,在使用缓存快速构建后,当我们任意修改入口文件的代码后会发现,代码压缩阶段的时间再次变为和初次构建时相近,也就是说,这一 Chunk 的 Terser 插件的缓存完全失效了,如下面的图片所示。</p>
|
||||
<p><img src="assets/Ciqc1F9kXRGAUqisAAEy5POkOmg344.png" alt="Drawing 8.png" />
|
||||
<img src="assets/CgqCHl9kXRaAH-yTAAEq_mHfoPo473.png" alt="Drawing 9.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9kXRGAUqisAAEy5POkOmg344.png" alt="png" />
|
||||
<img src="assets/CgqCHl9kXRaAH-yTAAEq_mHfoPo473.png" alt="png" /></p>
|
||||
<p>之所以会出现这样的结果,是因为,尽管在模块编译阶段每个模块是单独执行编译的,但是当进入到代码压缩环节时,各模块已经被组织到了相关联的 Chunk 中。如上面的示例,4 个模块最后只生成了一个 Chunk,任何一个模块发生变化都会导致整个 Chunk 的内容发生变化,而使之前保存的缓存失效。</p>
|
||||
<p>在知道了失效原因后,<strong>对应的优化思路也就显而易见了</strong>:尽可能地把那些不变的处理成本高昂的模块打入单独的 Chunk 中。这就涉及了 Webpack 中的分包配置——<a href="https://webpack.js.org/configuration/optimization/#optimizationsplitchunks">splitChunks</a>。</p>
|
||||
<h4>使用 splitChunks 优化缓存利用率</h4>
|
||||
@@ -268,7 +268,7 @@ optimization: {
|
||||
},
|
||||
...
|
||||
</code></pre>
|
||||
<p><img src="assets/CgqCHl9kXSeAOtCSAAEiD0YND3g006.png" alt="Drawing 10.png" /></p>
|
||||
<p><img src="assets/CgqCHl9kXSeAOtCSAAEiD0YND3g006.png" alt="png" /></p>
|
||||
<h3>其他使用缓存的注意事项</h3>
|
||||
<h4>CI/CD 中的缓存目录问题</h4>
|
||||
<p>在许多自动化集成的系统中,项目的构建空间会在每次构建执行完毕后,立即回收清理。在这种情况下,默认的项目构建缓存目录(node_mo dules/.cache)将无法留存,导致即使项目中开启了缓存设置,也无法享受缓存的便利性,反而因为需要写入缓存文件而浪费额外的时间。因此,在集成化的平台中构建部署的项目,如果需要使用缓存,则需要根据对应平台的规范,将缓存设置到公共缓存目录下。这类问题我们会在第三模块部署优化中再次展开。</p>
|
||||
|
||||
@@ -170,25 +170,25 @@ function hide_canvas() {
|
||||
<p>但是只编译打包所改动的文件真的不能实现吗?这节课我们就来讨论这个话题(课程里完整的示例代码参见 <a href="https://github.com/fe-efficiency/lessons_fe_efficiency/tree/master/14_incremental_build">14_incremental_build</a>)。</p>
|
||||
<h3>Webpack 中的增量构建</h3>
|
||||
<p>上述只构建改动文件的处理过程在 Webpack 中是实际存在的,你可能也很熟悉,那就是在<strong>开启 devServer</strong>的时候,当我们执行 webpack-dev-server 命令后,Webpack 会进行一次初始化的构建,构建完成后启动服务并进入到等待更新的状态。当本地文件有变更时,Webpack 几乎瞬间将变更的文件进行编译,并将编译后的代码内容推送到浏览器端。你会发现,这个文件变更后的处理过程就符合上面所说的只编译打包改动的文件的操作,这就称为“<strong>增量构建”</strong>。我们通过示例代码进行验证(<em>npm run dev</em>),如下面的图片:</p>
|
||||
<p><img src="assets/CgqCHl9sTsWAbetxAAGoldlDrIw704.png" alt="Drawing 0.png" />
|
||||
<img src="assets/Ciqc1F9sTsmAJc8YAADz9x_Zsvo780.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/CgqCHl9sTsWAbetxAAGoldlDrIw704.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9sTsmAJc8YAADz9x_Zsvo780.png" alt="png" /></p>
|
||||
<p>可以看到,在开发服务模式下,初次构建编译了 47 个模块,完整的构建时间为 3306ms。当我们改动其中一个源码文件后,日志显示 Webpack 只再次构建了这一个模块,因此再次构建的时间非常短(24ms)。那么为什么在开发服务模式下可以实现增量构建的效果,而在生产环境下不行呢?下面我们来分析影响结果的因素。</p>
|
||||
<h3>增量构建的影响因素</h3>
|
||||
<h4>watch 配置</h4>
|
||||
<p>在上面的增量构建过程中,第一个想到的就是<strong>需要监控文件的变化</strong>。显然,只有得知变更的是哪个文件后,才能进行后续的针对性处理。要实现这一点也很简单,在“第 2 课时|界面调试:热更新技术如何开着飞机修引擎?”中已经介绍过,在 Webpack 中<strong>启用 watch 配置</strong>即可,此外在使用 devServer 的情况下,该选项会<strong>默认开启</strong>。那么,如果在生产模式下开启 watch 配置,是不是再次构建时,就会按增量的方式执行呢?我们仍然通过示例验证(<em>npm run build:watch</em>),如下面的图片所示:</p>
|
||||
<p><img src="assets/CgqCHl9sTtOAPzPRAAHMQJnGHlo474.png" alt="Drawing 2.png" />
|
||||
<img src="assets/CgqCHl9sTtiAB2seAAG0v0B0ORQ594.png" alt="Drawing 3.png" /></p>
|
||||
<p><img src="assets/CgqCHl9sTtOAPzPRAAHMQJnGHlo474.png" alt="png" />
|
||||
<img src="assets/CgqCHl9sTtiAB2seAAG0v0B0ORQ594.png" alt="png" /></p>
|
||||
<p>从结果中可以发现,在生产模式下开启 watch 配置后,相比初次构建,再次构建所编译的模块数量并未减少,即使只改动了一个文件,也仍然会对所有模块进行编译。因此可以得出结论,在生产环境下只开启 watch 配置后的再次构建<strong>并不能</strong>实现增量构建。</p>
|
||||
<h4>cache 配置</h4>
|
||||
<p>仔细查阅 Webpack 的配置项文档,会在菜单最下方的“其他选项”一栏中找到 <a href="https://v4.webpack.js.org/configuration/other-options/#cache">cache</a> 选项(需要注意的是我们查阅的是 <strong>Webpack 4 版本的文档</strong>,Webpack 5 中这一选项会有大的改变,会在下一节课中展开讨论)。这一选项的值有两种类型:布尔值和对象类型。一般情况下默认为<strong>false</strong>,即不使用缓存,但在开发模式开启 watch 配置的情况下,cache 的默认值变更为<strong>true</strong>。此外,如果 cache 传值为对象类型,则表示使用该对象来作为缓存对象,这往往用于多个编译器 compiler 的调用情况。</p>
|
||||
<p>下面我们就来看一下,在生产模式下,如果watch 和 cache 都为 true,结果会如何(npm run build:watch-cache)?如下面的图片所示:</p>
|
||||
<p><img src="assets/CgqCHl9sTuuAc0_4AAHBe2Lt3do732.png" alt="Drawing 4.png" />
|
||||
<img src="assets/Ciqc1F9sTvCAY2NvAAEtJYxCA_8121.png" alt="Drawing 5.png" /></p>
|
||||
<p><img src="assets/CgqCHl9sTuuAc0_4AAHBe2Lt3do732.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9sTvCAY2NvAAEtJYxCA_8121.png" alt="png" /></p>
|
||||
<p>正如我们所期望的,再次构建时,在编译模块阶段只对有变化的文件进行了重新编译,实现了<strong>增量编译</strong>的效果。</p>
|
||||
<p>但是美中不足的是,在优化阶段压缩代码时仍然耗费了较多的时间。这一点很容易理解:</p>
|
||||
<p>体积最大的 react、react-dom 等模块和入口模块打入了同一个 Chunk 中,即使修改的模块是单独分离的 bar.js,但它的产物名称的变化仍然需要反映在入口 Chunk 的 runtime 模块中。因此入口 Chunk 也需要跟着重新压缩而无法复用压缩缓存数据。根据前面几节课的知识点,我们对配置再做一些优化,将 vendor 分离后再来看看效果,如下面的图片所示:</p>
|
||||
<p><img src="assets/CgqCHl9sTvqAP1oIAAG2kbb-DGY688.png" alt="Drawing 6.png" />
|
||||
<img src="assets/CgqCHl9sTv6AYxTKAAFAsmUEZMg953.png" alt="Drawing 7.png" /></p>
|
||||
<p><img src="assets/CgqCHl9sTvqAP1oIAAG2kbb-DGY688.png" alt="png" />
|
||||
<img src="assets/CgqCHl9sTv6AYxTKAAFAsmUEZMg953.png" alt="png" /></p>
|
||||
<p>可以看到,通过上面这一系列的配置后(<strong>watch + cache</strong>),在生产模式下,最终呈现出了我们期望的<strong>增量构建</strong>效果:有文件发生变化时会自动编译变更的模块,并只对该模块影响到的少量 Chunk 进行优化并更新产物文件版本,而其他产物文件则保持之前的版本。如此,整个构建过程的速度大大提升。</p>
|
||||
<h3>增量构建的实现原理</h3>
|
||||
<p>为什么在配置项中需要同时启用 watch 和 cache 配置才能获得增量构建的效果呢?接下来我们从源码层面分析。</p>
|
||||
|
||||
@@ -190,9 +190,9 @@ module.exports = {
|
||||
...
|
||||
}
|
||||
</code></pre>
|
||||
<p><img src="assets/Ciqc1F9sT2WAI_vnAAGUeALmmZo570.png" alt="Drawing 0.png" />
|
||||
<img src="assets/CgqCHl9sT2qAexnjAADgsW9ijYU168.png" alt="Drawing 1.png" />
|
||||
<img src="assets/Ciqc1F9sT26AIkKYAAEVjcm9aeY144.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9sT2WAI_vnAAGUeALmmZo570.png" alt="png" />
|
||||
<img src="assets/CgqCHl9sT2qAexnjAADgsW9ijYU168.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9sT26AIkKYAAEVjcm9aeY144.png" alt="png" /></p>
|
||||
<p>可以看到,初次构建完整花费了 3282ms,而在不修改代码进行再次构建的情况下,只花费了不到原先时间的 1/10。在修改代码文件的新情况下也只花费了 628ms,多花费的时间体现在构建被修改的文件的编译上,这就实现了上一课时所寻求的<strong>生产环境下的增量构建</strong>。</p>
|
||||
<h4>Cache 基本配置</h4>
|
||||
<p>在 Webpack 4 中,cache 只是单个属性的配置,所对应的赋值为 true 或 false,用来代表是否启用缓存,或者赋值为对象来表示在构建中使用的缓存对象。而在 Webpack 5 中,<a href="https://webpack.js.org/configuration/other-options/#cache">cache</a> 配置除了原本的 true 和 false 外,还增加了许多子配置项,例如:</p>
|
||||
@@ -282,8 +282,8 @@ console.log(a)
|
||||
<p>可以看到产物代码中只有被引入的属性 a 和 console 语句,而其他两个导出属性 b 和 c 已经在产物中被排除了。</p>
|
||||
<h3>Logs</h3>
|
||||
<p>第三个要提到的 Webpack 5 的效率优化点是,它增加了许多内部处理过程的日志,可以通过 stats.logging 来访问。下面两张图是使用相同配置*stats: {logging: "verbose"}*的情况下,Webpack 4 和 Webpack 5 构建输出的日志:</p>
|
||||
<p><img src="assets/CgqCHl9sT6WAWzGiAACp4k0mjjw366.png" alt="Drawing 3.png" />
|
||||
<img src="assets/Ciqc1F9sT6qAeQs4AAMWioPCn4s820.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/CgqCHl9sT6WAWzGiAACp4k0mjjw366.png" alt="png" />
|
||||
<img src="assets/Ciqc1F9sT6qAeQs4AAMWioPCn4s820.png" alt="png" /></p>
|
||||
<p>可以看到,Webpack 5 构建输出的日志要丰富完整得多。通过这些日志能够很好地反映构建各阶段的处理过程、耗费时间,以及缓存使用的情况。在大多数情况下,它已经能够代替之前人工编写的统计插件功能了。</p>
|
||||
<h3>其他功能优化项</h3>
|
||||
<p>除了上面介绍的和构建效率相关的几项变化外,Webpack 5 中还有许多大大小小的功能变化,例如新增了改变微前端构建运行流程的 <a href="https://webpack.js.org/concepts/module-federation/">Module Federation</a> 和对产物代码进行优化处理的 Runtime Modules,优化了处理模块的工作队列,在生命周期 Hooks 中增加了 stage 选项等。感兴趣的话,你可以通过文章顶部的文档链接或官方网站来进一步了解。</p>
|
||||
|
||||
@@ -188,11 +188,11 @@ import { appendHTML } from './common.js'
|
||||
...
|
||||
import('https://cdn.jsdelivr.net/npm/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="aac6c5cecbd9c287cfd9ea9e849b9d849b9f">[email protected]</a>/slice.js').then((module) => {...})
|
||||
</code></pre>
|
||||
<p><img src="assets/CgqCHl9yo46AYuszAANDvM6jRMk647.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/CgqCHl9yo46AYuszAANDvM6jRMk647.png" alt="png" /></p>
|
||||
<p>从示例中可以看到,在没有任何构建工具处理的情况下,在页面中引入带有 type="module" 属性的 script,浏览器就会在加载入口模块时依次加载了所有被依赖的模块。下面我们就来深入了解一下这种基于浏览器加载 JS 模块的技术的细节。</p>
|
||||
<h3>基于浏览器的 JS 模块加载功能</h3>
|
||||
<p>从 caniuse 网站中可以看到,目前大部分主流的浏览器都已支持 JavaScript modules 这一特性,如下图所示:</p>
|
||||
<p><img src="assets/Ciqc1F9yo5aADhYKAAMTR4GJTG8708.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9yo5aADhYKAAMTR4GJTG8708.png" alt="png" /></p>
|
||||
<p>[图片来源:<a href="https://caniuse.com/es6-module">https://caniuse.com/es6-module</a>]</p>
|
||||
<p>我们来总结这种加载方式的注意点。</p>
|
||||
<h4>HTML 中的 Script 引用</h4>
|
||||
@@ -223,9 +223,9 @@ cd example-vite
|
||||
npm install
|
||||
npm run dev
|
||||
</code></pre>
|
||||
<p><img src="assets/CgqCHl9yo-mAWrIzAAOaSZguuaM643.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/CgqCHl9yo-mAWrIzAAOaSZguuaM643.png" alt="png" /></p>
|
||||
<p>可以看到,运行示例代码后,在浏览器中只引入了 src/main.js 这一个入口模块,但是在网络面板中却依次加载了若干依赖模块,包括外部模块 vue 和 css。依赖图如下:</p>
|
||||
<p><img src="assets/Ciqc1F9yo_GAWATTAACYUvrJKL4148.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/Ciqc1F9yo_GAWATTAACYUvrJKL4148.png" alt="png" /></p>
|
||||
<p>可以看到,经过 Vite 处理后,浏览器中加载的模块与源代码中导入的模块相比发生了变化,这些变化包括对外部依赖包的处理,对 vue 文件的处理,对 css 文件的处理等。下面我们就来逐个分析其中的变化。</p>
|
||||
<h4>对导入模块的解析</h4>
|
||||
<p><strong>对 HTML 文件的预处理</strong></p>
|
||||
|
||||
@@ -166,7 +166,7 @@ function hide_canvas() {
|
||||
<p>上节课我们通过分析“为什么不在本地环境进行部署”这个问题,来对比部署系统的重要性:一个优秀的部署系统,能够自动化地完整部署流程的各环节,无须占用开发人员的时间与精力,同时又能保证环境与过程的一致性,增强流程的稳定性,降低外部因素导致的风险。此外,部署系统还可以提供过程日志、历史版本构建包、通知邮件等各类辅助功能模块,来打造更完善的部署工作流程。</p>
|
||||
<p>这节课我就来为你介绍在企业项目和开源项目中被广泛使用的几个典型部署工具,包括 Jenkins、CircleCI、Github Actions、Gitlab CI。</p>
|
||||
<h3>Jenkins</h3>
|
||||
<p><img src="assets/Ciqc1F-AFmqADoTDAADSwJG8TWA117.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-AFmqADoTDAADSwJG8TWA117.png" alt="png" /></p>
|
||||
<p>Jenkins Logo</p>
|
||||
<p><a href="https://www.jenkins.io/">Jenkins</a> 是诞生较早且使用广泛的开源持续集成工具。早在 2004 年,Sun 公司就推出了它的前身 Husdon,它在 2011 年更名为 Jenkins。下面介绍它的功能特点。</p>
|
||||
<h4>功能特点</h4>
|
||||
@@ -178,10 +178,10 @@ function hide_canvas() {
|
||||
<li><strong>Job 配置</strong>:得益于其插件系统,在 Jenkins 的 Job 配置中可以灵活定制各种复杂的构建与部署选项,例如构建远程触发、构建参数化选项、关联 Jira、执行 Windows 批处理、邮件通知等。</li>
|
||||
<li><strong>API 调用</strong>:Jenkins 提供了 Restful 的 API 接口,可用于外部调用控制节点、任务、配置、构建等处理过程。</li>
|
||||
</ul>
|
||||
<p><img src="assets/Ciqc1F-AFoiAbc1YAAIiZzD3poU694.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-AFoiAbc1YAAIiZzD3poU694.png" alt="png" /></p>
|
||||
<p>Jenkins 中 Job 的基本配置界面</p>
|
||||
<h3>CircleCI</h3>
|
||||
<p><img src="assets/Ciqc1F-AFpGAfFjTAAAXBcXm7AQ619.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-AFpGAfFjTAAAXBcXm7AQ619.png" alt="png" /></p>
|
||||
<p>CircleCI Logo</p>
|
||||
<p><a href="https://circleci.com/product/">CircleCI</a> 是一款基于云端的持续集成服务,下面介绍它的功能特点。</p>
|
||||
<h4>功能特点</h4>
|
||||
@@ -193,10 +193,10 @@ function hide_canvas() {
|
||||
<li><strong>配置简化</strong>:在 CircleCI 中提供了开箱即用的用户体验,只需要少量配置即可快速开始构建项目。</li>
|
||||
<li><strong>API 调用</strong>:CircleCI 中也提供了 Restfull 的 API 接口,可用于访问项目、构建和产物。</li>
|
||||
</ul>
|
||||
<p><img src="assets/Ciqc1F-AFqKAU29WAARwRnLOcKU376.png" alt="Drawing 3.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-AFqKAU29WAARwRnLOcKU376.png" alt="png" /></p>
|
||||
<p>CircleCI 项目流水线示例界面</p>
|
||||
<h3>Github Actions</h3>
|
||||
<p><img src="assets/Ciqc1F-AFquAK06qAAATdguATCs007.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-AFquAK06qAAATdguATCs007.png" alt="png" /></p>
|
||||
<p>Github Actions Logo</p>
|
||||
<p><a href="https://github.com/features/actions">Github Actions</a>(GHA)是 Github 官方提供的 CI/CD 流程工具,用于为 Github 中的开源项目提供简单易用的持续集成工作流能力。</p>
|
||||
<h4>功能特点</h4>
|
||||
@@ -208,9 +208,9 @@ function hide_canvas() {
|
||||
<li><strong>社区支持</strong>:Github 社区中提供了众多工作流的模板可供选择使用,例如构建并发布 npm 包、构建并提交到 Docker Hub 等。</li>
|
||||
<li><strong>费用情况</strong>:Github Action 对于公开的仓库,以及在自运维执行器的情况下是免费的。而对于私有仓库则提供一定额度的免费执行时间和免费存储空间,超出部分则需要收费。</li>
|
||||
</ul>
|
||||
<p><img src="assets/CgqCHl-AFrqAZddtAAJj5zKVbrY255.png" alt="Drawing 5.png" /></p>
|
||||
<p><img src="assets/CgqCHl-AFrqAZddtAAJj5zKVbrY255.png" alt="png" /></p>
|
||||
<p>Github Actions 的工作流模板</p>
|
||||
<p><img src="assets/Ciqc1F-AFsGAVzEOAAF9PCfkPQQ795.png" alt="Drawing 6.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-AFsGAVzEOAAF9PCfkPQQ795.png" alt="png" /></p>
|
||||
<p>Github Actions 中的矩阵执行示例</p>
|
||||
<h3>Gitlab CI</h3>
|
||||
<p>Gitlab 是由 Gitlab Inc. 开发的基于 Git 的版本管理与软件开发平台。除了作为代码仓库外,它还具有在线编辑、Wiki、CI/CD 等功能。在费用方面,它提供了免费的社区版本(Community Edition,CE)和免费或收费的商用版本(Enterprise Edition,EE)。其中社区版本和免费的商用版本的区别主要体现在升级到付费商用版本时的操作成本。另一方面,即使是免费的社区版本,其功能也能够满足企业内的一般使用场景,因此常作为企业内部版本管理系统的主要选择之一,下面我们就来了解 Gitlab 内置的 CI/CD 功能。</p>
|
||||
@@ -220,7 +220,7 @@ function hide_canvas() {
|
||||
<li><strong>独立安装执行器</strong>:与前面两款产品不同的是,Gitlab 中需要单独安装执行器。Gitlab 中的执行器 Gitlab Runner 是一个独立运行的开源程序,它的作用是执行任务,并将结果反馈到 Gitlab 中。开发者可以在独立的服务器上<a href="https://docs.gitlab.com/runner/install/index.html">安装</a>Gitlab Runner 工具,然后依次执行<strong>gitlab-runner register</strong>注册特定配置的 Runner,最后执行<strong>gitlab-runner start</strong>启动相应服务。此外,项目中除了注册独立的 Runner 外,也可以使用共享的或组内通用的 Runner。</li>
|
||||
</ul>
|
||||
<p>当项目根目录中存在.gitlab-ci.yml 文件时,用户提交代码到 Git 仓库时,在 Gitlab 的 CI/CD 面板中即可看到相应的任务记录,当成功设置 gitlab-runner 时这些任务就会在相应的 Runner 中执行并反馈日志和结果。如下图所示:</p>
|
||||
<p><img src="assets/CgqCHl-AFtSAdFLVAAJ3DhDFMt0140.png" alt="Drawing 7.png" /></p>
|
||||
<p><img src="assets/CgqCHl-AFtSAdFLVAAJ3DhDFMt0140.png" alt="png" /></p>
|
||||
<p>Gitlab CI/CD 的任务列表示例界面</p>
|
||||
<h3>总结</h3>
|
||||
<p>最后我们来做一个总结。在今天的课程里,我们一起了解了 4 个典型 CI/CD 工具:Jenkins、CircleCI、Github Actions 和 Gitlab CI。</p>
|
||||
|
||||
@@ -200,7 +200,7 @@ time pnpm i
|
||||
</ul>
|
||||
<h4>环境状态的五个分析维度</h4>
|
||||
<p>在确定了安装工具和分析方式后,我们还需要对执行过程进行划分,下面我一共区分了 5 种项目执行安装时可能遇到的场景:</p>
|
||||
<p><img src="assets/Ciqc1F-EFtSATNnGAABjLC1FXMQ343.png" alt="Drawing 0.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-EFtSATNnGAABjLC1FXMQ343.png" alt="png" /></p>
|
||||
<blockquote>
|
||||
<p>注 1:除了第一种纯净环境外,后面的环境中都存在 Lock 文件。因为 Lock 文件对于提供稳定依赖版本至关重要。出于现实场景考虑,这里不再单独对比没有 Lock 文件但存在历史安装目录的场景。
|
||||
注 2: 为了屏蔽网络对解析下载依赖包的影响,所有目录下均使用相同注册表网址 registry.npm.taobao.org。
|
||||
@@ -209,7 +209,7 @@ time pnpm i
|
||||
<h3>不同维度对安装效率的影响分析</h3>
|
||||
<h4>纯净环境</h4>
|
||||
<p>首先来对纯净环境进行分析,不同安装方式的执行耗时统计如下:</p>
|
||||
<p><img src="assets/Ciqc1F-EFuOAf51jAABmxP3DGmM839.png" alt="Drawing 1.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-EFuOAf51jAABmxP3DGmM839.png" alt="png" /></p>
|
||||
<blockquote>
|
||||
<p>注 1:总安装时间为执行后显示的时间。而各阶段的细分时间在日志中分析获取。
|
||||
注 2:在 pnpm 的执行过程中并未对各阶段进行完全分隔,而是针对不同依赖包递归执行各阶段,这种情况在<strong>纯净环境中尤其明显</strong>,因此阶段时间上不便做单独划分。</p>
|
||||
@@ -222,7 +222,7 @@ time pnpm i
|
||||
</ul>
|
||||
<h4>Lock 环境</h4>
|
||||
<p>然后我们来考察 Lock 文件对于安装效率的影响。和第一种最纯净的情况相比,带有 Lock 文件的情况通常更符合现实中项目在部署环境中的初始状态(因为 Lock 文件可以在一定程度上保证项目依赖版本的稳定性,因此通常都会把 Lock 文件也保留在代码仓库中)。引入 Lock 文件后,不同安装工具执行安装的耗时情况如下:</p>
|
||||
<p><img src="assets/CgqCHl-EFwyAUyc0AABWRUrdf70141.png" alt="Drawing 2.png" /></p>
|
||||
<p><img src="assets/CgqCHl-EFwyAUyc0AABWRUrdf70141.png" alt="png" /></p>
|
||||
<blockquote>
|
||||
<p>注 1: Yarn 解析依赖阶段日志未显示耗时,因此标记为 0 秒。</p>
|
||||
</blockquote>
|
||||
@@ -233,7 +233,7 @@ time pnpm i
|
||||
</ul>
|
||||
<h4>缓存环境</h4>
|
||||
<p>缓存环境是在部署服务中可能遇到的一种情形。项目在部署过程中依赖安装时产生了本地缓存,部署结束后项目工作目录被删除,因此再次部署开始时工作目录内有 Lock 文件,也有本地缓存,但是不存在安装目录。这种情形下的耗时统计如下:</p>
|
||||
<p><img src="assets/CgqCHl-EFxqARvOCAABWYHKz7lk118.png" alt="Drawing 3.png" /></p>
|
||||
<p><img src="assets/CgqCHl-EFxqARvOCAABWYHKz7lk118.png" alt="png" /></p>
|
||||
<p>对结果的分析如下:</p>
|
||||
<ul>
|
||||
<li>从执行时间上看,各类型的安装方式的耗时都明显下降。</li>
|
||||
@@ -241,7 +241,7 @@ time pnpm i
|
||||
</ul>
|
||||
<h4>无缓存的重复安装环境</h4>
|
||||
<p>无缓存的重复安装环境在本地环境下部署时可能遇到,即当本地已存在安装目录,但人工清理缓存后再次执行安装时可能遇到。这种情况的耗时如下:</p>
|
||||
<p><img src="assets/Ciqc1F-EFyqAc4dbAABWpKpPTt4305.png" alt="Drawing 4.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-EFyqAc4dbAABWpKpPTt4305.png" alt="png" /></p>
|
||||
<p>对结果的分析如下:</p>
|
||||
<ul>
|
||||
<li>从上面的表格中可以看到,存在安装目录这一条件首先<strong>对链接阶段能起到优化的作用</strong>。对于下载阶段,除了使用 PnP 的两种安装方式外,当项目中已存在安装目录时,下载阶段耗时也趋近于零。其中 Yarn v1 表现最好,各主要阶段都直接略过,而 npm 和 pnpm 则多少还有一些处理过程。</li>
|
||||
@@ -249,7 +249,7 @@ time pnpm i
|
||||
</ul>
|
||||
<h4>有缓存的重复安装环境</h4>
|
||||
<p>最后是安装目录与本地缓存都存在的情况,耗时如下:</p>
|
||||
<p><img src="assets/CgqCHl-EFzKAOLpCAABS3Wdu4Zw636.png" alt="Drawing 5.png" /></p>
|
||||
<p><img src="assets/CgqCHl-EFzKAOLpCAABS3Wdu4Zw636.png" alt="png" /></p>
|
||||
<p>对结果的分析如下:</p>
|
||||
<ul>
|
||||
<li>无论对于哪种安装方式而言,这种情况都是最理想的。可以看到,各安装工具的耗时都趋近于零。其中尤其对于 Yarn v1 而言效率最高,而 pnpm 次之,npm 相对最慢。</li>
|
||||
|
||||
@@ -174,7 +174,7 @@ npm config set registry xxxx
|
||||
#yarn设置下载源
|
||||
yarn config set registry xxxx
|
||||
</code></pre>
|
||||
<p><img src="assets/Ciqc1F-JVzCALvraAAFJNvHfPYg442.png" alt="image.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-JVzCALvraAAFJNvHfPYg442.png" alt="png" /></p>
|
||||
<p>下载同样的依赖包,使用国内镜像源的速度只有官方源的 1/4。有条件的情况下可以在企业内网部署私有源,下载速度可以得到进一步提升。</p>
|
||||
<p>2.<strong>二进制下载源</strong>:对于一些依赖包(例如 node-sass 等),在安装过程中还需下载二进制文件,这类文件的下载不遵循 registry 的地址,因此需要对这类文件单独配置下载路径来提升下载速度。示例配置如下代码(更多配置可以参考<a href="https://npm.taobao.org/mirrors">国内的镜像网址</a>):</p>
|
||||
<pre><code>npm config set sass-binary-site https://npm.taobao.org/mirrors/node-sass
|
||||
@@ -212,7 +212,7 @@ npm config set puppeteer_download_host https://npm.taobao.org/mirrors
|
||||
<h4>提升压缩效率的工具</h4>
|
||||
<p>这里介绍两种压缩工具:Pigz 和 Zstd。更多压缩工具的选择以及性能对比可以参见<a href="https://community.centminmod.com/threads/round-4-compression-comparison-benchmarks-zstd-vs-brotli-vs-pigz-vs-bzip2-vs-xz-etc.18669/">参考文档</a>。</p>
|
||||
<p>首先我们对这两种工具和 tar 命令中默认的 Gzip 压缩选项的参数进行对比(数据来自上面的参考文档),如下面的表格所示:</p>
|
||||
<p><img src="assets/Ciqc1F-JXZOAFGWWAABuWKJh0YA453.png" alt="image" /></p>
|
||||
<p><img src="assets/Ciqc1F-JXZOAFGWWAABuWKJh0YA453.png" alt="png" /></p>
|
||||
<p>从表格中可以发现:</p>
|
||||
<ul>
|
||||
<li>对于同一款压缩工具来说,压缩等级越低,压缩速度越快。代价是相应的压缩率越低,压缩体积会相应增大。</li>
|
||||
|
||||
@@ -165,7 +165,7 @@ function hide_canvas() {
|
||||
<div><h1>23 结束语 前端效率工程化的未来展望</h1>
|
||||
<p>你好,我是李思嘉。</p>
|
||||
<p>本专栏的内容到这里就结束了。我们先来简单回顾一下整个课程的主要内容,如下图:</p>
|
||||
<p><img src="assets/Ciqc1F-WhHaAcAYHAAFR7grnZ_s239.png" alt="Lark20201026-160921.png" /></p>
|
||||
<p><img src="assets/Ciqc1F-WhHaAcAYHAAFR7grnZ_s239.png" alt="png" /></p>
|
||||
<p>在这个专栏中,我主要介绍且梳理了前端工程化中效率提升方向的知识,内容涵盖开发效率、构建效率和部署效率三个方面。希望你通过这个系列课程的学习,能建立起前端效率工程化方面相对完整的知识体系,同时在前端开发日常流程中的效率工程类问题方面,能找到分析和解决的新方向。</p>
|
||||
<p>当然,这些方向实际涵盖的概念与技术点非常广泛,并不容易完全掌握,除了已有的概念和技术之外,新的技术和方向也在不断涌现。下面我会对前端效率工程化相关的技术做一些展望。</p>
|
||||
<h3>云工作流</h3>
|
||||
|
||||
Reference in New Issue
Block a user