译者 Disclaimers: 翻译本文并非试图引发 Full Stack Rails 和 SPA 或 Microservices 支持者们之间的口水战,只是单纯觉得文章中的很多观点值得思考并与各位分享和讨论。文章下方评论区的一些不同意见也值得一看。
关于 Rails Monolith 这篇文章也非常推荐大家阅读:The Modular Monolith: Rails Architecture
medium 原文链接 DHH 鼓掌(看来很和他 monolith 的胃口😄)。最近很忙,一直没翻完,文章很长,内容丰富且作者文笔风格“飘逸”,加之本人非专业翻译(中英文差😅),错误或辞不达意之处请见谅并指出。以下是正文。
(注:英语里兔子洞会被用来比喻奇怪的,令人困惑的或荒谬的情况)
TL;DR. 单页面应用 (SPA) 之路黑暗且充满恐惧。你可以勇敢地和它们战斗。。。或者另选一条可以带你到相近目的地的路:现代 Rails。
记得当DHH 在 2012 年宣布 Turbolinks 时,我曾认为 Rails 在关注错误的目标。我当时的信念是,为用户交互提供即时响应时间是优秀用户体验的关键。由于网络延迟,这种交互性只有在你最小化对它的依赖,并在客户端上管理大量状态时才有可能。
我认为这对我当时正在开发的应用是必要的。考虑到这一点,我尝试了许多方法和框架来实现相同的模式:单页面应用 (SPA)。我相信 SPA wagon 才是未来™。经历几年之后,我并不确定未来是啥,但是我真心想要一个替代方案。
单页面应用是一个 javascript 应用,完成加载后,不再额外加载页面,完全负责接下来将发生的一切:渲染,从服务器下载数据,处理用户交互,执行屏幕更新。
此类应用感觉更像是原生应用程序,而非传统依赖服务器来响应交互的网页。例如,如果你用过 Trello,你可以看到创建卡片的速度快到爆。
当然,能力越大责任越大。在传统的 Web 应用中,您的服务器应用程序包括您的领域模型和规则,一些与数据库交互的数据访问技术,以及一个控制器层,安排如何根据 HTTP requests 渲染 HTML responses。
SPA 则比较复杂。你仍然需要一个服务器端应用程序,包括您的领域模型和规则,Web 服务器,数据库和一些数据访问技术… 和一些额外的东西:
对于服务器:
对于新的 Javascript 客户端:
总而言之,SPA 让你需要维护一个额外的应用。还有一系列新的问题需要处理。注意,你没有替换任何一个应用。你仍然需要服务器端应用程序(它现在只渲染 JSON 而不是 HTML)。
如果你从未使用单页应用程序,你可能会低估将面临的困难。因为我过去犯了同样的错误所以我知道。渲染 JSON?我可以应付。一个领域对象的富 Javascript 模型?听起来很有趣。而且,Hey,这个框架将完成所有繁重的工作。小菜一碟!
错了。
新应用程序和服务器之间的数据交换是一个需要解决的复杂问题。有两种相反的力量:
最重要的是,你需要考虑何时在每个页面上获取什么内容。我的意思是,你需要平衡加载时间,什么是立即需要的,可以懒惰获取,以及想出满足这一要求的 API 设计。
一些标准可能会有所帮助。JSON api,用于标准化您用于数据的 JSON 格式; 或GraphQL,用于在单个请求中精确地获取你所需的数据,想多复杂都行。但是没有一个可以从这些里拯救你:
这两个方面代表了相当多的额外工作。
人们将单页应用程序与速度联系起来,但事实是让它们快速加载是具有挑战性的。原因有多方面:
这并不意味着不可能使 SPA 加载速度快。我只是说它很难以及你需要计划一些东西,因为不会自然而然地因这种模式出现。
例如,基于 Ember 的 SPA 应用Discourse启动时间非常棒,但除这些以外,它们预先加载了一堆 JSON 数据作为初始 HTML 的一部分,以防止进一步的请求。并注意 discourse 团队自豪地痴迷于速度,他们的技能高于平均水平。在假设你可以轻松地在 SPA 中复制此类性能之前,请记住这一点。
对这个问题的一个雄心勃勃的想法是Isomorphic Javascript:在服务器上渲染你的初始页面并快速提供它,而在后台,SPA 加载并在准备就绪时进行控制。
这种方法要求在服务器上运行 JS 运行时,并且并非没有技术挑战。例如,开发人员必须计划 SPA 中使用的加载时事件,因为加载过程现在会改变。
我喜欢这个想法带来的共享代码可能性,但我还没有看到一个实现可以让我不走在相反的方向上。我也发现这个页面渲染过程有点可笑:
难道你不能只查询数据库,生成 HTML 并运行?
这并不完全公平,因为你将不会获得 SPA 并因为大部分魔法被框架所隐藏,但我仍觉得不妥。
编写有丰富 GUI 的应用很难。有充分理由认为它们是激发面向对象和许多其他设计模式的问题之一。
在客户端管理很多状态很困难。传统网站通常关注特定目的的页面,这些页面在重加载时会得到全新状态。另一方面,SPA 负责在使用期间管理所有状态和页面更新,并且必须确保一切都一致且平稳地移动。
实际上,如果您来自编写轻量,小段的 Javascript 代码以增强某些交互,而 SPA 会把这转化成你必须编写的大量额外的 Javascript 代码。你最好确保正确地设计。
SPA 框架有许多不同的架构:
大多数框架分支于传统的 MVC。Ember 最初深受 Cocoa MVC 的启发,但在最近的版本中改变了它的编程模型。
与传统的控制器/视图分割相比,有一种支持组件的倾向(其中一些,如 Ember 和 Angular,在后面的主要修订版中做到了这一点)。
它们都支持某种单向数据绑定。不鼓励双向数据绑定,因为它引入了副作用。
大多数框架都包含一些路由系统,它可以将 URL 映射到屏幕,并确定如何实例化组件以进行渲染。这对于 Web 来说是非常独特的,也是传统桌面 GUI 中不存在的东西。
大多数框架将 HTML 模板与 Javascript 代码分开,但 React 下赌注在 Javascript 中混合 HTML 生成并获得成功,因为它被社区大量采用。这些天也有关于在 JS 中嵌入 CSS 的炒作。
Facebook 的 flux 架构在业界产生了相当大的影响,像 Redux,vuex 和其他许多容器都受到了很大的启发。
在我见过的所有框架中,Ember 是我的最爱。我喜欢它的凝聚力,而且它是固执己见的。我也喜欢它的编程模型在最近版本中的演变,混合了传统的 MVC,组件和路由。
另一方面,我非常不喜欢 Flux/Redux 阵营。我见过这么多聪明人采用它,我多次努力学习和理解它。当我看到代码时,我无法避免难以置信地摇头。我不认为自己在遵循这样的模式编写代码并且同时感到中度快乐。
最后,我很难接受在充满 javascript 逻辑的组件中混合使用 HTML 和 CSS 是一个好主意。我理解这样解决的问题,但是我认为它引入的问题不会使这种方法值得。
除了个人偏好之外,最重要的是,如果您选择 SPA 路线,则需要解决一个非常复杂的问题:正确构建您的新应用。行业中远未就如何做到这一点达成共识。每年都会出现新的框架,模式和框架修订版本,这些框架修订版本会在很大程度上改变编程模型。您必须根据您的架构选择编写和维护大量代码,因此请务必仔细考虑这一点。
写 SPA 时,代码重复可能会是一个问题。
对于 SPA 逻辑,你需要一个丰富的对象模型来表示你的域及其规则。而且你的服务器逻辑仍需要相同的功能。这套做法只是一个等待发生的重复。
例如,假设你正在处理发票。你可能有一个 javascript Invoice 类,它公开了一个方法总和,它把所有明细相加,以便你渲染数量。在服务器上,你还需要一个带有总和方法的 Invoice 类,用于在通过电子邮件发送发票时计算该金额。看到了吗?客户端和服务器实现相同逻辑的 Invoice 类。重复的代码。
如上所述,Isomorphic Javascript 可以通过更容易共享代码来缓解此问题。我说缓解因为客户端和服务器对象之间的映射并不总是 1 对 1。你将会想要确保某些代码永远不会放弃你的服务器。很多代码只会在客户端有意义。而且,一些需要考虑的点不同(例如,服务器元素可能会将数据持久保存到数据库,但客户端对应方可能会使用某些远程 API)。即使可能,共享代码也是一个棘手问题。
你可以辩称你并不真正需要 SPA 中的丰富域模型,而是直接处理原始 JSON / javascript 对象,通过视图组件分发逻辑代码。你现在拥有相同的重复逻辑,但与你的视图代码混合在一起,只能祝您好运。
如果要在服务器和客户端之间共享渲染模板,也会发生同样的情况。例如,对于 SEO 目的,如果你想在检测到网络爬虫时发送服务器端生成的网站版本,该怎么办?你必须在服务器上再次编写模板,并确保它们从那一刻起保持同步。再次重复代码。
根据我的经验,不得不在客户端和服务器中复制逻辑或模板是增加编程不满的根源。你第一次这样做还行。当你做到的第 20 次,你会摇头。当你做到的第 50 次是,你会想知道所有这些 SPA 的东西是否都是必要的。
根据我的经验,构建强健的 SPA 比编写强健的服务器端生成的 Web 应用程序要困难得多。
首先,无论你多么小心,无论你写多少次测试。你编写的代码越多,你将拥有的 bug 就越多。如果我坚持太多,SPA 意味着一大堆额外的代码来编写和维护。
其次,如前所述,构建丰富的 GUI 很困难,导致由许多彼此交互的元素组成的复杂的系统。你编写的系统越复杂,你将拥有的错误就越多。与使用 MVC 的 Model-2 变体的传统 Web 应用程序相比,SPA 的复杂性是疯狂的。
例如,为了保持服务器中的数据一致性,你可以利用数据库约束,模型验证和事务。如果出现问题,你将回复错误消息。在客户端中,事情有点复杂。很多地方都可能出错,只是因为很多事情正在发生。也许某些记录成功保存,其他一些记录失败。也许你在某些操作过程中断线了。你需要确保 UI 始终保持一致,并且当错误发生时应用程序会正常恢复。这一切都是可行的,当然,只是更难。
这听起来很愚蠢但是,对于构建 SPA,你需要了解如何执行它的开发人员。同样,你不应低估 SPA 的复杂性,你不应该假设任何有经验的 Web 开发人员具有正确的动机和常识,可以从头开始编写优秀的 SPA。你需要正确的技能和经验,或者假设将要做出重大错误。我知道这是因为这正是我的情况。
这对你的公司来说可能比你想象的更具挑战性。SPA 方法鼓励专家团队而不是多面手:
有可能你最终会遇到无法在 SPA 上工作的人以及无法在服务器端工作的人,只因为他们不知道如何。
这种专业化可能非常适合 Facebook 或 Google 以及由多层工程兵力组成的团队。但这会是你的 6 人开发团队吗?
现代 Rails 中包含三个部分可能让你在设计现代 Web 应用时重新思考:
不使用就很难体会一些方法感觉如何。因此,我将在以下部分中对Basecamp进行一些引用。我与 Basecamp 无关,除了是一个快乐的用户。关于这篇文章,它只是现代 Rails 的一个很好的实例,你可以免费试用。
Turbolinks 背后的想法很简单:通过执行
替换的 ajax 请求整页重载,从而加速应用。使其工作的内部巫术被隐藏起来。作为开发人员,您可以专注于传统的服务器端流程。Turbolinks 很大地受到pjax的启发,并经历了多次修订。
我曾经担心它的表现。我错了。速度大幅提高。说服我的是在一个项目中使用它,但你可以试用 Basecamp 并玩一玩。尝试使用某些元素创建项目,然后点击不同的部分进行导航。这将让您对 Turbolinks 的感受有所了解。
我不认为 Turbolinks 的新鲜性是令人兴奋的(pjax 是 8 岁)。或者因为它的技术复杂性。让我感到惊讶的是,与 SPA 替代方案相比,如此简单的想法能够数量级地提高生产力。
让我重点介绍它消除的一些问题:
数据交换。你没有它们。无需序列化 JSON,设计 API 或考虑以高效的方式满足客户端需求的数据查询。
初始加载。与 SPA 不同,它通过设计鼓励快速加载。对于渲染页面,您可以直接从数据库中获取所需的数据。有效地查询关系数据库或缓存 HTML 等问题都被很好解决了。
架构:您不需要复杂的架构来组织您的 Javascript 代码。您只需要专注于正确构建您的服务器端应用,你用 SPA 也得做这些。
服务器上的 MVC,在 Rails 和许多其他框架使用的变体中,比用于构建丰富的 GUI 的任何模式简单得多:接收请求,处理数据库以满足它并呈现 HTML 页面作为回应。
最后,总是替换
的约束有一个很好的效果:你可以专注于页面的初始渲染,而不是更新特定的部分(或更新一些状态,在 SPA 世界中)。在一般情况下,它只是再次呈现一切。请注意,我不是在讨论解决问题,而是在消灭问题。例如,GraphQL 或 SPA Rehydration 是解决非常复杂问题的超聪明解决方案。但是,会不会不是试图找到解决方案,而是将自己置于不存在这些问题的情况下呢?这是工作中的问题重述。我花了很多年才完全理解这种问题解决方案的力量。
当然,Turbolinks 不是一个没有问题的银弹。最大的抱怨是它可以破坏现有的 Javascript 代码:
还记得 15 年前通过 Ajax 渲染 HTML 时很性感吗?猜猜怎么了?它仍然是你工具箱中的绝佳资源:
你可以通过点击右上角的按钮打开个人资料菜单,了解这种方法在 basecamp 中的感受:
通过 Ajax 在 Basecamp 中打开一个下拉菜单,其中包含 GET 请求
感觉很快。从开发方面来说,你不必关心 JSON 序列化和客户端渲染的东西。你可以用所有 Rails goodies 在服务器上渲染该片段。
Rails 多年来一直使用的类似资源是服务器生成的 JavaScript 响应(SJR)。它们让你用被在客户端中执行的 javascript 响应 Ajax 请求(通常是表单提交)。它提供了与 ajax 渲染 HTML 片段相同的好处:它感觉非常快,你可以重用服务器端代码,并且可以直接访问数据库以构建回应。
如果你去 Basecamp 尝试创建一个新的 todo,你可以感觉一下。单击“Add this todo”后,服务器将保存 todo 并返回 Javascript 片段将新 todo 附加到 dom。
我认为今天许多开发人员都不屑一顾地看待 Ajax 渲染和 SJR 响应。我记得我也是一样一样的。它们是一种工具,因此可能被滥用和错用。但如果使用得当,它们就是一个了不起的解决方案。让你以极低的成本提供出色的用户体验和互动性。可悲的是,除非你先打一些 SPA 战斗,否则很难欣赏 Turbolinks。
Stimulus是几个月前发布的 Javascript 框架。它不关心渲染,也不关心基于 Javascript 的状态管理。相反,它只是一种很好的,现代的组织你用来增强 HTML 的 JavaScript 的方式:
如果你拥抱 Rails way,你的 Javascript 将专注于增强服务器端生成的 HTML 和增强交互(Rails 称之为Javascript sprinkle)。Stimulus 意在组织这样的代码。它不是 SPA 框架,也不假装是一个。
我在一些项目中使用了 Stimulus,我非常喜欢它。它删除了一堆样板代码,它基于最新的 Web 标准,读起来很爽。我特别喜欢的东西:它现在是做某事的标准方式,直到现在,每个应用程序决定如何做。
Turbolinks 通常以“获得 SPA 的所有好处而没有任何不便”来营销。我不认为这是完全正确的:
使用现代 Rails 构建的应用程序感觉很快,但对于不依赖于服务器的交互,SPA 仍然会感觉更快。
有些情况下 SPA 更合理。如果你需要提供高层次的交互性,必须管理大量状态,执行复杂的客户端逻辑等,SPA 框架将使您的生活更轻松。
现在,开发是权衡的游戏,并且在此游戏中:
Modern Rails 可让你构建足够快且感觉很棒的应用程序。
对于各种各样的应用程序,Rails 使你能够以很少的代码和复杂性实现相同的功能。
我相信使用 Rails 可以通过 10%的工夫而达到 SPA 可提供的 90%。Rails 在生产力上杀死了 SPA。在 UX 方面,我认为许多开发人员犯了和我同样的错误,认为 SPA UX 是不可战胜的。不是这样。实际上,如上所述,你最好在构建 SPA 时知道自己在做什么,否则 UX 实际上会更糟。
我观察到大量采用 SPA 框架的公司,以及无数关于用 SPA 方式做花哨事的文章。我认为有很多“不使用正确的工具”的现象,因为我坚信认为适用于 SPA 的应用类型是有限的。
我提到合理,是因为 SPA 很难。我希望我在本文中已经在一些事上说服了你。我并不是说创建优秀的 SPA 是不可能的,或者现代 Rails 应用就一定很棒,或是只有一条路径超级难,另一条路径容易很多。
在研究写这篇文章时,我偶然发现了这条推文:
If we would start webdev from scratch and had to choose between:
— Kitze (@thekitze) June 28, 2018
- CSS vs css-in-js
- REST vs GraphQL
- Templates vs JSX
No sane person would choose the first options.
这让我大笑,因为我会选第一个选项,除非替代方案是合理的。这也代表了一种开发者思维方式,喜欢复杂性并在其中蓬勃发展,知道认为其他有不同标准的人是疯狂的。
多年来,我艰难地学会了复杂性通常是一种选择。但是在编程世界中,选择简单是非常困难的。我们非常尊重复杂性,接受简单性通常意味着思维不同,根据定义,这很难。
请记住,你可以选择让自己摆脱困境。如果你选择 SPA 之路,确保其合理且你了解挑战。如果你不确定,尝试不同的方法并亲眼看看。也许 Facebook 或谷歌在他们的规模上没有做出这样决定的奢侈,但你很可能有。
如果你是多年前放弃 Rails 方式的 Rails 开发人员,我建议您重新审视它。我想你会感到愉快的。
写在最后的个人想法:用过 React 之后尝试 rails-ujs + turbolinks + stimulus 感觉出乎意料的舒服顺畅,效率也不错,配合 webpack,是 rails 社区应对 SPA 的一套替代方案,基本可以确定将成为标配。和 React, Vue, Angular 相比各有利弊,要看应用场景和需求。不过 jquery 这套思路被边缘化的趋势难以阻挡。