HTML-over-the-wire的简史
Rails的关键支柱之一是"重视集成系统"。当整个行业都在向微服务、高度解耦的前端和团队以及通过乐高积木编程的诱人歌曲迈进时,Rails却倾向于用一个系统来完成所有工作--即所谓的 "宏伟的巨石"。
Basecamp、GitHub和Shopify等应用没有在客户端JavaScript MVC框架中重建Rails中已有的大部分功能,而是利用 "线上HTML "的概念实现快速的页面加载。
在他具有开创意义的RailsConf 2016演讲中,Sam Stephenson介绍了这个堆栈的各个部分。
通过使用Turbolinks(或类似的库,如pjax或Inertia)和快速的HTML响应(通过缓存和避免过多的数据库查询来获得低于100ms的响应时间),你可以建立高性能的页面,同时仍然坚持无状态HTTP响应和服务器端逻辑的低调优势。
正如Sam所指出的,这确实是一个 "网络开发的黄金时代":

因此,当大部分行业都进入了JavaScript的兔子洞--为反应式渲染、功能性 状态管理容器和大约 七十种 不同的 客户端 路由 库创造了新的创新--Rails领域的悄然反叛者正在磨练这些技术,并沿着无聊的服务器渲染的HTML构建应用程序。
在2020年,我们看到这些工具的复兴,兴奋(至少在Twitter的一个小角落!)达到了一个热潮,因为Basecamp推出了HEY:一个功能齐全的电子邮件客户端,只有很小的JavaScript足迹,推动了HTML-over-the-wire方法的界限。

Turbolinks/Stimulus 20XX:未来
2014-2016年的堆栈是:
- Turbolinks/pjax
- Rails UJS +
js.erb模板(服务器生成的JavaScript响应)。 - 大量的HTML片段缓存
- Rails资产管道和CoffeeScript
你甚至可以进一步追溯这些技术的起源。我最近收到了一个链接,是一个有近15年历史的REST "微格式",名为"AHAH:异步HTML和HTTP",它是我们今天如此兴奋的想法的一个早期版本。(看到David Hansson被列为贡献者,你不应该感到惊讶!)。
现在,一个 "最先进的 "2020版还包括:
- StimulusJS(也见AlpineJS),用于轻量级的事件管理、数据绑定和 "洒 "的行为。
- 通过新的
<template>命令方式与Turbolinks进行部分更新(取代了js.erb,并支持CSP) - 通过ActionCable实时更新Turbolinks(也见StimulusReflex/CableReady)。
- 对Webpack、ES6以及Tailwind和PurgeCSS等新CSS方法的第一方支持
这个堆栈是非常强大的,开发经验允许你真正的飞行。你可以用一个小团队构建快速和互动的应用程序,同时还能体验到2014年时代的香草Rails代码库的乐趣。
但是,多年来,重度JavaScript SPA的单一文化使得人们很难了解这个堆栈。这个社区充满了实践者,他们使用这些工具来构建软件和业务。只是没有产生同样水平的内容,所以这些工具中有许多是不为人知的,可能是无法接触到的。
我的贡献之一是通过展示一些真实世界的例子(而不是一个TODO列表或计数器),为那些想了解更多的人照亮道路。一旦你看到你如何使用像Stimulus和HTML响应这样的工具来构建功能,而不是使用像React这样的工具,事情就会开始变得简单。
让我们建立一些真实的东西:悬浮卡
当你将鼠标悬停在你的应用程序中的某个东西上时,悬浮卡会在一个弹出的气泡中显示额外的上下文信息。你可以在GitHub、Twitter、甚至维基百科上看到这种UI模式的例子。

使用HTML-over-the-wire方法在Rails中构建这一功能非常容易。
计划是这样的:
- 构建一个控制器动作,将悬停卡渲染为HTML
- 编写一个小的Stimulus控制器,在你悬停时获取悬停卡的HTML。
...就这样了。
我们不需要制作API端点,也不需要弄清楚如何组织我们需要的所有数据。我们不需要去找React或Vue来做客户端组件。
这种枯燥的Rails方法的好处是,这个功能是非常简单的,而且它的构建也同样简单明了。它很容易对代码进行推理,而且具有超强的扩展性。
在这个例子中,让我们为一个运动鞋市场的应用建立事件反馈。

当你把鼠标悬停在一只鞋上时,你会看到图片、名称、价格等等。对用户来说也是一样,你可以看到每个用户的小档案。
前端(刺激+获取)。
链接的标记看起来像:
<!-- app/views/shoes/feed.html.erb -->
<div
class="inline-block"
data-controller="hovercard"
data-hovercard-url-value="<%= hovercard_shoe_path(shoe) %>"
data-action="mouseenter->hovercard#show mouseleave->hovercard#hide"
>
<%= link_to shoe.name, shoe, class: "branded-link" %>
</div>
注意:我们使用的是Stimulus 2.0预览版的API!
Stimulus的一个伟大的特点是,你可以阅读标记并了解正在发生的事情,而不需要潜入JavaScript。
在不了解其他实现的情况下,你可以猜到它是如何工作的:这个链接被包裹在一个hovercard 控制器中,当你悬停时(通过mouseenter 和mouseleave 事件),卡片被显示或隐藏。
正如在《编写更好的激励控制器》中所建议的,你应该把悬停卡片的URL作为一个数据属性传入,这样我们就可以为多种类型的卡片重新使用hovercard_controller 。这也使我们不必在JavaScript中重复应用程序的路线:
// app/javascript/controllers/hovercard_controller.js
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["card"];
static values = { url: String };
show() {
if (this.hasCardTarget) {
this.cardTarget.classList.remove("hidden");
} else {
fetch(this.urlValue)
.then((r) => r.text())
.then((html) => {
const fragment = document
.createRange()
.createContextualFragment(html);
this.element.appendChild(fragment);
});
}
}
hide() {
if (this.hasCardTarget) {
this.cardTarget.classList.add("hidden");
}
}
disconnect() {
if (this.hasCardTarget) {
this.cardTarget.remove();
}
}
}
这是我们要为这个功能编写的所有JavaScript:只有大约30行,我们可以将其用于应用程序中的任何其他悬停卡。这个控制器也没有什么特定的应用,你可以把它拉到一个单独的模块中,并在不同的项目中重新使用它。它是完全通用的。
该控制器使用fetch API来调用提供的Rails端点,获得一些HTML,然后将其插入DOM中。作为一个小的改进,我们使用Stimulustarget API进行数据绑定,以保存对卡片的引用,这样以后在这个链接上的悬停就可以简单地显示/隐藏标记,而无需再进行网络请求。

我们还选择在离开页面时移除该卡片(通过disconnect 生命周期方法),但你也可以选择隐藏该卡片,这取决于你希望缓存如何工作。
后台(Rails + 服务器渲染的HTML)
前端没有什么神奇之处,后端也是如此:
# config/routes.rb
Rails.application.routes.draw do
resources :shoes do
member do
get :hovercard
end
end
end
设置一个路由,用于/shoes/:id/hovercard
# app/controllers/shoes_controller.rb
class ShoesController < ApplicationController
...
def hovercard
@shoe = Shoe.find(params[:id])
render layout: false
end
end
写一个基本的控制器动作,唯一不同的是我们设置了layout: false ,这样我们就不会为这个端点使用全局应用布局。
你甚至可以在浏览器中直接访问这个路径,快速迭代内容和设计。当使用像Tailwind这样的基于实用的造型方法时,工作流程会变得更好,因为你甚至不需要等待你的资产捆绑来重建
<!-- app/views/shoes/hovercard.html.erb -->
<div class="relative" data-hovercard-target="card">
<div data-tooltip-arrow class="absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content">
<div class="flex space-x-3 items-center w-64">
<%= image_tag @shoe.image_url, class: "flex-shrink-0 h-24 w-24 object-cover border border-gray-200 bg-gray-100 rounded", alt: @shoe.name %>
<div class="flex flex-col">
<span class="text-sm leading-5 font-medium text-indigo-600">
<%= @shoe.brand %>
</span>
<span class="text-lg leading-0 font-semibold text-gray-900">
<%= @shoe.name %>
</span>
<span class="flex text-sm text-gray-500">
<%= @shoe.colorway %>
<span class="mx-1">
·
</span>
<%= number_to_currency(@shoe.price.to_f / 100) %>
</span>
</div>
</div>
</div>
</div>
悬浮卡是用服务器端的ERB模板建立的,与Rails应用程序中的其他页面一样。我们设置了data-hovercard-target ,以方便在Stimulus控制器中绑定该元素。
最后的修饰
data-tooltip-arrow 允许我们用一点CSS为气泡添加一个小三角形。如果你有更高级的需求,你可以添加一个像Popper这样的库,但是这个单一的CSS规则非常有效,而且不需要任何外部依赖。
/* app/javascript/stylesheets/application.css */
[data-tooltip-arrow]::after {
content: " ";
position: absolute;
top: 100%;
left: 1rem;
border-width: 2rem;
border-color: white transparent transparent transparent;
}
然后就可以了!我们已经建立了悬停卡!

如果我们想在我们的应用程序中为另一个模型类型(如用户配置文件)添加一个悬停卡,这几乎感觉是在作弊。我们可以使用相同的Stimulus控制器。我们所需要做的就是添加用户的特定模板。
<!-- app/views/users/hovercard.html.erb -->
<div class="relative" data-hovercard-target="card">
<div data-tooltip-arrow class="absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content">
<div class="flex space-x-3 items-center p-1">
<%= image_tag @user.gravatar_url, class: "flex-shrink-0 h-16 w-16 object-cover bg-gray-100 rounded inset shadow-inner", alt: @user.name %>
<div class="flex-1 flex flex-col">
<span class="font-bold text-lg"><%= @user.name %></span>
<div class="flex space-x-1 items-center text-sm">
<svg class="text-orange-400 fill-current h-4 w-4" viewBox="0 0 20 20">...</svg>
<span class="text-gray-500 italic"><%= @user.bio %></span>
</div>
<span class="text-gray-400 text-xs mt-1">
Kickin' it since <%= @user.created_at.year %>
</span>
</div>
</div>
</div>
</div>

把它带到下一个层次
如果你想进一步扩展这个功能,有几个想法你可以考虑:
- 通过提取Rails
partial,使用github/view_component这样的宝石,或者使用Tailwind@apply指令在你的样式表中创建组件,来删除hovercard模板中的一些重复内容。 - 使用CSS过渡来淡入淡出,为hovercard制作动画。
- 添加一个延迟或花哨的 "方向性瞄准"(如亚马逊的巨型下拉菜单),以便您可以更容易地将鼠标移动到悬浮卡上
- 如果您使用
fetchAPI的AbortController,取消一个待定的AJAX请求 - 探索在Rails中使用片段缓存来缓存悬停卡(假设数据不是特定于用户或会话的)。
收起它
这个堆栈是一封写给网络的情书。使用链接和表单。渲染HTML。在服务器上和数据库中保持你的状态。让浏览器处理导航。添加一些互动性的元素来改善体验。对许多人来说,这感觉是一种倒退,但在我看来,这是在回到事情应该有的样子。
怀疑是很自然的,尤其是在当前 "JS所有的东西 "的环境下。但是在你真正理解它之前,你真的必须给这些工具一个尝试。一旦你看到构建软件的经典方法仍然可以完成工作,你就很难再回到调试node_modules 冲突或在这几年的框架中重建HTML表单了。
在今年的RailsConf远程主题演讲中,DHH谈到了发生在软件中的黑格尔辩证法的循环钟摆。新的想法每隔几年就会被回收和重新发现,现在是一个很好的时机,可以顺势而为。