这两天几个朋友都讨论了 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 的界面。
处理这种场景,有几种解决方案:
turbolinks 自己处理。这种用一句话概括,就是什么都不管,点击了之后,服务器渲染出完整的 html, 之后 turbolinks 再拿到这个完整的 html 做 body 的重新渲染。这种好处很明显:不需要额外的开发量。但是也有一些缺点:比如页面其他地方有 js, 也会走一遍 turbolinks:load
. 再比如如果点击时候内容不在首屏,会出现页面跳动的问题。那么怎么解决这个呢?就有第二种方式,前端组件,如 典型的 React Component
前端组件,如 React Component. 好处明显,因为是前端控制,所以可以更新 React Component 里的内容,只需要后端返回 api 即可。缺点也很简单:前后端逻辑对接很麻烦,特别是自己一个人写前后端的时候,觉得有种自己制造困难的幻觉。那么有没有其他方式呢?也有,SJR
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.
还是拿刚的例子,如果使用 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 有一定的帮助