Real-time updates with Turbo Streams

本章节,我们将学习如何使用Action Cable 广播 Turbo Stream templates,来让我们的页面进行实时跟新。

Turbo Stream format 可以仅用几行代码就与 Action Cable结合,来让我们的页面实时更新,当然与:群聊,通知,邮箱服务是类似的。

让我们用邮箱服务来举例,比如当我们收到一封新的邮箱,我们不想去手动的刷新让它显示,相反我们希望它能自己在页面上更新,而不需要我们操作什么。

而实现这一功能对于Rails来说很容易,因为在Rails5时就发布了Active Cable。本章将要讨论的Turbo Rails的一部分是建立在Action Cable之上的,而实现该功能,也就更加简单了。

我们要做什么

来想象一下,如果有许多人同时使用我们的quote编辑器,他们更希望实时看到同事们都写了什么。

Quotes#index页面:

  • 任何时候一个成员创建了新的quote,我们希望该quote立刻被加到我们的quotes列表的最上面
  • 任何时候一个成员修改了一个quote,我们希望修改的内容,能立刻显示在页面上
  • 任何时候一个成员删除了一个quote,我们希望被删除的内容,能立刻消失

这听起来很麻烦。但这个需求可以让我们的学习如何使用Turbo Stream来在首页中实时的更新,

使用Turbo Stream广播新建的quotes

为了做到这一点,我们必须告诉Quote模型去广播新建的quote的HTML在创建后,让我们来改改

# app/models/quote.rb

class Quote < ApplicationRecord
  # All the previous code

  after_create_commit -> { broadcast_prepend_to "quotes", partial: "quotes/quote", locals: { quote: self }, target: "quotes" }
end

让我们先写下这些代码,当我们在浏览器中感受一下,就能更清晰的知道代码的含义了

首先,我们使用了after_create_commit回调去通知Rails,在每次向数据库内新加一条数据时,执行这个lambda表达式

第二段lanbda表达式中的代码就更复杂了,它会通知rails,新建的quote对应的HTML应该被广播到那些订阅了quotes stream的用户那里,并在DOM中放到id为quotes的节点前面

我们将会解释到底该怎么做,但现在我们应注意生成的HTML是什么样子的。

<turbo-stream action="prepend" target="quotes">
  <template>
    <turbo-frame id="quote_123">
      <!-- The HTML for the quote partial -->
    </turbo-frame>
  </template>
</turbo-stream>

有没有感觉这个代码很眼熟,和我们上一节中QutoesController#create将新创建的数据放到quotes列表的前面,所生成的HTML代码是一样的。

唯一不同的是,这次的HTML是通过WebSocket传递的,而不是通过ajax的响应


注意:这里的例子我们是将新加的数据放到最前面,我们当然也可以使用broadcast_append_to去把新加的数据,放到列表的后面


为了能够订阅到quotes流,我们需要在Quotes#index中加入下面的代码

<%# app/views/quotes/index.html.erb %>

<%= turbo_stream_from "quotes" %>

<%# All the previous HTML markup %>

而这段代码生成的HTML是这样子的:

<turbo-cable-stream-source
  channel="Turbo::StreamsChannel"
  signed-stream-name="very-long-string"
>
</turbo-cable-stream-source>

可以看到生成了一段来源于Turbo JavaScript Library的自定义标签,用订阅用户到在channel属性中命名的通道,更具体地说,是在signed-stream-name属性中命名的流。

Turbo::StreamsChannel里面的channel参数名就是Action Cable channel 的名字,Turbo Rails总会使用这个channel,所以这个参数名始终是一样的。

signed-stream-name参数是使用quotes的一个签名,它是为了防止一些恶意用户干预并从流中获取我们的HTML。这个我们会在下一章中细讲,现在你只需要知道这个长的字符串,解码后就quotes

现在所有的Quotes#index页面中的用户都能监听到这个Turbo::StreamsChannel,并等待quotes流中的订阅数据,每当给数据库添加一个新的数据时,这些用户将收到Turbo Stream format中的HTML,并把数据放到相应的位置。

现在让我们看看是不是像我们预想的一样,下面会介绍两种方式去测试我们的代码

Testing Turbo Streams in the console

本章中,每次我们对Quote模型做修改时,我们都需要重启rails console在测试之前,否则会出现些意料之外的事儿


注意:我们在console中测试前,需要确保Redis被正确的配置在应用中。

在开发环境,你的config/cable.yml应该长下面的样子:

# config/cable.yml

development:
  adapter: redis
  url: redis://localhost:6379/1

# All the rest of the file

如果情况一致,那你可以忽略下面的提示了。


否则,你应该下载Redis,然后运行:bin/rails turbo:install,它会修改config/cable.yml文件中的配置,如果没问题了,就可以继续往下了


现在我们在浏览器中,打开Quotes#index页面,然后在rails console中创建一个新的quote:

Quote.create!(name: "Broadcasted quote")

然我们在console logs中看看发生了什么?第一件事儿:

TRANSACTION (0.1ms)  begin transaction
Quote Create (0.4ms)  INSERT INTO "quotes" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "Broadcasted quote"], ["created_at", "2021-10-16 12:03:54.401034"], ["updated_at", "2021-10-16 12:03:54.401034"]]
TRANSACTION (0.8ms)  commit transaction

可以看到,插入一条新的数据,然后事务提交,再往下:

Rendered quotes/_quote.html.erb (Duration: 0.5ms | Allocations: 285)
[ActionCable] Broadcasting to quotes: "<turbo-stream action=\"prepend\" target=\"quotes\"><template><turbo-frame id=\"quote_908005754\">\nThe HTML of our quotes/_quote.html.erb partial</turbo-frame></template></turbo-stream>"

内容很长,但确实很有趣的一部分

首先我们注意到,通过ActionCable广播了一段HTML到名字为quotes的流中,由于我们刚才在Quotes#index页面中加入了turbo_stream_from 'quotes',所以我们可以订阅到Stream,并获取到它广播通知的HTML

其次我们注意到,被广播通知的HTML是在Turbo Stream format中,它会通知Turbo去将