软件架构的核心思想,就是推断软件系统各个组件之间数据流动的方式。软件架构的质量取决于你设法推断这些数据流的难易程度!本文要讲的内容,就是在今天的 Web 应用程序背后探索这些数据流和最终的体系结构。Web 应用已从简单的静态网站(双层结构)发展为复杂的多层次、SPA 和 SSR 驱动的 API 优先系统。CMS 系统已发展成无头(Headless)和内容优先的系统。
当代前端架构
现代应用程序在各个层面都非常接近桌面应用程序。当今环境中的一些显著变化包括:
Web 不再局限于台式机平台。我们的设备类型多种多样,如平板电脑、手机、智能手表和智能眼镜等等。事实上,智能手机驱动的 Web 流量超过了台式机平台。
作为 Web 的支柱,JavaScript 已经发展成为一种成熟的语言,并且还在不断发展,不断引入众多新功能。
文件系统、相机、PWA 和硬件传感器等新型 API 都能用在 Web 应用程序中。
用户体验是用户在互相竞争的服务之间做出选择的决定性因素。
网络基础设施有所改善,但与此同时又有数以十亿计的收入底层用户正在加入 Web 世界。流媒体和视频点播都变成了日常事务。
人们基于同一套 JS 技术栈来使用各种工具和交叉编译器构建原生移动应用程序。
许多新的框架和工具使用 JavaScript 作为目标语言,而非将其用作源语言。一些不错的例子有 Elm、PureScript 和 ReasonML 等等。
当代前端架构是这些不断变化的需求的反映。顾名思义,它们建立在整个前端社区从过去学到的经验之上。
早期的软件架构模式建立在有限的硬件功能上并尊重这一事实。今天的情况已经变了。计算能力在不断提升,而且软件架构反映了这种观点。
以上假设可以与今天的前端架构相关联。这些架构融合了三大核心原则。
数据流占据舞台中心
考虑到前端代码需要运行的领域众多、环境多样,业务逻辑占据了中心位置。作为工程师,我们花费更多时间来阅读和调试,而非编写代码;我们必须直观地跟踪代码。每当修复错误或向现有代码库添加新功能时,了解其可能导致的影响和回归是至关重要的。
在大型代码库中做到这一点的唯一方法是确保我们了解数据在应用程序中的流动方式。这是软件架构最重要的目标。
今天,任何框架都是围绕这条关键原则构建的。明确区分状态和视图是非常重要的——鉴于简单的基于事件的容器在向更复杂的状态容器(如 Redux、Vuex 和 Ngrx 等)转变,这也是显而易见的。由于我们严重依赖事件或发布/订阅系统,数据(或控制流)将流经应用程序的每一寸角落。对数据流的重视不再局限在局部环境。相反,作为一种核心思想,现在我们要在整个应用程序中考虑数据流。
Angular 1 已经证明双向数据流(即时只局限在视图或组件上)可以带来纷繁复杂的控制流。此外,React 已经证明单向数据流很容易推断,因此包括 Angular 2 在内的现代框架也遵循了这一理念。
基于组件的架构
转向基于组件的用户界面是数据流第一原则的必然结果。在谈论组件时,有三个方面需要考虑。
首先是数据流的重点:传统架构侧重于水平分工。但是基于数据流的思维要求垂直分工。在敏捷世界中,这就是我们设想用户故事的方式。基于 MVC 的架构不容易做到这一点。没有简单的方法可以通过一项功能共享或复用 UI 代码(这里的问题是——当功能在横向组织推动下在整个代码库中传播时,我们怎样才能真正隔离功能呢!)。但是,封装入一个完整功能的组件可以轻松配置为可共享和可打包的实体。
其次是不断发展的视图状态:过去,视图状态与业务状态相比占比较少。但随着应用程序变得更具交互性,其占比也得到了爆发式增长。视图状态需要接近实际视图。视图状态很复杂,通常表示必需的时间关联数据,如动画和转换等。类似 MVC 的架构没有什么很好的方法来封装这种状态。在这里,作为封装核心单元的组件非常有用。
第三个方面与 UI 开发的原子单元有关。首先我们提出一个问题:
共享 UI 功能的最佳方式是什么?共享 UI 功能意味着它可以包含以下四个部分:
结构(HTML——视图)、样式(CSS——视图)、行为(JavaScript——视图模型)和业务逻辑(模型)。
于是组件的概念应运而生。我们意识到组件只是 MVVM 模式或 MV*模式的一个很好的实现……
但具体而言,如何表示 UI 组件?我能找到的最接近的概述是 Deric Baily 的博客文章。在语言级别,通常使用模块或包来表示可复用功能的构建块。JavaScript 也有模块。但这还不够。
组件的这种抽象概念允许框架作者根据他们的需要定义具体的实现。
此外,良好的组件是高度可组合的,可实现分形架构。例如,登录表单组件可以是标准登录页面的一部分,或者在会话超时时显示为对话框。一个组件只要定义好接收的属性和发送的事件,就可以在满足基础框架要求的前提下复用在任何地方。
组件可以是由 Webpack 等打包器生成的函数、类、模块或打包代码。
如前所述,组件只是一个 MVVM 模式的良好实现。并且由于组件的可组合性,我们将 MVVM 模式实现为分形(分形是一种自我重复的无止境模式)。这意味着我们在多个抽象级别处理多个独立的的 MVVM 控制流。
实现 MVVM 的无限组件分型——循环套着循环!是不是很像《盗梦空间》?
而且,这正是我们对架构所期望的。
一个好的架构可以实现多级抽象。它允许我们一次查看一个抽象(细节级别)而不必担心其他级别。这是制作可测试和可读解决方案的关键所在。
让框架处理 DOM
DOM 用起来既昂贵又繁琐。当状态(本地或全局)发生变化时,DOM 能以某种方式自动更新就最好了。此外,它应尽可能高效同时不干扰其他元素。在将 DOM 与状态同步时有些事情需要注意。
将 DOM 表示为模型的函数:与组件相结合,声明式数据绑定是用模型表示视图的一个很好的解决方案。Angular 有模板,React 使用 JSX,Vue 支持 JSX 和模板。结合数据绑定和组件,我们得到了一个完美的 MVVM。
更改检测:框架需要一种机制来识别状态中所有数据的变化。Angular 1 使用的是昂贵的消化周期,React 则支持不可变数据结构。使用不可变数据检测状态更改只是一个等式检查而已。不需要脏检查了!Vue.js依赖建立在 getter/setter 之上的反应系统;Angular使用区域检测更改;Vue 3 将使用 ES2015 代理处理反应性。
更新 DOM:在检测到实际更改后,框架需要更新 DOM。许多框架(如 React、Vue 和 Preact 等)使用虚拟 DOM(对时间优化),而 Angular 使用增量 DOM(对内存优化)。
定义当代前端架构
早期 GUI 架构的范围主要集中在代码结构和组织上。它主要关注模型与其表示之间的关系,也就是视图。那是当时的需求。
今天情况已经发生了变化。现代前端架构的需求已经远不止简单的代码组织。在社区中,我们已经把这些知识传承了下来。
大多数现代框架已经在组件的帮助下标准化了代码结构和组织概念。它们是当今架构的基本单元。
还应注意,架构与其实现框架密切相关。这可以追溯到这样一个事实,也就是 GUI 模式很复杂,并且由于它直接对接用户而无法独立于其实现来讨论。我们可以继续说一个框架就是它的架构。
设计良好的组件系统是任何前端架构或框架的最基本需求。除此之外,它必须解决前面讨论的许多其他问题,如声明式 DOM 抽象、显式数据流、更改检测等。大多数元素都在下图中高亮显示:
现代前端架构元素
考虑到所有这些因素,我们可能会想要创建一个参考架构,但这根本不可能。每个框架或库在实现时都有自己的库特性。例如,React 和 Vue 都支持单向数据流,但 React 更喜欢不可变数据,而 Vue 则支持可变性。因此,最好针对单个框架来全面了解其架构。
话虽如此,我们仍然可以尝试创建近似的参考架构,如下所示:
现代参考架构
到目前为止我们描述的所有特征或元素都包含在这个架构中。它干净地映射到三层架构,但第二层的中间件是可选的。第二层表示 UI 服务器,理解的人不多。UI 服务器只是一个服务器,旨在为现代重客户端的 UI 应用程序提供服务。它负责正交领域,如 SSO 集成、身份验证和授权,服务端渲染、会话管理、服务 UI 资产和缓存等。此外,当大规模应用程序某处部署 API 服务器时,它充当反向代理以调用 API 调用避免 CORS 问题。这里事实上的标准选择是 Node.js,因为许多框架都在 Node 中编写了 SSR 渲染器,而 Node 由于其异步特性而擅长处理大 I/O 请求。我们将在另一篇关于 Web 应用程序拓扑的文章中进一步讨论 UI 服务器。
当代架构最重要的变化是模型的概念。
模型不再被视为一个黑盒子。它被分隔成应用程序范围的全局状态和组件范围的本地状态。全局状态通常使用复杂的状态容器(如 Redux、Mobx 和 Vuex 等)管理。每个组件的本地状态是三个事物的联合——全局状态切片、组件的私有本地状态(异步数据、动画数据和 UI 状态等)和由父组件作为 props 传递的最终状态。我们可以将本地状态视为模型和视图模型的更好抽象。当我们将 GraphQL 添加到这个等式中时,状态管理会发生变化。(在后面的 GraphQL 部分中具体解释)
数据从上到下,从父组件流向子组件时是单向的。虽然框架允许数据直接反方向流动,但不鼓励这样做。相反,事件是从子组件中触发的。父组件可以监听或忽略它们。
不完整的蓝图
这个参考架构并没有真正捕捉到当代架构的全部本质。大多数 Web 流量由静态网站和 CMS(内容管理系统)驱动。现代工具链已经大大改变了我们开发和部署这些应用程序的方式。在 CMS 的情况下,他们通过解耦前端与后端而变得无头(Headless)。Strapi 和 Contentful 等的崛起就是明证。与此同时,我们不再使用纯 HTML 和 CSS 构建静态网站。静态站点构建器和生成器也变得非常流行。我们现在可以使用相同的前端框架来构建由复杂的构建工具辅助的静态网站。使用 React.js 时,我们可以使用 Gatsby.js 和 Vue.js,我们有 Nuxt.js。当我们编译代码时,它会生成一个静态网站,可以完整部署到任何静态 Web 服务器。这种技术称为预渲染,与服务端渲染对应。
这里我们有了另一个用于构建静态网站的当代架构。这里的思想是使用像 Strapi 一样的无头 CMS,并使用像 Gatsby 这样的静态网站构建器来构建前端。在构建中,当我们生成静态网站时,我们从 CMS 中提取所有数据并生成页面。
当作者更改无头 CMS 中的内容时,我们重新触发我们的静态网站的构建,使用新内容构建网站并立即部署到服务器或 CDN。
构建静态网站——现代方式
这个新的工作流程就像运行一个成熟的动态网站一样好用,也没有 CMS 的缺点,如复杂的部署和缓慢的加载时间等等…由于静态网站可以直接部署到 CDN。我们得以快速加载并改进缓存。我们还摆脱了静态网站的所有问题,如更新周期缓慢和缺乏可复用性等。这里引述Nuxt.js网站的愿景——
我们可以进一步考虑使用 nuxt generate 并托管在 CDN 上的电子商务 Web 应用程序。每当产品缺货或补充库存时,我们都会重新生成 Web 应用程序。但如果用户在此期间浏览这个 Web 应用程序,因为有了对电子商务 API 的 API 调用,应用将保持最新状态。无需再用服务器+缓存的多组实例!
总而言之,现代前端解决方案构建在基于组件的单向架构之上。
进一步考虑单向架构的话,可以通过多种方式实现它们。框架有自己的做事方式。一些不错的例子包括:
Flux(为 React 设计)
Redux(主要与 React 共用,但视图不可知)
MVU——模型视图更新(用于 Elm)
MVI——模型视图意图(用于 Cycle.js)
BEST、Vuex 和 Ngrx 等
Andre Staltz 在他的博客文章中很好地描述了这些模式:
AndréStaltz——单向用户界面架构
当代还是现代???
到目前为止,我们有意不用“现代”这个词,而一直在说的是“当代”。今天的架构实践仅仅是我们旧有理念的进化。我们社区试图将新事物融入现有的生态系统,总是留在边界内,很少打破常规。因此,“当代”这个词更能准确地描述这种理念。
在定义“当代”时,我们必须将所有松散的目标联系起来,必须将过去与现在和未来联系起来。我可以想到三种可能的链接——
过去——将今天的组件与历史上的 MV*相关联
现在——带有 Web 组件的场景
未来——函数组件
将今天的组件与历史上的 MV*联系起来?
到这里事情应该都很清楚,但可能会出现一个问题,那就是这些模式如何与之前的模式联系起来。组件不是 MVVM 或 MV*的更好实现吗?
如前所述,对于当代架构而言这只是一个底层的问题。然而,当代模式是关于整个应用的推断。它们处理的是更高级别的抽象。UI 组件是一个原子单元,它从父级接收全局状态切片,将其与自己的本地状态组合并将输出显示给用户。
单向模式可以解决更大的难题。它说的是在兄弟组件之间通信并维护应用程序范围的状态。如果组件允许垂直分工,这些模式会为整个应用程序带回水平分工。
如果还是有些糊涂,请考虑 Vue.js 这个例子。Vue 组件是 MVVM 的完美实现,同时我们可以使用 Vuex(Vue 的单向状态容器)来管理应用程序范围的状态。架构存在于多个抽象层次。
Web 组件的场景
组件架构几乎是所有框架的基础,人们正在尝试将组件的概念标准化为官方 Web 标准,尽管它们背后的推断方式完全不同。此外我将它们称为尝试,因为即使成为了标准,许多框架作者也担忧其可行性。
在本文中,最重要的关注点是数据流。Tom Dale很好地总结了这个问题:
关于其他问题需要看Rich Harris的博文:为什么我不用Web组件。
这并不是说当我们定义自己的技术栈时应该完全避免它们。一般的建议是从按钮、复选框和收音机等枝叶组件开始一步步慢慢来。总是要谨慎行事。
函数组件和 Hooks——这是啥?
当我们将组件当作 MVVM 的实现时,我们通常期望一个视图模型对象,它的 props 和方法由视图通过绑定使用。在 React、Vue 和 Angular 等情况下,它通常是类实例。但是这些框架还有函数组件(没有任何本地状态的组件)的概念,其中实例根本不存在。此外,React 最近引入了一种使用 Hooks 编写组件的新方法,允许我们在没有类语法的情况下编写有状态组件。
React Hooks——你注意到这里缺少“this”指针了吗?
这里的问题是——视图模型对象在哪里?Hooks 的创意很简单,但在跨调用维护本地状态的理念上完全不一样。但从架构的角度来看它仍然是之前的理念。我们可以将其视为简单的语法级别更改。我们都知道 JavaScript 语言中的类很糟糕,经常令人困惑,让开发人员很难编写干净的代码。Hooks 摆脱了类,从而解决了这个问题。
唯一改变的是视图模型的概念。无论是带有 Hooks 还是函数组件的有状态组件,我们都可以假设组件的视图模型对象是它的词法语境(Lexical Context)或闭包。该组件接收的所有变量、Hooks 值或 props 共同形成其视图模型。其他框架也采用了这一理念。
看起来函数组件就是未来趋势。不过我不会说 Hooks 是一个功能齐全的长期解决方案(听起来好奇怪),但在语法层面上它很优雅,并且可以缓解古老的类问题。如果你不认为语法很重要,请看看Svelte。
当代架构的下一阶段
与 Web 应用程序相关的每项新技术都会在某种程度上影响前端应用程序。目前有三种趋势——GraphQL、SSR 和编译器,这里必须具体介绍一下才算完整。
GraphQL
GraphQL 是一种服务端查询语言。你可能已经看过有人说它取代了 REST,但事实并非如此。当我们谈论 REST 时,它是一种元模式。在概念层面,它采用面向资源的架构来定义应用程序域模型;并且在实现层面,它使用 HTTP 协议的语义来交换这些资源,以赋予 Web 共享信息的方式。
现代业务需求很复杂,许多工作流程不能简单地作为 HTTP CRUD 类比的资源公开。这就是 REST 比较尴尬的地方。GraphQL 旨在消息传递级别替换 REST 的纯 HTTP 协议。GraphQL 提供了自己的消息传递封装,可以被 GraphQL 服务器理解,并且还支持查询服务端资源(域模型)。
但是 GraphQL 客户端实现的副作用是,GraphQL 已经开始侵占状态容器的职责。我们来看基本的事实,那就是客户端的模型只是服务端模型的一个子集,前者专门针对 UI 操作标准化,那么像 Redux/Flux 这样的状态容器只是在客户端缓存数据而已。
GraphQL 客户端内置了缓存支持,可跨多个请求来缓存。
这个简单的事实让开发人员可以省掉许多与状态管理相关的样板代码。在宏观层面,它的形态仍然有待观察。以下帖子详细描述了具体的机制:
一定要探索 GraphQL,因为它是未来趋势。
SSR——服务端渲染
过去服务端 MVC 是非常重要的,服务器会生成静态或动态 HTML 页面,这些页面很容易被搜索引擎抓取。由于客户端框架可以提供出色的用户体验,我们正逐渐在浏览器上渲染所有内容。完全客户端渲染的应用程序在请求服务器时返回的典型 HTML 页面几乎是一个空页面:
SPA 应用程序的初始 HTML 文件——几乎是空的!
一般来说这没什么问题,但在构建电子商务网站时遇到了麻烦,因为这类网站需要较快的加载速度和对 SEO 友好的可抓取内容。麻烦还不止于此,移动电话的互联网连接速度也很慢,而一些入门级设备的硬件也很差。搜索引擎对完全客户端渲染的应用程序抓取的能力有限。
为了缓解这个问题,老式的 SSR——服务端渲染又回来了。有了 SSR,我们可以在服务器上渲染客户端 SPA,合并状态,然后将完整渲染的页面发送到客户端。它减少了应用程序页面的初始加载时间,从而提高了网站的响应速度。
SSR 是下一步进化,它填补了客户端和服务端之间的鸿沟。
由于客户端代码是 JavaScript,我们需要服务端的等效引擎来执行 JS 代码。Node.js 作为 JavaScript 引擎是执行 SSR 的服务端技术栈的事实标准。虽然 SSR 设置可能变得非常丑陋和复杂,但许多流行的框架已经提供了一流的工具和更高级别的框架,为 SSR 提供了非常流畅的开发人员体验。
SSR 彻底改变了我们的日常开发工作流程。我们比客户端——服务端抽象更进一步。它引入了强制的三层架构,其中基于 Node.js 的服务器是关键的中间件。但从架构的角度来看——在数据流和分工层面所有这些都是相同的。SSR 既不引入新的数据流,也没有改变已有的存在。
编译器时代:Svelte——编译器还是缩小的框架?
我不知道该如何描述 Svelte 才好。我能说的是——当用户启动 Web 应用程序时,框架就在浏览器中运行。框架提供的转换抽象有其运行时成本。Svelte 是不一样的。
与静态站点构建器一样,Svelte 在构建时运行,将组件转换为高效的命令式代码,从外部更新 DOM。
所以 Svelte 是一个基于组件的框架,它展示了当代前端框架的所有特性,但同时它也是一个编译器。编译器将源代码编译为高性能的命令式 JavaScript 代码。作为一个编译器,它可以做许多其他框架不能做的事情:
在构建时提供可访问性警告
实现代码级优化
生成较小的包
在不破坏语法的前提下将 DSL 整合到 JavaScript 中
其宗旨是编写更少的代码。Svelte 证明编译器可以实现许多以前用纯 JavaScript 无法实现的功能。如果 Svelte 还不够,那么我们可以用Stencil.js,这是一个用于编写 Web 组件的 TypeScript+JSX 编译器。
其中一些想法已经成为某种形式的主流思想——Angular AOT 编译器和 Vue 单文件组件等等。然后还有其他人将这种思想推向极致:
Rich Harris的这篇演讲很好地展示了 Svelte 的底层哲学,并与 React 做了主观对比:
同样,编译器的前端开发前景也很光明。
还有其他方法!伟大的单体!
虽然完整的客户端框架现在风靡一时,但它并不是唯一的行事方式。Web 依旧是多样化的。仍然有许多应用程序是服务端驱动的,而且将继续这样做。
但这是否意味着它们的体验会很差?当然不是!架构的设计目标是支持产品,Basecamp 团队开发的框架Stimulus就做得很好。要了解他们的理念可以在这里查阅。
它是一个适度的框架,通过轻量级 JavaScript 提升后端渲染页面的交互性,同时采用最新实践和最新标准。Stimulus 通常与Turbolinks并用,以创建一流的 SPA 用户体验。(我是 Basecamp 的老用户了,发现它比其他许多 SPA 应用程序都更精致。)
Stimulus 在某种意义上是不一样的,因为它是通过 HTML 而非 JavaScript 驱动。状态在 HTML 而非 JavaScript 对象中维护。数据流非常简单:控制器附加到 DOM,它公开了可以附加到操作上的方法,从而执行进一步的操作。
你可能会想到,它很像 Backbone 和 Knockout 的时代——确实如此。目标很简单——为后端驱动 Web 提供的交互式前端框架。唯一的不同是 Stimulus 采用了现代社区标准和实践。
Strudel.js是另一个类似理念的适度框架。在 2019 年,我们可以使用像RE:DOM这样的当代 DOM 库。
虽然它们可能无法解决当代前端框架面临的所有问题,但它们给JavaScript审美疲劳的世界带来了一丝喘息之机。
总结
只有一个词能用来描述 GUI 架构——华丽。虽然对于前端软件开发来说,MVC 作为一种模式已经逝去了,但原则是永恒不变的。
我们从原始的 MVC 开始探索了著名的桌面模式。然后我们转到 Web 应用程序并使用相同的原则来得到了流行的模式。之后我们转向早期的独立客户端模式,最后全面讨论了 SPA 框架。
重要的一点是,今天的前端框架是面向组件的,它们将 MVC/MVVM 关注点作为一个层面,同时要处理新的环境和挑战。
最后,我们浏览了一遍前端架构的新面孔,包括 JavaScript 驱动的 SSR 和 GraphQL 的崛起。同时我们跳过了许多令人兴奋的新技术,如 HTTP2 和 WebAssembly 等,它们可以改变前端架构的未来,改变我们对应用程序的看法。
由于所有这些模式涉及的术语都有所重叠,并且通常是由社区各自独立开发的,因此很难根据进化时间来定义明确的线性时间轴。有时将不同的模式联系在一起是有问题的。此外为了简单起见,我们可以自由地描述某些概念,不用特别研究它们的细节。没有现成的命名法则可用,所以有些理念我用了自己发明的术语。
Web 应用程序拓扑是 Web 应用程序架构的另一个关系密切的领域。拓扑通常在技术栈选择、安全约束和性能等方面对前端开发产生深远影响。因此这将是下一篇文章的主题。
原文链接:
https://blog.webf.zone/contemporary-front-end-architectures-fb5b500b0231
评论