Ruby 聊一下 turbo stream

jicheng1014 · June 05, 2021 · Last by jicheng1014 replied at June 10, 2021 · 1425 hits

这两天几个朋友都讨论了 turbo 的内容,我继续来吹吹水

turbo 目前分了三个板块,分别是 Turbo Drive , Turbo frame 和 Turbo Streams. 前两个其实理解起来都不大,Turbo Drive 就是以前的 turbolinks 的改版,Turbo Frame 则跟原来的 iframe 的表现差不多。

但是 Turbo Stream, 大家知道,是解决页面局部更新,或者增加,删除内容的,但是这个却不太好类比,特别是 https://turbo.hotwire.dev/handbook/streams 的文档写的非常的模糊,所以导致很多人不是很理解。包括我最开始也认为 turbo stream 就是将渲染好的 html 利用 actionCable web-socket 推送过去更新。

后来再结合了 gorails 的视频之后,发现 其实,turbo stream 更像是对 SJR 的一层封装,至于是用 websocket 还是使用别的处理,其实关系不大。

那么,咱来说一个典型场景:

我有一个 计划任务,当他暂停的时候是一种界面,启动的时候是另外一种界面

当用户点击“Pause”或者 "Run" 的时候,请求 switch 接口 我们会更新整个 card 的界面。

处理这种场景,有几种解决方案:

  1. turbolinks 自己处理。这种用一句话概括,就是什么都不管,点击了之后,服务器渲染出完整的 html, 之后 turbolinks 再拿到这个完整的 html 做 body 的重新渲染。这种好处很明显:不需要额外的开发量。但是也有一些缺点:比如页面其他地方有 js, 也会走一遍 turbolinks:load. 再比如如果点击时候内容不在首屏,会出现页面跳动的问题。那么怎么解决这个呢?就有第二种方式,前端组件,如 典型的 React Component

  2. 前端组件,如 React Component. 好处明显,因为是前端控制,所以可以更新 React Component 里的内容,只需要后端返回 api 即可。缺点也很简单:前后端逻辑对接很麻烦,特别是自己一个人写前后端的时候,觉得有种自己制造困难的幻觉。那么有没有其他方式呢?也有,SJR

  3. SJR, Server Javascript Render, 服务器渲染一个 js, 之后执行,其实这种就很好理解 这里就是个替换逻辑,我们通过渲染一个 js, 在 js 模版里再渲染 card 的 html 代码即可 核心代码如下

    # switch.js.erb 
    $("#<%= dom_id(card)%>").replaceWith("<%= render card %>")
    

这段 js 模版 要做的事情: 找到 card 对应的 dom, 之后渲染 _card.html.erb 局部 html , 将渲染完成的代码替换

这种方式也有麻烦之处:你得去写一个 swich.js.erb 文件。当这种小请求特别多的时候,整个 view 的文件就会变的非常的多。

那么 现在有没有更好的选择呢,答案是,有,turbo stream.

  1. Turbo Stream 的本质 就是 将 符合 Turbo Stream 规定的格式运行 turbo js 里的几种基本 js(prepend, append replace destroy)

还是拿刚的例子,如果使用 Turbo Stream, 当安装了配套的 Gem turbo-rails 我们想更新 card, 只需要在 controller 里的 render 增加 一种格式,并指出是 replace

respond_to do |format|
      format.turbo_stream {
        render turbo_stream: turbo_stream.replace(card)
      }
#...
end

这时候,当 请求过来的时候 , rails 就会渲染 _card.html.erb 这个模版,且用 turbo stream 能理解的方式

<turbo-stream action="replace" target="card_5"><template>
<div class="bg-white rounded-sm shadow-sm card hover:shadow-md" id='card_5'>

      <!-- 具体的界面内容 --> 

</div>
</template></turbo-stream>

此时浏览器的 turbo 的 js 会读取 repsonse 的Content-Type: text/vnd.turbo-stream 的上面的模版,按照预定的 replace js 方法 找到 dom id='card_5' 的 节点,执行 action replace 也就是替换内容,具体的内容则为 template 里面的 html 代码

这样,turbo streams 就实现了对 card 的内容做了更改。其实观察一下,就可以发现,这个跟 SJR 非常像,只是少写了自己的 js view 而已。

那么如果我不使用 render , 而是传说中的 turbo stream 通过 websocket 推,是咋回事呢?

其实就是 通过 websocket 的特定频道,将这个模版渲染的内容发出去,之后 turbo 会接收这个频道的内容,之后将这个内容运行特定的 js

还是举刚才的例子

我们需要在 cards/index 中 加入下面的语句

  <%@cards.each do |card| %>
      <%= turbo_stream_from card %>
<%end %>

(特别注意:不要将这个 turbo_stream_from 放在 你要刷新的_card.html.erb 里面,否则在更新这个 card 后会重新订阅频道,造成问题)

此时 模版会渲染出

<turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name="IloybGtPaTh2Y21sbWJHVXZRMkZ3VTJOb1pXUjFiR1V2Tmci--efc485a5b72affcea7f826d06f0b33c62ee5b05a2e783a55077d8af0c017e803"></turbo-cable-stream-source>

turbo.js 就会引导 cable 监听到具体的频道 在访问有 card 的地址的时候,我们打开 chrome 调试器 看 ws 的 cable message, 里面就有类似

    .   identifier: "{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"IloybGtPaTh2Y21sbWJHVXZRMkZ3VTJOb1pXUjFiR1V2TlEi--ac9e9e25628401b8c09090c63290335a279a7cfc1e0af45328dd1c03be340e09\"}"
type: "confirm_subscription"

的内容。

那么 我们怎样向对应的频道里推 符合 turbo stream 模版格式的内容,就像 turbo_stream: turbo_stream.replace(card) 一样?

目前,我找到的方法是

Turbo::StreamsChannel.broadcast_action_to card, action: :replace, target: "card_#{card.id}", partial: "/cards/card", locals: {card: card}

在运行了上面的方法后,ruby 会在对应的频道里推下面的信息,

    .   identifier: "{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"IloybGtPaTh2Y21sbWJHVXZRMkZ3VTJOb1pXUjFiR1V2Tmci--efc485a5b72affcea7f826d06f0b33c62ee5b05a2e783a55077d8af0c017e803\"}"
message: "<turbo-stream action=\"replace\" target=\"card_6\"><template>\n<div class=\"bg-white rounded-sm shadow-sm card hover:shadow-md\" id='card_6'>\n  <div class=\"p-2\">\n\n    <a href=\"/cards/6\">\n  </div>\n  </div>\n</div>\n</template></turbo-stream>"


之后,就如同 在 render 里发生的事情一样,turbo js 会根据 tubo stream 的 action replace 来进行替换对应的 dom

在这里需要特别指出的是,如果你既有 render turbo.stream, 又有 channel 推的时候,turbo 会处理两次。怎么避免这个问题呢? 很简单 如果你确认要用 actionCable 更新 那么请在

respond 的时候 设置 format.turbo_stream {}, 即返回空,这样就只交给了 actionCable 来处理。

另外,如果你不想写类似 Turbo::StreamsChannel.broadcast_action_to 这种东西,也可以试试 在 model 里使用 Turbo::Broadcastable 提供的方法 具体参见 turbo-rails/broadcastable.rb at main · hotwired/turbo-rails · GitHub

好了,就简单的聊了下 turbo stream, 希望对大家理解 turbo stream 有一定的帮助

你好,问你个问题。

gem install redis ERROR: Error installing redis: invalid gem: package is corrupt, exception while verifying: wrong number of arguments (given 2, expected 1) (ArgumentError) in C:/Ruby30-x64/lib/ruby/gems/3.0.0/cache/redis-4.2.5.gem

这是个怎么解决呢?

Reply to 15235440141

试试删掉 C:/Ruby30-x64/lib/ruby/gems/3.0.0/cache/redis-4.2.5.gem 缓存内的 gem,重新安装。

另外,看你的文件路径好像是 windows。社区不推荐新手用 windows https://ruby-china.org/wiki/install_ruby_guide

Reply to 15235440141

借楼提问不太符合规范,建议自己开一个帖子

wrong number of arguments (given 2, expected 1)

这个错误大概率是由于 ruby3 的关键字参数特性修改引起的,这个 gem 应该还不支持 ruby3

Reply to spike76

不好意思,新人注册 7 天不能发帖

Reply to zhuoerri

好的,我试试

Reply to zhuoerri

Fetching: redis-4.2.5.gem (100%) ERROR: Error installing redis: invalid gem: package is corrupt, exception while verifying: wrong number of arguments (given 2, expected 1) (ArgumentError) in C:/Ruby30-x64/lib/ruby/gems/3.0.0/cache/redis-4.2.5.gem 删掉 重新执行也是一样,我是在 windows 上搭个 redis 集群开发

Reply to 15235440141

可能是 rubygems 版本的问题,我看你日志里的路径是 ruby/gems/3.0.0。 我本地用 rvm 安装 ruby 3.0 后,gem -v 返回 3.2.3, 能正常 gem install redis -v 4.2.5。

但如果我切换 rubygem 版本 rvm install rubygems 3.0.0 --force , 或者 gem update --system 3.0.0 也会报错提示wrong number of arguments (given 2, expected 1

另外,目前我测试的 turbo-rails beta5 版本 在 webpacker 下,turbo frame 似乎是有 bug 的:

如果 有一个 frame 返回了 form, 这个 form 提交后继续返回一个新的 frame, 这个新的 frame (由 form 提交产生) 虽然会被正确替换,但是这个 frame 的链接将无法再使用新的 frame

举个例子,在官方 demo 中,点击修改 room 名称,出现 修改 room 的 form, 修改完成 room 后,继续正确显示 room 信息,但是在这个 room 信息,再次点击修改,则无响应。

这个问题 在 assets 模式下 则没有任何问题

有兴趣的朋友可以看下 https://github.com/jicheng1014/hotwire-rails-demo-chat

是我从官方 fork 出来,之后切换到 webpacker 的情况,会复现我说的这个问题

Reply to zhuoerri

是版本问题,解决了,谢谢

另外根据帖子 https://ruby-china.org/topics/41360 的描述 其实 stream 也可以在 render 的时候 指定 turbo_stream 模版

指定 turbo_stream 的好处在于,你可以通过不同的 target, 更新不同的 dom

You need to Sign in before reply, if you don't have an account, please Sign up first.