前言:
眼前姐妹们对“mvc按钮单击事件”大体比较关心,咱们都想要知道一些“mvc按钮单击事件”的相关文章。那么小编也在网络上收集了一些有关“mvc按钮单击事件””的相关资讯,希望咱们能喜欢,你们一起来学习一下吧!作者 | Harshal Patil
译者 | 王强
编辑 | 张之栋、王文婧
早期的软件架构模式建立在有限的硬件功能上并尊重这一事实,然而,今天的情况已经变了。计算能力在不断提升,而软件架构印证了这种观点。本文从经典MVC说起,详尽解读了当代前端架构及下一阶段的展望。对于前端工程师,以及想从宏观层面理解现代Web应用程序架构的Web开发人员来说,均能从中获益。
软件架构的核心思想,就是推断软件系统各个组件之间数据流动的方式。
软件架构的质量取决于你设法推断这些数据流的难易程度!
本文要讲的内容,就是在今天的 Web 应用程序背后探索这些数据流和最终的体系结构。Web 应用已从简单的静态网站(双层结构)发展为复杂的多层次、SPA 和 SSR 驱动的 API 优先系统。CMS 系统已发展成无头(Headless)和内容优先的系统。
近年来,前端社区的面貌日新月异。早年流行的是 jQuery 引入的 DOM 注入算法,其很快被基于 MVC 的 Backbone.js 所取代。之后一夜之间,我们就身陷于双向和单向数据流架构的丛林之中。我们的成长足迹在某处中断了。曾几何时沉浸在 MVC 的世界是如何突然进入 React 开创的单向数据流时代的?它们之间有什么联系?随着本文的展开,我们将尝试解开这个难题。
虽然本文针对的是前端工程师,但想要从宏观层面理解现代 Web 应用程序架构的 Web 开发人员都能从本文中获益。软件体系背后有着大量活动——流程、需求收集、部署拓扑、技术栈等等,但那些已经超出了本文所涉及的范围。
必要的前置知识——什么是计算机?
计算机是一种从用户收集数据 / 信息,并立刻或稍后将处理过的数据 / 信息提供给用户的机器。计算机如何收集和展示这些数据呢?它使用软件应用来实现这一目的。
软件架构的关键是提供合理的手段来组成软件,同时保持一切井然有序。
这里的关键在于,软件应用正在处理的数据被称为模型或应用程序状态。一些勇士可能将其称为应用程序的域模型或业务逻辑。应用程序可以是桌面端也可以是 Web 端。
本文的宗旨是探索向(Web 前端的)用户表示这种应用程序状态的合理方式,同时让一切井然有序。我们将探索数据如何从模型流向视图层,仅此而已。
经典 MVC——起源
将数据与表示分离是(Web 端和桌面端)图形用户界面的核心思想。对于 MVC——模型 - 视图 - 控制器来说,将表示(视图)与关注域(模型)分离是其主导的设计理念。毫无疑问,MVC 是一项开创性的成果,其影响力绵远流长。
如果要为软件开发写出第一原则,那么它就会是 SoC——关注点分离。而且 MVC 模式可能是它第一个真正的落地实践。
MVC 是为 Smalltalk-80 语言推出的。在 MVC 中,视图(View)对象显示模型(Model)对象持有的数据。在我们全面研究 MVC 中的数据流之前,我们必须了解当时(约 20 世纪 70 年代)的软件应用环境:
这个 MVC 仅适用于桌面应用。那时离 Web 诞生还有几十年的时间。没有 Web,复杂的 GUI 驱动的操作系统也不存在。这意味着应用软件非常接近底层硬件。
以上这些限制对 MVC 影响很大。于是需要由控制器(Controller)对象负责响应键盘或鼠标等用户输入并转换为模型上的操作。此外,操作系统缺少 GUI 小部件意味着视图与屏幕内容无法对应。
相反,视图和控制器以配对的形式结合在一起。其中视图部分向用户显示输出内容,控制器部分接收来自用户的输入。应该注意的是,屏幕上的每个控件都有一个视图——控制器配对,这也是小部件(widget)理念的雏形。
今天,在 React、Vue 或 Angular 中,这种视图——控制器配对与组件的思想是一样的,尽管状态处理层面的具体机制有所不同。
关于 MVC 就介绍到这里,下面的图片展示了 MVC 中的数据流示例。在这个例子中,我们有一个带递增和递减按钮的简单计数器。计数器状态维持着模型。此外,我们把两个按钮简化成了一个来简化叙述。
经典 MVC
在协作方面:
视图和控制器包含对模型的直接引用,但反过来不行。这意味着模型不依赖 UI,可以在不担心 UI 问题的前提下做更改。模型实现了观察者(Observer)模式,被一个或多个视图对象注册(subscribe)。当模型更改时会触发事件,事件响应后视图也会更新。
MVC 中有两条数据流。在视图流中不涉及模型,只涉及 UI 的更改。显示按钮单击效果或对鼠标滚动事件作出响应就是视图流的例子。
MVC中的数据周期
今天我们不再使用这个 MVC 了,它有时被称为经典 MVC 或老爹的 MVC。
开始使用应用程序模型
人们很快就意识到应用程序状态无法与 GUI 完全隔离。我们总是要维护某种表示逻辑或视图状态。
复杂的图形界面需要额外的状态,这些状态只用来辅助 UI 小部件,实现更好的用户体验。
但这可能会导致一些问题。来看看之前的计数器示例。当计数器达到 10 时,我们必须将标签的颜色从黑色更改为红色以表示警告。这种颜色变化行为实际上不是业务逻辑或关注点。这是纯粹的美学部分(用户体验),需要加进来。真正的问题是——放在哪里?是模型还是视图?
由于这种表示逻辑或视图状态基本上是从域模型派生的状态,因此必须在模型对象中维护它。但是域模型又要维护视觉部分(比如说红色),这种定义就很尴尬。如果我们将它放在视图对象中,那么它会引入另一组问题。我们的标签小部件不再是通用的了,无法在其他地方复用。此外,在视图对象中放一个带有硬编码数字 10 的条件,意味着我们正在泄漏某些业务逻辑。
为了解决这个问题,我们在原始的 MVC 中添加了另一个实体——应用程序模型(Application Model,AM)。有了 AM 以后,视图——控制器配对不再直接访问模型了。相反,它们向 AM 事件注册并使用它来访问所需的数据。
应用程序模型MVC
数据流和经典 MVC 是一样的。当然,每种模式都有其优点和缺点,AM-MVC 也不例外。最突出的问题是AM没有对视图对象的直接引用,因此即使 AM 被设计为维持视图状态,也无法直接操作后者。
总的来说,引入应用程序模型会使视图特定的状态远离域层,并降低了视图对象的复杂性来简化视图对象。这和表示模型(Presentation Model)很像,这是 Martin Fowler 在其开创性研究中创造的概念:
表示模型的本质是一个完全自包含的类,它表示 UI 窗口的所有数据和行为,但没有任何用于在屏幕上渲染该 UI 的控件。视图只是将表示模型的状态显示在屏幕上。
现代桌面架构的时代
时光飞逝,世界也在不断变化,新一代操作系统开始展现威力。应用程序远离了底层硬件,它们之间加入了完整的内核、OS 驱动程序和实用程序。像 Windows 这样基于 GUI 的操作系统提供了开箱即用的 UI 小部件。
不再需要控制器来监听输入设备了。视图对象的理念改变了。
控制器的大多数功能都由操作系统接手。视图的理念发生了变化:之前它只是一个小部件,现在它是一个众多小部件的组合;一个视图可以包含另一个视图。视图在某种意义上变成了双向的,它响应用户操作并显示模型数据。
前端世界中视图的理念与这个概念非常相似。在 Web 环境中,视图是一个完整的页面。
经典 MVC 变得过时且难用。为了适应这些不断变化的环境,Dolphin 团队在 1995 年正在寻找一种创建用户界面的新模型。Dolphin 团队如何找出新设计的历史可参阅以下记录,本文不再赘述。
总而言之,该团队最终将 MVC 模型旋转了 60 度,他们称其为 Twisting the triad。于是我们有了 MVP。
在协作方面:
表示器(Presenter) 监督表示逻辑。表示器可以直接更改视图。视图将用户事件委派给表示器。根据实现,视图注册到模型上,并依赖表示器处理复杂逻辑;或者在其他情况下,视图只依赖表示器处理一切。
正如 Martin Fowler 在关于 GUI 架构的论文中总结的那样,他将 MVP 实现分为监督控制器 MVP 和被动视图 MVP。从图中可以看出它们的差异和各自的数据流。
MVP——模型视图表示器
MVVM——模型 - 视图 - 视图模型
MVP 很棒,有许多可能的变种和复杂的细节。但从前端应用程序的角度来看,MVVM 确实更胜一筹。在一些实现中,后者也被称为模型 - 视图 - 绑定器。MVVM 很像被动视图 MVP,但增加了数据绑定的功能。它是一种编程技术,将来自提供者和消费者的数据源绑定在一起并同步。它摆脱了我们过去需要用来保持视图和模型同步的许多样板代码。这样我们就能在高得多的抽象级别上工作了。在协作方面:
视图模型(ViewModel) 是一个对象,它公开视图所使用的可绑定属性和方法。MVVM 有额外的绑定器(Binder) 实体,负责使视图与视图模型保持同步。每次视图模型上的属性更改时,视图都会自动更新以反映 UI 上的更改。
MVVM——Model View ViewModel
MVVM 中的数据绑定已经成为许多前端库的基础,包括 Knockout、Angular、Vue.js 和 React 等。
我们将在 Web 应用程序部分再讨论数据绑定。
进入 Web 应用程序领域
就像原始的 MVC 模式一样,出现了一种用于 Web 应用程序的模式。它被称为 Web MVC。实际上,Web 应用程序的构建和部署方式让 MVC 在 Web 端比在桌面端更加顺其自然。
社区的主要困惑是不知道桌面 MVC 和 Web MVC 是两种不同的模式。要是 Web MVC 当初不叫这个名字,大家就会清楚多了。
Web 应用程序是分布式应用程序的子类别。虽然 MVC 对 Web 应用程序感觉更自然一些,但一般来说构建分布式应用程序是很困难的。代码的某些部分在服务器上运行,同时还要保留在客户端(例如浏览器)上。
大规模 Web 应用程序架构的重点在于确定代码的哪个部分应该在哪里执行。我们有服务端驱动的应用程序或富客户端驱动的应用程序。在两者之间,我们可以无限自由组合。
在 MVC 的语境中讨论 Web 应用程序时有三个不同的数据周期,因此有三个 MVC 实现:服务端 MVC、浏览器内部的 MVC 和前端 MVC。浏览器负责三种类型交互的中介:
在客户端(JS+HTML)代码和服务端代码之间。在用户和服务端代码之间。在用户和客户端代码之间。
浏览器有自己的模型、视图和控制器。作为开发人员,我们不必担心浏览器 MVC。
服务端 MVC,亦即 2 号模型
服务端 MVC 的第一个众所周知的实现是 Sun Microsystems 针对 Java Web 应用程序的 2 号模型(Model 2)。
服务端 MVC——前端视角
这个 MVC 与传统的 MVC 非常相似,但由于数据跨越客户端和服务端边界时数据流周期时间呈指数级上升,因此前者多出来很多复杂性。需要注意的一些事项有:
桌面 MVC 有两个数据周期,而 Web MVC 有三个数据周期。有两种视图周期。首先是客户端视图周期,例如滚动事件、键盘输入等。其次是服务端视图周期,例如页面刷新、超链接激活等。最后我们有一个模型周期,它在通过客户端 - 服务端边界时有额外的时间复杂度。前端控制器(Front Controller):是通常由底层技术栈提供的组件,用于处理 HTTP 请求调度。例如 Java Web 应用程序中的 Servlet 容器、ASP.NET 中的 IHttpHandler 或 Node.js 中的 HTTP.Server 类:
今天的 SSR——服务端渲染则是完全不同的概念。然而事实并非如此。由于整个 HTML/ 内容是由服务端生成的,并且不涉及客户端 JavaScript 代码,因此完全使用服务端 MVC 构建的 Web 应用程序仍被视为 SSR。
超越服务端
从这里开始就有意思了。不知不觉中,几乎所有浏览器都开始提供 JavaScript 引擎。在我看来正是 AJAX 改变了 Web 应用程序。谷歌是最早的实践者,在其邮件客户端和地图应用中采用了这项技术。
这个世界由服务端 MVC 生成 HTML+JS。JS 代码遍布所有页面。JavaScript 主要用来减少服务端视图周期以改善用户体验。表单提交、输入验证等内容由客户端代码处理。
它是 Web 应用程序历史上最流行的架构。大多数 B2C 应用程序、SEO 友好网站,尤其是使用 CMS(内容管理系统)构建的网站都在使用它。客户端代码量取决于应用程序的需求。
这样的架构从未真正标准化,因此没有名称。它一直在渐进发展,仍被认为是 Web MVC 的扩展。ASP.NET MVC、Java Struts、Python Django、Ruby ROR 和 PHP CodeIgniter 是流行框架的一些例子,它们大量使用服务端 MVC 或 Web MVC。
当然,这种标准模式还有很多变体,但它们对当代前端架构没有任何实际影响,可以忽略。
基础 RIA——富互联网应用架构
了解过这些背景,我们现在准备讨论当代前端架构。当代前端架构主要解决的是构建 RIA——富互联网应用程序的问题。RIA 的确切定义并不存在,因为很多事物都能用它描述。但总的来说,RIA 或富 Web 应用是应用程序的一个类别,其中应用程序严重依赖客户端代码,并且它们的用户体验非常接近桌面应用程序。它主要使用支持 SPA(单页面应用程序)的框架构建。来自 Web MVC 的 服务端视图周期的数据流这里不存在。它通常只有一个初始 HTML 页面,然后使用客户端代码和路由解决方案来渲染后续页面。
构建 RIA 是一项复杂的操作,这种操作从以前基于桌面的 GUI 架构学习发展而来。视图模型、观察者、组件等就是从这些架构中借用的一些理念。Oliver steel 在他 15 年前的博客文章中(相信我,它是最出色的文章之一),为理解 RIA 数据流提供了很好的参考架构:
客户端变重了——SPA 的时代
RIA 参考架构和 Web MVC 之间最显著的区别是前者的顶层架构中缺少控制器和视图。然而字面意义上讲它们并没有消失。如果我们看一下底层,控制器和视图仍然存在,只是承担的角色大大缩减了。后端主要是 API 驱动的。视图仅限于生成 JSON,控制器负责编排传入请求并将其映射到适当的业务工作流。
GUI 模式很难?
如果你已经深入探索了前面的模式,那么你将意识到 GUI 模式与其他软件工程模式有所不同。以可复用的面向对象软件的元素为例:大多数模式都独立于技术或语言,但 GUI 模式并非如此。这些模式适用于人机交互的边界。用户和副作用本质上是模式的一部分。注意:可复用的面向对象软件的元素已经指出了经典 MVC 的本质。
GUI 模式适用于 HCI——人机交互的边界。用户和副作用本质上是模式的一部分。
因此,在不考虑基础框架或语言的情况下几乎不可能在理论上讨论它们。到目前为止,我们可以在相当高的抽象级别上探索这些模式。但当我们接近本文的核心时,我们将依靠不同的库或框架来描述这些模式。
大多数 Web UI 模式可以分为三个阶段,即进化、变革和当代阶段。
前端图谱——宏观视角
进化模式只是服务端 MVC 的扩展。它们并不会试图改变方向或发明全新的事物。它们会一步一步地改进,以补足现有应用程序的缺陷。而变革模式是将前端应用开发与服务端驱动的工作流程分离的那些理念。它们的到来标志着 SPA 应用的兴起。当代模式就是对这些变革模式的二次修正,也是前端社区前进的大方向。
DOM 注入算法
由 jQuery 引入和掌握的这项技术是编写大规模客户端应用程序的真正起步,尽管 jQuery 并未真正解决架构问题。它旨在简化当浏览器中存在太多不一致时的 DOM 操作。它提供了与浏览器无关的 API。
虽然我不觉得这是有意为之,但 jQuery 把 DOM API 简化到了很难将其与正常的语言 API 区分的程度。这反过来又让开发人员将 DOM API(视图层)与业务逻辑(模型)完美地混合在一起。
需要指出的一点是,它仍然在同一服务端 MVC 的上下文中。这只是一个扩展。没有真正的控制反转。视图 / 页面的总体控制仍然由服务端代码驱动。
jQuery——HTML 代码
jQuery——DOM 注入算法
在上面的代码片段中,模型、视图和表示器 / 控制器被混合成一个单体代码结构。当模型只包含一个属性时就是这种情况。想象一下,尝试构建一个没有服务端视图周期的 Web 应用程序(比如 SPA),做这样的事情是没有意义的。与 DOM 交互的代码简洁地注入了其他应用程序逻辑,这就是为什么它被称为 DOM 注入算法(注意:我不确定 DOM 注入这个术语是不是什么规范名称)
Backbone.js——MV*
正如我们在 jQuery 中看到的那样,在开发应用程序时显然缺少构建和组织代码的方法。于是 Backbone.js 应运而生,成为了新的进化解决方案。它是首批将 MVC 风格带给客户端的库之一。
Backbone.js 数据流
我们看一看 Backbone 数据流示意图,就可以清楚地看到模型和视图,但是没有控制器的等效对象。模式在不断发展,客户端 MVC 只是之前 MVC 架构的进化。在这个进化过程中,大多数 JavaScript 社区都同意模型和视图的定义,但针对控制器没有形成共识。考虑到客户端环境 ,控制器的理念并不合适。控制器留待进一步解释。
至于 Backbone 而言,控制器不存在。那么它适合哪里?是 MVC、MVP 还是 MVVM?借用服务端 MVC 的定义,控制器有两个职责:以传入的 HTTP 请求的形式响应用户操作,并协调模型以生成视图(HTML 页面)。在 Backbone 的情况下,视图和路由器共享这些职责,但是缺少独立的控制器或表示器的概念。
一些开发人员认为 Backbone 是 MVP,其中视图类似于表示器,模板(Template)是视图,而 Backbone模型和集合(Collection)构成模型。
正如 Addy Osmani 在他的博客中所说,最好不要强行把 Backbone 归类到任何特定的设计模式。设计模式应被视为应用程序结构的灵活指南,由此看来 Backbone 既不像 MVC 也不像 MVP。相反,它借鉴了多种架构模式中的一些最佳概念,并创建了一个运行良好的灵活框架。
这就是 MV* 或 模型 - 视图 - 其他 的诞生历程。强烈推荐 Addy Osmani 的博客文章:理解 MVC 和 MVP——适合 JavaScript 和 Backbone 开发人员。----------------------
与之前的 jQuery 相比,Backbone 可以生成更加结构化的代码。
Backbone.js 中的视图模板
在 Backbone.js 中创建一个模型
在 Backbone.js 中创建一个视图实例
在 Backbone.js 中链接视图和模型
前文中我将 Backbone 称为下一个进化解决方案。原因是它只是补充并扩展了服务端 MVC。例如,如果我们的后端是 RESTful,意味着前端代码只是用来表示服务端模型的手段,那么 Backbone 已预置为与 API 同步:
Backbone.js 中自动生成的集合方法
而且 Backbone 中还有许多其他小型约定,感觉只是扩展而已。总之,Backbone 可能不是当时唯一的解决方案,但它在代码结构和组织领域确实做出了开创性的工作。像 jQuery 一样,它被许多产品采用。
Knockout.js——前端的数据绑定
Knockout.js 是我们基本模式的最后一个案例。它旨在为 JavaScript 实现 MVVM——模型 - 视图 - 视图模型。它也确实做到了。Backbone 解决的是代码组织和结构的问题,而 Knockout 是在声明式数据绑定的帮助下有效地实现视图层。声明式绑定的优点与其他声明式编程结构相同:
易读:声明式代码易于编程。减少样板:绑定允许我们在视图模型更改时自动更新 DOM,并在视图通过用户输入更改时更新视图模型。可观察:Knockout 在事件之上提供更高级别的抽象。这允许 Knockout 自动跟踪视图模型props 之间的依赖项。如果需要,我们可以注册可观察属性。
Knockout.js 视图——双向绑定
Knockout.js 中的视图模型——使用计算属性响应
虽然 Knockout 为视图和视图模型提供了定义良好的结构,但却没关注应用程序模型的问题。这使得 Knockout 高度专注、可以广泛适应,因为它可以用作库而非框架。我见过有人用它构建迷你 SPA 应用程序,其中 Web 应用程序有多个页面,每个页面都是一个小型的 Knockout 应用。这个StackOverflow 答案清楚地定义了 Knockout 的 MVVM 实现的范围:
通常假设 Knockout 模型驻留在服务端。视图模型只是使用 Ajax 或等效方式询问服务端模型。
它在 DOM 更新用途上取代了 jQuery 和像 Handlebars 这样的模板解决方案,但仍然使用 jQuery 来制作动画、Ajax 等实用程序。与 Backbone 结合后,它可以作为 MVVM 模式的完整实现。理论上讲这可能已成事实,但在此之前,这些概念已经被下一代工具链吸纳过去了。
接下来 Angular 1、Aurelia、Ember.js 等代表的变革旅程开始了。
由于它与.NET 世界紧密相关,因此被广泛用于 ASP.NET MVC 应用程序中。像 Backbone 一样,它是另一种进化解决方案,针对的是稍微不一样的问题。而且客户端代码依旧只是服务端 MVC 模式的扩展。服务端仍然是主导架构。彼时 Backbone 和 Knockout 常被拿来对比,但实际上它们是免费的解决方案。
Knockout 和 Backbone 都是 JavaScript 库。不知何故,Backbone 被视为一个框架。为什么?这里没有确定的答案,但可能是因为视角不同。由于强调代码结构,Backbone 始终采用更高级别的抽象处理。此外,Backbone 从没想过要取代无处不在的 jQuery(即使在 2019 年,最流行的 100 万个网站中有七成都在使用 jQuery),而 Knockout 与核心 jQuery 功能高度重合(比如 DOM 操作),这自然让 Knockout 举步维艰。因此与 Backbone 相比,Knockout 的应用被大大局限了。但它仍然是前端社区首批实现的声明式数据绑定之一,值得在这里专门提到它。
Angular 1——给我控制权
jQuery 对 DOM 意味着什么,Angular 1 就对一般意义的前端生态系统有着同样的影响。它永久改变了构建大规模客户端应用程序的概念。作为一个框架,它引入了许多概念——模块系统、依赖注入、控制反转和更简单的数据绑定等。
曾几何时,选择正确的 JavaScript 库,并为前端构建完美的技术栈仍然是一件痛苦的事。Angular 1 提供了一种更简单但有凝聚力的替代方案。Ember.js 和其他类似的框架也可以这么说,但是 Angular 1 的普及度是完全不同的层面,称得上是那个时代的选择。
从某种意义上说 Angular 1 是变革解决方案,它不再是对服务端 MVC 的简单扩展,而是在页面上洒满了客户端代码。Angular 1 使 SPA 成为构建下一代用户体验的几乎是事实上的一流解决方案。
是框架还是库?
先前的解决方案更像是库而不是框架。毫无疑问,Angular 1 是一个明确定义的框架。框架和库之间的关键区别是 IOC——控制反转。作为框架,Angular 1 具备:
明确定义的 MVVM:Angular 1 具有清晰的模型、视图和视图模型对象。依赖注入(DI):Angular 1 对 DI 提供一流支持,为模型对象提供生命周期管理。在 Angular 1 中,应用程序的模型使用服务(Service) 实现。默认情况下服务是一个单例实体,对于模型对象来说通常很理想。数据绑定和更改检测:数据绑定是声明式的,更改检测是自动的。数据绑定是 MVVM 必要的组件。绑定是双向的。此外更改检测几乎是自动的(Angular 1 真的很神奇)。它为开发人员省去了许多样板代码。事件的使用和整体注册机制往往也会简化。这在基于 MVC 的系统中非常普遍。事件还会降低代码可读性,因为某些工作流的数据流分布在代码中。模块系统:Angular 1 引入了特定于框架的模块系统。模块是几乎所有语言的代码组织的基础。JavaScript 直到 2015 年才有模块系统(浏览器直到 2018 年才支持它)。Angular 在组织方面领先时代。
与此同时,Angular 1 也因其引入的复杂性而受到批评。最重要的批评指出它是在服务端构造之后建模的,前端开发人员并不习惯这样,有些缺陷无法忽视:
命名空间冲突:虽然 DI 很棒,但它是使用全局命名空间的 Service Locator 模式实现的,于是服务必须添加前缀。双向数据绑定:当时这可能是个好主意,但之后人们意识到它并不能真正扩展。特别是 React 的兴起使双向数据绑定成为可选项。不小心的话,双向绑定可以带来许多面条代码。令人困惑的术语:Angular 1 使用的一些术语显然令人困惑。例如,Angular 1 用 $scope 作为视图模型,但它也有控制器可以扩充 $scope 对象。可能正确的名称可以是 VMFactory 之类。此外,Angular 1 有三种服务配方,其中一种名为 Service。
还有许多其他小问题。另外,Angular 2 或简称 Angular 是一个彻底的更新,它就像一个全新的框架。除了名字和少量概念之外,两代版本之间找不到什么共同点。
Angular.js——概览
多年来,Angular 1 发布了一些小版本更新,修复了许多复杂的小细节。最重要的是增加了组件模型,组件模型是前端世界大多数理念的交汇点。
Angular 1 在前端社区的遗产影响深远。它的优缺点帮助社区了解了软件架构的重要性,并为编写可扩展应用程序的工作提供了基准。它的缺点 / 不足成为了未来架构解决问题的基础。
当代前端架构
现代应用程序在各个层面都非常接近桌面应用程序。当今环境中的一些显著变化包括:
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 添加到这个等式中时,状态管理会发生变化。
数据从上到下,从父组件流向子组件时是单向的。虽然框架允许数据直接反方向流动,但不鼓励这样做。相反,事件是从子组件中触发的。父组件可以监听或忽略它们。
不完整的蓝图
这个参考架构并没有真正捕捉到当代架构的全部本质。大多数 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是怎样取代Redux的。
----------------------
用React Apollo瘦身我们的Redux代码。
----------------------
一定要探索 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 应用程序架构的另一个关系密切的领域。拓扑通常在技术栈选择、安全约束和性能等方面对前端开发产生深远影响。因此这将是下一篇文章的主题。
我们希望本文能够帮助你更好地理解前端架构的元素。求分享扩散!
英文原文:
标签: #mvc按钮单击事件