动机
在BootrAils,我们根本不使用Hotwire。我们认为不使用它,启动一个MVP会更简单。也就是说,Hotwire看起来很有前途,值得特别关注,因为它是默认与Rails一起发货的。
为什么是Hotwire
Hotwire试图在编写全栈式Web应用时限制JavaScript的使用。它不是一个lib或gem,而是一组实现这一目标的功能。有趣的是,也有适用于其他服务器端框架的Hotwire,比如Django。在本教程中,我们将看到如何在Rails中使用它。
什么是Hotwire
因此,Hotwire是一套技术。
-
涡轮,本身是由:
-
Turbo Drive:每个链接都不会在点击时触发全页面重载。相反,只有
<body>标签内的HTML会被替换。 -
涡轮框架:将把一个页面分解成可以单独更新的内容片段。在Turbo Frames之前,一个页面=一个URL。
-
涡轮流:与涡轮框架的想法相同,但范围更广,我们将看到如何。
-
Turbo Native:针对移动设备--在本教程中不涉及。
-
-
刺激剂:
- Stimulus是一个微小的JS工具,它不渲染任何HTML(与大多数JS前端框架相反),而是在现有的HTML之上增加一些响应性。
Rails + Hotwire教程的先决条件
$> ruby -v
ruby 3.0.0p0 // you need at least version 3 here
$> bundle -v
Bundler version 2.2.11
$> npm -v
8.3.0 // you need at least version 7.1 here
$> yarn -v
1.22.10
创建新的Rails项目,从头开始
在你的shell中键入.NET技术。
mkdir railshotwire && cd railshotwire
echo "source 'https://rubygems.org'" > Gemfile
echo "gem 'rails', '~> 7.0.0'" >> Gemfile
bundle install
bundle exec rails new . --force -d=postgresql
创建简单的文件(目前没有Hotwire:)。
继续在你的shell中键入 。
# Create a default controller
echo "class HomeController < ApplicationController" > app/controllers/home_controller.rb
echo "end" >> app/controllers/home_controller.rb
# Create another controller
echo "class OtherController < ApplicationController" > app/controllers/other_controller.rb
echo "end" >> app/controllers/other_controller.rb
# Create routes
echo "Rails.application.routes.draw do" > config/routes.rb
echo ' get "home/index"' >> config/routes.rb
echo ' get "other/index"' >> config/routes.rb
echo ' root to: "home#index"' >> config/routes.rb
echo 'end' >> config/routes.rb
# Create a default view
mkdir app/views/home
echo '<h1>This is home</h1>' > app/views/home/index.html.erb
echo '<div><%= link_to "go to other page", other_index_path %></div>' >> app/views/home/index.html.erb
# Create another view
mkdir app/views/other
echo '<h1>This is another page</h1>' > app/views/other/index.html.erb
echo '<div><%= link_to "go to home page", root_path %></div>' >> app/views/other/index.html.erb
# Create database and schema.rb
bin/rails db:create
bin/rails db:migrate
很好!运行
bin/rails s
并打开你的浏览器,在本地看到你的应用程序。你应该看到像这样的东西。

localhost
热线:涡轮驱动器
打开你的浏览器DevTools的 "网络 "标签。
现在通过点击链接从主页导航到另一个页面。你能注意到什么?导航是通过XHR实现的。每次导航时都不重新加载CSS,也不重新加载JavaScript。事实上,唯一被重新加载的是HTMLbody 标签内的DOM差异。

涡轮驱动
为了什么?
- 根据我们的经验,感知到的性能差距绝对是巨大的,每次点击都会立即响应,即使是已部署的、可生产的应用程序。这意味着不仅是在本地主机上,在你的电脑上,这些改进可能不会被注意到。
- 然而,这也伴随着一些问题,如DOM闪烁,更少的可访问性,和一些更奇怪的行为。
Turbo Drive非常强大,但需要时间和耐心来掌握它。
Hotwire : 涡轮框架
涡轮框架允许开发者将当前页面分割成若干块,当新的数据来自服务器时,这些块可以被单独更新。
涡轮框架的经典用例有::
- 内联版
- 标签式的内容
- 搜索、排序和过滤数据
让我们看一个例子。
像这样改变app/controllers/home_controller.rb 。
class HomeController < ApplicationController
# @route GET /turbo_frame_form
def turbo_frame_form
end
# @route POST /turbo_frame_submit
def turbo_frame_submit
extracted_anynumber = params[:any][:anynumber]
render :turbo_frame_form, status: :ok, locals: {anynumber: extracted_anynumber, comment: 'turbo_frame_submit ok' }
end
end
用这个内容添加app/views/home/turbo_frame_form.html.erb 。
<section>
<%= turbo_frame_tag 'anyframe' do %>
<div>
<h2>Frame view</h2>
<%= form_with scope: :any, url: turbo_frame_submit_path, local: true do |form| %>
<%= form.label :anynumber, 'Type an integer (odd or even)', 'class' => 'my-0 d-inline' %>
<%= form.text_field :anynumber, type: 'number', 'required' => 'true', 'value' => "#{local_assigns[:anynumber] || 0}", 'aria-describedby' => 'anynumber' %>
<%= form.submit 'Submit this number', 'id' => 'submit-number' %>
<% end %>
</div>
<div>
<h2>Data of the view</h2>
<pre style="font-size: .7rem;"><%= JSON.pretty_generate(local_assigns) %></pre>
</div>
<% end %>
</section>
然后像这样改变你的routes.rb 。
Rails.application.routes.draw do
get 'home/index'
get 'other/index'
get '/home/turbo_frame_form' => 'home#turbo_frame_form', as: 'turbo_frame_form'
post '/home/turbo_frame_submit' => 'home#turbo_frame_submit', as: 'turbo_frame_submit'
root to: "home#index"
end
最后像这样改变主视图,在app/views/home/index.html.erb
<h1>This is home</h1>
<div><%= link_to "go to other page", other_index_path %></div>
<%= turbo_frame_tag 'anyframe' do %>
<div>
<h2>Home view</h2>
<%= form_with scope: :any, url: turbo_frame_submit_path, local: true do |form| %>
<%= form.label :anynumber, 'Type an integer (odd or even)', 'class' => 'my-0 d-inline' %>
<%= form.text_field :anynumber, type: 'number', 'required' => 'true', 'value' => "#{local_assigns[:anynumber] || 0}", 'aria-describedby' => 'anynumber' %>
<%= form.submit 'Submit this number', 'id' => 'submit-number' %>
<% end %>
<div>
<% end %>
重新启动你的本地网络服务器,刷新你的本地浏览器,你应该可以在第一时间看到这个。

默认视图
现在在字段中输入任何数字,并提交表格。像这样的东西应该出现。

运行中的涡轮框架
只有框架部分发生了变化,第一个标题和第一个链接并没有移动。
热线:涡轮流
理论
Turbo Streams通过WebSocket、SSE或响应表单提交来传递页面变化,只需使用HTML和一组类似CRUD的操作。
换句话说,你可以:
- 在响应POST/PUT/PATCH/DELETE动作时,更新一个HTML块(GET不工作)
- 向所有用户广播一个变化,而不需要刷新任何浏览器。
最简单的例子
像这样改变app/controllers/other_controller.rb 。
class OtherController < ApplicationController
def post_something
respond_to do |format|
format.turbo_stream { }
end
end
end
然后像这样改变你的routes.rb 。
Rails.application.routes.draw do
get 'home/index'
get 'other/index'
get '/home/turbo_frame_form' => 'home#turbo_frame_form', as: 'turbo_frame_form'
post '/home/turbo_frame_submit' => 'home#turbo_frame_submit', as: 'turbo_frame_submit'
# Add this line below
post '/other/post_something' => 'other#post_something', as: 'post_something'
root to: "home#index"
end
很好!现在每次到达'/other/post_something'端点时,rails会自动尝试找到app/views/other/post_something.turbo_stream.erb模板。
添加app/views/other/post_something.turbo_stream.erb ,内容如下。
<turbo-stream action="append" target="messages">
<template>
<div id="message_1">This changes the existing message!</div>
</template>
</turbo-stream>
很好 !这意味着响应将尝试附加(见 "action "属性)id为 "messages "的turbo-frame模板。
最后将app/views/other/index.html.erb ,内容如下。
<h1>This is another page</h1>
<div><%= link_to "go to home page", root_path %></div>
<div style="margin-top: 3rem;">
<%= form_with scope: :any, url: post_something_path do |form| %>
<%= form.submit 'Post something' %>
<% end %>
<turbo-frame id="messages">
<div>An empty message</div>
</turbo-frame>
</div>
现在启动你的本地网络服务器
bin/rails s
打开你的网络浏览器,并进入 "其他 "页面。

其他页面
点击 "Post something "按钮。

增加一行
很好 !Turbo Streams允许开发者在提交后附加一条信息,而不需要重新加载页面。这发生得相当无缝。
如果我们想 "替换 "信息,而不是 "添加 "新的信息呢?
改变app/views/other/post_something.turbo_stream.erb ,内容如下。
<turbo-stream action="replace" target="messages">
<template>
<div id="message_1">This changes the existing message!</div>
</template>
</turbo-stream>
只有第1行的属性 "action "发生了变化:action现在被替换了。
你可以在你的本地浏览器中检查一切是否正常工作。
Hotwire : Turbo native
Turbo native有助于在移动设备上构建应用程序,这里将不涉及。但是我们不会删除这一段,这样你就会完全知道Turbo Native是Hotwire的一部分 :)
刺激
Hotwire实际上有一个JS工具,好像Hotwire的目标就是要避免它。原因是Turbo-*工具不足以覆盖所有场景。有一些情况下,仍然需要JS。为了限制对它的需求,Stimulus认为HTML是唯一的真理来源(对于那些了解Redux的人来说)。
像这样改变app/views/other/index.html.erb 。
<h1>This is another page</h1>
<div><%= link_to "go to home page", root_path %></div>
<div style="margin-top: 2rem;">
<%= form_with scope: :any, url: post_something_path do |form| %>
<%= form.submit 'Post something' %>
<% end %>
<turbo-frame id="messages">
<div>An empty message</div>
</turbo-frame>
</div>
<div style="margin-top: 2rem;">
<h2>Stimulus</h2>
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="click->hello#greet">
Greet
</button>
<span data-hello-target="output">
</span>
</div>
</div>
像这样修改app/javascript/controllers/hello_controller.js 。
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
greet() {
this.outputTarget.textContent =
`Hello, ${this.nameTarget.value}!`
}
}
在localhost打开你的浏览器,进入 "其他 "页面,然后玩这个例子。
这个例子是直接从文档中摘取的。这足以让我们理解Stimulus的目标:在前端增加互动性。
结论性的想法
Hotwire很不错。最好的消息是,用户体验--也就是你的终端用户,推动了现在的炒作和创新。
他们说,减少JavaScript,增加响应性。
这部分是正确的。在实践中,需要一些时间来真正正确地处理它。我们遇到了一些关于可访问性的问题和一些复杂的用例,Stimulus没有能力解决。除此以外,它可能会给你节省一些jQuery/vanillaJS的麻烦。对于MVP来说,Hotwire可以被看作是可有可无的,因为它增加了一层复杂性--这就是为什么我们在bootrails中跳过它。