Ruby 聊一下 turbo stream

jicheng1014 · 2021年06月05日 · 最后由 jicheng1014 回复于 2021年06月10日 · 761 次阅读

这两天几个朋友都讨论了 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

这是个怎么解决呢?

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

15235440141 回复

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

wrong number of arguments (given 2, expected 1)

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

spike76 回复

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

zhuoerri 回复

好的,我试试

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 集群开发

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 的情况, 会复现我说的这个问题

zhuoerri 回复

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

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

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

需要 登录 后方可回复, 如果你还没有账号请 注册新账号