first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View File

@ -0,0 +1,65 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 建立上帝视角,全面系统掌握前端效率工程化
你好,我是李思嘉,从事前端开发十余年,曾先后在多家大型互联网公司从事前端架构工作,历经很多项目从 0 到 1 的搭建过程,也做了不少前端效率优化和性能提升等工程化的实践。
目前,我在贝壳找房前端架构组任资深工程师,专注于公司内前端通用构建平台,以及前端开发工具生态的服务建设。工作中,我接触过不少项目搭建、开发提效、构建优化、部署工具和容器化等方面的技术细节,也沉淀出了一套关于前端工程化的方法论,希望在这里分享给你。
为什么要学习前端效率工程化
通常,一个中高级前端工程师,除了要完成业务功能开发目标外,还要对所开发项目的效率、性能、质量等工程化维度去制定和实施技术优化目标,其中以提升效率为目标的优化技术和工具就属于效率工程化的范畴。
对于公司而言,团队效率可以直接带来人工投入产出比的提升,因此效率提升通常会被作为技术层面的一个重点优化方向。而在面试中,对效率工程化的理解程度和实践中的优化产出情况,也是衡量前端工程师能力高低的常见标准。
例如,在拉勾网搜索前端相关职位,可以看到中高级以上的前端工程师岗位需求中大都会要求熟练掌握 webpack 构建工具、具备开发效率实践经验等。只有具备这方面的能力,你才能应对和优化复杂项目,保证团队高效产出。
拉勾网搜索“前端效率工程”的岗位情况
然而,大部分时间都投身在业务开发中的前端同学,在效率工程化方面经常面临很多困扰:
由于缺乏系统化知识,对于项目中的效率问题常常不知从何处着手,甚至找错解决方向。比如在解决项目构建效率问题时,考虑的通常是增加优化插件的方向,但有时候问题的关键点可能只是一些 source map 、include 之类的基本参数设置不当。
由于缺少工程化的视野,难以发现工作中的效率提升点和制定针对性的提升方案,导致技术优化实践少,成长慢。
技术晋升和面试求职中,由于缺少方法论和深度思考,在讲解技术优化细节时常常回答得不完整或者有错漏、混淆,很难在能力表现上脱颖而出。
要解决这些问题,单单凭借个人知识积累往往成长缓慢,难以打开视野,最有效的方式是找到自己的短板来做针对性提升。而只有全面、系统地掌握效率的影响因素以及其中的技术细节,才能在面对实际问题时明确分析思路,快速找到症结所在,制定有针对性的优化方案。
但和语言类教程不同,你很难找到系统讲解前端效率工程化的课程。因为,效率工程化涉及的知识点更为繁杂,散落在大量的细节优化实践中,需要人为地去总结和梳理完善。而这,正是我写作这门课的初衷。
课程设计
在这个课程中,我梳理了前端开发工作流程中和效率提升相关的知识点和案例,希望借此能帮你构筑一个系统性的前端效率知识体系,建立正确的问题解决思路,让你进行效率优化时有据可依。
课程共 22 篇,分别从开发效率、构建效率和部署效率 3 个维度来展开讲解。
第一部分:开发效率。 开发是我们日常工作过程中最熟悉的部分。工欲善其事必先利其器,在这一模块,我会主要分析各种项目在开发过程中的效率提升点,例如在项目启动时如何选择和配置自定义脚手架、如何配置我们的开发联调环境等效率优化细节,还会介绍包括时下流行的云开发、无代码工具、低代码工具等提效新思路。希望你在学习之后,能够在未来的项目开发中自如地选择和搭建最适合自身的开发工具集。
第二部分:构建效率。 如果说开发是我们日常投入最多的工作,那么等待构建结果就是日常耗费最多的非开发时间了。在这一模块,我会分析那些影响到 webpack 构建时间的关键因素,并详细分析对应的解决方案和工具。此外,我还会进一步讲解 webpack 5 中新的效率提升方案,并带你了解 no-bundle 类构建工具的优缺点。希望通过这些内容的学习,来帮助你建立完整的构建工具优化思路,进一步优化你的项目构建效率 最大程度消灭那些无谓的等待时间。
第三部分:部署效率。 代码从构建到部署是前端能力的延伸。许多企业日常工作中的代码部署使用的是前后端通用的 CI/CD 系统,而前端开发人员在使用过程中较少能对其中的流程效率进行优化。在这一模块,我将介绍那些业界常用的 CI/CD 系统 ,并分析其中前端项目的效率优化点,以及从打包机方案到容器化方案、前端项目在部署时的注意点和优化空间。 希望学习完这部分内容,你能结合所在企业的技术特点,来打造或优化适合你前端项目的部署流程。
你将获得
全面、系统的效率工程化知识体系。我会带你系统学习相关知识,而不是碎片化获取,让你补全短板,提升个人技术实力。
对实际项目输出针对性优化方案的能力。正确的方法比努力更重要,有了正确的思路方法,才能在实际工作中快速定位症结、避免跑偏,避免把力气花在一些细枝末节上。
丰富的实战经验分享。我将从常用的开发效率提升工具讲到 webpack 底层的技术细节,再到部署工具中的效率优化分析,高度还原真实的业务场景,带你了解前端效率工程优化的全过程。
面试 Offer 收割利器。课程中的许多案例,都是前端工程化方向面试题的重灾区,我将指出容易被忽略的内容考点,让你既能在整体上对效率工程化有一个由点到面的认识,也能深入掌握关键的技术细节。
作者寄语
最后前端效率工程化涉及前端日常工作的各个环节90% 的复杂度都藏匿在冰山之下,也因此很多人在解决效率问题时 “只见树木,不见森林”,希望这个专栏可以帮你建立上帝视角,让你体会到“哦,原来效率优化还有这些方面!”的感觉。
单点问题的解决往往只关注当下,但系统化的解决方案,有助于增长你的长期价值。希望这个课程能够让你有新的启发,也希望你在留言区和我说说你的成长与困惑,与众人一起前行。

View File

@ -0,0 +1,356 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 项目基石:前端脚手架工具探秘
你好,我是李思嘉,前端效率工程化这门课我们会讨论一个前端项目从开发到构建和部署这一系列工作流程的效率问题。在开发效率篇里,我们会讨论一系列影响开发效率的流程和工具。工欲善其事必先利其器,第一课时,我们首先从开发一个新项目时最基础的准备工作讲起。
当你准备开发一个新项目时,在进入到实际业务编码前,通常需要做很多的基础准备工作,这里会遇到的问题有:
要准备好一个项目的基础开发设施,需要投入大量时间和精力,这部分的工作计量是以天为单位的。
一个完备的项目基础环境就像一个精密的仪器,只有各部分都充分协调后才能运转正常。要在较短时间内配置一个技术栈完整、辅助功能丰富、兼顾不同环境下构建优化目标的项目基础代码,通常需要开发人员在工程领域长久的知识储备与实践总结,而这对于经验相对较少的开发人员而言是一个不小的挑战。
不同的项目需求和团队情况,对应我们在使用基础设施时的选择可能也各不相同,因此我们并不能依靠一套固定不变的模板,而是需要根据不同的现状来使用不同的基础设施。这又增加了整体时间成本。
而脚手架工具,正是为了解决这些问题而诞生的。
利用脚手架工具,我们可以经过几个简单的选项快速生成项目的基础代码。
使用脚手架工具生成的项目模板通常是经过经验丰富的开发者提炼和检验的,很大程度上代表某一类项目开发的最佳实践,相较于让开发者自行配置提供了更优选择。
同时,脚手架工具也支持使用自定义模板,我们也可以根据项目中的实际经验总结、定制一个脚手架模板。
因此,对于一个熟练的前端工程师来说,要掌握的基本能力之一就是通过技术选型来确定所需要使用的技术栈,然后根据技术栈选择合适的脚手架工具,来做项目代码的初始化。一个合适的脚手架,可以为开发人员提供反复优化后的开发流程配置,高效地解决开发中涉及的流程问题,使得工程师能够快速上手,并提升整个开发流程的效率和体验。当然,前提是建立在选择对了脚手架工具并深入掌握其工作细节的基础上。
那么下面我们先来谈谈脚手架工具究竟是什么。
什么是脚手架
说到脚手架Scaffold 这个词,相信你并不陌生,它原本是建筑工程术语,指为了保证施工过程顺利而搭建的工作平台,它为工人们在各层施工提供了基础的功能保障。
而在软件开发领域,脚手架是指通过各种工具来生成项目基础代码的技术。通过脚手架工具生成后的代码,通常已包含了项目开发流程中所需的工作目录内的通用基础设施,使开发者可以方便地将注意力集中到业务开发本身。
那么对于日常的前端开发流程来说,项目内究竟有哪些部分属于通用基础设施呢?让我们从项目创建的流程说起。对于一个前端项目来说,一般在进入开发之前我们需要做的准备有:
首先我们需要有 package.json它是 npm 依赖管理体系下的基础配置文件。
然后选择使用 npm 或 Yarn 作为包管理器,这会在项目里添加上对应的 lock 文件,来确保在不同环境下部署项目时的依赖稳定性。
确定项目技术栈,团队习惯的技术框架是哪种?使用哪一种数据流模块?是否使用 TypeScript使用哪种 CSS 预处理器?等等。在明确选择后安装相关依赖包并在 src 目录中建立入口源码文件。
选择构建工具,目前来说,构建工具的主流选择还是 webpack (除非项目已先锋性地考虑尝试 nobundle 方案),对应项目里就需要增加相关的 webpack 配置文件,可以考虑针对开发/生产环境使用不同配置文件。
打通构建流程,通过安装与配置各种 Loader 、插件和其他配置项,来确保开发和生产环境能正常构建代码和预览效果。
优化构建流程,针对开发/生产环境的不同特点进行各自优化。例如,开发环境更关注构建效率和调试体验,而生产环境更关注访问性能等。
选择和调试辅助工具,例如代码检查工具和单元测试工具,安装相应依赖并调试配置文件。
最后是收尾工作,检查各主要环节的脚本是否工作正常,编写说明文档 README.md将不需要纳入版本管理的文件目录记入 .gitignore 等。
正如下面简单的示例项目模板,经历了上面这些步骤后我们的项目目录下就新增了这些相关的文件:
package.json 1) npm 项目文件
package-lock.json 2) npm 依赖 lock 文件
public/ 3) 预设的静态目录
src/ 3) 源代码目录
main.ts 3) 源代码中的初始入口文件
router.ts 3) 源代码中的路由文件
store/ 3) 源代码中的数据流模块目录
webpack/ 4) webpack 配置目录
common.config.js 4) webpack 通用配置文件
dev.config.js 4) webpack 开发环境配置文件
prod.config.js 4) webpack 生产环境配置文件
.browserlistrc 5) 浏览器兼容描述 browserlist 配置文件
babel.config.js 5) ES 转换工具 babel 配置文件
tsconfig.json 5) TypeScript 配置文件
postcss.config.js 5) CSS 后处理工具 postcss 配置文件
.eslintrc 7) 代码检查工具 eslint 配置文件
jest.config.js 7) 单元测试工具 jest 配置文件
.gitignore 8) Git 忽略配置文件
README.md 8) 默认文档文件
而通过脚手架工具,我们就能免去人工处理上的环节,轻松地搭建起项目的初始环境,直接进入到业务开发中。接下来我们就先来看一下前端领域的几个典型脚手架工具,了解这几个脚手架所代表的不同设计理念,接着我们会重点分析两个代表性脚手架工具包内的技术细节,以便在工作中更能得心应手地使用和优化。
三种代表性的前端脚手架工具
Yeoman
[图logo-yeoman]
Yeoman 是前端领域内较早出现的脚手架工具,它由 Google I/O 在 2012 年首次发布。Yeoman 提供了基于特定生成器Generator来创建项目基础代码的功能。时至今日在它的网站中能找到超过 5600 个不同技术栈的代码生成器。
作为早期出现在前端领域的脚手架工具,它没有限定具体的开发技术栈,提供了足够的开放性和自由度,但也因此缺乏某一技术栈的深度集成和技术生态。随着前端技术栈的日趋复杂化,人们更倾向于选择那些以具体技术栈为根本的脚手架工具,而 Yeoman 则更多用于一些开发流程里特定片段代码的生成。
Create-React-App
[图logo-create-react-app]
Create React App后简称 CRA )是 Facebook 官方提供的 React 开发工具集。它包含了 create-react-app 和 react-scripts 两个基础包。其中 create-react-app 用于选择脚手架创建项目,而 react-scripts 则作为所创建项目中的运行时依赖包,提供了封装后的项目启动、编译、测试等基础工具。
正如官方网站中所说的CRA 带来的最大的改变,是将一个项目开发运行时的各种配置细节完全封装在了一个 react-scripts 依赖包中,这大大降低了开发者,尤其是对 webpack 等构建工具不太熟悉的开发者上手开发项目的学习成本,也降低了开发者自行管理各配置依赖包的版本所需的额外测试成本。
但事情总有两面性,这种近乎黑盒的封装在初期带来便利的同时,也为后期的用户自定义优化带来了困难。虽然官方也提供了 eject 选项来将全部配置注入回项目,但大部分情况下,为了少量优化需求而放弃官方提供的各依赖包稳定升级的便利性,也仍不是一个好的选择。在这种矛盾之下,在保持原有特性的情况下提供自定义配置能力的工具 react-rewired 和 customize-cra 应运而生。
Vue CLI
[图logo-vue-cli]
正如 Create-React-App 在 React 项目开发中的地位, Vue 项目的开发者也有着自己的基础开发工具。Vue CLI 由 Vue.js 官方维护,其定位是 Vue.js 快速开发的完整系统。完整的 Vue CLI 由三部分组成:作为全局命令的 @vue/cli、作为项目内集成工具的 @vue/cli-service、作为功能插件系统的 @vue/cli-plugin-。
Vue CLI 工具在设计上吸取了 CRA 工具的教训,在保留了创建项目开箱即用的优点的同时,提供了用于覆盖修改原有配置的自定义构建配置文件和其他工具配置文件。
同时在创建项目的流程中Vue CLI 也提供了通过用户交互自行选择的一些定制化选项例如是否集成路由、TypeScript 等,使开发者更有可能依据这些选项来生成更适合自己的初始化项目,降低了开发者寻找模板或单独配置的成本。
除了技术栈本身的区别之外,以上三种脚手架工具,实际上代表了三种不同的工具设计理念:
Yeoman 代表的是一般开源工具的理念。它不提供某一技术栈的最佳实践方案,而专注于实现脚手架生成器的逻辑和提供展示第三方生成器。作为基础工具,它的主要目标群体是生成器的开发者,而非那些需要使用生成器来开发项目的人员,尽管后者也能通过前者的实践而受益。
CRA 代表的是面向某一技术栈降低开发复杂度的理念。它通过提供一个包含各开发工具的集成工具集和标准化的开发-构建-测试三步流程脚本,使得开发者能无障碍地按照既定流程进行 React 项目的开发和部署。
Vue CLI 代表的是更灵活折中的理念。它一方面继承了 CRA 降低配置复杂度的优点,另一方面在创建项目的过程中提供了更多交互式选项来配置技术栈的细节,同时允许在项目中使用自定义配置。这样的设计在一定程度上增加了模板维护的复杂度,但是从最终效果来看,能够较大程度满足各类开发者的不同需求。
了解脚手架模板中的技术细节
刚上手开发项目时,我们通过上述脚手架提供的开箱即用的能力可以很容易地上手开发项目,但是往往在开发过程中遇到问题时又需要回过头来查询文档,看脚手架中是否已有相应解决方案。而如果我们对该脚手架足够熟悉,就能减少这类情况下所花费的时间,提升开发效率。所以在这里,我们先来聊一下该如何了解一个脚手架。
要了解一个脚手架,除了学会如何使用脚手架来创建项目外,我们还需要了解它提供的具体功能边界,提供了哪些功能、哪些优化。这样我们才能在后续的开发中更得心应手,后续的优化也更有的放矢。
还是以上面的 CRA 和 Vue CLI 为例,除了通过脚手架模板生成项目之外,项目内部分别使用 react-scripts 和 vue-cli-service 作为开发流程的集成工具。接下来,我们先来对比下这两个工具在开发与生产环境命令中都使用了哪些配置项,其中一些涉及效率的优化项在后面的课程中还会详细介绍。
webpack loaders
从下面表格中我们可以发现,在一般源文件的处理器使用方面,两个脚手架工具大同小异,对于 babel-loader 都采用了缓存优化Vue 中还增加了多线程的支持。在样式和其他类型文件的处理上 Vue 默认支持更多的文件类型,相应的,在 CRA 模板下如果需要支持对应文件就需要使用 customize-cra 等工具来添加新处理模块。
webpack plugins
在与构建核心功能相关的方面html、env、hot、css extract、fast ts check两者使用的插件相同而在其他一些细节功能上各有侧重例如 React 的 inline chunk 和 Vue 的 preload。
(第三方工具)
webpack.optimize
两者在代码优化配置中相同的部分包括:都使用 TerserPlugin 压缩JavaScript 都使用 splitChunks 做自动分包 参数不同。CSS 的压缩分别采用上面表格中的 OptimizeCssAssetsWebpackPlugin 和 OptimizeCssNanoPlugin 。react-scripts 中还开启了 runtimeChunk 以优化缓存。
webpack resolve
在 resolve 和 resolve loader 部分,值得一提的是两者都使用 PnpWebpackPluginpnp 来加速使用 Yarn 作为包管理器时的模块安装和解析,感兴趣的同学可以 进一步了解,我们在后面构建和部署的篇章中也会再次谈到。
通过上述几方面的对比,我们就对这两个典型脚手架工具提供的构建集成能力有了一个大概的了解。这有助于我们在使用具体工具时快速定位问题的边界,同时在使用其他脚手架工具和模板时,我们也可以参照和借鉴上面的最佳实践方案。下一步,我们再来讨论定制专属脚手架模板的问题。
如何定制一个脚手架模板
虽然官方提供的默认脚手架模板已经代表了对应技术栈的通用最佳实践,但是在实际开发中,我们还是时常需要对通过这些脚手架创建的模板项目进行定制化,例如:
为项目引入新的通用特性。
针对构建环节的 webpack 配置优化,来提升开发环境的效率和生产环境的性能等。
定制符合团队内部规范的代码检测规则配置。
定制单元测试等辅助工具模块的配置项。
定制符合团队内部规范的目录结构与通用业务模块,例如业务组件库、辅助工具类、页面模板等。
通过将这些实际项目开发中所需要做的定制化修改输出为标准的脚手架模板,我们就能在团队内部孵化出更符合团队开发规范的开发流程。一方面最大程度减少大家在开发中处理重复事务的时间,另一方面也能减少因为开发风格不一导致的团队内项目维护成本的增加。接下来,我们就结合上面提到的三个脚手架工具来分别看下如何定制专属的脚手架模板。
使用 Yeoman 创建生成器
脚手架模板在 Yeoman 中对应的是生成器 Generator。作为主打自由制作和分享脚手架生成器的开源工具 Yeoman 为制作生成器提供了丰富的 API 和 详细的文档。在这里,我们简单概述一下,一个基本的复制已有项目模板的生成器包含了:
生成器描述文件 package.json其中限定了 name、file、keywords 等对应字段的规范赋值。
作为主体的 generators/app 目录,包含生成器的核心文件。该目录是执行 yo 命令时的默认查找目录, Yeoman 支持多目录的方式集成多个子生成器,篇幅原因我就不在这里展开了。
app/index.js 是生成器的核心控制模块,其内容是导出一个继承自 yeoman-generator 的类,并由后者提供运行时上下文、用户交互、生成器组合等功能。
app/templates/ 目录是我们需要复制到新项目中的脚手架模板目录。
基本目录结构如下所示:
generator-[name]/
package.json
generators/
app/
templates/...
index.js
其中 app/index.js 的核心逻辑如下:
var Generator = require('yeoman-generator')
module.exports = class extends Generator {
writing() {
this.fs.copyTpl(
this.templatePath('.'),
this.destinationPath('.'))
}
install() {
this.npmInstall()
}
}
writing 和 install 是 Yeoman 运行时上下文的两个阶段,在例子中,当我们执行下面的创建项目命令时,依次将生成器中模板目录内的所有文件复制到创建目录下,然后执行安装依赖。
在完成生成器的基本功能后,我们就可以通过在生成器目录里 npm link ,将对应生成器包挂载到全局依赖下,然后进入待创建项目的目录中,执行 yo 创建命令即可。 (如需远程安装,则需要先将生成器包发布到 npm 仓库中,支持发布到 @scope/generator-[name] 。)
至此,制作 Yeoman 的生成器来定制项目模板的基本功能就完成了。除了基本的复制文件和安装依赖外, Yeoman 还提供了很多实用的功能,例如编写用户交互提示框或合成其他生成器等,可供开发者定制功能体验更完善的脚手架生成器。
为 create-react-app 创建自定义模板
为 create-react-app 准备的自定义模板在模式上较为简单。作为一个最简化的 CRA 模板,模板中包含如下必要文件:
README.md用于在 npm 仓库中显示的模板说明。
package.json用于描述模板本身的元信息 (例如名称、运行脚本、依赖包名和版本等) 。
template.json用于描述基于模板创建的项目中的 package.json 信息。
template 目录:用于复制到创建后的项目中,其中 gitignore 在复制后重命名为 .gitignore public/index.html和src/index 为运行 react-scripts 的必要文件。
具体目录结构如下所示:
cra-template-[template-name]/
README.md (for npm)
template.json
package.json
template/
README.md (for projects created from this template)
gitignore
public/
index.html
src/
index.js (or index.tsx)
在使用时,同样还是需要将模板通过 npm link 命令映射到全局依赖中,或发布到 npm 仓库中,然后执行创建项目的命令。
npx create-react-app [app-name] --template [template-name]
为 Vue CLI 创建自定义模板
相比 CRA 模板而言Vue 的模板中变化最大的当属增加了 meta.js/json 文件,用于描述创建过程中的用户交互信息以及用户选项对于模板文件的过滤等。
[template-name]/
README.md (for npm)
meta.js or meta.json
template/
此外Vue 的 template 目录中包含了复制到项目中的所有文件,并且在相关文件中还增加了 handlebars 条件判断的部分,根据 meta.js 中指定用户交互结果选项来将模板中带条件的文件转换为最终生成到项目中的产物。如以下代码所示:
template/package.json
...
"dependencies": {
"vue": "^2.5.2"{{#router}},
"vue-router": "^3.0.1"{{/router}}
},
...
meta.js
...
prompts: {
...
router: {
when: 'isNotTest',
type: 'confirm',
message: 'Install vue-router?',
},
...
}
使用自定义模板创建项目的命令为:
npm install -g @vue/cli-init
vue init [template-name] [app-name]
这样就完成了脚手架的定制工作。有了定制化后的脚手架,我们就可以在之后的创建项目时直接进入到业务逻辑的开发中,而不必重复地对官方提供的标准化模板进行二次优化。
总结
使用脚手架工具是提升开发效率的第一项内容。通过今天的学习,我们了解了脚手架的使用场景,了解了 3 个典型脚手架工具的特点,并分析了 React 和 Vue 官方提供的脚手架工具中的构建集成技术细节。最后,对于希望将业务中使用的更具定制化的基础代码转变为新的脚手架模板的同学,我们也了解了如何在不同工具环境下创建和使用自定义模板。
课程最后,我想请你来回想一下:你在项目开发中使用的是哪一种脚手架工具和模板?使用的理由是?你可以将答案写在留言区与大家一起讨论。
下个课时我们将要学习的是一个大家一直在使用但是很少了解其中细节的技术点:热更新技术。

View File

@ -0,0 +1,351 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 界面调试:热更新技术如何开着飞机修引擎?
你好,我是李思嘉,上一课时我们聊了几个典型脚手架的特点和使用,课后给大家留的思考题是在实际项目里使用的是哪一类脚手架工具以及使用的理由,希望你能通过这个题目来思考工具之间的差异性。这个思考的过程有助于加深我们对工具本身的细节认知,也能够锻炼技术选型的思维。
这一课时,我们再来聊聊前端开发过程中一个经典的提高开发效率的技术:浏览器的热更新。
什么是浏览器的热更新
看见浏览器热更新,相信你很容易想到 webpack 和 webpack-dev-server 。确实,现在各类型的脚手架工具在创建项目时通常已配置好了开启各种优化选项的 webpack ,其中自然也包含了开发服务器。大家在上手开发时,可以简单地执行 npm start (cra) 或 npm run serve (vue cli),就能体验到热更新的效果。
但是在我过去担任中高级前端岗位的面试官时经常发现很多来面试的同学对于到底什么是热更新都很难讲清楚热更新是保存后自动编译Auto Compile还是自动刷新浏览器Live Reload还是指 HMRHot Module Replacement模块热替换这些不同的效果背后的技术原理是什么呢今天我们就来回答下这些问题。
先来看下,究竟什么是浏览器的热更新。浏览器的热更新,指的是我们在本地开发的同时打开浏览器进行预览,当代码文件发生变化时,浏览器自动更新页面内容的技术。这里的自动更新,表现上又分为自动刷新整个页面,以及页面整体无刷新而只更新页面的部分内容。
与之相对的是在早期开发流程中,每次代码变更后需要手动刷新浏览器才能看到变更效果的情况。甚至于,代码变更后还需要手动执行打包脚本,完成编译打包后再刷新浏览器。而使用浏览器的热更新,可以大大减少这些麻烦。
webpack 中的热更新配置
下面我们就以 webpack 工具为例,来看下四种不同配置对结果的影响(完整示例代码 https://github.com/fe-efficiency/lessons_fe_efficiency/02_webpack_hmr
一切依赖手动
首先来看第一个最简单的配置,在入口 js 中我们简单地打印一个文本,然后在构建配置里只有最简单的 entry 和 mode 配置。
src/index0.js
function render() {
div = document.createElement('div')
div.innerHTML = 'Hello World0';
document.body.appendChild(div)
}
render()
webpack.config.basic.js
module.exports = {
entry: './src/index0.js',
mode: 'development',
}
package.json
"scripts": {
"build:basic": "webpack --config webpack.config.basic.js"
}
当我们执行 npm run build:basic 时webpack 将 entry 中的源文件 index0.js 打包为 dist/main.js并退出进程。流程很简单但是如果我们接下来改动了源文件的输出文本会发现由于构建配置中没有任何对应处理所以在保存后打包后的文件内容并没有更新。为了同步改动效果我们需要再次手动执行该命令。
Watch 模式
第二种配置是 watch 模式。为了摆脱每次修改文件后都需要手动执行脚本才能进行编译的问题webpack 中增加了 watch 模式,通过监控源码文件的变化来解决上面不能自动编译问题。我们可以在配置脚本中增加 watch:true如下
webpack.config.watch.js
{...
watch: true
...}
package.json
"scripts": {
"build:watch": "webpack --config webpack.config.watch.js"
}
当我们执行 npm run build:watchwebpack 同样执行一次打包过程,但在打包结束后并未退出当前进程,而是继续监控源文件内容是否发生变化,当源文件发生变更后将再次执行该流程,直到用户主动退出(除了在配置文件中加入参数外,也可以在 webpack 命令中增加 watch 来实现)。
有了 watch 模式之后,我们在开发时就不用每次手动执行打包脚本了。但问题并未解决,为了看到执行效果,我们需要在浏览器中进行预览,但在预览时我们会发现,即使产物文件发生了变化,在浏览器里依然需要手动点击刷新才能看到变更后的效果。那么这个问题又该如何解决呢?
Live Reload
第三种配置是 Live Reload。为了使每次代码变更后浏览器中的预览页面能自动显示最新效果而无须手动点击刷新我们需要一种通信机制来连接浏览器中的预览页面与本地监控代码变更的进程。在 webpack 中,我们可以使用官方提供的开发服务器来实现这一目的,配置如下:
webpack.config.reload.js
{...
devServer: {
contentBase: './dist', //为./dist目录中的静态页面文件提供本地服务渲染
open: true //启动服务后自动打开浏览器网页
}
...}
package.json
"scripts": {
"dev:reload": "webpack-dev-server --config webpack.config.reload.js"
}
当我们执行 npm run dev:reload从日志中可以看到本地服务 http://localhost:8080/ 已启动,然后我们在浏览器中输入网址 http://localhost:8080/index.html (也可以在 devServer 的配置中加入 open 和 openPage 来自动打开网页)并打开控制台网络面板,可以看到在加载完页面和页面中引用的 js 文件后,服务还加载了路径前缀名为 /sockjs-node 的 websocket 链接,如下图:
通过这个 websocket 链接,就可以使打开的网页和本地服务间建立持久化的通信。当源代码发生变更时,我们就可以通过 Socket 通知到网页端,网页端在接到通知后会自动触发页面刷新。
到了这里,在使用体验上我们似乎已经达到预期的效果了,但是在以下场景中仍然会遇到阻碍:在开发调试过程中,我们可能会在网页中进行一些操作,例如输入了一些表单数据想要调试错误提示的样式、打开了一个弹窗想要调试其中按钮的位置,然后切换回编辑器,修改样式文件进行保存。可是当我们再次返回网页时却发现,网页刷新后,之前输入的内容与打开的弹窗都消失了,网页又回到了初始化的状态。于是,我们不得不再次重复操作才能确认改动后的效果。对于这个问题,又该如何解决呢?
Hot Module Replacement
第四种配置就是我们常说的 HMRHot Module Replacement模块热替换了。为了解决页面刷新导致的状态丢失问题webpack 提出了模块热替换的概念。下面我们通过一个复杂一些的示例来了解热替换的配置与使用场景:
src/index1.js
import './style.css'
...
src/style.css
div { color: red }
webpack.config.hmr.js
{...
entry: './src/index1.js',
...
devServer: {
...
hot: true
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
package.json
"scripts": {
"dev:hmr": "webpack-dev-server --config webpack.config.hmr.js"
}
在上面的代码改动中,我们只是在源码部分新增导入了一个简单的 CSS 文件,用于演示热替换的效果。在配置文件中,首先我们在 devServer 配置中新增了 hot:true其次新增 module 的配置,使用 style-loader 和 css-loader 来解析导入的 CSS 文件。其中 css-loader 处理的是将导入的 CSS 文件转化为模块供后续 Loader 处理;而 style-loader 则是负责将 CSS 模块的内容在运行时添加到页面的 style 标签中。
当我们执行 npm run dev:hmr 命令,可以看到页面控制台的网络面板与上个示例并无区别,而在审查元素面板中可以看到源码中的 CSS 被添加到了页面头部的新增 style 标签中。
而当修改源码中 CSS 的样式后,再回到网页端,我们则会发现这样一些变化:
首先在网络面板中只是新增了两个请求hot-update.json 和 hot-update.js而不像上一个立即刷新的示例中那样会刷新页面重载所有请求。
其次,在审查元素面板中我们可以看到,在页面的头部新增了 hot-update.js并替换了原先 style 标签中的样式内容。
正如我们所见,对于代码中引入的样式文件,可以通过上述设置来开启热替换。但是有同学也许会问,我们为什么不像上一个例子中那样改动 JS 的内容(例如改动显示的文本)来观察热替换的效果呢?原因在于,简单改动 JS 中的显示文本并不能达到热替换的效果。尽管网络端同样新增了 hot-update.json 和 hot-update.js但紧随其后的是如上一个示例一般的刷新了整个页面。
那么,为什么导入的 CSS 能触发模块热替换,而 JS 文件的内容修改就失效了呢?要回答这个问题,我们还得从 webpack 的热更新原理说起。
webpack 中的热更新原理
下图是 webpackDevServer 中 HMR 的基本流程图,完整的 HMR 功能主要包含了三方面的技术:
watch 示例中体现的,对本地源代码文件内容变更的监控。
instant reload 示例中体现的,浏览器网页端与本地服务器端的 Websocket 通信。
hmr 示例中体现的,也即是最核心的,模块解析与替换功能。
也就是说在这三种技术中,我们可以基于 Node.js 中提供的文件模块 fs.watch 来实现对文件和文件夹的监控,同样也可以使用 sockjs-node 或 socket.io 来实现 Websocket 的通信。而在这里,我们重点来看下第三种, webpack 中的模块解析与替换功能。
webpack 中的打包流程
在讲 webpack 的打包流程之前我们先解释几个 webpack 中的术语:
module指在模块化编程中我们把应用程序分割成的独立功能的代码模块。
chunk指模块间按照引用关系组合成的代码块一个 chunk 中可以包含多个 module。
chunk group指通过配置入口点entry point区分的块组一个 chunk group 中可包含一到多个 chunk。
bundlingwebpack 打包的过程。
asset/bundle打包产物。
webpack 的打包思想可以简化为 3 点:
一切源代码文件均可通过各种 Loader 转换为 JS 模块 module模块之间可以互相引用。
webpack 通过入口点entry point递归处理各模块引用关系最后输出为一个或多个产物包 js(bundle) 文件。
每一个入口点都是一个块组chunk group在不考虑分包的情况下一个 chunk group 中只有一个 chunk该 chunk 包含递归分析后的所有模块。每一个 chunk 都有对应的一个打包后的输出文件asset/bundle
在上面的 hmr 示例中,从 entry 中的 ./src/index1.js 到打包产物的 dist/main.js以模块的角度而言其基本流程是
唯一 entry 创建一个块组chunk group name 为 main包含了 ./src/index1.js 这一个模块。
在解析器中处理 ./src/index1.js 模块的代码,找到了其依赖的 ./style.css找到匹配的 loader: css-loader 和 style-loader。
首先通过 css-loader 处理,将 css-loader/dist/cjs.js!./src/style.css 模块(即把 CSS 文件内容转化为 js 可执行代码的模块,这里简称为 Content 模块)和 css-loader/dist/runtime/api.js 模块打入 chunk 中。
然后通过 style-loader 处理,将 style-loader/dist/runtime/injectStylesIntoStyleTag.js 模块 (我们这里简称为 API 模块),以及处理后的 .src/style.css 模块(作用是运行时中通过 API 模块将 Content 模块内容注入 Style 标签)导入 chunk 中。
依次类推,直到将所有依赖的模块均打入到 chunk 中,最后输出名为 main.js 的产物(我们称为 Asset 或 Bundle
上述流程的结果我们可以在预览页面中控制台的 Sources 面板中看到,这里,我们重点看经过 style-loader 处理的 style.css 模块的代码:
style-loader 中的热替换代码
我们简化一下上述控制台中看到的 style-loader 处理后的模块代码,只看其热替换相关的部分。
//为了清晰期间,我们将模块名称注释以及与热更新无关的逻辑省略,并将 css 内容模块路径赋值为变量 cssContentPath 以便多处引用,实际代码可从示例运行时中查看
var cssContentPath = "./node_modules/css-loader/dist/cjs.js!./src/style.css"
var api = __webpack_require__("./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
var content = __webpack_require__(cssContentPath);
...
var update = api(content, options);
...
module.hot.accept(
cssContentPath,
function(){
content = __webpack_require__(cssContentPath);
...
update(content);
}
)
module.hot.dispose(function() {
update();
});
从上面的代码中我们可以看到,在运行时调用 API 实现将样式注入新生成的 style 标签,并将返回函数传递给 update 变量。然后,在 module.hot.accept 方法的回调函数中执行 update(content),在 module.hot.dispose 中执行 update()。通过查看上述 API 的代码,可以发现 update(content) 是将新的样式内容更新到原 style 标签中,而 update() 则是移除注入的 style 标签,那么这里的 module.hot 究竟是什么呢?
模块热替换插件HotModuleReplacementPlugin
上面的 module.hot 实际上是一个来自 webpack 的基础插件 HotModuleReplacementPlugin该插件作为热替换功能的基础插件其 API 方法导出到了 module.hot 的属性中。
在上面代码的两个 API 中hot.accept 方法传入依赖模块名称和回调方法,当依赖模块发生更新时,其回调方法就会被执行,而开发者就可以在回调中实现对应的替换逻辑,即上面的用更新的样式替换原标签中的样式。另一个 hot.dispose 方法则是传入一个回调,当代码上下文的模块被移除时,其回调方法就会被执行。例如当我们在源代码中移除导入的 CSS 模块时,运行时原有的模块中的 update() 就会被执行,从而在页面移除对应的 style 标签。
module.hot 中还包含了该插件提供的其他热更新相关的 API 方法,这里就不再赘述了,感兴趣的同学可以从 官方文档中进一步了解。
通过上面的分析,我们就了解了热替换的基本原理,这也解释了为什么我们替换 index1.js 中的输出文本内容时,并没有观察到热更新,而是看到了整个页面的刷新:因为代码中并未包含对热替换插件 API 的调用,代码的解析也没有配置额外能对特定代码调用热替换 API 的 Loader。所以在最后我们就来实现下 JS 中更新文本内容的热替换。
示例JS 代码中的热替换
./text.js
export const text = 'Hello World'
./index2.js
import {text} from './text.js'
const div = document.createElement('div')
document.body.appendChild(div)
function render() {
div.innerHTML = text;
}
render()
if (module.hot) {
module.hot.accept('./text.js', function() {
render()
})
}
在上面的代码中,我们将用于修改的文本单独作为一个 JS 模块,以便传入 hot.accept 方法。当文本发生变更时,可以观察到浏览器端显示最新内容的同时并未触发页面刷新,验证生效。此外, accept 方法也支持监控当前文件的变更,对应的 DOM 更新逻辑稍做调整也能达到无刷新效果,区别在于替换自身模块时示例中不可避免地需要更改 DOM。
从上面的例子中我们可以看到,热替换的实现,既依赖 webpack 核心代码中 HotModuleReplacementPlugin 所提供的相关 API也依赖在具体模块的加载器中实现相应 API 的更新替换逻辑。因此,在配置中开启 hot:true 并不意味着任何代码的变更都能实现热替换,除了示例中演示的 style-loader 外, vue-loader、 react-hot-loader 等加载器也都实现了该功能。当开发时遇到 hmr 不生效的情况时,可以优先确认对应加载器是否支持该功能,以及是否使用了正确的配置。
总结
在这一讲中我们讨论了浏览器的热更新,并结合示例代码,了解了 webpack 中实现热更新的不同配置,并进一步分析了其中热替换技术的原理。相信你通过这一讲,对 hmr 的原理和实现就有了一定的概念。在实际项目的开发中,我们可以确认项目是否正确开启了热替换的功能。
课后布置一个小作业供有兴趣的同学们巩固:找到一个实现热替换的 Loader看看其代码中都用到了哪些相关的 API。

View File

@ -0,0 +1,238 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 构建提速:如何正确使用 SourceMap
在上一课时中我们聊了开发时的热更新机制和其中的技术细节,热更新能帮助我们在开发时快速预览代码效果,免去了手动执行编译和刷新浏览器的操作。课后留的思考题是找一个实现了 HMR 的 Loader查看其中用到哪些热替换的 API希望通过这个题目能让你加深对相关知识点的印象。
那么除了热更新以外,项目的开发环境还有哪些在影响着我们的开发效率呢?在过去的工作中,公司同事就曾问过我一个问题:为什么我的项目在开发环境下每次构建还是很卡?每次保存完代码都要过 1~2 秒才能看到效果,这是怎么回事呢?其实这里面的原因主要是这位同事在开发时选择的 Source Map 设定不对。今天我们就来具体讨论下这个问题。首先,什么是 Source Map 呢?
什么是 Source Map
在前端开发过程中,通常我们编写的源代码会经过多重处理(编译、封装、压缩等),最后形成产物代码。于是在浏览器中调试产物代码时,我们往往会发现代码变得面目全非,例如:
因此我们需要一种在调试时将产物代码显示回源代码的功能source map 就是实现这一目标的工具。
source-map 的基本原理是,在编译处理的过程中,在生成产物代码的同时生成产物代码中被转换的部分与源代码中相应部分的映射关系表。有了这样一张完整的映射表,我们就可以通过 Chrome 控制台中的”Enable Javascript source map”来实现调试时的显示与定位源代码功能。
对于同一个源文件,根据不同的目标,可以生成不同效果的 source map。它们在构建速度、质量反解代码与源代码的接近程度以及调试时行号列号等辅助信息的对应情况、访问方式在产物文件中或是单独生成 source map 文件)和文件大小等方面各不相同。在开发环境和生产环境下,我们对于 source map 功能的期望也有所不同:
在开发环境中,通常我们关注的是构建速度快,质量高,以便于提升开发效率,而不关注生成文件的大小和访问方式。
在生产环境中,通常我们更关注是否需要提供线上 source map , 生成的文件大小和访问方式是否会对页面性能造成影响等,其次才是质量和构建速度。
Webpack 中的 source map 预设
在 Webpack 中,通过设置 devtool 来选择 source map 的预设类型,文档中共有 20 余种 source map 的预设(注意:其中部分预设实际效果与其他预设相同,即页面表格中空白行条目)可供选择,这些预设通常包含了 “eval” “cheap” “module” “inline” “hidden” “nosource” “source-map” 等关键字的组合,这些关键字的具体逻辑如下:
webpack/lib/WebpackOptionsApply.js:232
if (options.devtool.includes("source-map")) {
const hidden = options.devtool.includes("hidden");
const inline = options.devtool.includes("inline");
const evalWrapped = options.devtool.includes("eval");
const cheap = options.devtool.includes("cheap");
const moduleMaps = options.devtool.includes("module");
const noSources = options.devtool.includes("nosources");
const Plugin = evalWrapped
? require("./EvalSourceMapDevToolPlugin")
: require("./SourceMapDevToolPlugin");
new Plugin({
filename: inline ? null : options.output.sourceMapFilename,
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
fallbackModuleFilenameTemplate:
options.output.devtoolFallbackModuleFilenameTemplate,
append: hidden ? false : undefined,
module: moduleMaps ? true : cheap ? false : true,
columns: cheap ? false : true,
noSources: noSources,
namespace: options.output.devtoolNamespace
}).apply(compiler);
} else if (options.devtool.includes("eval")) {
const EvalDevToolModulePlugin = require("./EvalDevToolModulePlugin");
new EvalDevToolModulePlugin({
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
namespace: options.output.devtoolNamespace
}).apply(compiler);
}
如上面的代码所示, devtool 的值匹配并非精确匹配某个关键字只要包含在赋值中即可获得匹配例如foo-eval-bar 等同于 evalcheapfoo-source-map 等同于 cheap-source-map
Source Map 名称关键字
各字段的作用各不相同,为了便于记忆,我们在这里简单整理下这些关键字的作用:
false即不开启 source map 功能,其他不符合上述规则的赋值也等价于 false。
eval是指在编译器中使用 EvalDevToolModulePlugin 作为 source map 的处理插件。
[xxx-…]source-map根据 devtool 对应值中是否有 eval 关键字来决定使用 EvalSourceMapDevToolPlugin 或 SourceMapDevToolPlugin 作为 source map 的处理插件,其余关键字则决定传入到插件的相关字段赋值。
inline决定是否传入插件的 filename 参数,作用是决定单独生成 source map 文件还是在行内显示,该参数在 eval- 参数存在时无效。
hidden决定传入插件 append 的赋值,作用是判断是否添加 SourceMappingURL 的注释,该参数在 eval- 参数存在时无效。
module为 true 时传入插件的 module 为 true 作用是为加载器Loaders生成 source map。
cheap这个关键字有两处作用。首先当 module 为 false 时,它决定插件 module 参数的最终取值,最终取值与 cheap 相反。其次,它决定插件 columns 参数的取值,作用是决定生成的 source map 中是否包含列信息,在不包含列信息的情况下,调试时只能定位到指定代码所在的行而定位不到所在的列。
nosourcenosource 决定了插件中 noSource 变量的取值,作用是决定生成的 source map 中是否包含源代码信息,不包含源码情况下只能显示调用堆栈信息。
Source Map 处理插件
从上面的规则中我们还可以看到,根据不同规则,实际上 Webpack 是从三种插件中选择其一作为 source map 的处理插件。
EvalDevToolModulePlugin模块代码后添加 sourceURL=webpack:///+ 模块引用路径,不生成 source map 内容,模块产物代码通过 eval() 封装。
EvalSourceMapDevToolPlugin生成 base64 格式的 source map 并附加在模块代码之后, source map 后添加 sourceURL=webpack:///+ 模块引用路径,不单独生成文件,模块产物代码通过 eval() 封装。
SourceMapDevToolPlugin生成单独的 .map 文件,模块产物代码不通过 eval 封装。
通过上面的代码分析,我们了解了不同参数在 Webpack 运行时起到的作用。那么这些不同参数组合下的各种预设对我们的 source map 生成又各自会产生什么样的效果呢?下面我们通过示例来看一下。
不同预设的示例结果对比
下面,以课程示例代码 03_develop_environment 为例,我们来对比下几种常用预设的差异(为了使时间差异更明显,示例中引入了几个大的类库文件):
*注1“/”前后分别表示产物 js 大小和对应 .map 大小。
*注2“/”前后分别表示初次构建时间和开启 watch 模式下 rebuild 时间。对应统计的都是 development 模式下的笔者机器环境下几次构建时间的平均值,只作为相对快慢与量级的比较。
不同预设的效果总结
从上图的数据中我们不难发现,选择不同的 devtool 类型在以下几个方面会产生不同的效果。
质量:生成的 source map 的质量分为 5 个级别,对应的调试便捷性依次降低:源代码 > 缺少列信息的源代码 > loader 转换后的代码 > 生成后的产物代码 > 无法显示代码(具体参见下面的不同质量的源码示例小节)。对应对质量产生影响的预设关键字优先级为 souce-map = eval-source-map > cheap-module- > cheap- > eval = none > nosource-。
构建的速度:再次构建速度都要显著快于初次构建速度。不同环境下关注的速度也不同:
在开发环境下:一直开着 devServer再次构建的速度对我们的效率影响远大于初次构建的速度。从结果中可以看到eval- 对应的 EvalSourceMapDevToolPlugin 整体要快于不带 eval- 的 SourceMapDevToolPlugin。尤其在质量最佳的配置下eval-source-map 的再次构建速度要远快于其他几种。而同样插件配置下,不同质量配置与构建速度成反比,但差异程度有限,更多是看具体项目的大小而定。
在生产环境下:通常不会开启再次构建,因此相比再次构建,初次构建的速度更值得关注,甚至对构建速度以外因素的考虑要优先于对构建速度的考虑,这一部分我们在之后的构建优化的课程里会再次讨论到。
包的大小和生成方式:在开发环境下我们并不需要关注这些因素,正如在开发环境下也通常不考虑使用分包等优化方式。我们需要关注速度和质量来保证我们的高效开发体验,而其他的部分则是在生产环境下需要考虑的问题。
不同质量的源码示例
源码且包含列信息
源码不包含列信息
Loader转换后代码
生成后的产物代码
开发环境下 Source Map 推荐预设
在这里我们对开发环境下使用的推荐预设做一个总结(生产环境的预设我们将在之后的构建效率篇中再具体分析):
通常来说,开发环境首选哪一种预设取决于 source map 对于我们的帮助程度。
如果对项目代码了如指掌,查看产物代码也可以无障碍地了解对应源代码的部分,那就可以关闭 devtool 或使用 eval 来获得最快构建速度。
反之如果在调试时,需要通过 source map 来快速定位到源代码,则优先考虑使用 eval-cheap-modulesource-map它的质量与初次/再次构建速度都属于次优级,以牺牲定位到列的功能为代价换取更快的构建速度通常也是值得的。
在其他情况下,根据对质量要求更高或是对速度要求更高的不同情况,可以分别考虑使用 eval-source-map 或 eval-cheap-source-map。
了解了开发环境下如何选择 source map 预设后,我们再来补充几种工具和脚手架中的默认预设:
Webpack 配置中,如果不设定 devtool则使用默认值 eval即速度与 devtool:false 几乎相同、但模块代码后多了 sourceURL 以帮助定位模块的文件名称。
create-react-app 中,在生产环境下,根据 shouldUseSourceMap 参数决定使用source-map或 false在开发环境下使用cheap-module-source-map不包含列信息的源代码但更快
vue-cli-service 中,与 creat-react-app 中相同。
除了上面讨论的这些简单的预设外Webpack 还允许开发者直接使用对应插件来进行更精细化的 source map 控制,在开发环境下我们首选的还是 EvalSourceMapDevToolPlugin。下面我们再来看看如何直接使用这个插件进行优化。
EvalSourceMapDevToolPlugin 的使用
在 EvalSourceMapDevToolPlugin 的 传入参数中,除了上面和预设相关的 filename、append、module、columns 外,还有影响注释内容的 moduleFilenameTemplate 和 protocol以及影响处理范围的 test、include、exclude。这里重点看处理范围的参数因为通常我们需要调试的是开发的业务代码部分而非依赖的第三方模块部分。因此在生成 source map 的时候如果可以排除第三方模块的部分而只生成业务代码的 source map无疑能进一步提升构建的速度例如示例
webpack.config.js
...
//devtool: 'eval-source-map',
devtool: false,
plugins: [
new webpack.EvalSourceMapDevToolPlugin({
exclude: /node_modules/,
module: true,
columns: false
})
],
...
在上面的示例中,我们将 devtool 设为 false而直接使用 EvalSourceMapDevToolPlugin通过传入 module: true 和 column:false达到和预设 eval-cheap-module-source-map 一样的质量,同时传入 exclude 参数,排除第三方依赖包的 source map 生成。保存设定后通过运行可以看到,在文件体积减小(尽管开发环境并不关注文件大小)的同时,再次构建的速度相比上面表格中的速度提升了将近一倍,达到了最快一级。
类似这样的优化可以帮助我们在一些大型项目中,通过自定义设置来获取比预设更好的开发体验。
总结
在今天这一课时中我们主要了解了提升开发效率的另一个重要工具——source map 的用途和使用方法。我们分析了 Webpack 中 devtool 的各种参数预设的组合规则、使用效果及其背后的原理。对于开发环境,我们根据一组示例对比分析来了解通常情况下的最佳选择,也知道了如何直接使用插件来达到更细致的优化。
限于篇幅原因,关于 source map 这一课时还有两个与提效无关的小细节没有提到,一个是生成的 source map 的内容,即浏览器工具是如何将 source map 内容映射回源文件的,如果你感兴趣可以通过这个链接进一步了解;另一个是我们在控制台的网络面板中通常看不到 source map 文件的请求,其原因是出于安全考虑 Chrome 隐藏了 source map 的请求,需要通过 net-log 来查询。
最后还是留一个小作业:不知道你有没有留意过自己项目里的 source map 使用的是哪一种生成方式吗?可以根据这一课时的内容对它进行调整和观察效果,也欢迎你在课后留言区讨论项目里对 source map 的优化方案。

View File

@ -0,0 +1,200 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 接口调试Mock 工具如何快速进行接口调试?
上一课时我们讲了 source map 在开发调试中的作用,以及不同的 source map 策略对于构建时间和调试效果的影响。在课后留的讨论题是观察你项目里在开发和生产环境下使用的是哪一种 source map 类型,因为很多时候当我们用了预设好的脚手架工具后,这些细节可能不太关注到,希望借着这个题目能让你对这个方面的细节有更深入的理解。
今天我们来聊一下前端开发流程中的 Mock 工具使用问题。
什么是 Mock
Mock 在程序设计中是指使用模拟Mock的对象来替代真实对象以测试其他对象的行为。而在前端开发流程中我们说的 Mock 通常是指模拟数据(俗称假数据)以及生成和使用模拟数据的工具与流程。那么为什么要使用 Mock 数据呢?是因为在实际中,我们经常遇到以下令人困扰的问题。
在一个前后端分离的项目开发流程中,项目的开发时间通常分为三块:前端开发时间 t1后端开发时间 t2前后端联调时间 t3。理想情况下整体的项目开发时间是 <=maxt1t2+t3即前后端同时开发两端都开发完成后进入联调。甚至再进一步为了提高效率也可以将整个开发流程按功能点进行更细粒度地拆分即在开发时间内也可以在部分功能开发完成后立即进行这一部分的联调以期望利用碎片化的时间来减少后期完整联调的时间。
但现实中,随着项目前端交互流程的日益复杂化,在开发流程中,前端往往需要依赖一定的数据模型来组织页面与组件中的交互流程 ,而数模型又依赖着后端提供的 API 接口。也就是说,在新项目新功能的开发流程中,前端的开发时间多少,不只取决于自身开发部分的耗时,还依赖于后端开发完成的时间。那么如何实现前端的无依赖独立开发以提升效率呢?
假设在后端实际 API 功能完成之前,我们能获得对应的模拟数据作为接口的返回值来处理前端交互中的数据模型,待开发完成进入联调后再将假数据的部分切换到真实的后端服务接口数据,这样开发阶段的阻碍问题就解决了。事实上,使用 Mock 数据已成为前端开发流程中必不可少的一环。
选择 Mock 方案的考量标准
对于在前端开发中使用 Mock 数据的需求,实现路径有很多,例如:
可以直接在代码中侵入式地书写静态返回数据来调试相关逻辑。
可以使用后端开发服务作为 Mock 服务,将未实现的功能在后端返回 Mock 数据。
可以通过一些本地 Mock 工具,使用项目本地化的 Mock 规则文件来生成 Mock 数据。
可以使用功能更丰富的接口管理工具来提供独立的 Mock 能力。
这里面,第一种书写静态返回数据的方式和第二种开发服务端返回假数据的方式可能是前端同学从直觉上最容易理解和实践的。但是对于第一种方案而言,代码的维护成本、复杂接口的数据实现和处理以及特殊字段的额外处理等因素,都导致了它在实际开发过程中的使用场景非常局限;而第二种方案仍然依赖后端提供相应的服务,在独立性、稳定性与灵活性方面也难以达到 Mock 方案达成前端独立开发的要求。
剩下的两种实现方式则各有其适用场景和局限性:在后端已提供接口文档,而团队未使用接口管理工具的情况下,第三种本地化的 Mock 工具使用成本更低;而反之,第四种则有一定的前期搭建和维护成本,但在前后端达成一致使用接口管理工具的情况下,整体效率更高。
除了考虑不同实现路径外,对于相同的实现方式,可选择的工具也各有不同。在讨论具体的 Mock 方案之前,我们先来聊下选择的参考依据:
仿真度Mock 数据作为实际前后端调用时的数据模拟,需要在接口定义上尽可能与后端实际提供接口的各方面保持一致。从接口名称、调用方法、请求头信息到返回头信息,返回值字段一致性越高,在后期切换到联调实际接口时花费的时间越短。因此,使用 Mock 数据前,需要使前后端在事先对接口的定义上达成一致。因此,数据定义的仿真度是决定实际模拟过程效率和质量的首要因素。这部分的工作通常在开发初期通过接口文档的方式来提供,或由提供类似功能的 Mock 工具来提供。
易用性:在定义完接口文档之后,下一步是生成 Mock 数据。通常一个高效的 Mock 工具需要具备将接口文档自动转换为 Mock 接口的能力。接口文档作为前后端联调的一致性保证,当接口发生任何变化时都会首先更新到文档中,并自动反映到提供的 Mock 数据中。同样,后端提供的真实服务也应当完整通过 Mock 接口的测试,而这种自动输出 Mock 数据,以及自动测试接口的能力也是整个 Mock 方案易用性的考量标准之一。
灵活性:通常来说,实际的接口调用中我们会根据不同的调用方式与传入参数等条件来输出不同的返回值,前端根据不同条件下返回值的差异做不同的交互处理。因此,在使用 Mock 工具的过程中,对不同条件下返回不同数据的 Mock 期望能力也是我们选择 Mock 方案的考虑点。
以上几点构成我们选择 Mock 方案的基本考虑标准。接下来我们来了解一些前端领域主流的 Mock 工具。
几种主流的 Mock 工具介绍
Mock.js
Mock.js 是前端领域流行的 Mock 数据生成工具之一,后续许多功能更丰富的工具和系统在各自的 Mock 功能部分都将它作为基础设施。
Mock.js 的核心能力是定义了两类生成模拟数据的规范数据模板定义规范Data Template Definition, DTD与数据占位符定义规范Data Placeholder Definition, DPD以及实现了应用相应规范生成模拟数据的方法。
数据模板定义规范DTD
数据模板定义规范约定了可以通过“属性名|生成规则:属性值”这样的格式来生成模拟数据,例如(完整示例代码参见 04_mock
Mock.mock({
"number|1-100": 1
})
//Result: number为1-100内随机数例如{number: 73}
Mock.mock({
"boo|1-100": true
})
//Result: boo为true或false其中true的概率为1%,例如{boo: false}
Mock.mock({
"str|1-100": '1'
})
//Result: str为1-100个随机长度的字符串'1'。例如{str: '11111'}
从上面的例子可以看到,属性名只是作为生成数据的固定名称,而同样的生成规则下,随着属性值的不同,生成规则对应的内部处理逻辑也不同。在 Mock.js 中,共定义了 7 种生成规则min-max、min-max.dmin-dmax、min-max.dcount、count、count.dmin-dmax、count.dcount、+step。根据这 7 种规则,再结合不同数据类型的属性值,就可以定义出任意我们所需要的随机数据生成逻辑。
数据占位符定义规范 DPD
数据占位符定义规范则是对于随机数据的一系列常用类型预设,书写格式是’@占位符(参数 [, 参数] )’。如以下例子:
Mock.mock('@email')
//Result: 随机单词连接成的email数据例如"[email protected]"
Mock.mock('@city(true)')
//Result: 随机中国省份+省内城市数据,例如:"吉林省 辽源市"
Mock.mock({'aa|1-3':['@cname()']})
//Result: aa值为随机3个中文姓名的数组例如{aa: ['张三','李四','王五']}
Random.image('200x100', '#894FC4', '#FFF', 'png', '!')
//Result: 利用dummyimage库生成的图片url, "http://dummyimage.com/200x100/894FC4/FFF.png"
从这些例子中可以看到占位符既可以用于单独返回指定类型的随机数据又能结合数据模板作为模板中属性值的部分来生成更复杂的数据类型。Mock.js 中定义了 9 大类共 42 种占位符,相关更多占位符的说明和示例可以从官网中查找和使用。
其他功能
除了提供生成模拟数据的规范和方法外Mock.js 还提供了一些辅助功能,包括:
Ajax 请求拦截Mock.mock 方法中支持传入 Ajax 请求的 url 和 type用于拦截特定 url 的请求,直接将模拟数据作为响应值返回。这一功能方便我们在项目本地中使用 Mock 数据做调试,其原理是覆盖了原生的 XMLHttpRequest 对象,因此对于使用 fetch 作为接口请求的 API 的项目还不能适用。此外,提供了 Mock.setup 方法来设置拦截 Ajax 请求后的响应时间。
数据验证Mock.valid 方法,验证指定数据和数据模板是否匹配。这一功能可以用于验证后端 API 接口的返回值与对应 Mock 数据的规则描述是否冲突。
模板导出Mock.toJSONSchema将 Mock.js 风格的数据模板转换为 JSONSchema。可用于将数据模板导入到其他支持 JSON Schema 格式的工具中。
Faker.js
Faker.js 是另一个较热门的模拟数据生成工具。与 Mock.js 相比Faker.js 主要提供的是指定类型的随机数据,对应 Mock.js 中的占位符类型数据。在 API 的使用方面较直观,使用示例如下:
//单独使用api方法
var randomName = faker.name.findName(); // Rowan Nikolaus
var randomEmail = faker.internet.email(); // [email protected]
var randomCard = faker.helpers.createCard(); // random contact card containing many properties
//使用fake来组合api
faker.fake("{{name.lastName}}, {{name.firstName}} {{name.suffix}}")
// outputs: "Marks, Dean Sr."
除了在数据生成的规则上没有 Mock.js 的数据模板规则那样灵活以外,对于一般的数据模拟需求, Faker.js 已能很好地满足。此外,它还支持多种语言的本地化包,满足国际化站点开发的需求。
以上两种工具在实际项目使用中,都需要在项目本地编写数据生成模板或方法,而后根据一定的方式拦截 API 请求并指向本地生成的 Mock 数据。拦截的方法可以类似 Mock.js 的覆盖 API 调用对象,也可以是通过网络代理将后端域名指向本地目录。
这种本地植入模拟数据生成器的方式可以在一定程度上提升前端独立开发调试的效率,但从整体前后端工作的效率上来看,并非最佳选择:
在使用 TypeScript 的项目中,数据模板和 TypeScript 类型需要通过人工来保持一致,缺乏自动检验的功能。
仍然需要后端编写完整的接口文档后才能开始编写数据生成逻辑。
本地模拟数据规则本质上和接口文档脱离,当后端接口字段发生变化时无法感知,导致沟通调试成本增加,也对基于模拟数据的单元测试的效果产生影响。
对于第一点,有种解决思路是基于 TypeScript 接口类型描述对象来自动生成模拟数据。而对于后面两点,解决方案是将接口文档和 Mock 数据服务,以及接口测试工具结合在一起,合并成相关功能链路集成的平台和工具,例如下面介绍的两个:
YApi
YApi 定义是开发、产品、测试人员共同使用的接口管理服务,其功能特点主要包括:
支持接口的定义、修改、运行、集合测试等。
提供 Mock 服务,以定义的接口可以通过服务直接获取 Mock 数据。Mock 定义中支持 JSON Schema 和 Mock.js不支持函数功能
支持 Swagger 多种接口描述的数据导入与导出。
支持部署到内网服务以及自定义插件。
Apifox
Apifox 是一个桌面应用类的接口管理工具。与 YApi 相比,除了使用方式不同外,其主要特点还包括:
支持接口调试工具 Postman 的特色功能例如环境变量、Cookie/Session 全局共享等。
对同一个接口支持多种用例管理(成功用例、错误用例等)。
Mock 数据功能方面支持自定义期望,支持自定义占位符规则等。
支持生成自动业务代码和接口请求代码,支持自定义代码模板等。
以上两种接口管理工具都包含了提供对应接口的 Mock 服务的能力。相比于单独提供生成 Mock 数据能力的 Mock.js 和 Faker.js这类工具解决了接口定义与 Mock 数据脱离的问题:
在接口定义阶段,支持后端服务内定义的 Swagger 等 OPEN API 风格的接口定义数据直接导入生成接口文档,也支持在工具界面内填写字段创建,创建时支持设定返回值的 Mock 描述。
在接口定义完成后,即可直接访问工具提供的 Mock 服务接口供前端调用。
在后端接口开发过程中,可通过工具提供的接口调试功能进行开发调试。
在接口完成后的任意时间点,支持接口的自动化测试来保证功能与描述的一致性。
通过这样的流程串联,来解决前后端开发过程中的接口联调效率问题。
总结
通过这一课时的学习,我们一起讨论了 Mock 工具在前后端分离开发流程中起到的作用,以及选择 Mock 方案的一般考量标准,并重点介绍了几种 Mock 工具:有专注于提供生成模拟数据这一核心能力的 Mock.js 和 Faker.js也有更平台化的内置 Mock 功能的 YApi 和 Apifox。大家在项目的开发过程中可以根据自身项目的情况来选择使用。
课后讨论题:在你的项目开发中是否有用到本地或者服务化的 Mock 工具呢?有用到的话谈谈你的使用感受吧。
00:00
前端工程化精讲

View File

@ -0,0 +1,290 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 编码效率:如何提高编写代码的效率?
上一课时我们讨论了开发过程中 Mock 工具的使用。Mock 工具能帮助解决前端开发过程中的接口依赖问题,作为开发流程规范的一环,起到提升整体开发效率的作用。
今天我们来聊聊如何提高开发时的编码效率的问题。
俗话说,不会“偷懒”的程序员不是好程序员。一名好的程序员会不断地思考如何把重复的流程通过程序来自动化地完成,从而把剩下的时间投入到真正需要思考和有创造性的工作中去。那么如何才能在开发过程中更专业地“偷懒”呢?
提高编码效率的不同方式
对于前端同学而言,第一讲脚手架工具就是提高编码效率最直接的例子,通过脚手架帮我们生成一个项目的基础代码后,就免去了投入其中的时间。那么在开发过程中又有哪些方式可以帮助我们提高编码的效率呢?
以最终实现相同代码产出为目标,开发时提高编码效率的方式大致可以分为两类:
通过使用预处理语言相比原语言来说预处理语言通常抽象度更高提供更多封装好的工具方法更有利于提高编码的效率。可以通过对应的预处理器Preprocessor将预处理语言在编译时转换为更完整的普通语法代码例如 Sass 到 CSS
通过代码生成(例如 IDE 的自动完成):以达到在编写时自动生成代码的作用,因而无须在编译时进行额外处理。
下面我们就来分别讨论下这两种方式,以及它们对应的工具。
使用预处理语言和预处理器
预处理语言指的是在原有语言的语法基础上,提供更多新的内置功能及精简语法,以便提高代码复用性和书写效率。下面我们就列举几个前端开发中涉及的典型预处理语言,来聊一聊它们在功能和使用上的特点,以便在开发中能够更有针对性地选择和使用。
Sass/Less/Stylus
Sass2006Less2009和 Stylus2010 是三种目前主流的 CSS 预处理语言,有同学在创建新项目时可能不知道该选择哪一种,下面我会从几个方面对它们做一个比较,来作为你选择使用的参考依据:
从流行程度来看:
从第一课时脚手架部分的介绍中能发现react-scripts 中集成了 sass-loader而 vue-cli-service 中则同时支持这三种预处理器。
几个主流的 UI 库的使用情况是: Bootstrap4、 Antd 和 iView 使用 Less ElementUI 使用 Sass。
此外2019 年的前端工具调查也显示上面三种工具的使用人数依次递减,即使用人数最多的是 Sass、 其次是 Less、最后是 Stylus。如下图所示
在实现的功能方面:这三种 CSS 的预处理语言都实现了变量Variables、嵌套Nesting、混合 Mixins、运算Operators、父选择器引用Parent Reference、扩展Extend和大量内建函数Build-in Functions。但是与另外两种语言相比Less 缺少自定义函数的功能(可以使用 Mixins 结合 Guard 实现类似效果),而 Stylus 提供了超过 60 个内建函数,更有利于编写复杂的计算函数。
在语法方面Sass 支持 .scss 与 .sass 两种文件格式。差异点是 .scss 在语法上更接近 CSS需要括号、分号等标识符而 Sass 相比之下,在语法上做了简化,去掉了 CSS 规则的括号分号等 (增加对应标识符会导致报错) 。Less 的整体语法更接近 .scss。Stylus 则同时支持类似 .sass 的精简语法和普通 CSS 语法。语法细节上也各不相同,示例如下:
//scss
$red: '#900';
div {
color: $red;
}
//sass
$red: '#900'
div
color: $red
//less
@green: '#090';
div {
color: @green;
}
//stylus
$blue = '#009'
div
color: $blue
从安装使用方面来看:
Sass 目前有两种 npm 编译安装包,即基于 LibSass 的 node-sass 和基于 dart-sass 的 Sass。官方推荐为 dart-sass它不仅在安装速度上更快而且支持更多 Sass 内置特性,且支持 Yarn 的 PnP 功能。
如果使用 Webpack 构建,三种语言对应的预处理器分别是 sass-loader、 less-loader、 stylus-loader。需要注意的是 sass-loader 和 stylus-loader 安装时都需要同时安装独立编译包 Sass / node-sass 和 Stylus而 less-loader 则不强制要求(也可以单独安装并在配置中指定不同的编译包版本)。此外,如第一课时中提到的, sass-loader 在处理 Partial 文件中的资源路径时需要增加 resolve-url-loader以及 sass-loader 中需要开启 sourceMap 参数) 以避免编译时的报错。对应的stylus-loader 需要增加 “resolve url” 参数,而 less-loader 则不需要。具体示例参见课件代码05_coding_efficiency。
Pug
对于 HTML 模板的预处理语言选择而言目前主流的是Pug**这里考虑的是预处理语言对于效率的提升因此一些纯模板语言如EJS则不在讨论范围内。此外基于其他技术栈的模板语言例如 Ruby 的Haml和Slim**,在前端工程化中的应用也并不多,因此也不在这里讨论)。
Pug 的前身名叫Jade20102016 年时因为和同名软件商标冲突而改名为了 Pug。语法示例如下
//pug
html
head
body
p.foo Hello World
//html
<html><head></head><body><p class="foo">Hello World</p></body></html>
在功能方面除了简化标签书写外还支持迭代Iteration、条件Condition、扩展Extend、包含Include、混合Mixins等逻辑功能。
在 Vue 开发中Vue 文件的 template 支持添加 lang=“pug”相应的在 vue-cli-service 的 Webpack 配置中内置了pug-loader 作为预处理器。而在 React 开发中,则可以通过 babel 插件获得支持。
其他
上面主要介绍了 CSS 和 HTML 的主流预处理语言,同样具有精简语法功能的还有对应 JavaScript 的 CoffeeScript 和对应 JSON 的 YAML 等。但是,由于 JavaScript 代码本身的逻辑性要重于输入的便捷性,且随着 ES6 语法的普及,原本 CoffeeScript 诞生时要解决的问题已逐渐被 ES6 的语法所取代,因此目前主流的开发技术栈中已不再有它的身影。而 YAML 语言目前主要在一些配置上使用,例如 Dockerfile 和一些持续集成工具CI的配置文件在开发语言中并不涉及因此这里也不展开介绍了。
使用代码生成工具
除了使用上面介绍的预处理语言进行开发外,我们也可以使用 IDEIntegrated Development Environment集成开发环境即我们通常说的编辑器的相关预设功能来帮助生成代码。这些功能主要包括智能帮助、Snippet 和 Emmet注意对于预处理语言文件来说通常 IDE 中需要安装对应文件类型的识别扩展,才能在文件中使用这些辅助功能)。这里重点介绍后两种。
通常在 IDE 中会默认内置一些智能帮助(例如 VSCode 中的IntelliSense功能例如输入时的联想匹配、自动完成、类型提示、语法检查等。但是很多场景下常常有些固定格式的语句或代码块需要重复输入这个时候就需要用到下面介绍的功能了。
Snippet
Snippet 是指开发过程中用户在 IDE 内使用的可复用代码片段,大部分主流的 IDE 中都包含了 Snippet 的功能,就像使用脚手架模板生成一个项目的基础代码那样,开发者可以在 IDE 中通过安装扩展来使用预设的片段,也可以自定义代码片段,并在之后的开发中使用它们。
以 VS Code 为例,在扩展商店中搜索 Snippet 可以找到各种语言的代码片段包。例如下图中的Javascript(ES6) code snippets提供了 JavaScript 常用的 import 、console 等语句的缩写。安装后,输入缩写就能快速生成对应语句。
除了使用扩展包自带的预设片段外IDE 还提供了用户自定义代码片段的功能。以 VS Code 为例通过选择菜单中的”Code-首选项-用户片段”,即可弹出选择或新增代码片段的弹窗,选择或创建对应 .code-snippets 文件后即可编辑自定义的片段。就像下面示例代码中我们创建了一个简单的生成 TypeScript 接口代码的片段,保存后在项目代码里输入 tif 后再按回车,就能看到对应生成的片段了:
//sample.code-snippets
{
"Typescript Interface": { //片段名称下面描述不存在时显示在IDE智能提示中
"scope": "typescript", //语言的作用域,不填写时默认对所有文件有效
"prefix": "tif", //触发片段的输入前缀字符(输入第一个字符时即开始匹配)
"body": [ //片段内容
"interface ${1:IFName} {", //$1,$2..为片段生成后光标位置通过tab切换
"\t${2:key}: ${3:value}", //${n:xx}的xx为占位文本
"}"
],
"description": "output typescript interface" //描述,显示在智能提示中
}
}
//任意.ts文件中输入tif+回车后即可生成下面的代码同时光标停留在IFName处
interface IFName {
key: value
}
通过上面演示的自定义功能,我们就可以编写自身开发常用的个性预设片段了。相比使用第三方预设,自定义的预设更灵活也更便于记忆。两者相结合,能够大大提升我们编码的效率。同时,针对实际业务场景定制的自定义片段文件,也可以在团队内共享和共同维护,以提升团队整体的效率。
Emmet
Emmet****(前身为 Zen Coding是一个面向各种编辑器几乎所有你见过的前端代码编辑器都支持该插件的 Web 开发插件,用于高速编写和编辑结构化的代码,例如 Html 、 Xml 、 CSS 等。从下面官方的示例图中可以看到,简单的输入 ! 或 html:5 再输入 tab 键,编辑器中就会自动生成完整的 html5 基本标签结构(完整的缩写规则列表可在官方配置中查找):
它的主要功能包括:
缩写代码块:
支持子节点(>)、兄弟节点(+)、父节点(^)、重复(*)、分组等节点关系运算符。
支持 id、 class、序号 $ 和其他用[]表示的自定义属性。
支持用 {} 表示的内容文本。
支持在不带标签名称时根据父节点标签自动推断子节点。
#main>h1#header+ol>.item-$${List Item$}*3^footer
//转换为
<div id="main">
<h1 id="header"></h1>
<ol>
<li class="item-01">List Item1</li>
<li class="item-02">List Item2</li>
<li class="item-03">List Item3</li>
</ol>
<footer></footer>
</div>
CSS 缩写:支持常用属性和值的联合缩写,例如以下代码:
m10 => margin:10px
p100p => padding: 100%
bdrs1e => border-radius: 1em;
自定义片段Emmet 也允许使用用户自定义的缩写规则。以 VS Code 为例,首先修改设定中 emmet.extensionsPath 字段,指向包含自定义规则 snippets.json 文件的目录,然后在对应文件中增加 Emmet 规则即可(保存规则文件后需要 reload 编辑器才能使规则生效)。例如,在下面的示例中分别为 html 和 css 增加了 dltd 和 wsnp 缩写规则:
{
"html": {
"snippets": {
"dltd": "dl>(dt+dd)*2"
}
},
"css": {
"snippets":{
"wsnp": "white-space: no-wrap"
}
}
}
上述工具的一般使用建议
在介绍完上面的两类工具的基本信息和一般使用方法后,接下来我们再按照前端开发过程中的基本语言类型来讨论下不同语言的工具选择建议。
Html
Html 语言在如今组件化的前端项目中是作为一个组件的模板存在的。而编写组件模板与纯 Html 的区别在于组件模板中通常已经由框架提供了数据注入Interpolation以及循环、条件等语法组件化本身也解决了包含、混入等代码复用的问题。因此在使用提效工具时我们用到的主要还是简化标签书写的功能而不太涉及工具本身提供的上述逻辑功能混用逻辑功能可能反而导致代码的混乱和调试的困难。当然简化标签书写既可以选择使用 Pug 语言,也可以使用 Emmet。
使用 Pug 的好处主要在于,对于习惯书写带缩进的 html 标签的同学而言上手更快,迁移成本低,且整体上阅读体验会更好一些。
而 Emmet 则相反,取消缩进后作为替代需要通过关系标识符来作为连接,书写习惯上迁移成本略高一些,且由于是即时转换,转后代码的阅读体验与 Html 没有区别。相对而言,由于可以自定义 Snippet 来使用常用缩写,因此使用熟练后实际效率提升会更明显一些。
CSS
毫无疑问,改进 CSS 书写效率就不会出现预处理语言和 Emmet 二选一的情况了:
对于项目中需要大量书写 CSS 的情况来说,使用预处理语言赋予的更强的代码抽象和组织能力,同时结合 Emmet 提供的属性缩写功能,能够大大提升整体 CSS 开发的效率。
另一方面,如果项目中主要使用 UI 组件库来呈现界面(例如大部分的中后台系统),而只需要少量编写自定义样式的话,使用 Emmet 的优先级更高。
CSS 预处理语言的选择上,由于主要功能的相似性,团队统一选择其一即可。
JavaScript/TypeScript
JS/TS 的开发过程是非结构化的,通常 IDE 自带的一系列智能帮助功能一般情况下就足以提供良好的开发体验。这里可以用到的提效工具主要还是使用 Snippet第三方扩展提供的常用语句的缩写结合开发者自定义的常用片段例如一个特定类型的模块初始化片段等可以成为我们开发的瑞士军刀再次提升编码效率。
总结
这一课时我们讨论了两种类型的提效工具:预处理语言和代码生成工具。这两种工具在一些场景下是功能重叠的,例如 Pug 和 Emmet 中的 html 生成,在其他场合下则各有功效,相辅相成。熟练运用预处理语言提供的各种细节语法功能,善于总结开发中常用的自定义片段,掌握这些技能后,我们的开发效率就能走上一个新的台阶。
这一课的讨论题是:第一课里我们讨论了用脚手架工具生成整个项目的初始化代码的过程,在这一课里我们也聊了用代码生成工具来生成代码片段的过程,那么在你的项目里有没有使用过其他粒度的代码生成工具呢?例如生成一个完整页面的工具?欢迎留言分享,我们会在之后的课程里再次展开这个话题。

View File

@ -0,0 +1,223 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 团队工具:如何利用云开发提升团队开发效率?
上一讲我们讨论了在开发时提升编码效率的相关工具。在项目开发过程中我们可以编写自定义的代码片段缩写规则来提升个人的编码效率。那么如果想要把这些规则分享给团队内的其他成员或自己的其他电脑设备时又该怎么做呢带着这个问题我们就进入到今天要聊的主题云开发Cloud Development
和之前介绍的适用于个人的开发提效工具不同,云开发的优势主要是在团队效率的提升方面。在这一讲里,我会介绍云开发和普通开发模式的区别,目前典型的云开发产品,以及云开发模式的应用场景。下面我就进入到第一个问题:什么是云开发。
软件开发环境的对比
在具体介绍什么是云开发之前,让我们先来看下普通的日常开发流程是什么样的。
个人电脑开发环境
通常我们都使用个人电脑作为日常的开发环境,对应的开发流程通常是:
基础环境准备在个人电脑中准备开发环境所需设施下载安装开发所需各种应用程序以前端为例IDE、Node、Git、Yarn……调试各种配置文件.bashrc、npmrc……安装必要IDE 插件并调试IDE 配置项UI、编码格式、Snippets……等。
下载代码:将项目源代码从代码仓库(例如 Git Repo中下载到个人电脑的开发目录下。
安装项目依赖。
运行开发服务。
编码调试。
执行任务Lint 检查、格式化检查、单元测试等)。
上述开发流程的流畅度一定程度上依赖于所使用电脑的硬件配置,因此程序员往往需要高性能配置的个人电脑。
远程开发
远程开发是将开发环境部署到远程服务器,然后通过个人电脑的 IDEIntegrated Development Environment ,集成开发环境) 进行远程连接来进行开发的方式(例如通过 VS Code 中的 Remote SSH 插件)。和传统的个人电脑开发环境相比,远程开发模式的优点主要在于:
由远程的开发服务器来承载项目数据存储和运行计算的需求,从而解放了对个人电脑资源的占用和对性能的要求。
由于个人电脑只提供了用户操作界面和远程连接的能力,因此大大减少了访问设备变更对于项目开发的影响,例如在更换新电脑或在家使用不同电脑设备的情况下,只需要安装 IDE 和少量配置,就能快速获得一致的开发体验,而无须再重复进行基础环境的准备。
远程开发的主要问题在于:
需要申请单独的开发机资源。
新申请的开发机也需要和个人电脑环境一样,人工进行基础环境的准备工作。
将开发机单独用于远程开发,在资源分配上可能存在资源利用不充分的问题。
云开发
云开发模式是在上述远程开发模式的基础之上发展而来的,将开发环境托管,由远程开发服务器变更为云服务。个人电脑通过 IDE 或云服务提供的浏览器界面访问云端工作区进行开发。云开发模式在继承远程开发模式优点的基础上,更能提升效率的原因在于:
通过容器化技术将开发环境所需基础设施应用程序、配置文件、IDE 插件、IDE 设定项等)集成到基础镜像中,大大提升开发环境准备的效率。同时,同样的基础环境也避免了相同项目不同开发集成环境导致的环境差异类问题。
通过服务化的云开发平台,简化使用流程,解决个人使用远程开发时可能遇到的技术困难,使得刚入职的新人也能够快速上手。
对于团队而言,能够提升团队协作效率。云开发模式有利于流程规范的统一,有利于团队成员共享开发工具,同时支持多人访问相同开发环境,有助于结对编程等协作流程。
对于公司而言,使用弹性化的云端容器环境有利于资源利用率的提升和硬件资产成本的降低。
典型云开发产品介绍
以下表格是一些已经推出的云开发产品,感兴趣的话,你可以根据自己所接触过的云厂商来进一步了解。
产品
厂商
基础 IDE
IDE 类型
代码托管方式
VS Codespace
微软
VS Code
Web/VS/VSC
云端Asure/自维护
Gitpod
Eclipse
Theia
Web/Desktop
云端/自维护(限制用户数量)
CloudIDE
阿里云
KAITIAN IDE
Web
云端
Cloud Studio
Coding.net (腾讯云)
VS Code
Web
云端5 个工作空间)
Cloud9
AWS
Cloud9
Web
云端AWS
本课时重点介绍的是以 VS Codespace 为代表的云开发产品,以及以 Theia 为代表的 WebIDE 框架。
微软Visual Studio Codespace
Visual Studio Codespace以下简称Codespace是微软 VS Code 团队 2019 年推出的云开发环境产品,该产品的特点是:
支持三种访问客户端VS CodeVisual Studio IDE以及 Web。
提供收费的云托管Azure环境与免费的自维护环境两种服务方式仍需要注册 Azure 账号来访问)。
内置多人协作工具 Live Share 和 AI 智能代码提示功能 InteliCode。
自定义环境基础配置,可定制化开发环境基础设施,例如 Linux 版本、工具、端口、变量、 IDE 插件等。
自定义个性化配置,定制环境中各类配置文件,例如 .bashrc.editorconfig 等。
Eclipse: Theia
Eclipse Theia以下简称 Theia是 Eclipse 基金会推出的 VS Code 的替代产品,它的定位是以 NodeJS 和 TS 为技术栈开发的云端和桌面端的 IDE 基础框架,于 2017 年启动, 2018 年发布了对应的 Web 端 IDE 产品 Gitpod。
Theia 和 VS Code 的技术相同点有:
编辑器核心都基于 Monaco Editor。
都支持 Language Server ProtocolLSP
都支持 Debug Adepter ProtocolDAP
都支持 VS Code 的插件体系。
官方总结,与 VS Code 相比Theia 的不同之处在于:
架构上更模块化,更易于自定义。
从一开始就被设计成同时运行于桌面和云端。
由厂商中立的开源基金会开发维护。
开发独立的 WebIDE 是云开发产品的首选,目前 VS Code 并未开源功能完整的 Web 版本(目前开源的 Web 版本仅可用于预览功能),但 Thiea 有开源可定制化的版本。
云开发模式的技术要素
一般来说云开发模式依赖的技术要素主要有三个方面WebIDE容器化以及能够对接其他云服务。
WebIDE
继 VS Code 2019 年发布 Codespace 后, Eclipse 基金会于 2020 年初也发布了 Theia 1.0 版本。 WebIDE 在功能体验上已达到和桌面 IDE 相同的水平(尽管在初始化阶段会有不同程度的额外耗时)。同时, WebIDE 还具有以下优点:
便于平台化定制:在团队使用时可通过定制 WebIDE 来实现通用的功能扩展和升级,而无须变更团队成员的桌面 IDE例如使用微信开发者工具软件的同学在工具发布新版本时需要各自处理升级而 Web 版则无须如此)。
流程体验上更平滑:虽然基本使用仍然是打开一个包含源代码的工作空间容器进行开发,但是通过和代码仓库以及 CI/CD 工具的对接,可以在很多流程节点上做到平滑的体验(例如,测试环境下修复 Bug可以通过工具在查找到对应的提交版本后点击进入到 IDE 界面进行修复、测试和提交,相比于原先需要线下操作的流程而言,效率会上升一个台阶)。
容器化
容器化以往在服务部署中应用较多。在云开发中的用途主要有:
为每个用户的每个项目创建独立的工作空间。
基于容器化的分层结构,可以方便地在基础环境、项目、用户等维度做镜像继承,便于团队成员维护相同项目时提升环境创建效率。
相比个人虚拟机,有利于提升资源利用率,同时环境搭建更便捷。
云服务对接
在一些云厂商的云开发产品中,除了容器化工作空间和 WebIDE 之外,也包含了与其他上下游服务的对接。例如在阿里云的 CloudIDE 产品中,就包含了一键部署等功能。而在自研的体系内,也可通过类似的方式将各个环节的工作流程进行串联,从而形成整体工作流程的效率提升。
云开发的效率提升应用场景
当我们以团队的方式来实践云开发时,能够找到一些效率优化的切入点,下面仅列举一些代表性的应用场景。
项目篇
加速创建新项目:在云开发模式下,可以将包含依赖安装的项目模板存储为镜像,开发者选择镜像并创建容器后即可直接预览效果或进入开发,免去下载模板与安装依赖的时间。
项目依赖版本统一npm 依赖包在不同环境下安装时,版本自动升级的问题常常对开发测试造成影响(尽管可以通过 “npm ci” 等命令锁定版本,但在实际业务中普及率并不高,这个问题在部署效率篇中会再次谈到)。而在云开发模式下,可以将 node_modules 依赖目录(或 Yarn 的 .pnp 目录)与依赖版本做关联,存储为独立镜像,供开发、测试、部署使用。在相应的流程中就可以免去安装依赖,以达到各环境下依赖版本的统一管理,同时也提升了各环境的处理效率。
工具篇
开箱即用的开发环境:在开发环境维度上,通过云开发模式,可以将开发所需的不同基础环境以及各种应用程序制作成开发环境镜像,供开发者自由选择。刚入职的新人无须花费大量时间去学习和安装调试项目的开发环境,真正达到开箱即用的效果。
自定义辅助工具的快速共享和共建:前端工具的共享不再局限于各自安装 npm 包的方式通用的配置、公共的依赖、针对特定项目类型的代码片段、emmet 缩写等,一切能想到的辅助工具,都可以在云平台的模式下快速落地,集成到各开发者的工作空间中。同时,对于工具的作者来说,在云平台的方式下也更容易获取到安装使用量等数据反馈,让优秀的工具得以呈现和传播。
流程篇
连接代码仓库与开发环境:云开发的模式可以缩短代码仓库与开发环境的距离,通过项目与开发环境的配置关联,可以从代码仓库的任意 commit 直连创建云端工作空间或进入已有工作空间。
连接 Pipeline 与开发环境:在构建部署的过程中,遇到构建问题或其他测试流程问题的情况时,可以通过对应的提交信息,直连创建临时修复用途的项目工作空间,以便快速调试和修复部署。
使用云开发的注意点
尽管随着 WebIDE 的兴起,越来越多的云开发产品开始呈现,但是作为一种新兴的工作模式,在尝试规模化使用前还是需要考虑到可能出现的一些问题:
代码安全问题:代码安全是首先需要考虑的问题。通常在代码仓库中我们会设置具体项目的访问权限,云开发模式下的镜像与空间访问设计上也应当注意对这部分权限的验证。此外,对于公司内部的项目,在使用云开发模式时应当首选支持内部部署的云服务或搭建自维护的云服务,而非将代码上传到外部云空间中。
服务搭建与维护:要在团队内使用云开发的功能,需要考虑服务搭建的方式和成本。对于大厂而言,云服务资源和技术建设比较丰富,搭建自维护的云开发服务可以提供更多灵活的功能;而对于中小规模的技术团队而言,购买使用一些支持内部部署的现有云开发服务是更好的选择。
服务降级与备份:由于云开发模式下将开发环境与工作代码都存储于云端,需要考虑当云端服务异常时的降级策略。例如是否有独立的环境镜像可供下载后离线使用,以及工作空间内的暂存代码是否有备份,可供独立下载使用。
总结
这一课时我们先介绍了云开发的概念,以及相比于现在的开发方式,它能解决哪些方面的问题。然后一起了解了几款有代表性的云产品,其中需要重点关注的是 VS Code 系的 Codespace 产品。此外,如果你对定制 WebIDE 感兴趣,从 Theia 入手会是比较好的选择。
在介绍完产品后,我们又讨论了云开发这种模式的一般技术要素,以及使用它所能带来的几个比较明确的效率提升场景。最后还有几个新技术对应的风险点,在真正尝试选择云开发方案前需要被考虑到。
今天的课后思考题是:希望你能实际体验课中讲到的一些产品,可以在课后讨论中分享你使用后的感受。

View File

@ -0,0 +1,121 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 低代码工具:如何用更少的代码实现更灵活的需求
在进入到这一课的内容之前先让我们来回顾下,通过脚手架工具生成初始化代码,以及通过 Snippet 工具生成代码片段的方式。两种方案的相同在于,通过简单的输入和选择就能获得预设后的项目代码。这种转化方式对于效率的提升效果是清晰可见的。于是有人就想到,能不能更进一步,将我们日常开发的手写代码过程,都转变为通过使用工具来快速生成呢?于是就有了开发效率篇的最后两节内容:低代码开发和无代码开发。这节课我们先来谈低代码开发。
什么是低代码开发
低代码开发Low-Code DevelopmentLCD是一种很早被提出2011的开发模式开发者主要通过图形化用户界面和配置来创建应用软件而不是像传统模式那样主要依靠手写代码。对应的提供给开发者的这类低代码开发功能实现的软件称为低代码开发平台Low-Code Development Platform LCDP。低代码开发模式的开发者通常不需要具备非常专业的编码技能或者不需要某一专门领域的编码技能而是可以通过平台的功能和约束来实现专业代码的产出。
从定义中我们可以看到,低代码开发的工作方式主要依赖操作图形化的用户界面,包括拖拽控件,以及修改其中可被编辑区域的配置。这种可视化的开发方式,可以追溯到更早的 Dreamwaver 时期。而随着前端项目的日趋复杂,这种方式已不再适应现代项目的需求,于是渐渐被更专业的工程化的开发模式所取代。
但是,快速生成项目代码的诉求从未消失。人们也慢慢找到了实现这个目的的两种路径:
一种是在高度定制化的场景中,基于经验总结,找到那些相对固定的产品形态,例如公司介绍、产品列表、活动页面等,开放少量的编辑入口,让非专业开发者也能使用。下一课介绍的无代码开发,主要就是面向这样的场景需求。
另一类则相反,顺着早期可视化开发的思路,尝试以组件化和数据绑定为基础,通过抽象语法或 IDE 来实现自由度更高、交互复杂度上限更高的页面搭建流程。这种项目开发方式通常需要一定的开发经验与编码能力,只是和普通编码开发方式相比,更多通过操作可视化工具的方式来达到整体效率的提升,因此被称为低代码开发。
在实际场景中,尤其是商用的低代码平台产品,往往提供的是上面两种开发方式的结合。
低代码开发的典型应用场景
低代码开发的一类典型应用场景是在 PC 端中后台系统的开发流程中,原因如下:
尽管中后台系统的具体页面布局并不固定,但整体 UI 风格较统一,可以基于统一的 UI 组件库来实现搭建,通过组件拖拽组合即可灵活组织成不同形态功能的页面,因此适用于低代码类型的开发模式。
中后台系统涉及数据的增删改查,需要有一定的编码调试能力,无法直接通过 UI 交互完成,因此不适用无代码开发模式。
以中后台系统的开发为目标,低代码开发的方式还可以细分为以下两种:基于编写 JSON 的开发方式,和基于可视化操作平台的开发方式,下面我们来依次介绍一下。
基于编写 JSON 的低代码开发
当我们去审视一个项目前端部分的最终呈现时,可以发现:
一个项目的前端部分本质上呈现的是通过路由连接的不同页面。而前端开发的目标就是最终输出页面的展示与交互功能。
如果学过浏览器基本原理你会知道每一个页面的内容在浏览器中最终都归结为DOM 语法树DOM Tree+ 样式Style+ 动态交互逻辑Dynamic Logic
在组件化开发的今天,一个规范定义的组件包含了特定功能的 DOM 子树和样式风格。因此页面的内容又可以定义为组件树Component Tree+ 动态交互逻辑Dynamic Logic
而基于 JSON-Schema 的低代码开发的切入逻辑是:
在特定场景下,例如开发中后台增删改查页面时,大部分前端手动编写的代码是模式化的。
页面组件结构模板和相应数据模型的代码组织,可以替换为更高效的 JSON 语法树描述。
通过制定用于编写的JSON 语法图式JSON Schema以及封装能够渲染对应 JSON 语法树的运行时工具集,就可以提升开发效率,降低开发技术要求。
下图中的代码就是组件语法树示例(完整的示例代码参见 07_low_code我们通过编写一个简单的 JSON 语法树以及对应的编译器,来展示低代码开发的模式。
编写 JSON 开发的高效性
编写 JSON 语法树开发的高效性体现在:
由于只用编写 JSON ,而隐藏了前端开发所需的大量技术细节(构建、框架等),因此降低了对开发人员的编码要求,即使是非专业的开发人员,也可以根据示例和文档完成相应页面的开发。
由于只用编写 JSON ,大量的辅助代码集成在工具内部,整体上减少了需要生成的代码量。
可以对中后台系统所使用的常用业务组件进行抽象,然后以示例页面或示例组件的方式,供用户选择。
编写 JSON 开发的缺点
但另一方面,这种方式也存在着一些不足:
输入效率:单从组件结构的描述而言,使用 JSON 描述的代码量要多于同等结构的 JSX 语法(参见示例代码 07_low_code对于有经验的前端开发者而言通常无法第一时间感受到效率的提升。
学习记忆成本:由于引入了新的 JSON 语法图式,无论对于前端开发者、后端开发者还是非专业的人员来说,上手的学习成本都不可避免。此外,不同组件存在不同属性,要在实际编写过程中灵活运用,对记忆量也是一个考验。而反复查阅文档又会造成效率的下降(对于这个问题,有个优化方案是利用 IDE Snippets 的选项功能生成对应的语法提示)。
复用性和可维护性:对于多页面存在可复用业务组件的情况,在 JSON 编写的模式下往往需要手动复制到各页面 JSON 中,牺牲了复用组件的可维护性。此外,对于功能复杂的页面,对应的 JSON 长度也会让维护体验变得不太美好。
问题排查难度增加:这个问题涉及面向人群,如果是非专业的人员从事 JSON 的开发过程,当遇到问题时,在如何排查上可能造成阻碍,因此通常需要配备额外的专业人员来提供技术支持。
针对编写 JSON 过程中的输入效率、记忆成本和可维护性等问题,许多低代码工具进一步提供了可视化操作平台的工作方式。下面再让我们来了解下,这种方式是怎么解决上述问题的。
基于可视化操作平台的低代码开发
可视化的低代码操作平台把编写 JSON 的过程变成了拖拽组件和调试属性配置,如下图所示,这样的交互方式对用户来说更直观友好,开发效率也会更高。
可视化操作平台的基本使用方式
绝大部分的可视化操作平台都将界面布局分为三个区域:左侧的组件选择区,中部的预览交互区以及右侧的属性编辑区。这三个区域的排布所对应的,也是用户生成页面的操作流程:
首先,在左侧面板中选择组件。
然后,拖入中间预览区域,并放置到合适的容器块内。
最后,调试右侧面板中新移入的组件属性。
调试完成后,进行下一个组件的循环操作直到整个页面搭建完成。
可视化操作平台的生产效率影响因素
通常来说,在组件数量不变的情况下,编写 JSON 的产出效率更大程度上取决于编写页面的开发者的技术熟练度。但在使用可视化操作平台时却并非如此:我们会看到,平台本身的很多方面也会直接影响使用者的产出:
首先,平台的功能完备性直接决定了用户产出的上限:开发者不可能在平台里使用组件区没有显示的组件,也不可能创建编辑区不存在的属性。这就迫使平台开发者需要尽可能完整地陈列所有类型的组件,以及通过定义组件类型描述,来获取所有可以被编辑的属性和方法。包括用户交互和数据对组件的影响,这些都需要平台以合适的使用方式提供给用户。
其次,平台的逻辑自洽性决定了用户产出的质量:在代码的组织上,不同组件之间不可以任意组合,错误的组合可能导致显示与功能的异常。如果平台只是简单罗列所有组件,而对其中的规则不加以限制,就可能导致用户在使用过程中出现意料外的产出结果。所以,平台开发者需要有一套完整的组件关联关系表,并反映到交互呈现中。
最后,平台提供的交互易用性决定了用户的产出效率:尽管大部分低代码平台都提供了相似的区域操作逻辑,但真正影响用户使用效率的往往是很多细节的控制。例如,与单纯依靠光标选取组件相比,在侧边栏提供节点树的方式可以更大程度减少误选;与简单陈列所有组件相比,合适的分类,以及当选择特定组件时筛选出可添加的部分组件,更能减少用户搜索的时间,同时减少可能的出错;一些平台提供了操作栈回放的功能,能减少用户误操作后的修复成本,等等。
低代码开发的产品
低代码开发的产品有很多,其中既包括商用的产品,例如 Kony、OutSystems、Mendix、Appian、iVX国内也包括开源类的产品例如阿里飞冰、百度 Amis、贝壳河图、Vvvebjs、react-visual-editor 等。这里就不一一介绍了,感兴趣的话,你可以进一步搜索了解。
总结
这节课我们介绍了低代码开发的概念和它的基本应用场景,也了解了低代码开发的两种基本开发模式:基于编写 JSON 的方式和基于可视化操作平台的方式。
前者对普通的项目开发流程做了抽象,将编写不同功能模块的代码变为只编写组件语法树描述信息,这种方式在一定程度上降低了使用者的技术要求,提升了开发的效率,但是在一些方面仍然不甚理想。而平台化的开发模式相对而言解决了编写 JSON 模式下的一些问题,但是要搭建一个功能完备、使用逻辑自洽和交互性良好的平台也并非易事。
通过这节课的学习,希望能为你提供一种新的项目技术方案,在合适的应用场景下,可以考虑使用低代码工具来提升开发效率。今天的课后思考题是:这节课里讲到的低代码工具主要面向什么样的用户群体?

View File

@ -0,0 +1,192 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 无代码工具:如何做到不写代码就能高效交付?
在开始今天的课程前,我们先来简单回顾下上节课的思考题:低代码工具主要面向什么样的用户群体呢?低代码工具本质上是对组件化开发流程的简化,但在开发过程中,仍然可能进行编码调试。因此,它面向的用户群体应该是具有一定技术基础的开发人员,专业的后端开发也可以使用这类工具来快速开发项目中的前端功能。
在介绍低代码开发的时候,经常伴随出现另一个名词:无代码开发。今天我们就来近距离地了解这种开发模式的相关知识。
无代码开发模式的出现
在讨论无代码开发之前,我们先来看下这种开发模式出现的原因:
有需求量大且更新频率快的小型项目:例如不同主题内容的运营活动页面、招聘页面等。
这些项目流程模式基本相同但又具有一定的定制性:例如一个优惠券活动,需要投放到不同城市,因此文案内容、图片背景和优惠券金额等都可能不同。
开发人员成本昂贵,供不应求:尽管开发人员可以通过代码复用等方式来提升开发类似代码的效率,但是总体而言这类简单项目从定制开发到上线的流程,仍然以天为单位,在面对大量雷同的项目时,开发效率仍不能满足用户方的需求。
非互联网企业缺少技术资源:许多非互联网中小型企业内部缺少完整的技术团队,无法通过自身技术资源解决日常的互联网开发需求。
无代码开发模式正是为这些问题而量身定制的。
无代码开发介绍
无代码开发No-Code Development / Codeless Development是指通过非手写代码工具来产出代码的方式。这类工具被称为无代码开发平台No-Code Development PlatformNCDP
无代码开发和低代码开发的区别
从下面的表格中可以看到无代码开发和低代码开发的主要区别,包括目标人群、目标产品、开发模式等 7 个维度。
区别维度
低代码开发
无代码开发(面向非开发)
无代码开发(面向准开发)
目标人群
主要面向有一定技术基础的开发人员
主要面向非开发岗位人员(例如运营人员,设计人员)
主要面向准开发人员(对开发思维的需求随项目难度递增)
目标产品
主要为 B 端中后台
主要为 C 端活动或 H5
结合前两者
开发模式
编写 JSON/操作图形化交互平台(偏重前端)
操作图形化交互平台(偏重前端)
操作图形化交互平台(前端到后端)
基础设施
通用的组件库与渲染流程
典型的页面/项目模板,以及与视觉呈现相关的组件
前后端组件
可自由定制的内容
组件的选择、布局、属性、数据、交互
可视化数据(文本、媒体、动画等)的编辑
前端可视化数据,后端数据与逻辑功能等
数据接口
通常由独立后端单独开发提供
无数据接口,或通常由平台方提供标准化的接口
基于云基础设施的数据功能
部署
可单独部署
通常由平台方提供云服务部署
通常由平台方提供云服务部署
其中,可以把市面上的无代码开发模式进一步细分为两类:
一类完全面向非开发人员,如百度 H5对应开发的目标产品主要是模式化的 C 端活动与其他 H5 页面类型(例如招聘页面,测试小游戏等);
另一类面向准开发人员,即缺少代码经验且希望开发全栈产品的个人或团队,在目标产品和定制功能上更全面,但是相应的学习使用成本也更高,这类产品以 iVX 为代表。
下面,我们就来进一步了解下这两种开发模式的异同。
典型产品分析
面向非开发人员的无代码开发产品
这类产品的设计目标就是将一些固定类型的项目生产流程,由代码开发转变为操作图形化交互工具。
企业内部的定制化搭建平台
例如面向 C 端的企业经常会有推广拉新的开发需求,以红包活动为例,如果按照普通的代码开发流程,需要经历以下 6 个流程才能最终上线:
产品确定活动流程,交付产品文档与原型。
设计师设计页面,交付设计稿。
前端工程师开发活动的前端代码。
后端工程师开发活动的后端代码。
前后端联调后交付测试。
测试通过后部署上线。
这类活动通常是可复用的,然而针对不同时间段,或者同一时间段的不同推广渠道和推广地区,通常需要提供模式相同但外观与数据(例如红包金额)不同的活动产品,于是当需要复用时,会发现除了产品交付的流程不用变更外,后续所有开发部署流程都难以避免。这导致开发资源被低效地占用,生产效率也无法进一步提升。
而无代码开发产品可以完美地解决这一类问题,针对同一类型的活动项目,前后端工程师可以开发出对应的可视化活动搭建平台,提供:
选择活动类型并预览效果的功能。
文本、图片、活动金额、上下线时间等元素替换功能。
数据统计等辅助模块。
这种方法通过一次开发,即可让运营人员长期重复使用,解放了后续的开发资源,并且从流程上将普通项目开发的 6 个环节简化为两个环节:设计师设计页面,以及运营人员无代码地编辑内容。这将原先以天为单位的开发部署时间,缩短为以分钟为单位的编辑生成时间。
以上便是企业内部无代码开发的一类应用场景。
外部无代码搭建平台
另一类面向非开发人员的无代码开发产品,针对的是缺乏开发资源的企业和部门。对于一些常见的小型项目需求,例如招聘页面、报名页面等,它们往往需要借助外部提供的无代码开发平台。这类无代码开发平台包括百度 H5、MAKA、易企秀等。
百度 H5 编辑界面
这类产品的特点是:
场景类型固定:通常提供一些企业常见类型页面生成(招聘介绍、报名表单、宣传活动、答题测试等)。
设计模板丰富:通常都提供了大量经过设计的页面模板供用户选择,部分平台还提供了第三方设计师设计与发布设计模板的功能。
定制化功能多样:除了常见的文本和图片类型外,这类产品的 IDE 中通常还包含了媒体、表单、动画等多维度编辑功能。
后端功能较少:产品形态大多是纯前端的,即使涉及后端数据,例如表单提交,也只提供了基于云平台的上报数据统计,在 IDE 中没有自定义后端接口和数据字段的部分,这也和使用人群的定位一致。
部署在云端:通常都提供免费或收费的云端部署方案,以降低用户运维难度和操作成本。
使用人群细化:使用人群进一步分化为设计师与普通使用者。对于普通使用者而言,除了开发资源外,通常也不具备设计资源。于是设计师就可以使用平台提供的 IDE 工具,发布付费设计模板,供普通使用者选择。
面向准开发人员的无代码开发产品
而面向准开发人员的无代码开发产品,则有以下几点不同:
更为多样化的应用场景:同上述面向非开发人员的产品相比,这类产品最主要的功能是提供了描述性的后端的数据与功能模块,因此能够实现的应用场景也更为多样化和通用化。以 iVX 为例,可实现的应用场景从上面的 C 端产品扩展到了 B 端产品包括小程序、小游戏、H5、营销活动BPM、OA、CRM、ERP企业中台BI、大屏幕等。
iVX 编辑器中后端逻辑描述面板
目标人群的变化:应用场景扩展对应的是 IDE 功能的复杂化和操作学习成本的增加,于是目标人群也多少有些不同:
从一方面来看,这种功能增强型的无代码模式能够吸引更多有产品思维但缺少实际开发经验的个人或缺少开发资源的团队尝试使用,例如外包团队或早期创业团队。
但从另一方面看,要开发一个具有一定复杂度的项目,对开发人员的要求不只体现在代码能力方面,还需要开发人员对产品、全栈架构与交互逻辑层面有一定的认识和理解。无代码平台主要解决的是前一个层面的问题,对于后者,则仍然受到使用者的技术经验限制。这也在一定程度上也会造成无形的使用壁垒。
此外,使用者对这类全新的开发工具和流程的开发经验的掌握,很难迁移到其他开发工具和流程中,这也可能使使用者产生是否值得学习这类工具的疑虑。也许直到这类产品在市场中得到充分的验证,培养出足够多的使用者和需求方后,才能真正解决担忧。
总结
这节课我们介绍了和低代码开发对应的另一种开发模式:无代码开发。首先我们对比了两种开发模式不同维度的区别,又进一步介绍了无代码开发的两种不同方向:面向非开发人员的产品与面向准开发人员的产品。
面向非开发人员的无代码开发在企业内部与外部都有相应的应用场景:在企业内部我们可以将一些频率高的常用简易开发流程,固化为无代码开发产品,供运营或其他岗位人员使用;而在企业外部,也有不少免费或收费的无代码平台,将开发工具提供给缺乏技术资源的企业与个人;同时,设计师也可以在这类平台上制作自己的设计模板提供给用户。
面向准开发人员的无代码产品相比之下具有更广泛的使用场景,通过提供后端数据与逻辑的描述功能,用户可以通过 IDE 开发出具备前后端数据交互的复杂应用,进一步减少与普通代码开发的功能边界的差距。但相对的,要利用工具实现复杂应用,对用户的学习成本和思维培养也是一种新的挑战。究竟这种模式是否能被更多企业所接受,成为代码开发模式的替代呢?让我们拭目以待。
通过这一课,希望作为前端工程师的你能够在工作中考虑无代码开发的第一种应用场景,在合适的场景下开发相应的工具,来解放团队的开发资源,提升效率。而对于外部的无代码开发平台,如果感兴趣,希望你进一步了解,或许能对普通代码开发的模式有所借鉴。
最后,随着这一课的结束,我们就完成了开发效率篇的所有课程。今天的思考题是回顾本模块的内容,学会在工作中运用,如有疑问,欢迎你写在留言区。
下节课我们将进入第二个模块:构建效率篇。一起深入了解你所使用的构建工具中有哪些影响效率的知识点。

View File

@ -0,0 +1,159 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 构建总览:前端构建工具的演进
今天开始我们进入本专栏的第二模块:构建效率。本模块主要探讨如何优化构建细节。在这之前,我想先聊一聊前端开发历程中构建工具的演进。通过这节课,让你对构建工具的诞生发展及它们各自解决的问题有一个直观了解。
前端开发语言的诞生
前端开发语言发展历程回顾:
1991 年Tim Berners-Lee 发布了第一份 HTML 标准。
1994 年Håkon Wium Lie提出了 CSS 的概念,两年后 HTML 4.0 中首次支持了 CSS。IE3 也成了第一个支持 CSS 的商用浏览器。
1995 年Brendan Eich 发明了名为*Mocha*的浏览器脚本语言,并在随后命名为了*LiveScript几个月后又重新命名为我们现在所熟知的JavaScript*。
1997 年,随着 IE4 的发布,*DHTML*Dynaimic HTML的概念被提出指代用于创建包含交互和动画效果页面的一系列技术结合包括 HTML、CSS、JS、DOM 等。
2004 年,*Ajax*技术随着 Google 在 Gmail 中的广泛运用而逐渐被更多的 Web 开发者所使用(尽管最早的 Ajax 可以追溯到 1999 年 IE5 时代的 XMLHTTP ActiveX
前端主要的开发语言HTML、CSS 和 JS 都诞生在 20 世纪 90 年代。2000 年前,网页呈现和交互都较为简单,开发框架和处理工具都在孕育中,即便是早期前端开发者所熟知的 jQuery 框架,也远未出现。
文件压缩与合并工具
文件压缩与合并工具发展历程回顾:
2001 年Douglas Crockfold 发布了 JSMin工具用于去除 JS 代码中的注释和空格。
2004 年Dave Shea 在他的文章中参考早期游戏开发中使用的 Sprite 图方案,提出了 CSS Sprite 的概念,即将多张小图合成为一张大图,然后通过 CSS 控制在不同元素中使用图片的局部区域,从而减少网络请求,提升网页性能。
2006 年Yahoo 发布了 YUI 库,其中包含了基于 Java 的代码压缩工具 YUI Compressor。
2009 年Google 发布了 Closure Toolkit其中包含的 Closure Compiler 提供了比 YUI Compressor更多的代码优化功能并支持 Source Map 和多文件合并。
2010 年Mihai Bazon 发布了压缩工具UglifyJS并在 2012 年的升级版本 UglifyJS2 中增加了对 Source Map 的支持。
2000 年以后的 10 年里,随着更多 CSS 与 JS 框架的诞生,代码优化的工具也应运而生。为了获得更好的访问体验,开发者需要更少的资源连接数与更小的文件体积,这就分别对应了两类工具:文件压缩工具和合并工具。
在压缩工具方面:从 JSMin、YUI Compressor 到 Closure Compiler 和 UglifyJS压缩与优化的性能不断完善。
在合并工具方面CSS Sprite 技术的提出解决了网页中大量素材图片的加载性能问题而在此期间Sprite 图片还主要通过设计工具来手动生成,例如 PS 等,直到下个十年才出现自动化的生成工具。而代码文件的合并,可以在命令行中通过输出到文件手动完成;此外在 Closure Compiler 工具中也包含了将多个文件合并为一个的参数。
这个时期的工具有一些共同点,例如都是基于其他语言(例如 C 和 Java实现的因此需要安装对应的依赖环境这些工具也都需要通过命令行执行。对于用户来说随着开发项目的增多容易造成效率的低下。这类问题直到下一个十年随着 NodeJS 的发布才逐渐改变。
包管理工具
包管理工具发展历程回顾:
2009 年Ryan Dahl 发布了第一个版本的Node.js。
2010 年Node.js 核心开发人员Isaac Z. Schlueter 编写了对应环境的包管理工具npm。
2012 年Twitter 发布了名为Bower的前端依赖包管理工具。
2016 年Facebook 发布了 npm registry 的兼容客户端Yarn。
人们可以把代码包发布到 npm 中
2009 年 NodeJS 发布,这对前端开发领域产生了深远的影响。一方面,许多原先基于其他语言开发的工具包如今可以通过 NodeJS 来实现,并通过 npmNode Package Manager即 node 包管理器)来安装使用。大量的开发者开始将自己开发的工具包发布到 npm registry 上,包的数量在 2012 年初就达到了 6,000 个,而到 2014 年,数字已经上升到了 50,000 个。
另一方面安装到本地的依赖包在前端项目中如何引用开始受到关注。Twitter 发布的 Bower 旨在解决前端项目中的依赖安装和引用问题,其中一个问题是,在 npm 安装依赖的过程中会引入大量的子包在早期版本npm 3 之前)中会产生相同依赖包的大量重复拷贝,这在前端项目中会导致无谓的请求流量损耗。而 Bower 在安装依赖时则可以避免这类问题。然而随着更多模块化打包工具的诞生,它的优势逐渐被其他工具所取代。直到 2017 年Bower 官方宣布废弃这个项目。
著名的 node_modules hell源自 reddit 用户 xaxaxa_trick
npm 的另一个饱受诟病的问题是本地依赖管理算法的复杂性以及随之而来的性能、冗余、冲突等问题。而 2016 年发布的 Yarn 正是为解决这些问题而诞生的。和 npm 相比Yarn 的主要优点有:
安装速度:由于 Yarn 在安装依赖时采用的是并行操作,以及它在缓存依赖包时相比 npm 缓存的数据更完整,因此它在初次与重复安装依赖时,普遍都会比 npm 更快。
稳定性npm 5 引入的 package-lock 文件,在每次执行 npm install 时仍然会检查更新符合语义规则的依赖包版本,而 yarn.lock 则会严格保证版本的稳定性尽管yarn.lock 不能保证 node_modules 的拓扑稳定性)。
PlugnPlayPnPYarn 2.0 发布了 PnP的功能在更早期的 1.12 版本中就已实现。PnP 方案具有提升项目安装与解析依赖的速度,以及多项目共享缓存(与普通缓存相比,免去了读写 node_modules 的大量 I/O 操作),节省占用空间等优势。
任务式构建工具
任务式构建工具发展历程回顾:
2012 年Ben Alman 发布了基于任务的构建工具 Grunt。
2013 年Eric Schoffstall 发布了流式的构建工具 Gulp。
随着 NodeJS 和 npm 的发布,大量的前端工具包发布到 npm 仓库,开发者通过简单的命令行指令就可以方便地下载和使用,前端的工程化也在这一时期开始蓬勃发展。其中一种趋势就是,使用自动化的任务式构建工具来替代手工执行各种处理命令。
Grunt 和 Gulp 这两种任务式的构建工具的基本组成包括核心的处理工具grunt-cli/gulp-cli、配置文件Gruntfile/Gulpfile以及一系列常用的任务插件Clean、Watch、Copy、Concat、Uglify、CssMin、Spritesmith……。在项目里通过编写配置文件就可以定义工作流程中的各种自动化构建处理例如在发生变更时通过 Watch 插件监控文件,从而自动执行代码的检查与压缩等。
Grunt vs Gulp
这两种工具的差异性主要体现在:
读写速度Gulp 在处理任务的过程中基于 NodeJS 的数据流,本质上是操读写内存,而 Grunt 则是基于临时文件,因此在读写速度上 Gulp 要快于Grunt。
社区使用规模:截止编写课程的时间点,在 npmjs.com 的周下载量方面Gulp 为 1,200,000+,约是 Grunt 的两倍。而在插件数量方面Grunt 社区提供了超过 6000 个不同功能的插件,而 Gulp 社区的插件数量则是 4000 多个。
配置文件的易用性:相比描述不同插件配置信息的 Gruntfile 而言,使用 pipe 函数描述任务处理过程的方式通常更易于阅读,但编写时需要对数据流有更深入的理解。
任务式的构建工具,虽然解决了开发流程中自动化执行预设任务的问题,但不能解决项目中代码如何组织成不同功能的代码包、不同代码之间如何相互依赖等问题。而解决这类问题的方式就是:模块化。
模块化:模块定义与模块化的构建工具
模块化发展历程回顾:
2009 年Kevin Dangoor 发起了 ServerJS 项目,后更名为 CommonJS其目标是指定浏览器外的 JS API 规范(例如 FS、Stream、Buffer 等)以及模块规范 Modules/1.0。这一规范也成为同年发布的 NodeJS 中的模块定义的参照规范。
2011 年RequireJS 1.0 版本发布,作为客户端的模块加载器,提供了异步加载模块的能力。作者在之后提交了 CommonJS 的 Module/Transfer/C 提案,这一提案最终发展为了独立的 AMD 规范。
2013 年面向浏览器端模块的打包工具Browserify发布。
2014 年,跨平台的前后端兼容的模块化定义语法 UMD发布。
2014 年Sebastian McKenzie 发布了将 ES6 语法转换为 ES5 语法的工具 6to5并在之后更名为Babel。
2014 年Guy Bedford 对外发布了 SystemJS 和 jspm 工具,用于简化模块加载和处理包管理。
2014 年,打包工具 Webpack 发布了第一个稳定版本。
2015 年ES6ES2015规范正式发布第一次从语言规范上定义了 JS 中的模块化。
2015 年Rich Harris 发布的 Rollup 项目,基于 ES6 模块化,提供了 Tree Shaking 的功能。
模块化的不同规范
CommonJS在 CommonJS 出现之前,一个 JS 类库只能通过暴露全局对象的方式,供其他 JS 文件使用这样的方式有着诸多的问题例如变量污染等。CommonJS 作为非浏览器端的 JS 规范,它的基本要素如下:
模块定义:一个模块即是一个 JS 文件,代码中自带 module 指向当前模块对象;自带 exports=module.exports且 exports 只能是对象,用于添加导出的属性和方法;自带 require 方法用于引用其他模块。完整的 module 对象可参考NodeJS 中的相关介绍。
模块引用:通过引用 require() 函数来实现模块的引用,参数可以是相对路径也可以是绝对路径。在绝对路径的情况下,会按照 node_modules 规则递归查找,在解析失败的情况下,会抛出异常。
模块加载require() 的执行过程是同步的。执行时即进入到被依赖模块的执行上下文中,执行完毕后再执行依赖模块的后续代码。可参考官方文档中说明这一过程的示例代码。
AMDCommonJS 的 Modules/1.0 规范从一开始就注定了只能用于服务端,不能用于浏览器端。这一方面是因为模块文件中没有函数包裹,变量直接暴露到全局;另一方面则因为浏览器端的文件需要经过网络下载,不适合同步的依赖加载方式,因此出现了适用于浏览器端的模块化规范 AMD。AMD 规范的基本要素如下:
模块定义通过define(id?, dependencies?, factory) 函数定义模块。id 为模块标识dependencies 为依赖的模块factory 为工厂函数。factory 传入的参数与 dependencies 对应,若不传 dependencies则 factory 需要默认传入 require、exports以及 module或只传入 require但使用 return 做导出。
模块引用:最早需要通过 require([id], callback) 方式引用,之后也支持了类似 CommonJS 的 var a = require(a) 的写法。
UMDUMD 本质上是兼容 CommonJS 与 AMD 这两种规范的代码语法糖,通过判断执行上下文中是否包含 define 或 module 来包装模块代码,适用于需要跨前后端的模块。
ES ModuleECMA 规范组织在 2015 年 6 月发布的 ES6 版本中,首次提出了 JS 标准的模块化概念,具体要素如下:
模块定义:模块内支持两种导出方式,一种通过 export 关键字导出任意个数的变量,另一种通过 export default 导出,一个模块中只能包含一个 default 的导出类型。
模块引用:通过 import 关键字引用其他模块。引用方式分为静态引用和动态引用。静态引用格式为*import importClause from ModuleSpecifier*import 表达式需要写在文件最外层上下文中;动态引用的方式则是 import(),返回 promise 对象。
下面我们介绍一些模块化的构建工具。
模块化的构建工具
RequireJS正如前面介绍的RequireJS 的核心功能是支持 AMD 风格的模块化代码运行。
Browserify与前者不同Browserify 的目标是让 CommonJS 风格的代码也运行在浏览器端,除了提供语法糖外,还提供了一些经过处理后且在浏览器端运行的 NodeJS 的核心模块。
BabelBabel 的定位一直是 Transformer即语法转换器它承担着将 ES6、JSX 等语法转换为 ES5 语法的核心功能,被广泛地运用于其他构建工具中。
SystemJSSystemJS 是兼容各种模块化规范的运行时工具。
WebpackWebpack 一方面兼容各种模块化规范的标识方法,另一方面将模块化的概念延伸到其他类型的文件中,创造性地打造了一种完全基于模块的新的构建体系。在下一节课中我会再深入讲解。
RollupRollup 在诞生之初率先实现了 Tree Shaking 功能,以及天然支持 ES6 模块的打包。虽然这些主要功能在 Webpack 发展的后续版本中也逐步支持,但其简单的 API 仍然广受许多库开发者的青睐。
总结
这节课中,我们讨论了前端构建工具的演进:从早期的单独功能的压缩与合并工具,到 NodeJS 与包管理工具的诞生,随之而来的是任务式构建工具的发展,以及模块化概念与工具的不断探索。我们现在使用的构建工具,一方面立足于这些过去积累下来的前人的经验与智慧,另一方面也顺应着不断发展的前端开发需求。
本节课的课后讨论题是:最后出现的模块化的构建工具为什么能取代任务式的构建工具呢?以 Webpack 为例来聊聊看吧。
下节课,我们深入到 Webpack 的体系中,来聊一聊 Webpack 的完整工作流程。

View File

@ -0,0 +1,320 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 流程分解Webpack 的完整构建流程
上节课我们聊了过去 20 余年里,前端项目开发时的工程化需求,以及对应产生的工具解决方案,其中最广泛运用的构建工具是 Webpack。这节课我们就来深入分析 Webpack 中的效率优化问题。
要想全面地分析 Webpack 构建工具的优化方案,首先要先对它的工作流程有一定理解,这样才能针对项目中可能存在的构建问题,进行有目标地分析和优化。
Webpack 的基本工作流程
我们从两方面来了解 Webpack 的基本工作流程:
通过 Webpack 的源码来了解具体函数执行的逻辑。
通过 Webpack 对外暴露的声明周期 Hooks理解整体流程的阶段划分。
其中会涉及对 Webpack 源代码的分析,源代码取自 Webpack 仓库的 webpack-4 分支,而最新的 Webpack 5 中的优化我们会在后续课程中单独分析。
通常,在项目中有两种运行 Webpack 的方式:基于命令行的方式或基于代码的方式。
两种示例的代码分别如下(具体示例参照 10_webpack_workflow
//第一种:基于命令行的方式
webpack --config webpack.config.js
//第二种:基于代码的方式
var webpack = require('webpack')
var config = require('./webpack.config')
webpack(config, (err, stats) => {})
webpack.js 中的基本流程
无论用哪种方式运行 Webpack本质上都是 webpack.js 中的 Webpack 函数。
这一函数的核心逻辑是:根据配置生成编译器实例 compiler然后处理参数执行 WebpackOptionsApply().process根据参数加载不同内部插件。在有回调函数的情况下根据是否是 watch 模式来决定要执行 compiler.watch 还是 compiler.run。
为了讲解通用的流程,我们以没有 watch 模式的情况进行分析。简化流程后的代码示例如下:
const webpack = (options, callback) => {
options = ... //处理options默认值
let compiler = new Compiler(options.context)
... //处理参数中的插件等
compiler.options = new WebpackOptionsApply().process(options, compiler); //分析参数,加载各内部插件
...
if (callback) {
...
compiler.run(callback)
}
return compiler
}
Compiler.js 中的基本流程
我们再来看下运行编译器实例的内部逻辑,具体源代码在 Compiler.js 中。
compiler.run(callback) 中的执行逻辑较为复杂,我们把它按流程抽象一下。抽象后的执行流程如下:
readRecords读取构建记录用于分包缓存优化在未设置 recordsPath 时直接返回。
compile 的主要构建过程,涉及以下几个环节:
newCompilationParams创建 NormalModule 和 ContextModule 的工厂实例,用于创建后续模块实例。
newCompilation创建编译过程 Compilation 实例,传入上一步的两个工厂实例作为参数。
compiler.hooks.make.callAsync触发 make 的 Hook执行所有监听 make 的插件(例如 SingleEntryPlugin.js 中,会在相应的监听中触发 compilation 的 addEntry 方法。其中Hook 的作用,以及其他 Hook 会在下面的小节中再谈到。
compilation.finish编译过程实例的 finish 方法,触发相应的 Hook 并报告构建模块的错误和警告。
compilation.seal编译过程的 seal 方法,下一节中我会进一步分析。
emitAssets调用 compilation.getAssets(),将产物内容写入输出文件中。
emitRecords对应第一步的 readRecords用于写入构建记录在未设置 recordsPath 时直接返回。
在编译器运行的流程里,核心过程是第二步编译。具体流程在生成的 Compilation 实例中进行,接下来我们再来看下这部分的源码逻辑。
Compilation.js 中的基本流程
这部分的源码位于 Compilation.js 中。其中,在编译执行过程中,我们主要从外部调用的是两个方法:
addEntry从 entry 开始递归添加和构建模块。
seal冻结模块进行一系列优化以及触发各优化阶段的 Hooks。
以上就是执行 Webpack 构建时的基本流程,这里再稍做总结:
创建编译器 Compiler 实例。
根据 Webpack 参数加载参数中的插件,以及程序内置插件。
执行编译流程:创建编译过程 Compilation 实例,从入口递归添加与构建模块,模块构建完成后冻结模块,并进行优化。
构建与优化过程结束后提交产物,将产物内容写到输出文件中。
除了了解上面的基本工作流程外还有两个相关的概念需要理解Webpack 的生命周期和插件系统。
读懂 Webpack 的生命周期
Webpack 工作流程中最核心的两个模块Compiler 和 Compilation 都扩展自 Tapable 类用于实现工作流程中的生命周期划分以便在不同的生命周期节点上注册和调用插件。其中所暴露出来的生命周期节点称为Hook俗称钩子
Webpack 中的插件
Webpack 引擎基于插件系统搭建而成,不同的插件各司其职,在 Webpack 工作流程的某一个或多个时间点上对构建流程的某个方面进行处理。Webpack 就是通过这样的工作方式,在各生命周期中,经一系列插件将源代码逐步变成最后的产物代码。
一个 Webpack 插件是一个包含 apply 方法的 JavaScript 对象。这个 apply 方法的执行逻辑,通常是注册 Webpack 工作流程中某一生命周期 Hook并添加对应 Hook 中该插件的实际处理函数。例如下面的代码:
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.run.tap("HelloWorldPlugin", compilation => {
console.log('hello world');
})
}
}
module.exports = HelloWorldPlugin;
Hook 的使用方式
Hook 的使用分为四步:
在构造函数中定义 Hook 类型和参数,生成 Hook 对象。
在插件中注册 Hook添加对应 Hook 触发时的执行函数。
生成插件实例,运行 apply 方法。
在运行到对应生命周期节点时调用 Hook执行注册过的插件的回调函数。如下面的代码所示
lib/Compiler.js
this.hooks = {
...
make: new SyncHook(['compilation', 'params']), //1. 定义Hook
...
}
...
this.hooks.compilation.call(compilation, params); //4. 调用Hook
...
lib/dependencies/CommonJsPlugin.js
//2. 在插件中注册Hook
compiler.hooks.compilation.tap("CommonJSPlugin", (compilation, { contextModuleFactory, normalModuleFactory }) => {
...
})
lib/WebpackOptionsApply.js
//3. 生成插件实例运行apply方法
new CommonJsPlugin(options.module).apply(compiler);
以上就是 Webpack 中 Hook 的一般使用方式。正是通过这种方式Webpack 将编译器和编译过程的生命周期节点提供给外部插件,从而搭建起弹性化的工作引擎。
Hook 的类型按照同步或异步、是否接收上一插件的返回值等情况分为 9 种。不同类型的 Hook 接收注册的方法也不同,更多信息可参照官方文档。下面我们来具体介绍 Compiler 和 Compilation 中的 Hooks。
Compiler Hooks
构建器实例的生命周期可以分为 3 个阶段:初始化阶段、构建过程阶段、产物生成阶段。下面我们就来大致介绍下这些不同阶段的 Hooks
初始化阶段
environment、afterEnvironment在创建完 compiler 实例且执行了配置内定义的插件的 apply 方法后触发。
entryOption、afterPlugins、afterResolvers在 WebpackOptionsApply.js 中,这 3 个 Hooks 分别在执行 EntryOptions 插件和其他 Webpack 内置插件,以及解析了 resolver 配置后触发。
构建过程阶段
normalModuleFactory、contextModuleFactory在两类模块工厂创建后触发。
beforeRun、run、watchRun、beforeCompile、compile、thisCompilation、compilation、make、afterCompile在运行构建过程中触发。
产物生成阶段
shouldEmit、emit、assetEmitted、afterEmit在构建完成后处理产物的过程中触发。
failed、done在达到最终结果状态时触发。
Compilation Hooks
构建过程实例的生命周期我们分为两个阶段:
构建阶段
addEntry、failedEntry、succeedEntry在添加入口和添加入口结束时触发Webpack 5 中移除)。
buildModule、rebuildModule、finishRebuildingModule、failedModule、succeedModule在构建单个模块时触发。
finishModules在所有模块构建完成后触发。
优化阶段
优化阶段在 seal 函数中共有 12 个主要的处理过程,如下图所示:
每个过程都暴露了相应的 Hooks分别如下:
seal、needAdditionalSeal、unseal、afterSeal分别在 seal 函数的起始和结束的位置触发。
optimizeDependencies、afterOptimizeDependencies触发优化依赖的插件执行例如FlagDependencyUsagePlugin。
beforeChunks、afterChunks分别在生成 Chunks 的过程的前后触发。
optimize在生成 chunks 之后,开始执行优化处理的阶段触发。
optimizeModule、afterOptimizeModule在优化模块过程的前后触发。
optimizeChunks、afterOptimizeChunks在优化 Chunk 过程的前后触发,用于 Tree Shaking。
optimizeTree、afterOptimizeTree在优化模块和 Chunk 树过程的前后触发。
optimizeChunkModules、afterOptimizeChunkModules在优化 ChunkModules 的过程前后触发,例如 ModuleConcatenationPlugin利用这一 Hook 来做Scope Hoisting的优化。
shouldRecord、recordModules、recordChunks、recordHash在 shouldRecord 返回为 true 的情况下,依次触发 recordModules、recordChunks、recordHash。
reviveModules、beforeModuleIds、moduleIds、optimizeModuleIds、afterOptimizeModuleIds在生成模块 Id 过程的前后触发。
reviveChunks、beforeChunkIds、optimizeChunkIds、afterOptimizeChunkIds在生成 Chunk id 过程的前后触发。
beforeHash、afterHash在生成模块与 Chunk 的 hash 过程的前后触发。
beforeModuleAssets、moduleAsset在生成模块产物数据过程的前后触发。
shouldGenerateChunkAssets、beforeChunkAssets、chunkAsset在创建 Chunk 产物数据过程的前后触发。
additionalAssets、optimizeChunkAssets、afterOptimizeChunkAssets、optimizeAssets、afterOptimizeAssets在优化产物过程的前后触发例如在 TerserPlugin 的压缩代码插件的执行过程中,就用到了 optimizeChunkAssets。
代码实践:编写一个简单的统计插件
在了解了 Webpack 的工作流程后,下面我们进行一个简单的实践。
编写一个统计构建过程生命周期耗时的插件,这类插件会作为后续优化构建效率的准备工作。插件片段示例如下(完整代码参见 10_webpack_workflow
class SamplePlugin {
apply(compiler) {
var start = Date.now()
var statsHooks = ['environment', 'entryOption', 'afterPlugins', 'compile']
var statsAsyncHooks = [
'beforeRun',
'beforeCompile',
'make',
'afterCompile',
'emit',
'done',
]
statsHooks.forEach((hookName) => {
compiler.hooks[hookName].tap('Sample Plugin', () => {
console.log(`Compiler Hook ${hookName}, Time: ${Date.now() - start}ms`)
})
})
...
}
})
module.exports = SamplePlugin;
执行构建后,可以看到在控制台输出了相应的统计时间结果(这里的时间是从构建起始到各阶段 Hook 触发为止的耗时),如下图所示:
根据这样的输出结果,我们就可以分析项目里各阶段的耗时情况,再进行针对性地优化。这个统计插件将在后面几课的优化实践中运用。
除了这类自己编写的统计插件外Webpack 社区中也有一些较成熟的统计插件例如speed-measure-webpack-plugin等感兴趣的话你可以进一步了解。
总结
这一课时起,我们进入了 Webpack 构建优化的主题。在这节课中,我主要为你勾画了一个 Webpack 工作流程的轮廓,通过对三个源码文件的分析,让你对执行构建命令后的内部流程有一个基本概念。然后我们讨论了 Compiler 和 Compilation 工作流程中的生命周期 Hooks以及插件的基本工作方式。最后我们编写了一个简单的统计插件用于实践上面所讲的课程内容。
今天的课后思考题是:在今天介绍的 Compiler 和 Compilation 的各生命周期阶段里,通常耗时最长的分别是哪个阶段呢?可以结合自己所在的项目测试分析一下。

View File

@ -0,0 +1,176 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 编译提效:如何为 Webpack 编译阶段提速?
上一课我们聊了 Webpack 的基本工作流程,分析了其中几个主要源码文件的执行过程,并介绍了 Compiler 和 Compilation 两个核心模块中的生命周期 Hooks。
上节课后的思考题是,在 Compiler 和 Compilation 的工作流程里,最耗时的阶段分别是哪个。对于 Compiler 实例而言,耗时最长的显然是生成编译过程实例后的 make 阶段,在这个阶段里,会执行模块编译到优化的完整过程。而对于 Compilation 实例的工作流程来说,不同的项目和配置各有不同,但总体而言,编译模块和后续优化阶段的生成产物并压缩代码的过程都是比较耗时的。
从这个思考题的答案中你也可以发现,不同项目的构建,在整个流程的前期初始化阶段与最后的产物生成阶段的构建时间区别不大。真正影响整个构建效率的还是 Compilation 实例的处理过程,这一过程又可分为两个阶段:编译模块和优化处理。今天我们主要讨论第一个阶段:编译模块阶段的效率提升。
优化前的准备工作
在进入实际优化分析之前,首先需要进行两项准备工作:
准备基于时间的分析工具:我们需要一类插件,来帮助我们统计项目构建过程中在编译阶段的耗时情况,这类工具可以是上一课中我们尝试手写的,也可以是使用第三方的工具。例如 speed-measure-webpack-plugin。
准备基于产物内容的分析工具:从产物内容着手分析是另一个可行的方式,因为从中我们可以找到对产物包体积影响最大的包的构成,从而找到那些冗余的、可以被优化的依赖项。通常,减少这些冗余的依赖包模块,不仅能减小最后的包体积大小,也能提升构建模块时的效率。通常可以使用 webpack-bundle-analyzer 分析产物内容。
在准备好相应的分析工具后,接下来,就开始分析编译阶段的具体提效方向。编译模块阶段所耗的时间是从单个入口点开始,编译每个模块的时间的总和。要提升这一阶段的构建效率,大致可以分为三个方向(这一节课的代码示例参见 11_build_efficiency
减少执行编译的模块。
提升单个模块构建的速度。
并行构建以提升总体效率。
减少执行构建的模块
提升编译模块阶段效率的第一个方向就是减少执行编译的模块。显而易见,如果一个项目每次构建都需要编译 1000 个模块,但是通过分析后发现其中有 500 个不需要编译,显而易见,经过优化后,构建效率可以大幅提升。当然,前提是找到原本不需要进行构建的模块,下面我们就来逐一分析。
IgnorePlugin
有的依赖包,除了项目所需的模块内容外,还会附带一些多余的模块。典型的例子是 moment 这个包,一般情况下在构建时会自动引入其 locale 目录下的多国语言包,如下面的图片所示:
但对于大多数情况而言,项目中只需要引入本国语言包即可。而 Webpack 提供的 IgnorePlugin 即可在构建模块时直接剔除那些需要被排除的模块,从而提升构建模块的速度,并减少产物体积,如下面的图片所示。
除了 moment 包以外,其他一些带有国际化模块的依赖包,例如之前介绍 Mock 工具中提到的 Faker.js 等都可以应用这一优化方式。
按需引入类库模块
第二种典型的减少执行模块的方式是按需引入。这种方式一般适用于工具类库性质的依赖包的优化,典型例子是 lodash 依赖包。通常在项目里我们只用到了少数几个 lodash 的方法,但是构建时却发现构建时引入了整个依赖包,如下图所示:
要解决这个问题,效果最佳的方式是在导入声明时只导入依赖包内的特定模块,这样就可以大大减少构建时间,以及产物的体积,如下图所示。
除了在导入时声明特定模块之外,还可以使用 babel-plugin-lodash 或 babel-plugin-import 等插件达到同样的效果。
另外,有同学也许会想到 Tree Shaking这一特性也能减少产物包的体积但是这里有两点需要注意
Tree Shaking 需要相应导入的依赖包使用 ES6 模块化,而 lodash 还是基于 CommonJS ,需要替换为 lodash-es 才能生效。
相应的操作是在优化阶段进行的换句话说Tree Shaking 并不能减少模块编译阶段的构建时间。
DllPlugin
DllPlugin 是另一类减少构建模块的方式,它的核心思想是将项目依赖的框架等模块单独构建打包,与普通构建流程区分开。例如,原先一个依赖 React 与 react-dom 的文件,在构建时,会如下图般处理:
而在通过 DllPlugin 和 DllReferencePlugin 分别配置后的构建时间就变成如下图所示,由于构建时减少了最耗时的模块,构建效率瞬间提升十倍。
Externals
Webpack 配置中的 externals 和 DllPlugin 解决的是同一类问题:将依赖的框架等模块从构建过程中移除。它们的区别在于:
在 Webpack 的配置方面externals 更简单,而 DllPlugin 需要独立的配置文件。
DllPlugin 包含了依赖包的独立构建流程,而 externals 配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包。
externals 配置的依赖包需要单独指定依赖模块的加载方式全局对象、CommonJS、AMD 等。
在引用依赖包的子模块时DllPlugin 无须更改,而 externals 则会将子模块打入项目包中。
externals 的示例如下面两张图,可以看到经过 externals 配置后,构建速度有了很大提升。
提升单个模块构建的速度
提升编译阶段效率的第二个方向,是在保持构建模块数量不变的情况下,提升单个模块构建的速度。具体来说,是通过减少构建单个模块时的一些处理逻辑来提升速度。这个方向的优化主要有以下几种:
include/exclude
Webpack 加载器配置中的 include/exclude是常用的优化特定模块构建速度的方式之一。
include 的用途是只对符合条件的模块使用指定 Loader 进行转换处理。而 exclude 则相反,不对特定条件的模块使用该 Loader例如不使用 babel-loader 处理 node_modules 中的模块)。如下面两张图片所示。
这里有两点需要注意:
从上面的第二张图中可以看到jquery 和 lodash 的编译过程仍然花费了数百毫秒,说明通过 include/exclude 排除的模块,并非不进行编译,而是使用 Webpack 默认的 js 模块编译器进行编译(例如推断依赖包的模块类型,加上装饰代码等)。
在一个 loader 中的 include 与 exclude 配置存在冲突的情况下,优先使用 exclude 的配置,而忽略冲突的 include 部分的配置,具体可以参照示例代码中的 webpack.inexclude.config.js。
noParse
Webpack 配置中的 module.noParse 则是在上述 include/exclude 的基础上,进一步省略了使用默认 js 模块编译器进行编译的时间,如下面两张图片所示。
Source Map
Source Map 对于构建时间的影响在第三课中已经展开讨论过,这里再稍做总结:对于生产环境的代码构建而言,会根据项目实际情况判断是否开启 Source Map。在开启 Source Map 的情况下,优先选择与源文件分离的类型,例如 “source-map”。有条件也可以配合错误监控系统将 Source Map 的构建和使用在线下监控后台中进行,以提升普通构建部署流程的速度。
TypeScript 编译优化
Webpack 中编译 TS 有两种方式:使用 ts-loader 或使用 babel-loader。其中在使用 ts-loader 时,由于 ts-loader 默认在编译前进行类型检查,因此编译时间往往比较慢,如下面的图片所示。
通过加上配置项 transpileOnly: true可以在编译时忽略类型检查从而大大提升 TS 模块的编译速度,如下面的图片所示。
而 babel-loader 则需要单独安装 @babel/preset-typescript 来支持编译 TSBabel 7 之前的版本则还是需要使用 ts-loader。babel-loader 的编译效率与上述 ts-loader 优化后的效率相当,如下面的图片所示。
不过单独使用这一功能就丧失了 TS 中重要的类型检查功能,因此在许多脚手架中往往配合 ForkTsCheckerWebpackPlugin 一同使用。
Resolve
Webpack 中的 resolve 配置制定的是在构建时指定查找模块文件的规则,例如:
resolve.modules指定查找模块的目录范围。
resolve.extensions指定查找模块的文件类型范围。
resolve.mainFields指定查找模块的 package.json 中主文件的属性名。
resolve.symlinks指定在查找模块时是否处理软连接。
这些规则在处理每个模块时都会有所应用,因此尽管对小型项目的构建速度来说影响不大,但对于大型的模块众多的项目而言,这些配置的变化就可能产生客观的构建时长区别。例如下面的示例就展示了使用默认配置和增加了大量无效范围后,构建时长的变化情况:
并行构建以提升总体效率
第三个编译阶段提效的方向是使用并行的方式来提升构建的效率。并行构建的方案早在 Webpack 2 时代已经出现,随着目前最新稳定版本 Webpack 4 的发布,人们发现在一般项目的开发阶段和小型项目的各构建流程中已经用不到这种并发的思路了,因为在这些情况下,并发所需要的多进程管理与通信所带来的额外时间成本可能会超过使用工具带来的收益。但是在大中型项目的生产环境构建时,这类工具仍有发挥作用的空间。这里我们介绍两类并行构建的工具: HappyPack 与 thread-loader以及 parallel-webpack。
HappyPack 与 thread-loader
这两种工具的本质作用相同,都作用于模块编译的 Loader 上,用于在特定 Loader 的编译过程中以开启多进程的方式加速编译。HappyPack 诞生较早,而 thread-loader 参照它的效果实现了更符合 Webpack 中 Loader 的编写方式。下面就以 thread-loader 为例,来看下应用前后的构建时长对比,如下面的两张图所示。
parallel-webpack
并发构建的第二种场景是针对与多配置构建。Webpack 的配置文件可以是一个包含多个子配置对象的数组,在执行这类多配置构建时,默认串行执行,而通过 parallel-webpack就能实现相关配置的并行处理。从下图的示例中可以看到通过不同配置的并行构建构建时长缩短了 30%
总结
这节课我们整理了 Webpack 构建中编译模块阶段的构建效率优化方案。对于这一阶段的构建效率优化可以分为三个方向:以减少执行构建的模块数量为目的的方向、以提升单个模块构建速度为目的的方向,以及通过并行构建以提升整体构建效率的方向。每个方向都包含了若干解决工具和配置。
今天课后的思考题是:你的项目中是否都用到了这些解决方案呢?希望你结合课程的内容,和所开发的项目中用到的优化方案进行对比,查漏补缺。如果有这个主题方面其他新的解决方案,也欢迎在留言区讨论分享。

View File

@ -0,0 +1,238 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 打包提效:如何为 Webpack 打包阶段提速?
上节课我们聊了 Webpack 构建流程中第一阶段,也就是编译模块阶段的提效方案,这些方案可以归为三个不同的优化方向。不知道大家课后有没有对照分析自己在项目里用到了其中的哪些方案呢?
今天我们就来继续聊聊 Webpack 构建流程中的第二个阶段,也就是从代码优化到生成产物阶段的效率提升问题(这节课的示例代码参照 12_optimize_efficiency
准备分析工具
同上节课一样,在分析优化阶段的提效方案之前,我们还是需要先来准备一个分析统计时间的工具。但不同的是,在优化阶段对应的生命周期 Hooks 有很多(参照第 10 讲中的内容)。因此在编写统计插件时,我们要将需要统计的 Hooks 划分为不同区间,如下面的代码所示:
WebpackTimingPlugin.js:
...
const lifeHooks = [
{
name: 'optimizeDependencies',
start: 'optimizeDependencies',
end: 'afterOptimizeDependencies',
},
{ name: 'createChunks', start: 'beforeChunks', end: 'afterChunks' },
...
];
...
let startTime
compilation.hooks[start].tap(PluginName, () => {
startTime = Date.now()
})
compilation.hooks[end].tap(PluginName, () => {
const cost = Date.now() - startTime
console.log(`[Step ${name}] costs: ${chalk.red(cost)}ms`)
})
...
使用后的效果如下图所示:
通过这样的插件,我们可以分析目前项目中的效率瓶颈,从而进一步为选取优化方案及评估方案效果提供依据。
优化阶段效率提升的整体分析
在“第 10 课时|流程分解Webpack 的完整构建流程”中,我们提到了下面的这张图。如图所示,整个优化阶段可以细分为 12 个子任务,每个任务依次对数据进行一定的处理,并将结果传递给下一任务:
因此,这一阶段的优化也可以分为两个不同的方向:
针对某些任务,使用效率更高的工具或配置项,从而提升当前任务的工作效率。
提升特定任务的优化效果,以减少传递给下一任务的数据量,从而提升后续环节的工作效率。
以提升当前任务工作效率为目标的方案
一般在项目的优化阶段,主要耗时的任务有两个:一个是生成 ChunkAssets即根据 Chunk 信息生成 Chunk 的产物代码;另一个是优化 Assets即压缩 Chunk 产物代码。
第一个任务主要在 Webpack 引擎内部的模块中处理,相对而言优化手段较少,主要集中在利用缓存方面,具体将在下节课中讨论。而在压缩 Chunk 产物代码的过程中会用到一些第三方插件,选择不同的插件,以及插件中的不同配置都可能会对其中的效率产生影响。
这节课我们重点来看压缩代码的优化方案。
面向 JS 的压缩工具
Webpack 4 中内置了 TerserWebpackPlugin 作为默认的 JS 压缩工具,之前的版本则需要在项目配置中单独引入,早期主要使用的是 UglifyJSWebpackPlugin。这两个 Webpack 插件内部的压缩功能分别基于 Terser 和 UglifyJS。
从第三方的测试结果看,两者在压缩效率与质量方面差别不大,但 Terser 整体上略胜一筹。
从本节课示例代码的运行结果npm run build:jscomp来看如下面的表格所示在不带任何优化配置的情况下3 个测试文件的构建结果都是 Terser 效果更好。
Terser 和 UglifyJS 插件中的效率优化
Terser 原本是 Fork 自 uglify-es 的项目Fork 指从开源项目的某一版本分离出来成为独立的项目),其绝大部分的 API 和参数都与 uglify-es 和 uglify-js@3 兼容。因此,两者对应参数的作用与优化方式也基本相同,这里就以 Terser 为例来分析其中的优化方向。
在作为 Webpack 插件的 TerserWebpackPlugin 中,对执行效率产生影响的配置主要分为 3 个方面:
Cache 选项:默认开启,使用缓存能够极大程度上提升再次构建时的工作效率,这方面的细节我们将在下节课中展开讨论。
Parallel 选项:默认开启,并发选项在大多数情况下能够提升该插件的工作效率,但具体提升的程度则因项目而异。在小型项目中,多进程通信的额外消耗可能会抵消其带来的益处。
terserOptions 选项:即 Terser 工具中的 minify 选项集合。这些选项是对具体压缩处理过程产生影响的配置项。我们主要来看其中的compress和mangle选项不同选项的压缩结果如下面的代码所示
//源代码./src/example-terser-opts.js
function HelloWorld() {
const foo = '1234'
console.log(HelloWorld, foo)
}
HelloWorld()
//默认配置项compress={}, mangle=true的压缩后代码
function(e,t){!function e(){console.log(e,"1234")}()}});
//compress=false的压缩后代码
function(e,r){function t(){var e="1234";console.log(t,e)}t()}});
//mangle=false的压缩代码
function(module,exports){!function HelloWorld(){console.log(HelloWorld,"1234")}()}});
//compress=falsemangle=false的压缩后代码
function(module,exports){function HelloWorld(){var foo="1234";console.log(HelloWorld,foo)}HelloWorld()}});
从上面的例子中可以看到:
compress 参数的作用是执行特定的压缩策略,例如省略变量赋值的语句,从而将变量的值直接替换到引入变量的位置上,减小代码体积。而当 compress 参数为 false 时,这类压缩策略不再生效,示例代码压缩后的体积从 1.16KB 增加到 1.2KB,对压缩质量的影响有限。
mangle 参数的作用是对源代码中的变量与函数名称进行压缩,当参数为 false 时,示例代码压缩后的体积从 1.16KB 增加到 1.84KB,对代码压缩的效果影响非常大。
在了解了两个参数对压缩质量的影响之后,我们再来看下它们对效率的影响。以上面表格中的 example-antd 为例,我制作了下面的表格进行对比:
从结果中可以看到当compress参数为 false 时,压缩阶段的效率有明显提升,同时对压缩的质量影响较小。在需要对压缩阶段的效率进行优化的情况下,可以优先选择设置该参数。
面向 CSS 的压缩工具
CSS 同样有几种压缩工具可供选择OptimizeCSSAssetsPlugin在 Create-React-App 中使用、OptimizeCSSNanoPlugin在 VUE-CLI 中使用以及CSSMinimizerWebpackPlugin2020 年 Webpack 社区新发布的 CSS 压缩插件)。
这三个插件在压缩 CSS 代码功能方面,都默认基于 cssnano 实现,因此在压缩质量方面没有什么差别。
在压缩效率方面,首先值得一提的是最新发布的 CSSMinimizerWebpackPlugin它支持缓存和多进程这是另外两个工具不具备的。而在非缓存的普通压缩过程方面整体上 3 个工具相差不大,不同的参数结果略有不同,如下面的表格所示(下面结果为示例代码中 example-css 的执行构建结果)。
CSSMinimizerWebpackPlugin 中默认开启多进程选项 parallel但是在测试示例较小的情况下多进程的通信时间反而可能导致效率的降低。测试中关闭多进程选项后构建时间明显缩短。
从上面的表格中可以看到,三个插件的构建时间基本相近,在开启 sourceMap 的情况下 CSSMinimizerWebpackPlugin 的构建时间相对较长。但考虑到只有这一新发布的插件支持缓存和多进程等对项目构建效率影响明显的功能,即使在压缩 CSS 的时间较长的情况下,还是推荐使用它。
以提升后续环节工作效率为目标的方案
优化阶段的另一类优化方向是通过对本环节的处理减少后续环节处理内容以便提升后续环节的工作效率。我们列举两个案例Split Chunks分包 和 Tree Shaking摇树
Split Chunks
Split Chunks分包是指在 Chunk 生成之后,将原先以入口点来划分的 Chunks 根据一定的规则(例如异步引入或分离公共依赖等原则),分离出子 Chunk 的过程。
Split Chunks 有诸多优点例如有利于缓存命中下节课中会提到、有利于运行时的持久化文件缓存等。其中有一类情况能提升后续环节的工作效率即通过分包来抽离多个入口点引用的公共依赖。我们通过下面的代码示例npm run build:split来看一下。
./src/example-split1.js
import { slice } from 'lodash'
console.log('slice', slice([1]))
./src/example-split2.js
import { join } from 'lodash'
console.log('join', join([1], [2]))
./webpack.split.config.js
...
optimization: {
...
splitChunks: {
chunks: 'all'
}
}
...
在这个示例中,有两个入口文件引入了相同的依赖包 lodash在没有额外设置分包的情况下 lodash 被同时打入到两个产物文件中,在后续的压缩代码阶段耗时 1740ms。而在设置分包规则为 chunks:all 的情况下,通过分离公共依赖到单独的 Chunk使得在后续压缩代码阶段只需要压缩一次 lodash 的依赖包代码,从而减少了压缩时长,总耗时为 1036ms。通过下面两张图片也可以看出这样的变化。
这里起作用的是 Webpack 4 中内置的 SplitChunksPlugin该插件在 production 模式下默认启用。其默认的分包规则为 chunks: async作用是分离动态引入的模块 (import(’…‘)),在处理动态引入的模块时能够自动分离其中的公共依赖。
但是对于示例中多入口静态引用相同依赖包的情况,则不会处理分包。而设置为 chunks: all则能够将所有的依赖情况都进行分包处理从而减少了重复引入相同模块代码的情况。SplitChunksPlugin 的工作阶段是在optimizeChunks阶段Webpack 4 中是在 optimizeChunksAdvanced在 Webpack 5 中去掉了 basic 和 advanced合并为 optimizeChunks而压缩代码是在 optimizeChunkAssets 阶段,从而起到提升后续环节工作效率的作用。
Tree Shaking
Tree Shaking摇树是指在构建打包过程中移除那些引入但未被使用的无效代码Dead-code elimination。这种优化手段最早应用于在 Rollup 工具中,而在 Webpack 2 之后的版本中, Webpack 开始内置这一功能。下面我们先来看一下 Tree Shaking 的例子,如下面的表格所示:
可以看到引入不同的依赖包lodash vs lodash-es、不同的引入方式以及是否使用 babel 等,都会对 Tree Shaking 的效果产生影响。下面我们就来分析具体原因。
ES6 模块: 首先,只有 ES6 类型的模块才能进行Tree Shaking。因为 ES6 模块的依赖关系是确定的,因此可以进行不依赖运行时的静态分析,而 CommonJS 类型的模块则不能。因此CommonJS 类型的模块 lodash在无论哪种引入方式下都不能实现 Tree Shaking而需要依赖第三方提供的插件例如 babel-plugin-lodash 等)才能实现动态删除无效代码。而 ES6 风格的模块 lodash-es则可以进行 Tree Shaking 优化。
引入方式:以 default 方式引入的模块,无法被 Tree Shaking而引入单个导出对象的方式无论是使用 import * as xxx 的语法,还是 import {xxx} 的语法,都可以进行 Tree Shaking。
sideEffects在 Webpack 4 中,会根据依赖模块 package.json 中的 sideEffects 属性来确认对应的依赖包代码是否会产生副作用。只有 sideEffects 为 false 的依赖包(或不在 sideEffects 对应数组中的文件),才可以实现安全移除未使用代码的功能。在上面的例子中,如果我们查看 lodash-es 的 package.json 文件,可以看到其中包含了 “sideEffects”:false 的描述。此外,在 Webpack 配置的加载器规则和优化配置项中,分别有 rule.sideEffects默认为 false和 optimization.sideEffects默认为 true选项前者指代在要处理的模块中是否有副作用后者指代在优化过程中是否遵循依赖模块的副作用描述。尤其前者常用于对 CSS 文件模块开启副作用模式,以防止被移除。
Babel在 Babel 7 之前的babel-preset-env中modules 的默认选项为 commonjs因此在使用 babel 处理模块时,即使模块本身是 ES6 风格的,也会在转换过程中,因为被转换而导致无法在后续优化阶段应用 Tree Shaking。而在 Babel 7 之后的 @babel/preset-env 中modules 选项默认为 auto它的含义是对 ES6 风格的模块不做转换(等同于 modules: false而将其他类型的模块默认转换为 CommonJS 风格。因此我们会看到,后者即使经过 babel 处理,也能应用 Tree Shaking。
总结
这节课我们主要讨论了代码优化阶段效率提升的方向和方法。这一阶段的优化方向大致可分为两类:一类是以提升当前任务工作效率为目标的方案,这部分我们讨论了压缩 JS 时选择合适的压缩工具与配置优化项,以及压缩 CSS 时对优化工具的选择。另一类是以提升后续环节工作效率为目标的方案,这部分我们讨论了 splitChunks 的作用和配置项,以及应用 Tree Shaking 的一些注意事项。希望通过本节课的学习,帮助你加深对这一阶段 Webpack 处理逻辑的理解,也能够对其中的一些优化方式有更清晰的理解。
今天的课后思考题是:回忆 Tree Shaking 的触发条件有哪些?在自己所在的项目里观察试验一下,看看哪些依赖代码在构建时应用了 Tree Shaking 优化,是否存在应该生效但在打包结果中没有被正确移除的代码?

View File

@ -0,0 +1,222 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 缓存优化:那些基于缓存的优化方案
上节课的思考题是 Webpack 4 中 Tree Shaking 的触发条件有哪些?我们一起来回忆一下,要让引入的模块支持 Tree Shaking一般有 4 点需要注意:
引入的模块需要是 ES6 类型的CommonJS 类型的则不支持。
引入方式不能使用 default。
引用第三方依赖包的情况下,对应的 package.json 需要设置 sideEffects:false 来表明无副作用。
使用 Babel 的情况下,需要注意不同版本 Babel 对于模块化的预设不同。
在前面的两节课中,我们讨论了 Webpack 在编译和优化打包阶段的提效方向,以及各自对应的实践方法。除了这些针对具体处理过程的优化方法外,还有一个特定类型的优化方法没有聊到,就是利用缓存数据来加速构建过程的处理。这节课我们就将介绍它。
缓存优化的基本原理
在讲缓存优化的原理之前我们先来看下面的例子,如下面的代码和图片所示(本节课的完整示例代码参见 13_cache
./src/example-basic.js
import _ from 'lodash'
可以看到,在没有增加任何优化设置的情况下,初次构建时在 optimizeChunkAssets 阶段的耗时是 1000ms 左右,而再次构建时的耗时直接降到了 18ms几乎可以忽略不计。
这里的原因就在于Webpack 4 内置了压缩插件 TerserWebpackPlugin且默认开启了缓存参数。在初次构建的压缩代码过程中就将这一阶段的结果写入了缓存目录node_modules/.cache/terser-webpack-plugin/)中,当再次构建进行到压缩代码阶段时,即可对比读取已有缓存,如下面的代码所示(相关的代码逻辑在插件的源代码中可以看到)。
terser-webpack-plugin/src/index.js:
...
if (cache.isEnabled()) {
let taskResult;
try {
taskResult = await cache.get(task); //读取缓存
} catch (ignoreError) {
return enqueue(task); //缓存未命中情况下执行任务
}
task.callback(taskResult); //缓存命中情况下返回缓存结果
...
const enqueue = async (task) => {
let taskResult;
if (cache.isEnabled() && !taskResult.error) {
await cache.store(task, taskResult); //写入缓存
}
}
}
以上就是 TerserWebpackPlugin 插件中利用缓存的基本原理。事实上,在 Webpack 构建流程中还有许多处理过程支持使用缓存,下面我们就来梳理编译和优化打包阶段分别有哪些任务环境可以用到缓存。
编译阶段的缓存优化
编译过程的耗时点主要在使用不同加载器Loader来编译模块的过程。下面我们来分别看下几个典型 Loader 中的缓存处理:
Babel-loader
Babel-loader 是绝大部分项目中会使用到的 JS/JSX/TS 编译器。在 Babel-loader 中,与缓存相关的设置主要有:
cacheDirectory默认为 false即不开启缓存。当值为 true 时开启缓存并使用默认缓存目录(./node_modules/.cache/babel-loader/),也可以指定其他路径值作为缓存目录。
cacheIdentifier用于计算缓存标识符。默认使用 Babel 相关依赖包的版本、babelrc 配置文件的内容,以及环境变量等与模块内容一起参与计算缓存标识符。如果上述内容发生变化,即使模块内容不变,也不能命中缓存。
cacheCompression默认为 true将缓存内容压缩为 gz 包以减小缓存目录的体积。在设为 false 的情况下将跳过压缩和解压的过程,从而提升这一阶段的速度。
开启缓存选项前后的构建时长效果如图所示(示例中运行 npm run build:babel可以看到由于开启了 Babel 的缓存,再次构建的速度比初次构建时要快了许多。
Cache-loader
在编译过程中利用缓存的第二种方式是使用 Cache-loader。在使用时需要将 cache-loader 添加到对构建效率影响较大的 Loader如 babel-loader 等)之前,如下面的代码所示:
./webpack.cache.config.js
...
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
},
],
}
...
执行两次构建后可以发现,使用 cache-loader 后,比使用 babel-loader 的开启缓存选项后的构建时间更短,如下图所示:
主要原因是 babel-loader 中的缓存信息较少,而 cache-loader 中存储的Buffer 形式的数据处理效率更高。下面的示例代码,是 babel-loader 和 cache-loader 入口模块的缓存信息对比:
//babel-loader中的缓存数据
{"ast":null,"code":"import _ from 'lodash';","map":null,"metadata":{},"sourceType":"module"}
//cache-loader中的缓存数据
{"remainingRequest":"...lessons_fe_efficiency/13_cache/node_modules/babel-loader/lib/index.js!.../lessons_fe_efficiency/13_cache/src/example-basic.js","dependencies":[{"path":"...lessons_fe_efficiency/13_cache/src/example-basic.js","mtime":1599191174705},{"path":"...lessons_fe_efficiency/13_cache/node_modules/cache-loader/dist/cjs.js","mtime":499162500000},{"path":".../lessons_fe_efficiency/13_cache/node_modules/babel-loader/lib/index.js","mtime":499162500000}],"contextDependencies":[],"result":[{"type":"Buffer","data":"base64:aW1wb3J0IF8gZnJvbSAnbG9kYXNoJzs="},null]}
优化打包阶段的缓存优化
生成 ChunkAsset 时的缓存优化
在 Webpack 4 中,生成 ChunkAsset 过程中的缓存优化是受限制的:只有在 watch 模式下,且配置中开启 cache 时development 模式下自动开启)才能在这一阶段执行缓存的逻辑。这是因为,在 Webpack 4 中,缓存插件是基于内存的,只有在 watch 模式下才能在内存中获取到相应的缓存数据对象。而在 Webpack 5 中这一问题得到解决,具体的我们会在后续课程中再次展开。
代码压缩时的缓存优化
在上一课时中曾提到,在代码压缩阶段,对于 JS 的压缩TerserWebpackPlugin 和 UglifyJSPlugin 都是支持缓存设置的。而对于 CSS 的压缩,目前最新发布的 CSSMinimizerWebpackPlugin 支持且默认开启缓存,其他的插件如 OptimizeCSSAssetsPlugin 和 OptimizeCSSNanoPlugin 目前还不支持使用缓存。
TerserWebpackPlugin 插件的效果在本节课的开头部分我们已经演示过了,这里再来看一下 CSSMinimizerWebpackPlugin 的缓存效果对比,如下面的图片所示,开启该插件的缓存后,再次构建的时长降低到了初次构建的 1/4。
以上就是 Webpack 4 中编译与优化打包阶段可用的几种缓存方案。接下来我们再来看下在构建过程中使用缓存的一些注意点。
缓存的失效
尽管上面示例所显示的再次构建时间要比初次构建时间快很多,但前提是两次构建没有任何代码发生变化,也就是说,最佳效果是在缓存完全命中的情况下。而现实中,通常需要重新构建的原因是代码发生了变化。因此如何最大程度地让缓存命中,成为我们选择缓存方案后首先要考虑的事情。
缓存标识符发生变化导致的缓存失效
在上面介绍的支持缓存的 Loader 和插件中,会根据一些固定字段的值加上所处理的模块或 Chunk 的数据 hash 值来生成对应缓存的标识符,例如特定依赖包的版本、对应插件的配置项信息、环境变量等。一旦其中的值发生变化,对应缓存标识符就会发生改变。这也意味着对应工具中,所有之前的缓存都将失效。因此,通常情况下我们需要尽可能少地变更会影响到缓存标识符生成的字段。
其中尤其需要注意的是,在许多项目的集成构建环境中,特定依赖包由于安装时所生成的语义化版本,导致构建版本时常自动更新,并造成缓存失效。因此,建议大家还是在使用缓存时根据项目的构建使用场景来合理设置对应缓存标识符的计算属性,从而尽可能地减少因为标识符变化而导致缓存失效的情况。
编译阶段的缓存失效
编译阶段的执行时间由每个模块的编译时间相加而成。在开启缓存的情况下,代码发生变化的模块将被重新编译,但不影响它所依赖的及依赖它的其他模块,其他模块将继续使用缓存。因此,这一阶段不需要考虑缓存失效扩大化的问题。
优化打包阶段的缓存失效
优化打包阶段的缓存失效问题则需要引起注意。还是以课程开头的 example-basic 为例,在使用缓存快速构建后,当我们任意修改入口文件的代码后会发现,代码压缩阶段的时间再次变为和初次构建时相近,也就是说,这一 Chunk 的 Terser 插件的缓存完全失效了,如下面的图片所示。
之所以会出现这样的结果,是因为,尽管在模块编译阶段每个模块是单独执行编译的,但是当进入到代码压缩环节时,各模块已经被组织到了相关联的 Chunk 中。如上面的示例4 个模块最后只生成了一个 Chunk任何一个模块发生变化都会导致整个 Chunk 的内容发生变化,而使之前保存的缓存失效。
在知道了失效原因后,对应的优化思路也就显而易见了:尽可能地把那些不变的处理成本高昂的模块打入单独的 Chunk 中。这就涉及了 Webpack 中的分包配置——splitChunks。
使用 splitChunks 优化缓存利用率
构建分包的好处有许多比如合并通用依赖、提升构建缓存利用率、提升资源访问的缓存利用率、资源懒加载等我们只讨论其中提升构建缓存利用率的部分。在上面示例的基础上只要对设定稍加更改webpack.cache-miss.config.js就会看到即使变更了入口模块的代码也不会对压缩阶段的时间产生多少影响因为主要的依赖包已经分离为独立的 Chunk如下面的代码和图片所示
./webpack.cache-miss.config.js
...
optimization: {
splitChunks: {
chunks: 'all',
},
},
...
其他使用缓存的注意事项
CI/CD 中的缓存目录问题
在许多自动化集成的系统中项目的构建空间会在每次构建执行完毕后立即回收清理。在这种情况下默认的项目构建缓存目录node_mo dules/.cache将无法留存导致即使项目中开启了缓存设置也无法享受缓存的便利性反而因为需要写入缓存文件而浪费额外的时间。因此在集成化的平台中构建部署的项目如果需要使用缓存则需要根据对应平台的规范将缓存设置到公共缓存目录下。这类问题我们会在第三模块部署优化中再次展开。
缓存的清理
缓存的便利性本质在于用磁盘空间换取构建时间。对于一个大量使用缓存的项目,随着时间的流逝,缓存空间会不断增大。这在只有少数项目的个人电脑中还不是非常大的问题,但对于上述多项目的集成环境而言,则需要考虑对缓存区域的定期清理。
与产物的持久化缓存相区别
这节课我们没有谈到浏览器端加载资源的缓存问题,以及相对应的如何在 Webpack 中生成产物的持久化缓存方法(即那些你可能比较熟悉的 hash、chunkhash、contenthash因为这一部分知识所影响的是项目访问的性能而对构建的效率没有影响。希望你在学习时清楚地区分这两者的区别。
总结
今天我们聊了 Webpack 常规构建效率优化的第三个方面缓存优化主题的内容。Webpack 的构建缓存优化分为两个阶段:编译阶段的针对 Loader 的缓存优化,以及优化打包阶段的针对压缩代码过程的缓存优化。除了了解这些优化的工具和设置外,在使用缓存时还需要额外注意如何减少缓存的失效。此外,针对不同的构建环境,还需要考虑到缓存目录的留存与清理等问题。
这节课的课后思考题是:课程中介绍的几种支持缓存的工具在设定上有哪些相似的功能选项?建议你在课后对它们做一个整理对比,以便加深印象。

View File

@ -0,0 +1,227 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 增量构建Webpack 中的增量构建
开始课程前我先来解答上一节课的思考题课程中介绍的几种支持缓存的插件TerserWebpackPluginCSSMinimizerWebpackPlugin和 Loaderbabel-loadercache-loader在缓存方面有哪些相同的配置项呢
通过对比不难发现,这些工具通常至少包含两个配置项:第一项用于指定是否开启缓存,以及指定缓存目录(值为 true 时使用默认目录,指定目录时也表示开启),配置名称通常是 cache 或 cacheDirectory第二项用于指定缓存标识符的计算参数通常默认值是一个包含多维度参数的对象例如这个工具模块的版本号、配置项对象、文件路径和内容等。这个配置项是为了确保缓存使用的安全性防止当源代码不变但相关构建参数发生变化时对旧缓存的误用。
下面开始本节课的学习。曾经有同事问我一个问题:为什么我只改了一行代码,却需要花 5 分钟才能构建完成?
你可能也有同样的疑问,但经过前面几节关于 Webpack 构建原理和优化的课程后,相信已经可以解答。尽管只改动了一行代码,但是在执行构建时,要完整执行所有模块的编译、优化和生成产物的处理过程,而不是只需要处理所改动的文件。大多数情况下,我们能做的是像前面几节课中讨论的那样,通过各种优化方案提升整体构建的效率。
但是只编译打包所改动的文件真的不能实现吗?这节课我们就来讨论这个话题(课程里完整的示例代码参见 14_incremental_build
Webpack 中的增量构建
上述只构建改动文件的处理过程在 Webpack 中是实际存在的,你可能也很熟悉,那就是在开启 devServer的时候当我们执行 webpack-dev-server 命令后Webpack 会进行一次初始化的构建构建完成后启动服务并进入到等待更新的状态。当本地文件有变更时Webpack 几乎瞬间将变更的文件进行编译,并将编译后的代码内容推送到浏览器端。你会发现,这个文件变更后的处理过程就符合上面所说的只编译打包改动的文件的操作,这就称为“增量构建”。我们通过示例代码进行验证(*npm run dev*),如下面的图片:
可以看到,在开发服务模式下,初次构建编译了 47 个模块,完整的构建时间为 3306ms。当我们改动其中一个源码文件后日志显示 Webpack 只再次构建了这一个模块因此再次构建的时间非常短24ms。那么为什么在开发服务模式下可以实现增量构建的效果而在生产环境下不行呢下面我们来分析影响结果的因素。
增量构建的影响因素
watch 配置
在上面的增量构建过程中,第一个想到的就是需要监控文件的变化。显然,只有得知变更的是哪个文件后,才能进行后续的针对性处理。要实现这一点也很简单,在“第 2 课时|界面调试:热更新技术如何开着飞机修引擎?”中已经介绍过,在 Webpack 中启用 watch 配置即可,此外在使用 devServer 的情况下,该选项会默认开启。那么,如果在生产模式下开启 watch 配置,是不是再次构建时,就会按增量的方式执行呢?我们仍然通过示例验证(*npm run build:watch*),如下面的图片所示:
从结果中可以发现,在生产模式下开启 watch 配置后,相比初次构建,再次构建所编译的模块数量并未减少,即使只改动了一个文件,也仍然会对所有模块进行编译。因此可以得出结论,在生产环境下只开启 watch 配置后的再次构建并不能实现增量构建。
cache 配置
仔细查阅 Webpack 的配置项文档,会在菜单最下方的“其他选项”一栏中找到 cache 选项(需要注意的是我们查阅的是 Webpack 4 版本的文档Webpack 5 中这一选项会有大的改变会在下一节课中展开讨论。这一选项的值有两种类型布尔值和对象类型。一般情况下默认为false即不使用缓存但在开发模式开启 watch 配置的情况下cache 的默认值变更为true。此外如果 cache 传值为对象类型,则表示使用该对象来作为缓存对象,这往往用于多个编译器 compiler 的调用情况。
下面我们就来看一下在生产模式下如果watch 和 cache 都为 true结果会如何npm run build:watch-cache如下面的图片所示
正如我们所期望的,再次构建时,在编译模块阶段只对有变化的文件进行了重新编译,实现了增量编译的效果。
但是美中不足的是,在优化阶段压缩代码时仍然耗费了较多的时间。这一点很容易理解:
体积最大的 react、react-dom 等模块和入口模块打入了同一个 Chunk 中,即使修改的模块是单独分离的 bar.js但它的产物名称的变化仍然需要反映在入口 Chunk 的 runtime 模块中。因此入口 Chunk 也需要跟着重新压缩而无法复用压缩缓存数据。根据前面几节课的知识点,我们对配置再做一些优化,将 vendor 分离后再来看看效果,如下面的图片所示:
可以看到通过上面这一系列的配置后watch + cache在生产模式下最终呈现出了我们期望的增量构建效果有文件发生变化时会自动编译变更的模块并只对该模块影响到的少量 Chunk 进行优化并更新产物文件版本,而其他产物文件则保持之前的版本。如此,整个构建过程的速度大大提升。
增量构建的实现原理
为什么在配置项中需要同时启用 watch 和 cache 配置才能获得增量构建的效果呢?接下来我们从源码层面分析。
watch 配置的作用
watch 配置的具体逻辑在 Webpack 的 Watching.js 中。查看源码可以看到,在它构建相关的 _go 方法中,执行的依然是 compiler实例的 compile 方法,这一点与普通构建流程并无区别。真正的区别在于,在 watch 模式下,构建完成后并不自动退出,因此构建上下文的对象(包括前一次构建后的缓存数据对象)都可以保留在内存中,并在 rebuild 时重复使用,如下面的代码所示:
lib/Watching.js
...
_go() {
...
this.compiler.hooks.watchRun.callAsync(this.compiler, err => {
const onCompiled = (err, compilation) => {
...
}
this.compiler.compile(onCompiled);
}
}
cache 配置的作用
cache 配置的源码逻辑主要涉及两个文件CachePlugin.js 和 Compilation.js。其中 CachePlugin.js 的核心作用是将该插件实例的 cache 属性传入 compilation 实例中,如下面的代码所示:
lib/CachePlugin.js
...
compiler.hooks.thisCompilation.tap("CachePlugin", compilation => {
compilation.cache = cache;
...
}
而在 Compilation.js 中,运用 cache 的地方有两处:
在编译阶段添加模块时若命中缓存module则直接跳过该模块的编译过程与 cache-loader 等作用于加载器的缓存不同,此处的缓存可直接跳过 Webpack 内置的编译阶段)。
在创建 Chunk 产物代码阶段若命中缓存Chunk则直接跳过该 Chunk 的产物代码生成过程。
如下面的代码所示:
lib/Compilation.js
...
addModule(module, cacheGroup) {
...
if (this.cache && this.cache[cacheName]) {
const cacheModule = this.cache[cacheName];
...
//缓存模块存在情况下判断是否需要rebuild
rebuild = ...
if (!rebuild) {
...
//无须rebuild情况下返回cacheModule并标记build:false
return {
module: cacheModule,
issuer: true,
build: false,
dependencies: true
}
}
...
}
if (this.cache) {
this.cache[cacheName] = module;
}
...
//无缓存或需要rebuild情况下返回module并标记build:true
return {
module: module,
issuer: true,
build: true,
dependencies: true
};
}
...
createChunkAssets() {
...
if ( this.cache && this.cache[cacheName] && this.cache[cacheName].hash === usedHash ) {
source = this.cache[cacheName].source;
} else {
source = fileManifest.render();
...
}
}
以上就是 Webpack 4 中 watch 和 cache 配置的作用原理。通过 Webpack 内置的 cache 插件,将整个构建中相对耗时的两个内部处理环节——编译模块和生成产物,进行缓存的读写处理,从而实现增量构建处理。那么我们是不是就可以在生产环境下直接使用这个方案呢?
生产环境下使用增量构建的阻碍
增量构建之所以快是因为将构建所需的数据项目文件、node_modules 中的文件数据、历史构建后的缓存数据等)都保留在内存中。在 watch 模式下保留着构建使用的 Node 进程,使得下一次构建时可以直接读取内存中的数据。
而生产环境下的构建通常在集成部署系统中进行。对于管理多项目的构建系统而言,构建过程是任务式的:任务结束后即结束进程并回收系统资源。对于这样的系统而言,增量构建所需的保留进程与长时间占用内存,通常都是不可接受的。
因此,基于内存的缓存数据注定无法运用到生产环境中。要想在生产环境下提升构建速度,首要条件是将缓存写入到文件系统中。只有将文件系统中的缓存数据持久化,才能脱离对保持进程的依赖,你只需要在每次构建时将缓存数据读取到内存中进行处理即可。事实上,这也是上一课时中讲到的那些 Loader 与插件中的缓存数据的存储方式。
遗憾的是Webpack 4 中的 cache 配置只支持基于内存的缓存,并不支持文件系统的缓存。因此,我们只能通过上节课讲到的一些支持缓存的第三方处理插件将局部的构建环节应用“增量处理”。
不过好消息是 Webpack 5 中正式支持基于文件系统的持久化缓存Persistent Cache。我们会在下一课时详细讨论包括这一特性在内的 Webpack 5 中的优化点。
总结
这节课我们主要讨论了构建处理的一种理想情况:增量构建。增量构建在每次执行构建时,只编译处理内容有修改的少量文件,从而极大地提升构建效率。
在 Webpack 4 中有两个配置项与增量构建相关watch 和 cache。当我们启用开发服务器时这两个选项都是默认启用的因此可以在开发模式下体验到增量构建带来的速度提升。
从内部原理的角度分析watch 的作用是保留进程,使得初次构建后的数据对象能够在再次构建时复用。而 cache 的作用则体现在构建过程中,在添加模块与生成产物代码时可以利用 cache 对象进行相应阶段结果数据的读写。显然,这种基于内存的缓存方式无法在生产环境下广泛使用。
今天的课后思考题是:在启用增量构建的情况下有时候可能还会遇到 rebuild 很慢的情况,试着分析原因。

View File

@ -0,0 +1,244 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 版本特性Webpack 5 中的优化细节
开始课程前,我们先来解答上一节课的思考题:为什么在开启增量构建后,有时候 rebuild 还是会很慢呢我们可以从两方面来找原因。首先Webpack 4 中的增量构建只运用到了新增模块与生成 Chunk 产物阶段,其他处理过程(如代码压缩)仍需要通过其他方式进行优化,例如分包和压缩插件的缓存。其次,过程中的一些处理会额外增加构建时间,例如生成 Source Map 等。因此还是需要通过统计各阶段的执行时间来具体问题具体分析。
然后开始这节课的学习。在上节课里,我们讨论了 Webpack 4 中增量构建的原理,也分析了为什么在生产环境下难以使用增量构建,其中最主要的一点是 Webpack 4 中没有基于文件系统的持久化缓存。这个问题在 Webpack 5 中得到了解决,这节课我们就来看看 Webpack 5 有哪些新的功能特性。
Webpack 5 中的效率优化点
Webpack 5 中的变化有很多,完整的功能变更清单参见官方文档,这里我们介绍其中与构建效率相关的几个主要功能点:
Persistent Caching
Tree Shaking
Logs
Persistent Caching
首先我们通过示例来看下 Webpack 5 中缓存方面的变化。
持久化缓存的示例
如下面的代码和图片所示:
./webpack.cache.config.js
...
module.exports = {
...
cache: {
type: 'filesystem',
cacheLocation: path.resolve(__dirname, '.appcache'),
buildDependencies: {
config: [__filename],
},
},
...
}
可以看到,初次构建完整花费了 3282ms而在不修改代码进行再次构建的情况下只花费了不到原先时间的 1/10。在修改代码文件的新情况下也只花费了 628ms多花费的时间体现在构建被修改的文件的编译上这就实现了上一课时所寻求的生产环境下的增量构建。
Cache 基本配置
在 Webpack 4 中cache 只是单个属性的配置,所对应的赋值为 true 或 false用来代表是否启用缓存或者赋值为对象来表示在构建中使用的缓存对象。而在 Webpack 5 中cache 配置除了原本的 true 和 false 外,还增加了许多子配置项,例如:
cache.type缓存类型。值为 memoryfilesystem分别代表基于内存的临时缓存以及基于文件系统的持久化缓存。在选择 filesystem 的情况下,下面介绍的其他属性生效。
cache.cacheDirectory缓存目录。默认目录为 node_modules/.cache/webpack。
cache.name缓存名称。同时也是 cacheDirectory 中的子目录命名,默认值为 Webpack 的 \({config.name}-\){config.mode}。
cache.cacheLocation缓存真正的存放地址。默认使用的是上述两个属性的组合path.resolve(cache.cacheDirectory, cache.name)。该属性在赋值情况下将忽略上面的 cacheDirectory 和 name 属性。
单个模块的缓存失效
Webpack 5 会跟踪每个模块的依赖项fileDependencies、contextDependencies、missingDependencies。当模块本身或其依赖项发生变更时Webpack 能找到所有受影响的模块,并重新进行构建处理。
这里需要注意的是,对于 node_modules 中的第三方依赖包中的模块出于性能考虑Webpack 不会跟踪具体模块文件的内容和修改时间而是依据依赖包里package.json 的 name 和 version 字段来判断模块是否发生变更。因此,单纯修改 node_modules 中的模块内容,在构建时不会触发缓存的失效。
全局的缓存失效
当模块代码没有发生变化,但是构建处理过程本身发生变化时(例如升级了 Webpack 版本、修改了配置文件、改变了环境变量等),也可能对构建后的产物代码产生影响。因此在这种情况下不能复用之前缓存的数据,而需要让全局缓存失效,重新构建并生成新的缓存。在 Webpack 5 中共提供了 3 种不同维度的全局缓存失效配置。
buildDependencies
第一种配置是cache.buildDependencies用于指定可能对构建过程产生影响的依赖项。
它的默认选项是{defaultWebpack: [“webpack/lib”]}。这一选项的含义是,当 node_modules 中的 Webpack 或 Webpack 的依赖项(例如 watchpack 等)发生变化时,当前的构建缓存即失效。
上述选项是默认内置的,无须写在项目配置文件中。配置文件中的 buildDenpendencies 还支持增加另一种选项 {config: [__filename]},它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。
version
第二种配置是 cache.version。当配置文件和代码都没有发生变化但是构建的外部依赖如环境变量发生变化时预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。
name
缓存的名称除了作为默认的缓存目录下的子目录名称外,也起到区分缓存数据的作用。例如,可以传入 cache: {name: process.env.NODE_ENV}。这里有两点需要补充说明:
name 的特殊性:与 version 或 buildDependencies 等配置不同name 在默认情况下是作为缓存的子目录名称存在的,因此可以利用 name保留多套缓存。在 name 切换时,若已存在同名称的缓存,则可以复用之前的缓存。与之相比,当其他全局配置发生变化时,会直接将之前的缓存失效,即使切换回之前已缓存过的设置,也会当作无缓存处理。
当 cacheLocation 配置存在时,将忽略 name 的缓存目录功能,上述多套缓存复用的功能也将失效。
其他
除了上述介绍的配置项外cache 还支持其他属性managedPath、hashAlgorithm、store、idleTimeout 等,具体功能可以通过官方文档进行查询。
此外,在 Webpack 4 中,部分插件是默认启用缓存功能的(例如压缩代码的 Terser 插件等),项目在生产环境下构建时,可能无意识地享受缓存带来的效率提升,但是在 Webpack 5 中则不行。无论是否设置 cache 配置Webpack 5 都将忽略各插件的缓存设置(例如 TerserWebpackPlugin而由引擎自身提供构建各环节的缓存读写逻辑。因此项目在迁移到 Webpack 5 时都需要通过上面介绍的 cache 属性来单独配置缓存。
Tree Shaking
Webpack 5 中的另一项优化体现在 Tree Shaking 功能方面。在之前课程中介绍过Webpack 4 中的 Tree Shaking 功能在使用上存在限制:只支持 ES6 类型的模块代码分析,且需要相应的依赖包或需要函数声明为无副作用等。这使得在实际项目构建过程中 Tree Shaking 的优化效果往往不尽如人意。而这一问题在 Webpack 5 中得到了不少改善。
Nested Tree Shaking
Webpack 5 增加了对嵌套模块的导出跟踪功能,能够找到那些嵌套在最内层而未被使用的模块属性。例如下面的示例代码,在构建后的结果代码中只包含了引用的内部模块的一个属性,而忽略了不被引用的内部模块和中间模块的其他属性:
//./src/inner-module.js
export const a = 'inner_a'
export const b = 'inner_b'
//.src/nested-module.js
import * as inner from './inner-module'
const nested = 'nested'
export { inner, nested }
//./src/example-tree-nested.js
import * as nested from './nested-module'
console.log(nested.inner.a)
//./dist/tree-nest.js
(()=>{"use strict";console.log("inner_a")})();
Inner Module Tree Shaking
除了上面对嵌套引用模块的依赖分析优化外Webpack 5 中还增加了分析模块中导出项与导入项的依赖关系的功能。通过 optimization.innerGraph生产环境下默认开启选项Webpack 5 可以分析特定类型导出项中对导入项的依赖关系,从而找到更多未被使用的导入模块并加以移除。例如下面的示例代码:
//./src/inner-module.js
export const a = 'inner_a'
export const b = 'inner_b'
export const c = 'inner_c'
//./src/example-tree-nested.js 同上面示例
//.src/nested-module.js
...
const useB = function () {
return inner.b
}
export const usingB = function () {
return useB()
}
//./dist/tree-nest.js (默认optimization.innerGraph = true)
... const t="inner_a",n="inner_b"} ...
//./dist/tree-nest.js (optimization.innerGraph = false)
... const t="inner_a"} ...
在 nested-module.js 中新增了导出项 usingB该导出项间接依赖导入项 inner.b而这一导出项在入口模块中并未使用。在默认情况下构建完成后只保留真正被使用的 inner.a。但是如果将优化项 innerGraph 关闭(且需要同时设置 concatenateModules:false构建后会发现间接引用的导出项没有被移除该导出项间接引用的 inner.b 也被保留到了产物代码中。
CommonJS Tree Shaking
Webpack 5 中增加了对一些 CommonJS 风格模块代码的静态分析功功能:
支持 exports.xxx、this.exports.xxx、module.exports.xxx 语法的导出分析。
支持 object.defineProperty(exports, “xxxx”, …) 语法的导出分析。
支持 require(xxxx).xxx 语法的导入分析。
例如下面的代码:
//./src/commonjs-module.js
exports.a = 11
this.exports.b = 22
module.exports.c = 33
console.log('module')
//./src/example-tree-commonjs.js
const a = require('./commonjs-module').a
console.log(a)
//./dist/tree-commonjs.js
()=>{var o={263:function(o,r){r.a=11,console.log("module")}}...
可以看到产物代码中只有被引入的属性 a 和 console 语句,而其他两个导出属性 b 和 c 已经在产物中被排除了。
Logs
第三个要提到的 Webpack 5 的效率优化点是,它增加了许多内部处理过程的日志,可以通过 stats.logging 来访问。下面两张图是使用相同配置*stats: {logging: “verbose”}*的情况下Webpack 4 和 Webpack 5 构建输出的日志:
可以看到Webpack 5 构建输出的日志要丰富完整得多。通过这些日志能够很好地反映构建各阶段的处理过程、耗费时间,以及缓存使用的情况。在大多数情况下,它已经能够代替之前人工编写的统计插件功能了。
其他功能优化项
除了上面介绍的和构建效率相关的几项变化外Webpack 5 中还有许多大大小小的功能变化,例如新增了改变微前端构建运行流程的 Module Federation 和对产物代码进行优化处理的 Runtime Modules优化了处理模块的工作队列在生命周期 Hooks 中增加了 stage 选项等。感兴趣的话,你可以通过文章顶部的文档链接或官方网站来进一步了解。
总结
在本节课上线后不久Webpack 5 的稳定版本将对外发布2020 年 10 月 10 日)。希望这节课能让你对它有一个初步的印象。
本节课我们主要了解了 Webpack 最新版本与构建效率相关的几个优化功能点,其中最重要的就是 Webpack 5 中引入的持久化缓存的特性。在这个部分我们讨论了如何开启和定制持久化缓存以及通过哪些方式可以让缓存主动失效以确保在项目里可以安全地享受缓存带来的效率提升。此外Webpack 5 中对于 Tree Shaking 的优化也能帮助我们更好地优化项目依赖,减小构建产物的体积。
本节课的课后思考题是:结合今天所讲的持久化缓存和日志统计,分析一下 Webpack 5 中都有哪些阶段使用到了缓存?
Webpack 构建效率优化的系列至此就告一段落了,下节课我们来介绍构建效率篇的最后一个主题:无包构建。

View File

@ -0,0 +1,228 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 无包构建:盘点那些 No-bundle 的构建方案
上节课我们讨论了 Webpack 的最新版本 Webpack 5 所带来的提效新功能。思考题是 Webpack 5 中的持久化缓存究竟会影响哪些构建环节呢?
通过对 compiler.cache.hook.get 的追踪不难发现:持久化缓存一共影响下面这些环节与内置的插件:
编译模块ResolverCachePlugin、Compilation/modules。
优化模块FlagDependencyExportsPlugin、ModuleConcatenationPlugin。
生成代码Compilation/codeGeneration、Compilation/assets。
优化产物TerserWebpackPlugin、RealContentHashPlugin。
正是通过这样多环节的缓存读写控制,才打造出 Webpack 5 高效的持久化缓存功能。
在之前的课程里我们详细分解了 Webpack 构建工具的效率优化方案这节课我们来聊一聊今年比较火的另一种构建工具思路无包构建No-Bundle/Unbundle
什么是无包构建
什么是无包构建呢?这是一个与基于模块化打包的构建方案相对的概念。
在“ 第 9 课时|构建总览:前端构建工具的演进”中谈到过,目前主流的构建工具,例如 Webpack、Rollup 等都是基于一个或多个入口点模块,通过依赖分析将有依赖关系的模块打包到一起,最后形成少数几个产物代码包,因此这些工具也被称为打包工具。只不过,这些工具的构建过程除了打包外,还包括了模块编译和代码优化等,因此称为打包式构建工具或许更恰当。
而无包构建是指这样一类构建方式:在构建时只需处理模块的编译而无须打包,把模块间的依赖关系完全交给浏览器来处理。浏览器会加载入口模块,分析依赖后,再通过网络请求加载被依赖的模块。通过这样的方式简化构建时的处理过程,提升构建效率。
这种通过浏览器原生的模块进行解析的方式又称为 Native-ESMNative ES Module。下面我们就通过一个简单示例来展示这种基于浏览器的模块加载过程16_nobundle/simple-esm如下面的代码和图片所示
//./src/index.html
...
<script type="module" src="./modules/foo.js"></script>
...
//.src/modules/foo.js
import { bar } from './bar.js'
import { appendHTML } from './common.js'
...
import('https://cdn.jsdelivr.net/npm/[email protected]/slice.js').then((module) => {...})
从示例中可以看到,在没有任何构建工具处理的情况下,在页面中引入带有 type=“module” 属性的 script浏览器就会在加载入口模块时依次加载了所有被依赖的模块。下面我们就来深入了解一下这种基于浏览器加载 JS 模块的技术的细节。
基于浏览器的 JS 模块加载功能
从 caniuse 网站中可以看到,目前大部分主流的浏览器都已支持 JavaScript modules 这一特性,如下图所示:
[图片来源https://caniuse.com/es6-module]
我们来总结这种加载方式的注意点。
HTML 中的 Script 引用
入口模块文件在页面中引用时需要带上type=“module”属性。对应的存在 type=“nomodule”即支持 ES Module 的现代浏览器,它会忽略 type=“nomodule” 属性的 script因此可以用作旧浏览器中的降级方案。
带有 type=“module” 属性的 script在浏览器中通过 defer 的方式异步执行(异步下载,不阻塞 HTML顺次执行即使是行内的 script 代码也遵循这一原则(而普通的行内 script 代码则忽略 defer 属性)。
带有 type=“module” 属性且带有async属性的 script在浏览器中通过 async 的方式异步执行(异步下载,不阻塞 HTML按该模块和所依赖的模块下载完成的先后顺序执行无视 DOM 中的加载顺序),即使是行内的 script 代码,也遵循这一原则(而普通的行内 script 代码则忽略 async 属性)。
即使多次加载相同模块,也只会执行一次。
模块内依赖的引用
只能使用 import … from ‘…’ 的 ES6 风格的模块导入方式,或者使用 import(…).then(…) 的 ES6 动态导入方式,不支持其他模块化规范的引用方式(例如 require、define 等)。
导入的模块只支持使用相对路径(’/xxx, ./xxx, ../xxx和 URL 方式https://xxx, http://xxx进行引用不支持直接使用包名开头的方式xxxx, xxx/xxx
只支持引用MIME Type为 text/javascript 方式的模块,不支持其他类型文件的加载(例如 CSS 等)。
为什么需要构建工具
从上面的技术细节中我们会发现,对于一个普通的项目而言,要使用这种加载方案仍然有几个主要问题:
许多其他类型的文件需要编译处理为 ES6 模块才能被浏览器正常加载JSX、Vue、TS、CSS、Image 等)。
许多第三方依赖包在通过第三方 URL 引用时,不仅过程烦琐,而且往往难以进行灵活的版本控制与更新,因此需要合适的方式来解决引用路径的问题。
对于现实中的项目开发而言,一些便利的辅助开发技术,例如热更新等还是需要由构建工具来提供。
下面,我们分析 Vite 和 Snowpack 这两个有代表性的构建工具是如何解决上面的问题的。
Vite
Vite 是 Vue 框架的作者尤雨溪最新推出的基于 Native-ESM 的 Web 构建工具。它在开发环境下基于 Native-ESM 处理构建过程,只编译不打包,在生产环境下则基于 Rollup 打包。我们还是先通过 Vite 的官方示例来观察它的使用效果,如下面的代码和图片所示(示例代码参见 example-vite
npm init vite-app example-vite
cd example-vite
npm install
npm run dev
可以看到,运行示例代码后,在浏览器中只引入了 src/main.js 这一个入口模块,但是在网络面板中却依次加载了若干依赖模块,包括外部模块 vue 和 css。依赖图如下
可以看到,经过 Vite 处理后,浏览器中加载的模块与源代码中导入的模块相比发生了变化,这些变化包括对外部依赖包的处理,对 vue 文件的处理,对 css 文件的处理等。下面我们就来逐个分析其中的变化。
对导入模块的解析
对 HTML 文件的预处理
当启动 Vite 时,会通过 serverPluginHtml.ts 注入 /vite/client 运行时的依赖模块,该模块用于处理热更新,以及提供更新 CSS 的方法 updateStyle。
对外部依赖包的解析
首先是对不带路径前缀的外部依赖包也称为Bare Modules的解析例如上图中在示例源代码中导入了 vue 模块,但是在浏览器的网络请求中变为了请求 /@module/vue
这个解析过程在 Vite 中主要通过三个文件来处理:
resolver.ts 负责找到对应在 node_modules 中的真实依赖包代码Vite 会在启动服务时对项目 package.json 中的 dependencies 做预处理读取并存入缓存目录 node_modules/.vite_opt_cache 中)。
serverPluginModuleRewrite.ts 负责把源码中的 bare modules 加上 /@module/ 前缀。
serverPluginModuleResolve.ts 负责解析加上前缀后的模块。
对 Vue文件的解析
对 Vue 文件的解析是通过 serverPluginVue.ts 处理的,分离出 Vue 代码中的 script/template/style 代码片段,并分别转换为 JS 模块,然后将 template/style 模块的 import写到script 模块代码的头部。因此在浏览器访问时,一个 Vue 源代码文件会分裂为 2~3 的关联请求(例如上面的 /src/App.vue 和 /src/App.vue?type=template如果 App.vue 中包含<style> 则会产生第 3 个请求 /src/App.vue?type=style
对 CSS 文件的解析
对 CSS 文件的解析是通过 serverPluginCSS.ts 处理的,解析过程主要是将 CSS 文件的内容转换为下面的 JS 代码模块,其中的 updateStyle 由注入 HTML 中的 /vite/client 模块提供,如下面的代码所示:
import { updateStyle } from "/vite/client"
const css = "..."
updateStyle("\"...\"", css) // id, cssContent
export default css
以上就是示例代码中主要文件类型的基本解析逻辑可以看到Vite 正是通过这些解析器来解决不同类型文件以 JS 模块的方式在浏览器中加载的问题。在 Vite 源码中还包含了其他更多文件类型的解析器,例如 JSON、TS、SASS 等,这里就不一一列举了,感兴趣的话,你可以进一步查阅官方文档。
Vite 中的其他辅助功能
除了提供这些解析器的能力外Vite 还提供了其他便捷的构建功能,大致整理如下:
多框架:除了在默认的 Vue 中使用外,还支持在 React 和 Preact 项目中使用。工具默认提供了 Vue、React 和 Preact 对应的脚手架模板。
热更新HMR默认提供的 3 种框架的脚手架模板中都内置了 HMR 功能,同时也提供了 HMR 的 API 供第三方插件或项目代码使用。
自定义配置文件:支持使用自定义配置文件来细化构建配置,配置项功能参考 config.ts。
HTTPS 与 HTTP/2支持使用 https 启动参数来开启使用 HTTPS 和 HTTP/2 协议的开发服务器。
服务代理:在自定义配置中支持配置代理,将部分请求代理到第三方服务。
模式与环境变量:支持通过 mode 来指定构建模式为 development 或 production。相应模式下自动读取 dotenv 类型的环境变量配置文件(例如 .env.production.local
生产环境打包:生产环境使用 Rollup 进行打包,支持传入自定义配置,配置项功能参考 build/index.ts。
Vite 的使用限制
Vite 的使用限制如下:
面向支持 ES6 的现代浏览器,在生产环境下,编译目标参数 esBuildTarget 的默认值为 es2019最低支持版本为 es2015因为内部会使用 esbuild 处理编译压缩,用来获得最快的构建速度)。
对 Vue 框架的支持目前仅限于最新的 Vue 3 版本,不兼容更低版本。
Snowpack
Snowpack 是另一个比较知名的无包构建工具,从整体功能来说和上述 Vite工具提供的功能大致相同主要差异点在 Snowpack 在生产环境下默认使用无包构建而非打包模式(可以通过引入打包插件例如 @snowpack/plugin-webpack 来实现打包模式),而 Vite 仅在开发模式下使用。示例代码参见 example-snow。下面我们简单整理下两者的异同。
与 Vite 相同的功能点
两者都支持各种代码转换加载器、热更新、环境变量(需要安装 dotenv 插件、服务代理、HTTPS 与 HTTP/2 等。
与 Vite 的差异点
相同的功能,实现细节不同:例如对 Bare Module 的处理除了转换后前缀名称不同外Vite 使用 /@module/ 前缀,而 Snowpack 使用 /web_modules/ 前缀)Vite 支持类似 “AAA/BBB” 类型的子模块引用方式,而 Snowpack 目前尚不支持。
工具稳定性截止写稿的时间点2020 年 9 月 21 日Vite 的最新版本为 v1.0.0-rc4仍未发布第一个稳定版本。而 Snowpack 自年初发布第一个稳定版本以来,已经更新到了 v2.11.1 版本。
插件体系除了版本差异外Snowpack 提供了较完善的插件体系,支持用户和社区发布自定义插件,而 Vite 虽然也内置了许多插件,但目前并没有提供自定义插件的相关文档。
打包工具在生产环境下Vite 使用 Rollup 作为打包工具,而 Snowpack 则需要引入插件来实现打包功能,官方支持的打包插件有 @snowpack/plugin-webpack 和 @snowpack/plugin-parcel暂未提供 Rollup 对应的插件。
特殊优化Vite 中内置了对 Vue 的大量构建优化,因此对 Vue 项目而言,选择 Vite 通常可以获得更好的开发体验。
无包构建与打包构建
通过上面的 Vite 等无包构建工具的功能介绍可以发现,同 Webpack 等主流打包构建工具相比,无包构建流程的优缺点都十分明显。
无包构建的优点
无包构建的最大优势在于构建速度快,尤其是启动服务的初次构建速度要比目前主流的打包构建工具要快很多,原因如下:
初次构建启动快:打包构建流程在初次启动时需要进行一系列的模块依赖分析与编译,而在无包构建流程中,这些工作都是在浏览器渲染页面时异步处理的,启动服务时只需要做少量的优化处理即可(例如缓存项目依赖的 Bare Modules所以启动非常快。
按需编译:在打包构建流程中,启动服务时即需要完整编译打包所有模块,而无包构建流程是在浏览器渲染时,根据入口模块分析加载所需模块,编译过程按需处理,因此相比之下处理内容更少,速度也会更快
增量构建速度快:在修改代码后的 rebuild 过程中,主流的打包构建中仍然包含编译被修改的模块和打包产物这两个主要流程,因此相比之下,只需处理编译单个模块的无包构建在速度上也会更胜一筹(尽管在打包构建工具中,也可以通过分包等方式尽可能地减少两者的差距)。
无包构建的缺点
浏览器网络请求数量剧增:无包构建最主要面对的问题是,它的运行模式决定了在一般项目里,渲染页面所需发起的请求数远比打包构建要多得多,使得打开页面会产生瀑布式的大量网络请求,将对页面的渲染造成延迟。这对于服务稳定性和访问性能要求更高的生产环境而言,通常是不太能接受的,尤其对不支持 HTTP/2 的服务器而言,这种处理更是灾难性的。因此,一般是在开发环境下才使用无包构建,在生产环境下则仍旧使用打包构建。
浏览器的兼容性:无包构建要求浏览器支持 JavaScript module 特性,尽管目前的主流浏览器已大多支持,但是对于需要兼容旧浏览器的项目而言,仍然不可能在生产环境下使用。而在开发环境下则通常没有这种顾虑。
总结
这节课我们主要讨论了今年比较热门的无包构建。
无包构建产生的基础是浏览器对 JS 模块加载的支持,这样才可能把构建过程中分析模块依赖关系并打包的过程变为在浏览器中逐个加载引用的模块。但是这种加载模块的方式在实际项目应用场景下还存在一些阻碍,于是有了无包构建工具。
在这些工具里,我们主要介绍了 Vite 和 Snowpack希望通过介绍他们的开发模式的基本工作流程和差异点让你对这类工具的功能特点有一个基本的了解。
今天的课后思考题是,为什么 Vite/Snowpack 这样的无包构建工具要比 Webpack 这样的打包构建工具速度更快呢?
随着这节课的结束,构建优化模块也就告一段落了。下节课开始我们将进入部署优化模块。

View File

@ -0,0 +1,105 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 部署初探:为什么一般不在开发环境下部署代码?
这节课我们将进入前端效率工程化的第三个模块——部署效率篇。本模块主要讨论两个方面的问题:第一个是在前端项目的构建部署流程里,除了使用构建工具执行构建之外,还有哪些因素会影响整个部署流程的工作效率?第二个是在部署系统中进行项目构建时,又会面临哪些和环境相关的问题和优化方案?
这节课我们先来讨论为什么要使用部署系统,而不是在本地环境下部署代码?
在分析这个问题之前,我们先对前端项目的部署流程进行界定。
前端项目的一般部署流程
在前端项目中,通常可以把在一个全新环境下的代码部署过程分为以下几个环节:
获取代码:从代码仓库获取项目代码,并切换到待部署的分支或版本。
安装依赖:安装项目构建所需要的依赖包。
源码构建:使用构建工具对项目源代码进行构建,生成产物代码。
产物打包:将部署所需的代码(通常指的是构建后的产物代码,如果是部署 Node 服务则还需要其他目录与文件)打成压缩包。
推送代码:将待部署的文件或压缩包推送至目标服务器的特定目录下,如果是推送压缩包的情况,还需执行解压。
重启服务:在部署 Node 服务的情况下,在代码推送后需要进行服务重启。
本地部署相比部署系统的优势
对于使用部署系统的项目而言,除了重启服务这一步骤在普通静态服务部署中不需要执行外,上述其他环节通常是每次构建都需要经历的。
而如果使用本地开发环境进行部署,则可以根据情况对前两个环节进行简化:
在获取代码的环节中,本地开发环境已经包含了项目的本地代码,同拉取完整的代码仓库相比,直接获取更新内容并切换分支或版本的处理要更快一些。
在安装依赖的环节中,本地开发环境通常已包含了构建所需的最新依赖包,即使切换到待部署版本后发现依赖版本有变更,更新依赖包的时间也比在空目录下完整安装依赖包的时间更短。
此外,本地部署还有另外两点优势是使用部署系统所不具备的:
增量构建:我们之前分析过增量构建的实现原理。在构建配置与项目依赖不发生变化的情况下,理论上,本地部署可以让构建进程长时间地驻留,以达到增量构建的效果。
快速调试:本地部署时,构建过程会直接在本地进行,因此有任何构建问题时可以第一时间发现并处理。相比之下,远程的部署系统则需要将一定的时间消耗在链路反馈和本地环境切换上。
因此,如果单从上面的部署环节来看,本地部署的效率一般优于部署系统,那么为什么在企业中通常不建议这样做呢?
本地部署的劣势
同远程部署系统相比,不管从安全性还是人员效率上看,本地部署都存在诸多问题:
流程安全风险
环境一致性
本地部署的第一个问题在于无法保证环境的一致性:
同一个项目不同开发人员的本地环境操作系统、NodeJS 版本等)都可能存在差异。
由于 NodeJS 语义化版本Semantic Version在安装时自动升级的问题不同开发人员的本地 node_modules 中的依赖包版本也可能存在差异。
开发人员的本地环境和部署代码的目标服务器环境之间也可能存在差异。
这些差异会导致项目代码的稳定性无法得到保障。例如对于一个 Node 项目而言,在一个 NodeJS 低版本环境下构建的产物,在 Node 高版本环境下就有可能启动异常。
因此,如果项目都由开发人员各自在本地部署,无疑会降低项目的稳定性,增加部署风险。
而使用远程统一的部署系统,一方面避免了不同开发人员的本地环境差异性,另一方面,部署系统的工作环境也可以与线上服务环境保持一致,从而降低环境不一致的风险。
过程一致性
同环境一致性的问题相似,本地部署的第二个问题是无法保证部署过程的一致性。所谓过程的一致性,就是尽可能地让每次部署的流程顺序、各环节的处理过程都保持一致,从而打造规范化的部署流程。本地部署依赖人工操作,这就可能因为操作中的疏漏,导致过程一致性无法得到保障。尽管可以通过将部署流程写入脚本等方式减少人工误操作的风险,但是这和通过部署系统将完整处理过程写入代码的方式相比,仍然不够安全可靠。同时,系统可以记录每次部署操作的细节日志,便于当出现问题时快速解决。
工作效率问题
可回溯性
可回溯性的问题可以从日志和产物两方面来看。
日志:在部署过程中我们可能遇到各种问题,例如构建失败、单元测试执行失败、推送代码失败、部署后启动服务失败等。遇到这些问题时,需要有相应的日志来帮助定位。尽管在本地部署执行时也会输出日志,但是这些日志是临时的,查阅不便,且本地部署的日志至多只能保留当前一次的处理日志,如果希望对历史部署过程进行查看分析,更不能使用这种方式。
产物:通常,部署系统中会留存最近几次部署的构建产物包,以便当部署后的代码存在问题时能够快速回滚发布。而本地部署在项目的开发目录下执行,因此通常只会保留最近一次的构建产物,这就阻碍了上述快速回滚的实现。
相对的,一个规范化的部署系统,则可以记录和留存每一次部署操作的细节日志,以及保留最近若干次的部署代码包,因此在可回溯性上又胜一筹。
人员分工
工作效率的第二个问题是人员分工问题,这个问题又可以从以下几个侧面来分析:
首先部署过程需要耗费时间。在本地部署当前项目的某一个分支时,无法同时对该项目进行继续开发,往往只能中断当前的工作,等待部署完成。
在这个前提下,一个项目中的多名开发人员如果各自在电脑中进行部署,无疑增大了上述流程安全的风险系数。但反过来,如果一个项目里只有个别开发者的本地环境拥有部署权限,则所有人的部署需求都会堆积到一起,大大增加对有权限的开发者的工作时间的占用。如果不能及时响应处理,也会延误其他人的后续工作。
此外由于分工角色的不同,在许多情况下,部署流程会主动由测试人员而非开发人员发起。当部署在开发人员的本地环境中进行时,会像上面多人开发集中部署那样彼此影响,也增加了相应的沟通成本。
CI/CD
持续集成Continuous IntegrationCI和持续交付Continuous DeliveryCD是软件生产领域提升迭代效率的一种工作方式开发人员提交代码后由 CI/CD 系统自动化地执行合并、构建、测试和部署等一系列管道化Pipeline的流程从而尽早发现和反馈代码问题以小步快跑的方式加速软件的版本迭代过程。
这个过程通常是各系统(版本管理系统、构建系统、部署系统等)以自动化的方式协同完成的。而本地部署依赖人工操作,所以并不支持这种自动化的处理过程。
总结
作为部署优化的开篇,这节课我们主要讨论了相比远程部署系统,本地部署的优缺点:尽管本地部署有着流程简化、快速调试等优点,但是相对应的也带来了流程安全风险和人员效率下降等问题。因此一般在规范化的企业技术研发流程中,通常都不使用本地人工操作这样的部署方式。
本节课的思考题是:回顾一下本地部署同远程部署系统相比有哪些优势呢?关于这两者的差异,还可以从其他维度进行分析吗?如果有的话,你可以在留言区回复,期待你的想法。

View File

@ -0,0 +1,115 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 工具盘点:掌握那些流行的代码部署工具
上节课我们通过分析“为什么不在本地环境进行部署”这个问题,来对比部署系统的重要性:一个优秀的部署系统,能够自动化地完整部署流程的各环节,无须占用开发人员的时间与精力,同时又能保证环境与过程的一致性,增强流程的稳定性,降低外部因素导致的风险。此外,部署系统还可以提供过程日志、历史版本构建包、通知邮件等各类辅助功能模块,来打造更完善的部署工作流程。
这节课我就来为你介绍在企业项目和开源项目中被广泛使用的几个典型部署工具,包括 Jenkins、CircleCI、Github Actions、Gitlab CI。
Jenkins
Jenkins Logo
Jenkins 是诞生较早且使用广泛的开源持续集成工具。早在 2004 年Sun 公司就推出了它的前身 Husdon它在 2011 年更名为 Jenkins。下面介绍它的功能特点。
功能特点
搭建方式Jenkins 是一款基于 Java 的应用程序,官方提供了 Linux、Mac 和 Windows 等各系统下的搭建方式,同时也提供了基于 Docker 的容器化搭建方式。此外Jenkins 支持分布式的服务方式,各任务可以在不同的节点服务器上运行。
收费方式Jenkins 是完全免费的开源产品。
多类型 JobJob 是 Jenkins 中的基本工作单元。它可以是一个项目的构建部署流程也可以是其他类型例如流水线Pipeline。在 Jenkins 中支持各种类型的 Job自定义项目、流水线、文件夹、多配置项目、Github 组织等。
插件系统Jenkins 架构中内置的插件系统为它提供了极强的功能扩展性。目前 Jenkins 社区中共有超过1500 个插件,功能涵盖了继续继承和部署的各个环节。
Job 配置:得益于其插件系统,在 Jenkins 的 Job 配置中可以灵活定制各种复杂的构建与部署选项,例如构建远程触发、构建参数化选项、关联 Jira、执行 Windows 批处理、邮件通知等。
API 调用Jenkins 提供了 Restful 的 API 接口,可用于外部调用控制节点、任务、配置、构建等处理过程。
Jenkins 中 Job 的基本配置界面
CircleCI
CircleCI Logo
CircleCI 是一款基于云端的持续集成服务,下面介绍它的功能特点。
功能特点
云端服务:由于 CircleCI 是一款基于云端的持续集成服务,因此无须搭建和管理即可直接使用。同时也提供了收费的本地化搭建服务方式。
收费方式CircleCI 的云端服务分为免费与收费两种,免费版本一个账号只能同时运行一个 Job同时对使用数据量、构建环境等有一定限制。而收费版本则提供了更多的并发构建数、更多的环境、更快的性能等。此外如第一点所述企业内部使用的本地化搭建服务方式也是收费的。
缓存优化CircleCI 的任务构建是基于容器化的,因此能够缓存依赖安装的数据,从而加速构建流程。
SSH 调试:它提供了基于 SSH 访问构建容器的功能,便于在构建错误时快速地进入容器内进行调试。
配置简化:在 CircleCI 中提供了开箱即用的用户体验,只需要少量配置即可快速开始构建项目。
API 调用CircleCI 中也提供了 Restfull 的 API 接口,可用于访问项目、构建和产物。
CircleCI 项目流水线示例界面
Github Actions
Github Actions Logo
Github ActionsGHA是 Github 官方提供的 CI/CD 流程工具,用于为 Github 中的开源项目提供简单易用的持续集成工作流能力。
功能特点
多系统:提供 Linux、Mac、Windows 等各主流操作系统环境下的运行能力,同时也支持在容器中运行。
矩阵运行:支持同时在多个操作系统或不同环境下(例如不同 NodeJS 版本的环境中)运行构建和测试流程。
多语言:支持 NodeJS、JAVA、PHP、Python、Go、Rust 等各种编程语言的工作流程。
多容器测试:支持直接使用 Docker-Compose 进行多容器关联的测试(而 CircleCI 中则需要先执行安装才能使用)。
社区支持Github 社区中提供了众多工作流的模板可供选择使用,例如构建并发布 npm 包、构建并提交到 Docker Hub 等。
费用情况Github Action 对于公开的仓库,以及在自运维执行器的情况下是免费的。而对于私有仓库则提供一定额度的免费执行时间和免费存储空间,超出部分则需要收费。
Github Actions 的工作流模板
Github Actions 中的矩阵执行示例
Gitlab CI
Gitlab 是由 Gitlab Inc. 开发的基于 Git 的版本管理与软件开发平台。除了作为代码仓库外它还具有在线编辑、Wiki、CI/CD 等功能。在费用方面它提供了免费的社区版本Community EditionCE和免费或收费的商用版本Enterprise EditionEE。其中社区版本和免费的商用版本的区别主要体现在升级到付费商用版本时的操作成本。另一方面即使是免费的社区版本其功能也能够满足企业内的一般使用场景因此常作为企业内部版本管理系统的主要选择之一下面我们就来了解 Gitlab 内置的 CI/CD 功能。
功能特点
与前面两款产品相似的是Gitlab CI 也使用 yml 文件作为 CI/CD 工作流程的配置文件,在 Gitlab 中,默认的配置文件名为 .gitlab-ci.yml。在配置文件中涵盖了任务流水线Pipeline的处理过程细节例如在配置文件中可以定义一到多个任务Job每个任务可以指定一个任务运行的阶段Stage和一到多个执行脚本Script等。完整的 .gitlab-ci.yml 配置项可参考官方文档。
独立安装执行器与前面两款产品不同的是Gitlab 中需要单独安装执行器。Gitlab 中的执行器 Gitlab Runner 是一个独立运行的开源程序,它的作用是执行任务,并将结果反馈到 Gitlab 中。开发者可以在独立的服务器上安装Gitlab Runner 工具然后依次执行gitlab-runner register注册特定配置的 Runner最后执行gitlab-runner start启动相应服务。此外项目中除了注册独立的 Runner 外,也可以使用共享的或组内通用的 Runner。
当项目根目录中存在.gitlab-ci.yml 文件时,用户提交代码到 Git 仓库时,在 Gitlab 的 CI/CD 面板中即可看到相应的任务记录,当成功设置 gitlab-runner 时这些任务就会在相应的 Runner 中执行并反馈日志和结果。如下图所示:
Gitlab CI/CD 的任务列表示例界面
总结
最后我们来做一个总结。在今天的课程里,我们一起了解了 4 个典型 CI/CD 工具Jenkins、CircleCI、Github Actions 和 Gitlab CI。
在这四款工具中Jenkins 是诞生最早也最广为人知的,它的优点在于插件功能丰富且完全开源免费,因此在企业中应用较多。但缺点在于缺少特定语言环境工作流的配置模板,使用成本相对较高。此外,它的服务器需要独立部署和运维。
CircleCI 和 Github Actions 都提供了基于容器化的云端服务的能力提供不同的收费策略以满足普通小型开源项目和大型私有项目的各类需求。两者相比CircleCI 胜在除了对接 Github 中的项目外,还支持 BitBucket、Heroku 等平台的流程对接。而 Github Actions 则是 Github 项目中内置的 CI/CD 工具,使用成本最低,且提供了矩阵运行、多容器测试、多工作流模板等特色功能。
Gitlab CI 则是企业中较受欢迎的版本管理工具。Gitlab 中内置 CI/CD 工具,和 CircleCI 与 Github Actions 相同的是Gitlab CI 也使用 yml 格式的配置文件,不同之处主要在于需要独立安装与配置 Runner。
本节课的课后思考题是:如果你所在的企业需要选择一款 CI/CD 工具,你选择的主要依据有哪些呢?以今天谈到的几款工具为例,谈谈你的选择和想法吧。

View File

@ -0,0 +1,185 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 安装提效:部署流程中的依赖安装效率优化
上节课我们主要介绍了几个典型的部署工具及其特点。课后的思考题是如果所在企业需要选择一款 CI/CD 工具,你选择的依据有哪些?如果是我会从这几个方面思考:选择付费系统还是免费系统,选择云服务还是自运维,所选的方案是否便于对接上下游系统流程,使用配置是否便捷,对用户而言是否有学习成本……希望这些能为你提供参考。
下面开始本节课的学习。我们在之前的课程中介绍过,前端项目的部署流程包含了代码获取、依赖安装、执行构建、产物打包等阶段,每个阶段的执行过程都有值得关注的效率提升点。在系列课程的第二模块构建效率篇中,我们已经讨论了执行构建阶段的主要优化点,而今天的课程则将详细分析在执行构建的前一个环节——依赖安装阶段中,又有哪些因素会对执行效率产生影响。
分析前的准备工作
五种前端依赖的安装方式
我们先来对比 5 种不同的前端依赖安装方式:
npmnpm 是 NodeJS 自带的包管理工具,也是使用最广泛的工具之一。在测试时我们使用它的默认安装命令 npm install不带额外参数示例代码参照 19_install/with_npm。
YarnYarn 是 Facebook 于 2016 年发布的包管理工具,和 npm 5 之前的版本相比Yarn 在依赖版本稳定性和安装效率方面通常更优(在 Github 中Yarn 的 star 数量是 npm 的两倍多,可见其受欢迎程度)。在测试时我们同样使用默认安装命令 Yarn, 不带额外参数,示例代码参照 19_install/with_yarn。
Yarn with PnPYarn 自 1.12 版本开始支持 PnP 功能,旨在抛弃作为包管理目录的 node_modules而使用软链接到本地缓存目录的方式来提升安装和模块解析的效率。在测试时我们使用 yarn pnp不带额外参数。示例代码参照 19_install/with_yarn_pnp1。
Yarn v2Yarn 在 2020 年初发布了 v2 版本,它和 v1 版本相比有许多重大改变,包括默认使用优化后的 PnP 等。v2 版本目前通过 Set Version 的方式安装在项目内部,而非全局安装。测试时我们使用安装命令 Yarn不带额外参数。示例代码参照 19_install/with_yarn_v2。
pnpmpnpm 是于 2017 年发布的包管理工具,和 Yarn 相同,它也支持依赖版本的确定性安装特性,同时使用硬连接与符号连接缓存目录的方式,这种方式相比于非 PnP 模式下的 Yarn 安装而言磁盘存储效率更高。测试时我们使用安装命令 pnpm install不带额外参数。示例代码参照 19_install/with_pnpm。
依赖安装的基本流程
在对影响效率的问题进行分析之前,我们需要先了解一下前端依赖安装的基本流程阶段划分,这有助于分析不同场景下执行时间的快慢因素,排除各工具的细节差异。前端项目中依赖包安装的主要执行阶段如下:
解析依赖关系阶段:这个阶段的主要功能是分析项目中各依赖包的依赖关系和版本信息。
下载阶段:这个阶段的主要功能是下载依赖包。
链接阶段:这个阶段的主要功能是处理项目依赖目录和缓存之间的硬链接和符号连接。
那么如何获取执行时间呢?
如何获取执行时间
上面的几种安装方式中npm 和 Yarn 在执行完成后的输出日志中会包含执行时间,而 pnpm 的输出日志中则没有。不过我们还是可以使用系统提供的 time 命令来获取,方法如下所示:
time npm i
time yarn
time pnpm i
如何获取执行日志
除了获取安装过程的执行时间外,如果需要进一步分析造成时间差异的原因,就需要从安装过程日志中获取更详细的执行细节,从中寻找答案。不同工具显示详细日志的方式也不同:
npm使用 npm 安装时需要在执行命令后增加verbose来显示完整日志。
Yarn v1Yarn v1 版本(包括 Yarn PnP的实现方法和 npm 一样,即通过增加 verbose 来显示完整日志。
Yarn v2Yarn v2 版本默认显示完整日志,可通过 json 参数变换日志格式,这里使用默认设置即可。
pnpmpnpm 安装时需要在执行命令后增加 reporter ndjson 来显示完整日志。
环境状态的五个分析维度
在确定了安装工具和分析方式后,我们还需要对执行过程进行划分,下面我一共区分了 5 种项目执行安装时可能遇到的场景:
注 1除了第一种纯净环境外后面的环境中都存在 Lock 文件。因为 Lock 文件对于提供稳定依赖版本至关重要。出于现实场景考虑,这里不再单独对比没有 Lock 文件但存在历史安装目录的场景。
注 2 为了屏蔽网络对解析下载依赖包的影响,所有目录下均使用相同注册表网址 registry.npm.taobao.org。
注 3以下时间统计的默认设备为 MacOS网速约为 20Mbit/s。
不同维度对安装效率的影响分析
纯净环境
首先来对纯净环境进行分析,不同安装方式的执行耗时统计如下:
注 1总安装时间为执行后显示的时间。而各阶段的细分时间在日志中分析获取。
注 2在 pnpm 的执行过程中并未对各阶段进行完全分隔,而是针对不同依赖包递归执行各阶段,这种情况在纯净环境中尤其明显,因此阶段时间上不便做单独划分。
对结果的分析如下:
在总体执行时间上npm < pnpm < Yarn v1 PnP < Yarn v1 < Yarn v2根据其他统计纯净环境下的执行时间是 pnpm < Yarn v1 PnP < Yarn v1 < npm推测主要是设备带宽等环境区别造成
在解析依赖阶段和下载阶段npm 略快于 Yarn v1主要原因是 Yarn 将网络并发数设置为 8源码中搜索 NETWORK_CONCURRENCY 可见 npm 中的并发数则是 10源码中搜索 npm.limit 可见 Yarn v2 在这两个阶段中性能尤其缓慢原因可能是其在设计上的重大变更在相应阶段中引入了更复杂的依赖分析算法和对下载包的额外处理
在链接阶段npm 耗时小于 Yarn v1 而大于 Yarn v1 PnP原因在于 npm 在下载阶段中处理了文件写入安装目录的过程Yarn v1 则在链接阶段进行写入 Yarn v1 PnP 则直接使用软硬链接而非复制文件因此效率更高Yarn v2 一方面因为同样采用软硬链接的方式另一方面链接的数据为 Zip 压缩包而非二进制缓存因此效率中等
Lock 环境
然后我们来考察 Lock 文件对于安装效率的影响和第一种最纯净的情况相比带有 Lock 文件的情况通常更符合现实中项目在部署环境中的初始状态因为 Lock 文件可以在一定程度上保证项目依赖版本的稳定性因此通常都会把 Lock 文件也保留在代码仓库中引入 Lock 文件后不同安装工具执行安装的耗时情况如下
1 Yarn 解析依赖阶段日志未显示耗时因此标记为 0
对结果的分析如下
从总的执行时间上看Lock 文件的存在对于各安装工具而言都起到了积极的作用安装时间都有一定程度的缩短
进一步细分安装各阶段可以发现Lock 文件主要优化的是依赖解析阶段的时间即在 Lock 文件存在的情况下项目在安装时直接通过 Lock 文件获取项目中的具体依赖关系与依赖包的版本信息因此这一阶段的耗时大多趋近于零
缓存环境
缓存环境是在部署服务中可能遇到的一种情形项目在部署过程中依赖安装时产生了本地缓存部署结束后项目工作目录被删除因此再次部署开始时工作目录内有 Lock 文件也有本地缓存但是不存在安装目录这种情形下的耗时统计如下
对结果的分析如下
从执行时间上看各类型的安装方式的耗时都明显下降
从细分阶段的耗时情况可以发现本地缓存主要优化的是下载依赖包阶段的耗时即在本地缓存命中的情况下免去了通过网络请求下载依赖包数据的过程对于使用 npm 的项目而言这一阶段还剩下解压缓存写入安装目录的耗时对于使用 Yarn 的项目而言这一阶段没有其他处理直接略过对于 pnpm 的项目而言这一阶段的处理中还剩下从缓存硬链接到项目安装目录的过程
无缓存的重复安装环境
无缓存的重复安装环境在本地环境下部署时可能遇到即当本地已存在安装目录但人工清理缓存后再次执行安装时可能遇到这种情况的耗时如下
对结果的分析如下
从上面的表格中可以看到存在安装目录这一条件首先对链接阶段能起到优化的作用对于下载阶段除了使用 PnP 的两种安装方式外当项目中已存在安装目录时下载阶段耗时也趋近于零其中 Yarn v1 表现最好各主要阶段都直接略过 npm pnpm 则多少还有一些处理过程
而使用 PnP 的两种安装方式因为内部机制依赖缓存本身不存在 node_modules因此在清除本地缓存的情况下仍需完整经历远程下载过程只不过由于其他安装后文件的存在例如 .pnp.js使得链接阶段的耗时相比 Lock 环境而言有所下降
有缓存的重复安装环境
最后是安装目录与本地缓存都存在的情况耗时如下
对结果的分析如下
无论对于哪种安装方式而言这种情况都是最理想的可以看到各安装工具的耗时都趋近于零其中尤其对于 Yarn v1 而言效率最高 pnpm 次之npm 相对最慢
对不同安装工具和不同安装条件的效率总结
不同安装条件
下面我们进行一个总结首先来看不同安装条件下的效率情况
对于项目的依赖安装过程来说效率最高的 3 个条件是存在 Lock 文件存在存在本地缓存存在和存在安装记录
3 个条件中Lock 文件的留存是最容易做到的也是最可能被忽略的大部分项目也都会保留在代码仓库中不过据我观察也存在不提交 Lock 文件的情况即在一些多人维护的项目中因为害怕处理冲突而主动忽略了 Lock 文件这是应该尽量避免的
本地缓存则是当安装记录不存在时最重要的优化手段 对于大部分部署系统而言本地缓存通常也是会默认保留在部署服务器上的因此需要注意的更多是磁盘空间与效率的平衡此外需要注意在部署服务的个别项目中执行清除缓存的操作也会影响其他项目
本地安装记录对于部署系统而言需要占据较多的磁盘空间因此在多数情况下部署系统默认不保留安装目录项目中如需考虑这一优化点建议确认所使用的部署系统是否支持相关设定
在安装条件方面其实还有一些额外的不容易量化的条件例如网速磁盘 I/O 速度等对于部署服务而言这些硬件条件的好坏也会直接影响用户的使用效率这里不再展开
不同安装工具
然后我们对不同安装工具的效率情况进行总结
1.单从效率而言各工具在不同安装条件下的优劣各有不同
如果考虑各种场景下的综合表现pnpm 是最稳定高效的其他工具在不同场景下多少都有表现不佳的情况
如果考虑现实情况中在部署环境下大多可以支持 Lock 文件和本地缓存的留存并且部分系统中也会保留安装目录 Yarn v1 是更好的选择
如果考虑只有 Lock 文件的情况 npm 的表现要优于 Yarn
在无安装目录的情况下Yarn v1 PnP 模式效率要高于普通模式
尽管 Yarn v2 在无缓存的情况都表现不佳但是它有一点优势是其他方式无法替代的即支持针对单个项目清除缓存而不影响全局
2.不同的安装工具除了对安装过程的效率会有影响外对构建过程也会产生影响Yarn v1 普通模式可以作为 npm 的直接替代不对构建产生影响而剩下的 PnP 模式Yarn v2 pnpm 则因为依赖存储的方式不同在构建解析时多少会有差异因此在项目中选择工具时需要综合考虑
总结
这节课主要讨论了部署流程中的依赖安装环节的执行细节问题依赖安装常常是一个容易被忽视的环节但是其中又有很多对执行效率产生影响的因素不同的安装方式和安装环境条件都可能对执行效率产生影响
希望通过今天的课程你可以掌握不同条件下的执行效率区别并应用到实际项目中今天的课后思考题是如果项目中使用的是 npm在最佳条件下是否可以像 Yarn 那样耗时更趋近于零呢试着从部署系统的角度考虑看看有没有解决方案

View File

@ -0,0 +1,124 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 流程优化:部署流程中的构建流程策略优化
上节课我们分析了部署流程中,安装依赖阶段执行效率的影响因素和执行过程细节。思考题是如果使用 npm 的话,在最佳条件下是否也可以达到像 Yarn 一样瞬间完成依赖安装呢?答案是当然可以。在今天课程的第一部分我们就将了解如何利用安装目录缓存达到这一效果。
我们将从部署流程效率优化的实战角度来分析几个主要耗时阶段的提效方法。部署流程的第一个耗时阶段就是依赖安装。这一阶段中影响执行效率的工具和方法在上节课中已经分析过,这节课主要从 CI 系统的开发者角度来讨论一下,作为服务各项目的 CI 系统,有哪些通用的效率优化方案。
依赖安装阶段的提效
提升依赖下载速度
项目在安装依赖时如果不能命中本地缓存,就需要通过远程下载的方式来获取远程依赖包的数据。因此对于 CI 系统而言,首先需要考虑的是如何提升下载的速度。除了硬件条件外,有以下因素会对下载速度产生影响:
1.依赖包下载源registry各依赖管理工具默认的下载源都是位于国外的换成国内的镜像源会对下载速度的提升十分明显。例如下面代码和图片中的例子
#npm设置下载源
npm config set registry xxxx
#yarn设置下载源
yarn config set registry xxxx
下载同样的依赖包,使用国内镜像源的速度只有官方源的 1/4。有条件的情况下可以在企业内网部署私有源下载速度可以得到进一步提升。
2.二进制下载源:对于一些依赖包(例如 node-sass 等),在安装过程中还需下载二进制文件,这类文件的下载不遵循 registry 的地址,因此需要对这类文件单独配置下载路径来提升下载速度。示例配置如下代码(更多配置可以参考国内的镜像网址):
npm config set sass-binary-site https://npm.taobao.org/mirrors/node-sass
npm config set puppeteer_download_host https://npm.taobao.org/mirrors
多项目共用依赖缓存
无论使用哪种依赖管理工具npm、Yarn、pnpm……本地缓存对于提升安装阶段的效率而言都是十分重要的。因此对于使用多台构建服务器的分布式 CI 系统而言,要考虑的是如何最大化地利用缓存。例如让使用相同依赖工具的项目共用相同的服务器,以及让技术栈相同的项目共用相同的服务器,从而使得新项目接入时最大可能地命中已有缓存。
安装目录缓存
在 CI 系统中,除了保留常规的依赖缓存外,还可以尝试对安装目录进行缓存。以 npm 为例:
缓存写入:在初次安装完成后,我们可以将安装完成后的 node_modules 目录内的内容与项目的 package-lock.json 文件的内容进行关联:以 package-lock.json 文件内容的 Hash 值作为缓存的 Key将 node_modules 目录压缩打包存储到缓存空间内。
缓存读取:再次执行安装过程时,首先判断当前代码的 package-lock.json 内容的 Hash 值是否能够命中缓存目录中的 Key 值。如果命中缓存,则说明项目的依赖版本并未发生变化,因此可以直接使用缓存中的 node_modules 压缩包解压,无须再执行依赖安装的完整流程。
这种对安装目录缓存的方式在命中时只需解压,几乎瞬间完成,与使用 Yarn 的项目命中缓存后的效果相似,适用于不能保留项目工作空间的 CI 系统,但是在使用时还是有一些注意点:
尽管这种人工缓存方式的过程效率较高,但是与原生的依赖缓存相比,对磁盘空间的利用率较低。原生的依赖缓存以单个依赖包为存储单元,即使项目升级了个别依赖包版本,剩下不变的大部分依赖包在安装时仍然可以命中缓存,缓存空间中只会新增变更的版本数据。而人工缓存安装目录的方式以 Lock 文件的 Hash 值为 Key当个别依赖版本发生变更时整个缓存即宣告失效需要在依赖安装结束后重新缓存整个安装目录。
影响安装的关联因素:对于前端项目而言,执行安装后的依赖包内容实际上不仅和项目中的依赖版本相关,甚至还和执行安装时的操作系统以及 NodeJS 版本有关。因此,对于分布式的 CI 系统而言,如果共用缓存空间,则必须在生成缓存 Key 时将这些变量因素也加入其中参与计算。
检测项目 Lock 文件
Lock 文件对于依赖安装过程的重要性在上节课已经讨论过了。在现实中,一方面需要项目的开发者注意对 Lock 文件的保存和维护,另一方面在 CI 系统的工作流程中也可以加入对 Lock 文件的检测。当项目安装流程结束时,如果发现有未提交的 Lock 文件,即表明项目代码仓库未对 Lock 文件进行追踪。此时可主动通知项目开发者对该问题进行关注和修复。
代码构建阶段的提效
构建阶段是整个部署流程中最耗时的一个环节。在之前的课程里,我们用了整个模块来讨论作为项目的开发者该如何提升构建过程中的效率。在这里,我们再以 CI 系统的开发者的角度来分析CI 系统又能在其中发挥什么作用。
CI 系统中的持久化缓存
CI 系统中项目的构建空间通常是临时的,在开始部署时创建项目工作目录,在部署结束后删除工作目录,以达到节约资源的目的。但是这种情况的弊端就在于无法利用构建过程中的持久化缓存机制。
以 Webpack 为例,项目执行构建后,中间过程的缓存默认都存放于 node_modules/.cache 目录下。在部署系统中,随着部署流程的结束,工作目录中的 node_modules 目录也被删除。因此再次构建时,无法利用持久化缓存来提升再次构建的效率。
即使使用了依赖安装阶段的安装目录缓存策略,也只会缓存初次构建后的 node_modules 目录。再次构建后,持久化缓存数据可能发生变化,但是由于安装目录缓存是跟随 Lock 文件的版本存储的,在 Lock 文件不变的情况下,无法更新持久化缓存数据。
因此,在 CI 系统中需要对项目的持久化缓存数据做单独的备份与还原:
备份:在项目构建结束后,对项目的目录结构进行扫描,找到 .cache 目录(也可以写入项目构建配置中),依据其相对项目根目录的路径生成备份目录名称(持久化缓存的读写由生成缓存的工具控制其验证规则,因此在备份时无须考虑缓存映射关系)。例如可以把项目中 /client/node_modules/.cache 多层目录转换为折叠目录 clientnode_modules.cache使用折叠目录的原因是便于解析和还原然后将其备份到 CI 系统专用的持久化缓存备份空间中。
还原:在部署过程进行到开始构建的阶段时,查看备份空间中是否存在对应项目的持久化缓存目录,若存在,则直接解析目录结构,将 .cache 还原回项目相应的目录中。
通过这样的机制,就可以使项目在 CI 系统中也可以享受持久化缓存带来的构建效率提升。
产物打包阶段的提效
CI 系统在构建结束后,需要将产物进行压缩打包,以便归档和在推送产物到服务器时减少传输数据量,提升传输效率。传统的打包过程默认使用 Gzip 作为压缩方式,但事实上我们还可以选择其他压缩工具提升效率。
提升压缩效率的工具
这里介绍两种压缩工具Pigz 和 Zstd。更多压缩工具的选择以及性能对比可以参见参考文档。
首先我们对这两种工具和 tar 命令中默认的 Gzip 压缩选项的参数进行对比(数据来自上面的参考文档),如下面的表格所示:
从表格中可以发现:
对于同一款压缩工具来说,压缩等级越低,压缩速度越快。代价是相应的压缩率越低,压缩体积会相应增大。
同样使用默认压缩等级Pigz 的压缩速度是 Gzip 的两倍多,而 Zstd 的压缩速度是 Gzip 的15 倍。
在都使用最高压缩等级的情况下Pigz 的压缩速度是 Gzip 的4 倍多,而 Zstd 的压缩速度可以达到 Gzip 的16 倍。
因此,在处理大文件目录的打包压缩时,可以考虑使用这些工具来替代默认的 Gzip 压缩方式。
但是在使用时还需要注意以下几点:
待压缩的文件体积越大,不同工具的压缩时间差异越明显。如果待压缩的内容体积不大(例如一些静态网站的打包产物),则仍旧可以使用默认的 Gzip 压缩。
Pigz 和 Zstd 都启用了并行处理,因此处理过程中 CPU 和内存的占用会比 Gzip 更高。
Pigz 的压缩产物和 Gzip 格式是兼容的,因此可以较广泛地作为 Gzip 的替代。相比之下Zstd 则是不兼容的方案,它的压缩产物在压缩时也需要使用 Zstd 才能处理,因此对于构建产物这样的资源,如果使用这种格式,则不光在 CI 系统中需要安装 Zstd且在部署推送的目标服务器上也需要安装和使用相应工具。如果不能统一工具则 Pigz 是更好的选择。事实上Zstd 往往被用于压缩与解压流程闭环的应用场景中,例如本文第一部分讲到的安装目录缓存就可以使用这种压缩格式,以获得最高的处理效率。
总结
这节课我们主要从实战角度出发来讨论部署流程的几个主要耗时环节的提效方法,包括依赖安装阶段的多维度提升安装效率、代码构建阶段的持久化缓存备份、产物打包阶段的提升压缩效率等。这些方法在实践中对于部署流程效率的提升都能起到较明显的积极作用。你可以课后结合自己所使用的部署工具来分析目前的工作流程是否有优化空间。
今天的课后思考题是:本节课我们在哪些方案中使用了缓存机制?它们各自的作用分别是什么呢?
下节课我们来聊聊一种特殊的 CI 系统:容器化构建部署的特点。

View File

@ -0,0 +1,138 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 容器方案:从构建到部署,容器化方案的优势有哪些?
上节课我们主要介绍了部署系统中各耗时环节的一些常用优化方案。课后思考题是:课程中提到了几种利用缓存的优化方案呢?如果你认真学习了课程内容,不难发现我一共提到了三种基于缓存的优化方案,它们分别是:多项目共用依赖缓存、依赖安装目录的缓存以及构建过程的持久化缓存备份。这些缓存方案不仅可以运用到传统的部署方式中,在今天介绍的容器化部署方案中也有各自的用武之地。
下面我就来介绍本节课的第一个话题:什么是容器化呢?
容器化的基本概念
什么是容器化
容器化Containerization通常是指以 Docker 技术为代表,将操作系统内核虚拟化的技术。和传统的虚拟机相比,容器化具有占用空间更小、性能开销更低、启动更快、支持弹性伸缩以及支持容器间互联等优势。
下面介绍 Docker 的几个基本概念。
Docker
通常我们提到的 Docker 是指运行在 Linux/Windows/macOS 中开源的虚拟化引擎用于创建、管理和编排容器此外Docker 也是发布和维护该开源项目的公司名称)。
镜像
Docker 中的镜像Image是指用于创建容器实例的基础虚拟化模板。对于开发人员来说可以把镜像理解为编程语言中的类通过一个镜像可以创建多个容器实例镜像之间也存在继承关系。通过 Docker 引擎可以构建、删除镜像,还可以将本地镜像 push 到远程仓库或者从远程拉取。例如一个基于 node:14 的镜像,在创建时不光包含了运行 node14 版本所需的 Linux 系统环境,还包含了额外打入到镜像内的 Yarn 程序。
容器
容器Container是 Docker 中的核心功能单元。通常一个容器内包含了一个或多个应用程序以及运行它们所需要的完整相关环境依赖。例如,基于上面例子中的 node:14 镜像,就可以创建出对应版本 NodeJS 的独立运行环境。通过 Docker 引擎可以对容器进行创建、删除、停止、恢复、与容器交互等操作。
数据挂载与数据卷
通常情况下容器内的基础数据来自创建容器的镜像。创建容器后,在容器内执行的命令如果导致容器内的数据产生变化,这些变更的数据会在容器删除的同时被清除,无法持久化保留。如果要解决持久化保留数据,可以采取两种方式:挂载容器的宿主环境(即执行启动容器命令所在的服务器)的目录或使用数据卷。数据卷可以理解为通过 Docker 引擎创建的宿主环境下的独立磁盘空间,用于在容器内读写数据,生命周期独立,不受容器生命周期的影响。
网络
Docker 容器的网络有多种驱动类型,例如 bridge、host、overlay 等。其中默认的 bridge 类型类似网桥,用于点对点访问容器间端口或者将容器端口映射到宿主环境下。而 host 则是直接使用宿主环境的网络。更多网络模型介绍可参照官方说明文档。
容器化的构建部署
在了解了容器化的基本概念后,我们再来看看什么是容器化的构建部署。
顾名思义,容器化的构建部署是把原先在部署服务器中执行的项目部署流程的各个环节,改为使用容器化的技术来完成,具体可以划分为操作镜像和操作容器两个阶段。
镜像阶段
镜像阶段的主要目标是创建一个用于部署代码的容器环境的工作镜像。以前端项目为例,工作镜像中一般会包含:特定版本的 node 环境、git、项目构建所需的其他依赖库等。有了这样的环境就可以在对应的容器中执行各部署环节。
构建镜像的具体内容写在 Dockerfile 文件中,例如下面的代码:
# 通过FROM指定父镜像
FROM node:12-slim
# 通过RUN命令依次在镜像中安装gitmake和curl程序
RUN apt-get update
RUN apt-get -y install git
RUN apt-get install -y build-essential
RUN apt-get install -y curl
这是一个基本的包含 node、git 等依赖程序的部署工作环境的镜像内容。
然后在 Dockerfile 所在目录下执行构建命令,即可创建相应镜像,如下所示:
docker build --network host --tag foo:bar .
容器阶段
容器阶段的主要目标就是基于项目的工作镜像创建执行部署过程的容器,并操作容器执行相应的各部署环节:获取代码、安装依赖、执行构建、产物打包、推送产物等。操作分为两个部分,创建容器与执行命令,如下面的代码所示:
# 创建容器
docker run -dit --name container_1 foo:bar bash
# 容器内执行命令
docker exec -it container_1 xxxx
容器化部署过程的优势
与传统的在单台服务器上以目录区分不同部署项目的方式相比,容器化的构建部署过程有以下优点:
环境隔离
每个项目在独立的容器内执行构建部署过程,保证容器与宿主环境之间,容器与容器之间的环境隔离,防止原先共用一台服务器时可能产生的互相影响(例如项目脚本中修改了全局配置或环境变量等)。同时,环境隔离还可以保证每个项目都可以自由定制专属的环境依赖,而不必担心对其他项目产生影响。
多环境构建
使用容器化部署,可以方便地针对同一个项目生成多套不同的构建环境,达到类似 Github Actions 中的矩阵构建效果,使项目可以同时检测多套环境下的集成过程。
便于调试
用户可通过 Xterm+SSH 的方式,通过浏览器访问部署系统中的容器环境。同传统的部署方式相比,用容器化的方式可以在部署遇到问题时让用户第一时间进入容器环境中进行现场调试,效率和便捷性大大提升。之前介绍过的部署工具 CircleCI 中就提供了这一调试功能。
环境一致性与迁移效率
由于部署过程的工作环境以 Docker 镜像的方式独立制作和存储,因此可以在支持 Docker 引擎的任意服务器中使用。使用时提供一致的工作环境,无须考虑不同服务器操作系统的差异。在迁移时也可以做到一键迁移,无须重复安装环境依赖。
容器化部署过程的挑战及建议
尽管容器化的部署方式有上述优势,但也面临一些问题,例如缓存方面和性能方面的问题等。
缓存问题
项目在独立容器中构建部署时,首先面临的就是缓存方面的问题:
依赖缓存:默认情况下,容器内的依赖缓存目录与宿主环境缓存目录不互通,这就导致每次依赖安装时,都无法享受宿主环境缓存带来的效率提升。同时,每次部署流程都在新容器中进行,这也导致在依赖安装的过程中,也无法读取历史依赖安装后的缓存数据。要解决这类问题,可以从两方面入手:生成容器时挂载宿主环境依赖缓存目录,以及上节课中提到的安装目录缓存。
构建缓存:在传统的服务器中执行部署时,可以通过留存历史构建目录的方式来保留构建缓存数据。而在容器化的情况下,每次部署过程都会基于新容器环境重新执行各部署环节,构建过程的缓存数据也会随着部署结束、容器移除而消失。因此在这种部署方式中,尤其需要重视持久化缓存数据的留存问题。你同样可以考虑从两个方向解决:在宿主环境中创建构建缓存目录并挂载到容器中,并在项目构建配置中将缓存目录设置为该目录,这样就可以将缓存直接写入宿主服务器目录中。或者按照上节课提到的持久化缓存的备份与还原方案,将缓存备份到宿主服务器或远程存储服务器中,之后在新部署流程中进行还原使用。
性能问题
通常情况下,与传统的服务器部署相比,容器化部署并没有明显的性能差异,但是在实践中也存在一些性能方面的特殊情况:
容器资源限制:在创建容器时可以通过参数来限制容器使用的 CPU 核心数和内存大小。和在普通服务器中执行部署时,完整使用所有系统资源的方式相比,限制系统资源的方式会在一定程度上导致执行过程性能的降低。在多项目使用的容器化集群方案中,这种限制通常是必要的,只是对于性能降低明显的项目而言,可以考虑在设置中分配更多的资源以提升执行效率。
copy-to-write性能问题的另一方面则体现在它独特的数据存储方式上。和传统的磁盘读写方式不同容器中的数据是分层的环境的数据来自镜像层而新增的数据则来自写入容器层。在读取数据时数据来自镜像层还是容器层并没有差别但当修改数据时如果修改或删除的是镜像层的数据容器会先将数据从镜像层复制到容器层然后进行相应操作。这种先复制后写入的模式称为 copy-to-write。因此如果在容器的部署流程中涉及对镜像层数据的修改时执行起来会比在普通服务器中的操作耗费更多时间。举个极端的例子如果我们把项目的依赖安装过程写入构建镜像中然后在容器内移动 node_modules 目录,会发现这个操作耗费的时间几乎等同于复制完整目录的时间。而同样的移动操作在普通服务器中几乎是瞬间完成的。因此,在使用容器化部署时,需要尽量避免将可变数据写入镜像中。
总结
这节课我们首先了解了以 Docker 为代表的容器化技术的基本概念:镜像、容器、数据挂载和网络等。然后讨论了容器化的构建部署需要经历的流程,先创建镜像,然后根据镜像创建容器,最后在容器内执行相关部署环节。接着分析了容器化部署的优势和劣势:容器化部署具有隔离性高、支持多环境矩阵执行、易于调试和环境标准化等优势,同时在使用时也需要额外注意对应的缓存和性能问题。
本节课的思考题是:容器化技术不仅可以应用在部署过程中,还更广泛地被应用在部署后的项目服务运行中。试比较这两种场景下对容器化技术需求的差异性。
下节课我们将进入部署效率模块的最后一节课:如何搭建基本的前端高效部署系统。

View File

@ -0,0 +1,272 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 案例分析:搭建基本的前端高效部署系统
上节课的思考题是容器化部署与容器化运行服务的差异点有哪些。这里我总结三个有代表性的供你参考:
容器持续时间不同:容器化部署的容器只在部署时创建使用,部署完成后即删除;而容器化服务则通常长时间运行。
容器互联:容器化部署中的容器通常无须访问其他容器;而容器化服务则涉及多容器互联,以及更多弹性伸缩的容器特性。
容器资源:容器化部署中涉及构建等 CPU 和 I/O 密集型处理;而容器化服务则对网络负载更敏感。
在今天的课程里,我将带你分析一个基本的前端部署系统的工作流程、基本架构和主要功能模块的技术点。学习了这部分的内容之后,再结合之前几节课关于部署效率的内容,我们就可以基本掌握一个高效的前端构建部署系统的开发思路了。
流程梳理
要搭建一个自动化的构建部署系统,首先需要理解使用这个部署系统的工作流程。
构建部署工作流程
在下图中,我演示了从用户提交代码到项目部署上线的整个过程中,部署系统与其他节点对接的流程示意图。
其中的主要环节如下:
WebhookWebhook 是一种不同服务之间,通过订阅或推送模式来传递信息的消息通知机制。部署系统将一个 Webhook 接口注册到代码管理系统CVS中。开发人员提交代码后触发 CVS 的 Webhook由 CVS 将提交事件通知给部署系统。
项目构建:部署系统在获取提交代码的消息后会创建构建任务,并推入待执行队列中,系统将依次执行任务队列中的构建任务。构建任务在执行时依次执行代码获取、依赖安装、代码构建和产物打包等环节。
产物部署构建完成后的发布代码一般分为两种模式Push 模式和 Pull 模式。在 Push 模式下,由部署系统通过 SCP 等方式将产物包推送到目标服务器,并执行解压重启等发布流程。在 Pull 模式下会提供下载接口,由下游发布环节调用,然后获取产物包以便执行后续发布流程。同时,下游环节会调用反馈接口,将发布结果反馈至部署系统。
结果反馈:构建结果与部署结果会通过通知模块(消息、邮件等)的方式,反馈至开发与测试人员。
系统使用辅助流程
除了核心的构建部署流程外,系统还需要具备可供用户正常使用的其他辅助功能流程:
登录与用户管理:系统需要获取使用者的基本信息,并对其在系统内的使用权限进行管理。
项目流程:系统需要具备完整的项目接入流程,包括在系统内新增项目、修改项目部署配置、获取项目列表与查看项目详情等。
构建流程:系统界面中需要呈现项目的构建记录列表、构建详情等信息,并能通过界面操控构建任务的状态变更(新建、开始、取消、删除等)。
发布流程:系统界面中需要呈现项目的发布记录列表,并能通过界面操控构建记录的发布等。
以上就是一个基本的前端部署系统的工作流程。限于篇幅原因,课程里不再展开其中各个功能模块的具体细节,而主要介绍最核心的构建任务流程的相关技术点。
构建流程技术模块分析
这部分主要介绍部署服务器环境准备、Webhook、任务队列等 6 个技术点,首先是部署服务器环境准备。
部署服务器环境准备
与普通的 Web 服务不同,用于项目构建部署的服务器需要具备构建部署流程所需的相关环境条件。在非容器化的情况下,如果所搭建的是分布式的服务,则需要尽量保证一些环境条件的一致,以便在不同项目使用和迁移时,保持过程和产物的稳定性。
需要保持一致的环境条件如下:
NodeJSNodeJS 的版本会直接对项目的依赖和构建产生影响,需要尽可能地保证各部署服务节点与线上运行服务环境的 NodeJS 版本一致。
全局依赖工具:它是项目中可能需要的 Yarn、pnpm 等全局安装的工具。你需要保证预先在服务器中安装了它们,并确认版本的一致性。
各类配置文件与环境变量:这指的是 npm 和 Yarn 的配置文件、系统的配置文件 .bash_profile 等。你需要保证在部署服务器中提前配置相关预设。
系统所需其他工具:这指的是项目部署中所需的其他工具,例如 Git、Pigz、Zstd 等。你需要保证它们已在部署服务中提前安装完成。
服务目录划分与维护:除了部署服务自身的目录外,在服务器中还需要规划项目构建的工作目录、项目产物目录、依赖缓存目录、持久化缓存目录等。各目录还需要有各自的监控与清理策略。
Webhook
要实现用户提交代码后部署系统立即收到相关消息的功能,就需要事先在 CVS 系统(例如 Gitlab、Github 等)中创建 Webhook。具体流程如下
在 CVS 系统中创建 Web 应用,用于用户在部署系统中调取 Oauth 授权并获得用户的授权信息,以便在后续流程中调用各类 API。
在部署系统中新增接收 Webhook 消息的路由,用于后续接收来自 CVS 的提交信息后,在部署系统中创建构建,并进行后续工作。
用户在部署系统中新增项目时,会调用创建 Webhook 的接口,将上述路由地址写入 CVS 系统的 Webhook 列表中。同时可以根据需求设置特定的 Webhook 参数,例如只监听特定的分支或只监听 Tag Push 等。
任务队列
在部署系统接收到 Webhook 传递的代码提交信息后下一步就是根据提交信息创建构建记录并执行构建任务。但是由于执行构建任务是耗时的对于同一个项目而言如果当前有正在执行的构建任务时执行任务的工作目录是处于使用状态的此时需要把这期间新创建的构建任务排入待执行队列中等待当前任务执行完毕后再从队列中获取下一个任务执行。即使使用容器化构建部署构建任务在独立容器内进行也需要对整个部署系统的同时执行任务数Concurrency设定限制。我们需要将超过限制数量的新增任务排入队列中避免过多任务同时执行耗尽集群计算资源。
在 NodeJS 中,有一些管理队列的工具可供选用,例如 Bull、Agenda 等。以 Bull 为例,下面的示例代码就演示了部署系统中创建队列、添加构建任务、任务处理、任务完成的流转过程。
// 创建任务队列
queue = new Queue(qname, {
redis: redisConfig,
})
queue.promiseDone = () => {}
queue.process(async (job, done) => {
const config = job.data
const task = new BuildTask(config) //创建并执行构建任务
queue.promiseDone = done //将任务完成函数赋值给外部属性,用于异步完成
})
return queue
}
export const queueJobComplete = async (id) => {
queue.promiseDone()
}
export const queueJobFail = async (id, err) => {
queue.promiseDone(new Error(err))
}
export const queueJobAdd= async (id, data) => {
queue.add(data, {
jobId: id, //jobId of queue
})
}
构建任务阶段与插件系统
在之前的课程介绍过,部署系统中一次完整的构建任务大致可分为以下基本阶段:
初始化阶段:系统新建构建任务,初始化各配置参数与任务状态数据。
获取代码阶段:根据任务配置,在任务工作目录中获取待构建的项目代码。
依赖安装阶段:在执行构建编译前进行依赖安装。依赖安装的脚本可以写在项目配置中,也可以由系统自主分析获取。
构建执行阶段:执行构建过程,输出产物代码。构建过程的执行脚本需要写在项目配置中。
产物打包阶段:将构建产物打包压缩,并存储到持久化备份目录中。
这些阶段的划分可以起到以下作用:
明确构建执行进展,当构建中断时便于定位到具体的执行阶段。
各阶段独立统计耗时,便于针对性优化。
可参照构建效率模块中介绍过的 Webpack 插件系统,使用 Tapable 定义各阶段的 Hooks从而将复杂的构建任务执行过程拆分到各功能插件中。这些插件可以是系统性的例如在依赖安装阶段可以应用依赖安装目录缓存插件在构建执行阶段前后可以应用构建持久化缓存插件。这些插件也可以是业务功能性的例如分支合并检查插件、代码规范检查插件等。
任务命令与子进程
和普通的 Web 服务不同,部署服务在对项目进行构建部署时,涉及许多命令行指令的调用。如下所示:
#依赖安装
npm install
#执行构建
npm run build
#产物打包
tar -zcf client.tar.gz dist/
在 NodeJS 程序中,这些调用需要通过子进程来完成,例如下面的代码:
import { spawn } from 'child_process'
export const spawnPromise = ({ commands, cwd, onStdout, onStderr }) => {
return new Promise((resolve, reject) => {
onStdout = onStdout || (() => {})
onStderr = onStderr || (() => {})
const subProcess = spawn('bash', { detached: true, cwd })
subProcess.on('close', (code, signal) => {
if (signal === 'SIGHUP') {
//abort callback immediately after kill
return reject()
}
if (code === 0) {
resolve('ok')
} else {
reject()
}
})
subProcess.stdout.setEncoding('utf8')
subProcess.stderr.setEncoding('utf8')
subProcess.stdout.on('data', onStdout)
subProcess.stderr.on('data', onStderr)
subProcess.stdin.on('error', (e) => {
notifySysError('subprocess stdin error', e)
reject(e)
})
commands.forEach((command) => {
subProcess.stdin.write(command + '\n')
})
subProcess.stdin.end()
})
}
我创建了一个 bash 的子进程,输入执行指令,然后监听输出信息和结束状态。通过这样的方式,即可控制各构件阶段指令的执行。
状态、事件与 Socket
除了把构建过程划分成各执行阶段外,还需要定义一次构建任务的所有可能状态:
初始化:该状态表示已部署服务接收到了来自 Webhook 的提交信息,并提取了构建所需的所有配置数据,同时也已创建了对应的构建记录。
队列中:该状态表示该构建任务已列入等待队列中。
进行中:该状态表示任务已开始执行。
已取消:该状态表示任务已被用户主动取消执行。
已成功:该状态表示构建任务已完成,用户可以进行下一步的发布流程。
已失败:该状态表示构建任务已失败,需要用户确认失败原因并调试修复。
已超时:该状态表示构建任务已超时。在实际使用过程中,如果发现一些异常情况,不会终止构建进程,因此需要设置超时时间来发现和反馈这些异常情况。
这 7 种状态中的后 4 种为终止状态。在部署系统中,需要将这些状态及时反馈到用户界面。
整个传递机制可以分为下面三个部分:
在构建任务中,当达到特定终止状态时,由服务进程触发相应事件。
在构建事件处理器中,根据监听到的不同事件执行相应的处理,例如对于构建成功的事件而言,我们需要变更数据库中的构建记录状态、执行自动发布的相关逻辑,以及将成功的状态通知到 Socket 处理器。
在 Socket 处理器中,服务器端触发相应的 Socket 消息,然后网页端在接收到 Socket 消息后,会变更页面中的构建记录显示状态。
总结
构建部署系统相对于我们日常比较熟悉的 B 端系统或 C 端 WebApp 而言,有一定的复杂性。但是只要理解了工作原理且掌握了整体架构,就可以按部就班地开发其中的各个模块,最后串接成一个功能完善、流程自洽的系统服务。所以本节课我们聊了两方面的内容:流程梳理和核心技术模块分析。
在流程梳理方面,首先你需要对构建部署的整体工作流程有一个比较清晰的认知,包括各服务间的对接、信息的传递等,其次掌握服务内部用户界面的各模块操作流程。在核心构建流程的模块分析方面,你需要了解操作层面的服务器环境的准备工作,代码架构层面的任务队列、构建任务阶段与状态拆分等。
希望通过这些内容,能让你对如何搭建高效的前端部署系统有一个初步印象。
到这里,我们的专栏就接近尾声了。下周还会更新一篇结束语,我会聊聊对开设课程的一些想法,包括对前端工程化领域的一些理解,以及对未来技术的展望。欢迎来听!
最后,我邀请你参与对本专栏的评价,你的每一个观点对我们来说都是最重要的。点击链接,即可参与评价,还有机会获得惊喜奖品!

View File

@ -0,0 +1,51 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 结束语 前端效率工程化的未来展望
你好,我是李思嘉。
本专栏的内容到这里就结束了。我们先来简单回顾一下整个课程的主要内容,如下图:
在这个专栏中,我主要介绍且梳理了前端工程化中效率提升方向的知识,内容涵盖开发效率、构建效率和部署效率三个方面。希望你通过这个系列课程的学习,能建立起前端效率工程化方面相对完整的知识体系,同时在前端开发日常流程中的效率工程类问题方面,能找到分析和解决的新方向。
当然,这些方向实际涵盖的概念与技术点非常广泛,并不容易完全掌握,除了已有的概念和技术之外,新的技术和方向也在不断涌现。下面我会对前端效率工程化相关的技术做一些展望。
云工作流
开发效率方面,由 Web IDE 发展而来的云开发工具,目前正逐渐成为几个大型厂商探索的方向之一。从开发到部署的完整云端工作流方式在未来可能成为标准。
AI 生成页面
和目前依赖设计资源与开发基建的低代码或无代码开发相比,基于 AI 的生成页面工具(例如微软的 Sketch2Code可以进一步解放生产力。目前无论是基于设计稿Sketch/PSD的精准生成方式还是基于草图乃至描述语句的 AI 匹配生成方式,仍有各自的局限性。但是随着 AI 技术的发展,这类产品可能会替代目前前端开发过程中的一些基础重复性工作。
Go/Rust
在构建效率方面Webpack 5 带来了更完整的缓存策略和代码优化策略,但是从底层性能上,构建工具本身的性能仍然受到 NodeJS 自身语言的限制。从 esbuild 工具的思路出发,基于 Go、Rust 等高性能语言的编译工具在未来或许能成为性能突破点之一。
No bundle & HTTP/3
构建效率另一个方向的发展来自无包构建。尽管无包构建工具在生产环境下仍然采用打包构建的方式,但随着网络技术(例如 HTTP/3的发展或许最终可以在生产环境下同样采用无包构建。此外渐进式的使用方式例如 vendor 部分打包而源码部分无包使用)也可能很快成为可实现的方向。
总结
希望我的专栏内容和对未来趋势的展望能对你有所帮助。学习时,你需要在日常工作中不断实践,结合具体的场景尝试可行的优化方案。
由于自身在团队中的职责不同,每个开发人员索要学习和侧重的点也不同:
如果你目前主要做的是具体项目的开发维护工作,那么分析现有项目的构建工具、构建配置就是一个很好的入手点。通过构建效率模块的相关课程,相信你可以比之前更全面地进行分析和优化。
如果你目前承担着多个项目的选型与架构工作,希望开发效率模块的一些视角可以为你带来思路。
如果你目前从事的是前端基础建设的相关工作,希望系列课程中提到的一些新的开发、构建和部署工具也能为你提供一些着手方向。
前端工程化是一个系统工程,不同的开发人员都可以在团队中找到适合的位置。通过不断地实战开发和经验积累,相信你可以逐步提升对技术的认识,增强技术实力。
如果你觉得课程不错,不要忘了推荐给身边的朋友。前路漫漫,一起加油!