2020 年已经结束,这一年里面因为疫情,生活和工作中大家都有受到一定的影响。但是在 2020 年里面前端技术的发展依然没有停止脚步。
而我们作为前端开发者,必定需要对技术的更新换代有所了解。虽然我们不需要去学习所有新出来的技术。但是时刻保持 “了解” 和 “理解” 这些技术是有必要的。
了解这些新的技术和趋势,有效让我们成为更好的开发者,同时在我们日常工作中,这些知识有效帮助我们去解决工作中的技术问题,或者在一个问题中看到更多的解决办法和可能性。
这篇文章盘点了 2020 年里面,关于前端的一些新技术、它们的发展和趋势。
但是我们并不需要所有都去深入了解,我们的重点应该放在这几个方面:
「1」微前端 (Miro frontends)
"微前端" 应该是我们 2020 年里听的最多的一个前端技术。现在非常多的大厂都在尝试这个新技术来解决大型前端项目中的问题。
虽然我们前端开发中有模块化(modular)的组件(components),但是它相比后端的 “微服务” 是大有不同的。
在了解 “微前端” 之前,我们先给没有接触过后端的同学科补一下后端的 “微服务” 知识。
微服务是什么?
!! 微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务由各个小型独立团队负责。
上面的专业术语不好理解的话,我们也可以这么理解。微服务 —— 也就是把单独一个业务写成一个独立的服务,有独立的服务器、接口体系、并且有一个单独的团队进行开发与维护。
就比如,淘宝的订单和用户两个 “模块”。一开始这些模块我们可能都会设计在一起的。这样开发起来需要的开发资源和时间就相对比较小。毕竟微服务不是一个 5 - 6 人团队可以 hold 得住的。
等业务变得非常庞大的时候,随着功能和数据的不断在扩大和延伸。这个时候订单和用户模块都变得非常的复杂。这个时候就需要我们把这两个模块单独的拆离出来开发和维护。因为,如果我们不拆离出来,每次改这些模块的一个小功能,都会涉及很多影响。我们就会发现这个系统越来越难迭代,改一个功能或者 bug 都会开始变得非常的困难。
这个就是为什么后端提出了微服务这个概念,为了解决大型应用的维护难题。把业务解耦、隔离、独立化就是解决方案。
既然后端会遇到系统复杂程度过高的情况,自然前端的业务和交互也会越来越复杂。所以最后前端也需要和后端一起建立 “微服务化” 的体系。
微前端是什么?
微前端的起源和后端一样,就是一个模块因为业务和功能的持续发展,导致模块内容的代码越来越复杂,越来越庞大,同时就会造成与其他业务有不可避免的互相影响的关系。
就是这些模块与模块之间的关系,让我们的系统变得越来越难以维护,大大减低了开发的 “敏捷性”。
那么怎么办呢?我们就让一个大型的 app 的业务,按模块拆分成一个一个小的 “服务” 模块。这些小的模块会与后端的接口一样,放在单独的服务器上运行,同时也会有单独的小团队进行专门的开发和维护。
!! 这种微型的独立前端架构体系和系统就叫做 “微前端”。
每个 “微前端” 应用的开发团队,拥有整个应用的生命周期中的自主性。这就包块独立开发、独立版本号管理、独立测试、独立打包、独立渲染、独立更新、独立部署。
在上面的图中,我们就可以看到微前端在一个研发中心里的组织架构。每一个微前端团队都会负责一个独立的业务模块。每一个模块都会有他们自己的核心业务。与其他业务的功能其实是相对独立的,但是业务的流程上他们就会通过数据流、交互流、接口等方面串联起来。
最终用户所体验到的系统,其实跟我们普通的应用没有什么区别。但是大型应用的开发和维护的角度来看,就获得了非常多的好处。
微前端的好处微前端案例
我们了解了微前端是什么,并且它有什么用。那么我们来看看一个微前端在实际开发场景中是怎么样的。
这里幻想一下,我们有一个博客的主页。主要的功能就是为了展示我们的文章内容。当然一个博客其实很简单,在正常开发场景下,也不会给它做微前端的架构设计。
但是如果我们真的想把它做的更强大,那也是可以的:
当然这个还不是很复杂,但是当我们继续往这个博客平台加入更多的功能,比如最后做成 CSDN、掘金、知乎这样的平台的时候,那么微前端就有必要了!
微前端集成方法
好,如果真的有那么一天,我们变成一个很大的博客平台。那么我们要怎么运用微前端呢?其实集成方法有很多种:
!! 我们这里就不一一去讲解了,如果我们真的哪一天遇到一个场景需要我们用微前端去解决我们的技术难题时。我们再去深入学习微前端即可。这里主要的目的是让我们了解清楚微前端是什么,能解决什么,它是怎么解决的即可。
其实所有的集成方法,都围绕着一个很自然的设计模式 —— 一个应用里面的每一个页面都是一个 “单独的” 微前端应用。而在每一个页面中都会有一个“单独的应用容器(single container application),而它的作用就是:
大概的页面设计如下:
!! 注意我们这里只讲到微前端的皮毛,其实微前端最核心并不在前端,更多的难点都是在 “云服务器”。如果没有服务器架构、部署、测试和后端接口等等的支撑,微前端是很难以实现的。所以在一般公司的应用场景,其实根本用不上微前端。如果用不上,了解到这里其实已经可以了。到用得上的时候再去深挖更多的知识即可!
「2」原子设计 (Atomic Design)
原子设计也是在 2020 年里面提到的非常多的一个概念。不过这个更多只是一个概念,并不是一个纯方法论。
这个概念是 Brad Frost 提出的,他用化学中的原子组成来分析和拆解一个 web 应用的组成成分。里面包换原子(Atoms)、分子(Molecules)、生物体(Organisms)等。
比如一个搜索分子(Search Molecule)就是由 (输入文本)+ (按钮)+ (标题)等原子所组成。然后这些分子组合起来就会变成一个生物体(Organism)。
而生物体(Organism)就会生存在我们页面的布局模版之中,而这些就可以具体化成一个页面,最后显示给到我们的用户。
Brad Frost 把我们前端的组件和界面的设计抽象成化学中的原子组成结构。让我们更加清晰的去理解一个页面、一个组件、一个元素的组成方式。
其实这个概念,可以让我们用一个全新的角度去看待模块化 UI(modular UI)。这种思维方式让我们更深入的理解一个组件的作用和 API,从而在设计组件的时候会更加的清晰和高效。
一个简单易懂的表示图:
原子(Atom)是什么?
原子是物质的基本组成部分。而在组件设计中,原子就是一个组件的最小元素单位,一个组件都是用多个原子所组成的。
在 web 界面中,原子就可以是:HTML 标签
!! 原子单独使用是没有任何意义的,一般都是需要组合其他原子一起使用才能真正发挥它们的作用。
分子(Molecule)是什么?
当我们把原子(Atoms)结合在一起时,事情开始变得更加的有趣。分子(Molecule)是一组结合在一起的原子,是化合物中最小的基本单位。每一个分子都有自己的特性,是我们设计系统的支柱(Backbone)。
比如,一个表格标题、输入框、按钮等元素,它们单独使用时没有太大用处的。但是如果我们把它们组合在一起,变成一个表格。这个时候它们就可以使用了。
在我们用原子组合分子时,我们需要遵循一个原则:“只做一件事,并且做好”。(这个听起来是不是有点像设计模式里面的:“单一职责”?是的,就是这个意思。)虽然分子可能很复杂,但根据经验,他们相对而然都是一些简单的原子而组成的,主要是为了可复用性而生。
生物(Organism)是什么?
分子可以作为我们组建界面的积木。我们可以把分子组合在一起来建立一个生物体(也就是我们的一个界面)。生物体是用一组分子组合而成的,而组成的生物体是界面的其中一部分,它有相对复杂,独特的特性。
就比如上图,我们在组合了一个网页 logo 部分的分子。然后我们把搜索分子和 logo 分子再组合在一起,就变成了界面上头部(生物体)的部分(header organism)。
这个就会开始变得越来越有趣了,生物体不是一个固定的组件,它是用多个分子所组合而成。不同的生物体可能存在使用相同的分子或者原子,但是他们的作用都是有所不同的。
就比如,我们的 logo 和文章图片两个不同的分子,他们都使用了 图片标签这个原子。但是这个 原子是放在了两个不同职责的分子里面,一个是用在头部作为 logo 展示的,一个是用在文章列表用来展示图片的。
!! 所以在组建分子和用分子组件生物体的时候,我们创建的组件必须是独立的、可移植、可重用的。
模版(Templates)
有了分子组成的生物体(Organism)后, 我们就可以用这些生物体来组建我们的界面模版。这里我们就可以打破化学的类比,拥有对用户和最终输出更有意义的东西。
模版主要是由多个生物体缝合在一起而形成的页面。在这里,我们开始看到组件和模块的设计开始融合在一起,可以看到布局之类的东西开始有所体现了。
模版是非常具体化的,它为所有的这些相对抽象的分子和生物体提供了上下文。模版阶段也是用户可以开始看到设计界面的地方。根据这个设计的创作者 Brad Frost 的使用经验,模版一开始是 HTML 线框,但随着时间的推移,它的保真度会逐渐提高,最终成为可交付的产品。
页面(Pages)
页面(pages)是模版的特定实例。这里,占位符内容被真实的、有代表性的内容所取代,以准确地表现方式描述了用户最终将要看到的内容。
页面是最真实的,因为它们是最有形态的,它通常是大多数人在开发过程中花最多时间来处理和优化的部分。
页面这个阶段非常的重要,因为这里是我们测试设计系统有效性的地方。在上下文本中查看一切,是我们能够回头来修改我们的分子(Molecules)、生物体(Organism)和模版(Templates)的阶段。
页面也是测试模版变化的地方。例如,我们可能想清楚地表达包含40个字符的标题最终展示到给用户是什么样子的,但是也想演示340个字符是什么样子。当用户的购物车中有一件商品应用了折扣的时候,商品的展示会是怎么样的。这些实际的情况会影响我们如何循环和构建我们的应用。
为什么用原子设计理念?
我们简单理解了原子设计后,我们就可以问问,为什么要用原子设计呢?
其实答案很简单,我们平时去设计一个应用的时候,即使我们没有意识去使用这种思维方式,但是我们一直都是以这种方式在设计的。
原子设计为设计系统提供了一种清晰的方式轮(概念为主),团队成员(开发者、产品经理、设计师等)能够通过实际看到的,摆在他们面前的步骤来更好地理解设计系统的概念。
原子设计赋予我们从抽象到具体的能力。正因如此,我们可以创建促进一致性和可伸缩性的系统,同时在最终的页面中显示内容。
!! 重点是通过组合而不是解耦的方式来构建应用。我们在一开始就创造了一个系统,而不是在事后才去挑选模式。
「3」封装样式和 Shadow DOM
组件开发最重要的一个方面是封装(Encapsulation)—— 就是能把标记(markup)和行为隐藏起来,并与页面上的其他代码分开,这样不同的部分就不会冲突,代码也可以保持整洁。
而要做到这样,Shadow DOM API 就是关键。它可以将一个隐藏的、独立的 DOM 附加到一个元素上。
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。
这里,有一些 Shadow DOM 特有的术语需要我们了解:
我们可以使用与常规 DOM 一样的方式来操作 Shadow DOM —— 例如,添加一个子节点、设置属性、以及为节点添加自己的样式,或者为整个 Shadow DOM 添加样式。
与普通 DOM 不一样的是,Shadow DOM 内部的元素始终不会影响它外部的元素(除了 ),这为封装提供了便利。
其实 Shadow DOM 并不是一个全新的东西,它已经被浏览器使用了很长一段时间了。浏览器用它来封装一个写元素的内部结构,以一个有着默认播放控制按钮 元素为例。
我们所能看到的只是一个 标签,实际上,在它的 Shadow DOM 中,包含了一系列的按钮和其他控制器。Shadow DOM 标准允许我们自己的元素(custom element)维护一组 Shadow DOM。
!! 这种方式也常常被认同为是组件样式封装的最佳解决方案。
基本用法
首先我们可以使用 方法来将一个 shadow root 附加到任何一个元素上。
这个方法接受一个配置对象作为参数,这个对象有一个 属性,它的值可以是 或者 :
: 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM, 例如使用 属性:
但是如果我们把 Shadow root 附加到一个自定义元素(Custom element)上,并且把 设置为 ,那么我们就不可以从外部获取到 Shadow DOM 了 —— myCustomElem.shadowRoot 将会返回 。
其实浏览器中的 就是这样的,里面就包含了一个不可访问的 Shadow DOM。
如果你想将一个 Shadow DOM 附加到 custom element 上,可以在 custom element 的构造函数中添加如下实现(目前,这是 shadow DOM 最实用的用法):
将 Shadow DOM 附加到一个元素之后,就可以使用 DOM APIs 对它进行操作,就和处理常规 DOM 一样。
!! 详细的使用方式,可以直接去看 MDN 的 《使用 shadow DOM》。
「4」TypeScript 的崛起
最近也有不少文章讲的好像 TypeScript 要一统整个前端江湖。有报告统计,80% 的开发者承认,他们想在下一个项目中使用或者学习 TypeScript。
虽然 TypeScript 还是有它的缺点的,但它让代码更容易理解(对于习惯编写面向对象的同学,确实好理解,但是不熟悉面向对象的同学,就有点难受了)。并且 TypeScript 有利于快速实现,上生产后的 bug 也更少。
那么我们应不应该转 TypeScript 呢?在决定之前,我们还是冷静一下,看看使用 TypeScript 的优缺点,然后根据我们自己团队的技术情况再做选择才是合理的。
使用 TS 的好处1. 代码更容易理解
很多时候当我们在看一段代码中的一个函数,我们都会问以下问题:
这个参数是可以接受那些的呢?
函数会返回什么值呢?
它需要什么外部数据呢?
它做了什么可以把输入参数变成输出的返回值呢?
在动态类型语言中,通常很难回答前三个问题。
但是在像 TypeScript 这样的静态类型语言中,我们可以立即从 IDE 和编译器中得到上述的所有问题的答案。 不再需要查看整个代码库,不断地向我们的同事提出问题,或者在生产上出错误的风险。
2. 更容易,更快的实现代码
当我想要实现一个新的功能或者组件,我们的工作流大概是这样子的:
初始化组件函数,建立它的构造函数的参数,编写剩下的代码。
如果它需要任何外部或复杂的数据(如用户或文章),那么就要将它保存在我们自己的内存中,并在代码中使用它。
将组件放入你的应用中,并将 props 传递给它。
然后就是测试这个组件,手动或者使用单元测试(这里我们需要确保它能接收到该有的 props,并且它能正常工作。)
如果有什么地方出现了 bug,那么就要回到我们的代码,试着找到哪里出了问题。再回到第 1 步。
上述是在使用 JavaScript 的开发流,那么如果我们用的是 TypeScript 的话就会变得更简单,更高效了:
初始化组件函数,定义它的类型,并且实现它。
如果它需要任何外部或复杂的数据,直接查找它们的接口并复用它们(全部或部分)。
将组件放入你的应用中,并将 props 传递给它。
就是这样!(如果在调用者和被调用者之间正确地匹配了类型定义,那么一切都应该能够完美地工作。现在唯一需要测试的是组件的实际业务逻辑。)
是不是简单很多了?这个也是为什么 TypeScript 出现低级错误概的率会更少的原因。
3. 代码容易重构
在开发中,我们肯定是会有很多代码在编写后,都需要我们去重构的。但是因为往往都涉及太多的东西和文件,所以我们开发者一听到一个项目需要重构代码基本都是这样:
在 TypeScript 中,这类重构的事情变得不那么可怕。往往一个重构只需要轻轻在 IDE 中按一下 “重命名符号(Rename Symbol)” 命令即可。
在动态类型语言中,同时重构多个文件时,我们能得到的最好的帮助就是使用RegExp 进行搜索和替换。
而在静态类型语言中,不再需要搜索和替换了。使用IDE命令,如“查找所有出现的情况”和“重命名符号”,可以看到应用程序中特定的函数、类或对象接口属性的所有出现情况。
如果我们想稍微改进我们的构建系统、重命名组件、还是修改对象或者删除废弃的属性,我们都不必担心破坏任何东西。TypeScript 会帮助我们找到重构后的这些相关联的东西的用法,重新命名它,并如果在我们重构后出现了类型不匹配的情况,它就会发出编译错误的警告。
4. 更少的 bugs
做了多年的前端开发,我们都知道如果有一个人在我们旁边,当我们拼写错变量名、使用了一个不一定会是 null 的值、或者传了一个对象类型参数给一个接收数组类型的函数的时候,能提醒一下我们,那么我们就可以省下 50% 的排错时间。
很高兴的告诉大家,这个好基友就是 TypeScript。
多谢它,现在想编写一个 bug 也变得相对难了。当我们的代码打包的时候,我们可以大概知道我们的代码是可以运行的通的。
5. 更少的样板测试
当我们确定我们传的参数是对的,那么我们就不用去测试所有地方调用了某个方法是否会报错,对方有没有用对参数,用对类型,用对我们提供的方法。
少了这些的顾虑,我们就可以更多的集中在编写业务逻辑的单元测试,而不是代码的语法和准确性的保障测试。
既然我们减少了检测代码中报错的问题,我们就可以花更多的时间来完成新功能和需求。这样我们的代码就不会很复杂,更不容易出错,更容易维护。
6. 帮助开发者写更好的代码
当我们在编写静态类型语言时,首先我们要思考,我们需要的数据类型是怎么样的,然后想我们想产出的数据又是怎么样的。这个过程就需要在我们坐下来开始写代码之前就要想清楚。
其实还有很多同学在开始实现一个功能和需求之前,缺乏了思考和分析。没有认真的思考过整个代码如何编写和逻辑的思路、实现的方法、数据的类型、函数的结构等。但是对于老司机程序员来说,很多都是先思考然后才去动键盘的。
那么 TypeScript 就会要求我们遵循这种好的开发思维。它鼓励我们在坐下来实现功能和代码之前,先要考虑清楚我们要实现的功能的接口类。
TypeScript 的缺点
说了那么多 TypeScript 的优点,我们也来讲讲它的缺点。
1. 需要编译步骤
如果我们是开发 node.js 后端的开发者,使用 TypeScript 确实会变得比较麻烦。因为我们需要先编译了 的文件,然后才能在 Node 上去运行。
当然如果我们有一个好的打包工具链,这个问题还是可以解决的。但是确实会给 Node.js 后端的同学带来本来没有的麻烦。
不过相对前端开发者来说,这个并不是任何问题。因为现代的前端开发流程,我们都会把所有的前端代码通过 、 等打包工具来编译我们的代码。
那么 文件在这个过程已经自动的帮我们编译过来了。所以我们不需要做任何附加的步骤。顶多也是在打包工具中使用 多安装一个编译插件。
2. 设置起来有点困难
不可置疑,这一点确实是事实。比如,在普通 Next.js 和使用 TypeScript 的 Next.js 之间,使用了 TypeScript 我们就要附加 Node.js 服务器、webpack 和 jest test 测试工具。
还有,当我们需要添加一个像 React、Redux、Styled-Components 等 library 的时候,我们都要为他们附加 。(比如 ,不过有一些包本身就自带有 TS typedefs 文件)
但我觉得这些问题并不大,因为在长期开发和维护一个项目的过程中,安装依赖包和设置一个 TypeScript 项目,相对比我们平时编写算法和业务逻辑的频率,其实是非常微小的一部分。基本上这些繁琐的事情都是一次过搞好就行,如果还是觉得繁琐,做一个 toolchain 工具,每次需要操作这些重复的事情的时候,直接用工具帮我们构建即可。
那要不要用 TypeScript 呢?
TypeScript 固然是好,毕竟 Vue 3 的重构中也在很多语言中最后选择了 TypeScript 作为他们的核心语言。
所以 TypeScript 的好处是显而易见的。但是任何的技术选型都要看我们自己所在的团队和公司。如果有 10 个开发者,9 个都不会 TypeScript,并且都不太愿意去学,或者公司根本给不到时间让我们团队去学习新的语言。
那么选择换这个语言就不是最佳的选择。
!! 没有最好的技术,只有最合适当下使用的技术。
「5」Web 组件
Web 组件就是前端的未来。 为什么呢?
因为纯 web 组件与任何的前端框架无关,他们可以在没有框架或者使用任何框架的情况下工作。
因为这些组件更加不受 “JS 疲劳(JavaScript Fatigue)” 所控制,并且大部分浏览器都是支持的。
因为它们的包大小和消耗是更有优势的,加上 VDOM 渲染拥有令人震惊的能力。
这些组件提供了自定义元素、一个 Javascript API (允许我们定义一种新的 html 标记)、html 模板来指定布局,当然还有 Shadow DOM(本质上是特定于组件的)。
!! 这里解释一下什么是 JS 疲劳 —— 就是当我们想学习前端的时候,第一个接触的语言必然就是 JavaScript,但是一旦开展学习后,就会发现这个水是真的深,一大窜的其他关联知识就会如同潮水一样扑面而来。包括,前端框架、Node.js、UI 框架、浏览器知识、工具链等等。学习前端一开始确实是一个非常疲劳的一个领域。
当我们思考未来 UI 开发,以及模块化、可重用性、封装性和标准化的原则时,组件(化)时代应该是怎么样子时,web 组件就会浮现在未来的蓝图上。