在Ruby on Rails应用里面使用Hotwire


Hotwire is a hot topic at the moment for every Rails developer. If you work with Rails, there is a good chance you have already heard a lot about it.
Hotwire 对当前每个Rails开发者来说都算得上是个热门话题, 如果你用Rails,这里有个你可能已经听过很多的好机会。

Hotwire is a completely new way of adding interactivity to your app with very few lines of code, and it works blazing fast by transmitting HTML over the wire. That means you can keep your hands clean from most Single Page Applications (SPA) frameworks. You can also keep your rendering logic centralized on the server, while still maintaining quick page load times and interactivity.
Hotwire是一个完全新的只用增加数行代码,就可以增加动态交互,快速的将HTML代码通过wire传输。
那就意味着你不必使用大多数单页应用框架,弄的乱七八糟的。
你可以继续在服务器端使用rendering的逻辑,保持快速的页面加载和交互性。

In this post, we'll look at the main components of Hotwire and how to use it in your Rails app. But first: what is Hotwire and why should you use it?
这篇文章中我们重点看Hotwire里的主要组件如何在Rails app中使用。
但是首先要看的是:什么是Hotwire和为什么你应该用它?

What Is Hotwire?
什么是Hotwire?

Hotwire is not a single library, but a new approach to building web and mobile applications by sending HTML over the wire. It includes Turbo, Stimulus, and Strada (coming later this year). We will discuss each of these in detail in the next section.
Hotwire是一个库,也是一个新方法:通过wire来 发送HTML的方式来创建web和mobile应用程序。
它包含Trubo,Stimulus,and Strada(今年2022会出来).
我们将会讨论这些细节在下一个章节.

Side note: While Hotwire is highly linked with Rails, it is completely language-agnostic, so it can work just as well with other applications. I have been using Stimulus in production on several non-Rails apps and some static websites. You can use Turbo without Rails as well.
Side note: 虽然Hotwire是一个高度贴合Rails,它完全是language-agnostic, 因此它能够work和其它应用程序。
我将 非Rails应用和一些静态网站使用Stimulus,且用在生产环境中。
你也可以使用Turbo without Rails。

But let us come back to the Rails world for now.
先让我们回到Rails的世界。

Why Use Hotwire in Your Rails App?
为什么在Rails App中使用Hotwire?

So when should you use Hotwire? The answer is anywhere you want to add interactivity to your application. For example, if you want:
你什么时候应该使用Hotwire?
答案是:任何你想给你应用程序增加交互的地方。

  • Some content to be displayed/hidden conditionally based on a user's interaction (e.g., an address form where the list of states automatically changes based on the selected country).   
  • 一些内容根据用户的操作显示和隐藏,比如:更加用户选择了某个国家,那对应的省份的显示。
  • To update some content in real-time (e.g., a feed like Twitter where new Tweets automatically get added to the page).
  • 去实时的更新内容(例如:推特的新的tweets自动增加到页面上)
  • To lazy-load some parts of your pages (e.g., inside an accordion, you can load the titles and mark the details to be lazy-loaded to speed up load times).
  • 延迟加载页面上的内容(例如:在一个折叠区域里,你可以加载标题并将细节标记为lazy-loaded用来加快加载时间。

Hotwire Components
Hotwire 组件

As mentioned before, Hotwire is a collection of new (and some old) techniques for building web apps.
之前提到过,Hotwire是一组新技术用来构建web应用。

Let's discuss each of these in the next few sections.
下面然我们来一一探讨各部分。

Turbo

HTML drives Turbo at its core. Turbo provides several techniques to handle HTML data coming over the wire and display it on your application without performing a full page reload. It is composed of:
HTML 以 Turbo为它的核心。
Turbo提供多个技术去处理来自wire的 HTML data,且显示这些内容而不必加载整页。它的组成部分有:

  • Turbo Drive
    • If you have used Turbolinks in the past, you will feel right at home with Turbo Drive. At its core, some JS code intercepts JavaScript events on your application, loads HTML asynchronously, and replaces parts of your HTML markup.
    • 如果你用过Turbolinks,你用Turbo Drive就像在家一样。它的核心是一些JS代码拦截JavaScript的事件,异步加载HTML,替换部分HTML的标记。

  • Turbo Frames
    • Turbo Frames decouple parts of your markup into different sections that can be loaded independently.
    • Turbo Frame分离你的标记为不同的sections,然后可以独立的加载
    • For example, if you have a blog application, the content of your post and the comments are two related but independent parts of the page. You can decouple them so navigation works independently or even load them asynchronously with turbo frames.
    • 例如,如果你有一个blog,你的post和comments内容是2个相关但又是页面上独立的部分。你可以分离他们通过不同的导航打开,或者通过turbo frames异步加载。
  • Turbo Streams
    • Turbo Streams offers utilities to easily bring in real-time data to your application. For example, let's say you are building a news feed like Twitter. You want to pull new tweets into a user's feed as soon as they are posted without reloading the page. Turbo Streams allow you to do this without writing a single line of JS.
    • Turbo Streams提供的功能可以很容易的在你的应用程序里面引入实时数据。例如,你创建了一个新闻比如Twitter.你想让把这个消息推给用户而不需要用户刷新页面。Turbo Streams允许你做这些而不写一句JS。
  • Turbo Native
    • Turbo Native lets you build a native wrapper around your web application. Navigations and interactions will feel native without you having to redo all the screens natively.
    • Turbo Native 让你可以给你的web应用构建一个native wrapper原生的壳, 导航和交互就像原生应用一样,而不需要你再去为不同的设备开发。
    • You'll keep delivering the rest of the application through the web. That way, you can focus on the really interactive parts of your application and get them right.
    • 你将继续通过web提供delivering应用程序的其它功能。那样的话,你就能集中精力在应用程序真正的交互部分,并把它们做好。

Stimulus

Stimulus is a JavaScript framework for writing controllers that interact with your HTML.
Stimulus 是一个JavaScript框架用来写 controllers 和HTMl交互。

Let's say we need to add some JavaScript attributes like data-controller, data-action, and data-target to elements on a page. We'll write a stimulus controller with access to elements that receives events based on those attributes. Here's an example:
比方说我们需要增加给页面上的元素一些JavaScript属性比如:data-controller, data-action, 和 data-target  。
我们将会些stimulus controller 访问这些元素,并基于这些属性attributes接收事件。看个例子:

<div data-controller="clipboard">
  PIN:
  <input data-clipboard-target="source" type="text" value="1234" readonly />
  <button data-action="clipboard#copy">Copy to Clipboard</button>
</div>

It is very easy to get an idea about what this does without even reading the associated Stimulus controller.
就算不去读关联的Stimulus controller, 这很容易知道这段代码的作用。

Here's a controller that goes with the HTML:
这是配合上面的HTML的controller。

// src/controllers/clipboard_controller.js
import { Controller } from "@hotwired/stimulus";
 
export default class extends Controller {
  static targets = ["source"];
 
  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value);
  }
}

That is at the core of Stimulus: keeping things simple and reusable.
那是Stimulus的核心,保存事情的简单和可复用。

Now, if you ever need a copy-to-the-clipboard button on another page, you can just re-use that controller. Add the data-* attributes on the markup to get everything working.
现在你需要在另外一个页面上增加 copy-to-clipboard 按钮,你就能够复用这个contrller了。
只需要在 各个标记markup上增加 data-×属性让这一切运转起来了。

Strada

Unfortunately, we don't know much about Strada yet. But it will allow a web application to communicate (and possibly perform actions) with a native app using HTML bridge attributes.
不幸的是,我们对Strada了解的还不多,但是它将运行一个web应用程序(可能通过actions的形式)去和原生app的HTML桥属性 进行交流。

How to Use Hotwire in Your Ruby on Rails Application
如何在你的Ruby on Rails应用程序里面使用Hotwire。

I don't want to spend too much time discussing Hotwire installation or a basic use case. The Hotwire team has already done an excellent job of it in their Hotwire screencast. For full instructions, see turbo-rails installation and Stimulus installation.
我不想花费太多时间讨论Hotwire的安装或一些基本的使用。
Hotwire 组员已经做的非常好了,在
Hotwire screencast. 里面可以看到。
完整的安装说明,可以查看
turbo-rails installationStimulus installation.

Let's jump straight into some common Hotwire use cases.
让我们直接进入一些常见的Hotwire的使用案例。

Endless Scroll
无尽的滚动

Using Turbo Frames, we can easily make a page with automatic pagination as the user scrolls. For this, we need to do two things:
使用Turbo Frames, 我们能够容易让用户在向下滚动的时候自动翻页。做到这一点,只需要做2件事:

  1. Render each "page" inside its own frame by appending the page number to the frame id (e.g., turbo_frame_tag "posts_#{@posts.current_page}").
    通过给turbo_frame添加一个当前页的页数作为 frame id 的方式,把每页page 放进它自己的frame里,
  2. Use a lazy frame for the next page so that it doesn't load automatically unless it comes into view.
    通过将下一页设置为 lazy frame ,下一页就不会自动加载,除非要展示下一页了。

<%= turbo_frame_tag "posts_#{@posts.current_page}" do %>
  <%= render @posts %>
  <% unless @posts.last_page? %>
    <%= turbo_frame_tag "posts_#{@posts.next_page}", :src => path_to_next_page(@posts), :loading => "lazy" do %>
      <%= render "loading" %>
    <% end  %>
  <% end  %>
<% end %>

Note that this example uses methods from Kaminari, but you can adapt it to any other pagination method.
注意这个例子使用的是Kaminari,你可以改用其它的分页方法。

We don't need anything special in the controller. A standard index method works:
我们不需要在controller任何特别的代码,一个标准的index方法就可以。

class PostsController < ApplicationController
  def index
    @posts = Post.page(params[:page]).per(params[:per_page])
  end
end

The trick here is that we use nested frames, with the frame for the next page nested inside the frame for the previous page. That way, when the first page loads, the frame for the next page is placed at the end. When the user scrolls to that frame, it is replaced with the content of the second page. The lazy frame for the third page renders at the end.
这里的一个伎俩就是:我们用嵌套的frames, 通过将下一页嵌入到上一页里。
那样的话,当第一页加载是,这个下一页的frame就被放在最下面,当用户滚动到这个frame时候,下一页的frame就被第二页的内容替换掉。
这个第三页的lazy frame放置在最后。

Dynamic Forms
动态Froms


You can easily implement dynamic forms with Hotwire without custom logic for toggling fields on the front end. This is a bit more involved than the endless scroll use case, as it includes the use of both Turbo Stream and Stimulus.
你可以使用Hotwire 来实施implement 动态表格,且不需要去自定义切换字段的前端逻辑。
这比Endless Scroll的案例要复杂点,因为它包含了Turbo和Stimulus。

Let's start with our form first.
我们先从form开始

<!-- app/views/posts/new.html.erb -->
<div data-controller="refresh-form" data-refresh-form-url="<%= refresh_form_posts_url(:target => "new_post") %>">
  <%= render "form" %>
</div>
 
<!-- app/views/posts/_form.html.erb -->
<%= form_for(@post, :data => { :target => "refresh-form.form" }) do |f| %>
  <%= f.select :kind, options_for_select([["News", :news], ["Blog", :blog]], @post.kind), {}, data: { action: "change->refresh-form#refreshForm" } %>
 
  <%= f.select :category, options_for_select(categories_for_kind(@post.kind), @post.category) %>
<% end %>

The form is simple enough — we display a kind select with News and Blog options. We want to change the available categories' values based on the kind that is selected (assuming that categories_for_kind(@post.kind) returns the list of categories for the given kind).
这个form是足够简单了,一个kind 的 select,可以选择 News 和Blog选项。
我们想改变这个可以用的categories的值,根据所选的kind(假设 categories_for_kind(@post.kind )返回这个categories列表。

If you look closer, you'll see that we've added some data attributes to the form. The data-target will link the form element to the RefreshFormController Stimulus Controller's form target. And the data-action with the value of change->refresh-form#refreshForm will call the refreshForm method on the linked Stimulus Controller every time the kind select is changed.
如果你看的仔细点,你会发现我们增加了一些data attributes 给这个表单。
这个data-target将链接到form 元素去 RefreshFormController  Stimulus Controller's form target.
这个data-action带的值是 change->refresh-form#refreshForm, 每当kind选中的值被改变, 它就会去调用 Stimulus Controllers 的refreshForm 方法,

Let's look at our Stimulus Controller:
让我们来看看Stimulus Controller:

// app/javascript/controllers/refresh_form_controller.js
import { Controller } from "stimulus";
import { put } from "@rails/request.js";
 
export default class extends Controller {
  static targets = ["form"];
 
  refreshForm() {
    put(this.data.get("url"), {
      body: new FormData(this.formTarget),
      responseKind: "turbo-stream",
    });
  }
}

On all refreshForm calls, we just make a new PUT request to the controller's URL (set using the data-refresh-form-url on the same element with a data-controller="refresh-form"). The important part here is that the responseKind is set to turbo-stream. The @rails/request library understands this response and performs instructions based on the response stream.
对于所有的refreshForm调用,我们做了一个PUT请求给controller's URL。 (来自data-refresh-form-url,   同时元素上还有个data-controller="refresh-form". 这里在stimulus controller里面this.data.get("url")就直接省掉了controller)。
重要的部分是 responseKind 是设置为turbo-stream.
这个@rails/request 库理解这个response, 而且根据返回的stream执行instructions

Now all that's left is to return the correct stream from our refresh_form call for Turbo to understand and update our form.
现在剩下的根据我们的refresh_form调用就是返回正确的stream 给Turbo去理解并更新我们的form表单。

class PostsController < ApplicationController
  def refresh_form
    @post = Post.new
    @post.attributes = post_params
    @post.valid?
    respond_to do |format|
      format.turbo_stream
    end
  end
end

Just update the attributes on the post and mark that you want to respond in a turbo_stream format (so that it looks up refresh_form.turbo_stream.erb).
只更新post的属性,并且标记你想用turbo_stream格式响应。(它会去找 refresh_form.turbo_stream.erb)

<!-- app/views/posts/refresh_form.turbo_stream.erb -->
<%= turbo_stream.replace params[:target] do %>
  <%= render "form" %>
<% end %>

In this step, we are reusing our form partial, wrapping it inside a turbo_stream with a replace action.
这一步,我们复用了我们的form partial, 包含在带有replace的action的 turbo_stream里面。

And that's all you need to get a dynamic form working. I know this looks a bit advanced, but the refresh stimulus controller is a shared part you can now use for all your dynamic forms by adding the correct data-* attributes. So essentially, you now get server-side dynamic form refresh without writing any new JS for other forms. Pretty awesome, right?
这是全部你需要的去让一个动态form运行起来要做的。
我知道这看上去有点高级,但是refresh stimulus controller 是一个共享的部分,你现在就能够通过 添加正确的data-×属性  将它用在所有的form里面。
因此基本上,你现在得到了服务端的表单刷新功能,但是你没用对其它form写任何JS。
Pretty awesome right? 是不是非常棒?

Append Content to Pages Without Reloading
增加内容而不用重新加载页面。

The next use case that Hotwire makes easy is streaming HTML over a WebSocket connection and updating a page with new content as it comes in. A good example of this is the GitHub comments section. You can implement this very easily using Turbo Streams.
下一个例子是:通过WebSocket传输streaming HTML让更新一个页面变得容易多了。一个好例子就是GitHub的评论,你可以用Turbo Streams非常容易的实现。

There are two parts to this.
这里有2部分。

First, we embed a turbo stream listener on the listing page that opens a WebSocket connection to the server and listens for events.
首先,我们嵌入一个turbo stream 监听 在列表页上,打开一个WebSocket链接到服务器,监听事件。

<!-- app/views/comments/index.html.erb -->
<div id="comments">
  <%= turbo_stream_from @post, :comments %>
 
  <% @comments.each do |comment| %>
    <%= render comment %>
  <% end %>
</div>

Next, we update the model to broadcast new comments to the stream.
下一步,我们更新这个model去广播新的comments到stream。

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
 
  after_create_commit :stream
 
  private
 
  def stream
    broadcast_prepend_later_to(post, :comments, target: :comments)
  end
end

You don't need anything else. Turbo will automatically render the app/views/comments/_comment.html.erb partial for each new comment and send it over a WebSocket connection. It will be picked up by Turbo's JS and prepended to the target with id comments.
你不需要其它的了,Turbo将会自动render app/views/comments/_comment.html.erb,将新的comment通过WebSocket传过去。
内容将被Turbo的JS获取picked up, 追加到comments。

Let's go one step ahead and add an indication to all newly added comments with a small Stimulus Controller.
让我们往前多走一步,使用一个小小的Stimulus Controller增加一个指示 给所有新的增加的comments,

First, modify the broadcast and comment partial to include the controller conditionally.
首先,修改广播和comment partial 去根据条件包含控制器。

# app/models/comment.rb
# ...
def stream
  broadcast_prepend_later_to(post, :comments,
                             target: :comments,
                             locals: { highlight: true })
end

<!-- app/views/comments/_comment.html.erb -->
<div <%= %s(data-controller="highlight") if local_assigns[:highlight] %>
  <%= comment.body %>
</div>

This small Stimulus controller adds a special highlight class on connection for 3 seconds and then removes it.
Stimulus controller显示3秒的高亮的样式,然后删除。

export default class extends Controller {
  connect() {
    this.element.classList.add("highlight");
    this.timeout = setTimeout(
      () => this.element.classList.remove("highlight"),
      3000
    );
  }
 
  disconnect() {
    clearTimeout(this.timeout);
  }
}

Note
: You also need to update the CSS highlighting based on the presence of that class.
备注:你也需要去更新CSS高亮,基于高亮的class的表现。

Once this controller is done, you can re-use it on anything that requires a highlight class. You could even modify it to get the duration and class name from data attributes if you need that flexibility.
完成此控制器后,您可以在任何需要高亮显示类的情况下重复使用它。如果需要灵活性,您甚至可以修改它以从数据属性中获取持续时间和类名

That's the great thing about Hotwire — it takes you a long way, and you don't have to dip your hands in JS. When you do need to write some JS, Stimulus gives you the tools to build small generic controllers that can be re-used.

Wrap Up and Further Reading

The Rails community has been really excited with the introduction of Hotwire, and rightly so.

In this post, we looked at the key components of Hotwire and how to use Hotwire in your Rails app. We touched on how you can bring your application to life using Turbo and Stimulus.

The official Hotwire screencast introduction and the Turbo documentation are great places to see what Hotwire and Turbo can do for you.

For advanced usage, I suggest heading over to the turbo-rails GitHub repo. Sadly, the documentation is a bit sparse, but if you are not afraid to get your hands dirty, read the code and inline comments in:

  1. Turbo::FramesHelper for Turbo Frames.
  2. Turbo::Broadcastable for broadcasting to Turbo Streams from the code.
  3. Turbo::Streams::TagBuilder for broadcasting to Turbo Streams as part of inline controller actions.

Happy coding!


P.S. If you'd like to read Ruby Magic posts as soon as they get off the press,
subscribe to our Ruby Magic newsletter and never miss a single post!



https://blog.appsignal.com/2022/07/06/get-started-with-hotwire-in-your-ruby-on-rails-app.html
阅读量: 359
发布于:
修改于: