使用 Hotwire 构建响应式 Rails 应用





5.00/5 (5投票s)
在本文中,我们将介绍 Hotwire 的基础知识,并使用它构建一个示例应用程序。
响应式 Rails 应用有哪些选择?
2013 年 6 月 25 日,Rails 4 发布,引入了 Turbolinks。Turbolinks 为 Rails 的“响应性”做了什么?Turbolinks 会拦截所有链接点击,而不是发送常规的 GET
请求,而是发送异步 JavaScript 请求 (AJAX) 来获取 HTML。然后它会合并获取页面的 head
标签,并替换整个页面的 body
标签,因此不会发生整页重载。不会重新加载样式表或脚本,这意味着页面导航更快。但它仍然是替换整个 body
,而不是页面中仅发生变化的部分。
但如果你只想重新加载发生变化的部分呢?嗯,你可以使用 Rails AJAX 助手,当你将某些元素标记为 data-remote='true'
时,这些元素会发送 AJAX GET/POST
请求而不是常规的 GET/POST
请求。Rails 会响应生成的 JS 代码,然后由浏览器执行这些代码来动态更新页面中的这些部分。
然后我们可以转到前端的一些 JS 组件(例如,使用 React),使应用程序感觉更加响应。因此,JS 组件发送 AJAX 请求,Rails 服务器以 JSON 数据响应。然后前端框架将收到的 JSON 转换为 DOM 元素并更新 DOM 以反映这些更改。这效果很好,唯一的缺点是它混合了页面上的服务器端渲染和客户端渲染。
如今,另一种更传统的方式是全力以赴,在前端使用 React、Vue 或其他 JS 框架,只使用客户端渲染,即所谓的单页应用程序 (SPA)。通过这种方法,前端是一个单独的应用程序,它向 Rails 服务器发送 AJAX 请求,而 Rails 仅作为 JSON API。如你可能知道,构建、维护和部署两个具有可互换数据的独立应用程序存在很多复杂性。
但如果你可以在不构建两个独立应用程序和编写大量 JavaScript 代码的复杂性的情况下构建 SPA 呢?Hotwire 可以帮助你。
什么是 Hotwire?
Hotwire 是一种构建类似 SPA 的 Web 应用程序的替代方法,它在服务器端使用 Rails 模板渲染所有 HTML,同时保持应用程序快速响应。在服务器端保留渲染可以简化您的开发体验并提高生产力。Hotwire 这个名字基本上是 `HTML Over the Wire` 的缩写,意思是发送生成的 HTML 而不是 JSON 从服务器到客户端。它也不需要你编写太多自定义 JavaScript 代码。Hotwire 由 Turbo 和 Stimulus 组成。
什么是 Turbo?
Turbo gem 是 Hotwire 的核心。它是一套动态更新页面的技术,通过将页面划分为可以部分更新的组件,并利用 Websockets 作为传输,从而加快导航和表单提交速度。如果你曾经在 Rails 中使用过 websockets,你很可能知道 Rails 使用 ActionCable 来处理 websockets 连接,并且它默认包含在 Rails 中。Turbo gem 包括:Turbo Drive、Turbo Frames 和 Turbo Streams。
Turbo Drive
Turbo Drive 用于拦截链接点击(就像 Turbolinks 以前所做的那样),并且还拦截表单提交。Turbo Drive 然后会合并页面的 head
标签并替换页面的 body
标签。与 Turbolinks 的情况相同,不会发生整页重载,这对某些页面来说可能很快,但对于 2022 年的应用程序来说并不是真正响应得当,因此你可能需要考虑仅更新页面上的某些部分,而不是整个 body。这时 Turboframes 就派上用场了。
Turbo Frames
你可以通过简单地将页面的某一部分包装在带有唯一 ID 的 turbo-frame
标签中,来将其变成一个 Turbo Frame。
<turbo-frame id="13">
...
</turbo-frame>
这使得框架内的任何交互都限定在该框架内。该框架内的任何交互都会向 Rails 服务器发送 AJAX 请求,服务器仅响应该框架的 HTML。这允许 Turbo 自动仅替换页面上的该框架。这不需要编写任何 JavaScript。
但如果你想同时更新页面的多个部分呢?这就是 Turbo Streams 可以帮助你的地方。
Turbo Streams
当用户与页面上的元素(例如,表单/链接)进行交互,并且 Turbo Drive 向服务器发送 AJAX 请求时,服务器会以 HTML 响应,该 HTML 由 Turbo Stream 元素组成。这些就像是 Turbo 跟随以更新受影响页面部分的指令。Turbo Streams 包括七种可用操作:append
、prepend
、(插入)before
、(插入)after
、replace
、update
和 remove
。
<turbo-stream action="append" target="target_a">
<template>
HTML
</template>
</turbo-stream>
<turbo-stream action="prepend" target="target_b">
<template>
HTML
</template>
</turbo-stream>
<turbo-stream action="replace" target="target_c">
<template>
HTML
</template>
</turbo-stream>
Turbo Streams 使用 ActionCable 通过 websockets 异步地将更新传递给多个客户端。同样,你可以在不编写任何 JavaScript 代码的情况下获得所有这些功能。但即使你出于任何原因需要一些自定义 JavaScript(例如,某些动画、日期选择器等),Hotwire 也有 Stimulus 来满足你的需求。
什么是 Stimulus?
就像 Rails 有其控制器和操作一样,Stimulus 允许你以类似的方式组织客户端代码。你有一个控制器,它是一个 JavaScript 对象,它定义了操作,即 Javascript 函数。然后,你使用 HTML 属性将控制器操作连接到页面上的交互式元素。然后,当 DOM 事件触发时,操作将作为响应运行。
让我们创建一个带 Hotwire 的示例 Rails 应用
阅读完以上内容,你可能在想:我该如何使用它?Hotwire 非常简单易用,我们只需要一个标准的 Rails 应用和一个 Redis 服务器。
首先,你需要安装 Ruby 3 和 Rails 7 以及 Redis 服务器,我将不介绍这些的安装过程,但你可以在你的平台上轻松找到任何你需要的说明。
因此,让我们设置一个新的 Rails 应用(我们将使用 Bootstrap 作为 CSS 选项,只是为了让我们的应用程序看起来更好一些)
rails new bookstore --css bootstrap
Rails 生成所有必要文件后,进入 app 目录
cd bookstore
Rails 7 的初始应用具备了开始使用 Hotwire 所需的一切,Gemfile 包括:Redis gem、Turbo-rails gem 和 Stimulus-rails。
确保你的 Redis 服务器正在运行。Rails 需要 Redis,因为它被 ActionCable 用来存储与 websockets 相关的信息。
Rails 连接到 Redis 服务器的默认地址和端口在 config/cable.yml 中设置。
development:
adapter: redis
url: redis://:6379/1
然后我们可以生成我们的模型、控制器和迁移,在我们的 Bookstore
的情况下,它们将是“Books
”。它将有一个 string title
,text
类型的 description
,以及一个 integer
类型的 likes
计数器。
rails g scaffold books title:string description:text likes:integer
让我们修复生成的迁移,以便我们添加的任何书籍在数据库中默认喜欢数为 0。
class CreateBooks < ActiveRecord::Migration[7.0]
def change
create_table :books do |t|
t.string :title
t.text :description
t.integer :likes, default: 0
t.timestamps
end
end
end
不要忘记为我们的应用程序创建数据库,在终端运行:
rake db:create db:migrate
让我们将图书列表页面设为应用程序的根页面,打开 config/routes.rb 并添加缺失的根声明。
Rails.application.routes.draw do
root 'books#index'
resources :books
end
然后你应该能够通过在终端中运行 rails server
命令或 ./bin/dev(该命令还会监视 css 和 js 更改)来启动 rails 服务器,并且当你访问浏览器中的 https://:3000 时,你应该会看到类似这样的内容。
让我们修改 Book
局部视图 app/views/books/_book.html.erb 为:
<%= turbo_stream_from "book_#{book.id}".to_sym %>
<%= turbo_frame_tag "book_#{book.id}" do %>
<div style="background: lightblue; padding: 10px; width: 400px;">
<h2><%= book.title %></h2>
<p><%= book.description %></p>
<br>
<%= button_to "Like (#{book.likes})",
book_path(book, book: { likes: (book.likes + 1) }), method: :put %>
</div>
<br/>
<% end %>
turbo_stream_from
告诉 Hotwire 使用 websocket 来更新由 :book_id
标识的框架,而 turbo_frame_tag
标识了一个框架,该框架可以通过更新的局部视图进行替换。
为了告诉 Turbo 我们希望将每本新创建的书添加到图书列表的开头,并在每次点击“喜欢”按钮时更新喜欢计数,我们需要将以下回调添加到 app/models/book.rb 文件中(我们还可以添加一个验证)。
class Book < ApplicationRecord
after_create_commit { broadcast_prepend_to :books }
after_update_commit { broadcast_replace_to "book_#{id}".to_sym }
validates :title, :description, presence: true
end
第一个告诉 Turbo 在创建时使用 :books
Turbo Stream 进行更新,第二个告诉使用 :book_id
来用更新替换局部视图。
然后让我们修复图书控制器中的排序,并添加新的图书变量赋值(这样我们就可以从根路径创建一个图书)在 app/controllers/books_controller.rb 中。
...
def index
@books = Book.order(created_at: :desc)
@book = Book.new
end
...
我们还应该编辑 app/views/books/index.html.erb 来添加我们的 Turbo Streams 和 Turbo Frames。
<h1>Books</h1>
<%= turbo_stream_from :books %>
<%= turbo_frame_tag :book_form do %>
<%= render 'books/form', book: @book %>
<% end %>
<%= turbo_frame_tag :books do %>
<%= render @books %>
<% end %>
为了避免在创建新图书或更新现有图书时重定向,并在同一主页面上停留,我们还需要编辑 app/controllers/books_controller.rb 中的 create 和 update 操作。
...
def create
@book = Book.new(book_params)
respond_to do |format|
if @book.save
format.html { redirect_to root_path }
else
format.turbo_stream { render turbo_stream:
turbo_stream.replace(@book, partial: 'books/form', locals: { book: @book }) }
format.html { render :new, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @book.update(book_params)
format.html { redirect_to root_path }
else
format.html { render :edit, status: :unprocessable_entity }
end
end
end
...
此时,我们的书店应用程序看起来应该是这样的:
结论
每当你通过主页上的表单创建新书时,Turbo 都会将其添加到图书列表的开头,而无需重新加载页面。如果你在浏览器中打开多个标签页 - 它将更新所有标签页。“喜欢”按钮也可以在没有页面重载的情况下工作,并更新所有标签页中图书的喜欢计数。所有这些都不需要一行 JavaScript 代码。这有多酷?
这个示例应用程序只是一个基本示例,展示了你在 Rails 中可以使用 Hotwire 做些什么,但你可以使用 Turbo 和 Stimulus 完成更复杂的事情。
因此,如果你想使用 Rails 开始一个全新的 SPA,请仔细考虑你是否需要 React、Vue 或其他前端框架,尝试 Hotwire 可能会更具成效。很有可能它会让你满意。
历史
- 2022 年 8 月 7 日:初始版本