上一篇文章以白话文的形式简单讲了一下Turbo Frames,这篇文章打算介绍Turbo的最后一个部件Turbo Streams。同样我会结合实际场景提供代码示例。

Turbo Frames的问题

上一篇文章讲过Turbo Frames的作用跟iframe很像,设定一个区域,区域中的任何操作都只会影响该区域的内容。在不需要编写JavaScript的前提下这种做法还是挺方便的,不过也有它的局限性每次请求的操作有限,只能影响区域内的内容。如果想要页面其他区间作出响应,依旧需要额外编写JavaScript代码。.

Turbo Streams进一步增强了我们对页面的操控能力。

Turbo Streams deliver page changes as fragments of HTML wrapped in self-executing <turbo-stream> elements. Each stream element specifies an action together with a target ID to declare what should happen to the HTML inside it.

依然是不需要写JavaScript代码,后端服务以声明的方式提供HTML模板,以DOM的id作为唯一标识,并配上对应的动作,Turbo Streams接收到这个模板之后就会

  1. 在页面中找到对应id的节点。
  2. 执行相关的动作。

模板大概长这个样子

<turbo-stream action="append" target="messages">
  <template>
    <div id="message_1">
      This div will be appended to the element with the DOM ID "messages".
    </div>
  </template>
</turbo-stream>

篇幅有限我就不全部贴了,官方文档都有。这里表示它会寻找id为messages的容器,然后往里面追加template标签中的内容。类似的动作还有prepend, replace, update, remove,意思也就是字面上的意思。

也就是说,它只提供了较为基本的5个操作,足以满足日常工作中的大部分DOM操作需求。如果需要更加复杂的交互,再考虑使用JavaScript来专门解决。

一次请求,多处变动

先把WebSocket或者SSE放一边,看看普通的请求模式Turbo Streams的能力。下面是我最近对博客系统管理后台的重写,需求很简单

  1. 提供一个文章上架/下架的按钮。
  2. 点击按钮往后台发送请求。
  3. 请求完成之后1)按钮的文字要变换。2. 列表中对应状态要改变。3. 提供对应的flash信息。

change-area.png

效果如下:

toggle-change.gif

虽然对一个自己用的系统来说,这种做法稍微有点过度优化了,直接跳页面岂不是简单很多?不过只要依赖于Turbo Streams做成这种交互并不会比重新刷页面要复杂多少,姑且一试。

Controller部分很简单,只需要提供turbo_stream的对应响应即可。

module Musk
  class PostsController < ApplicationController
    def toggle
      original = resource.draft
      resource.update(draft: !original)
      message = original ? '上架成功' : '下架成功'
      flash.now[:notice] = "《#{resource.title}》#{message}"

      respond_to do |format|
        format.turbo_stream { render :toggle, locals: { post: resource } } # turbo_stream响应
        format.html { redirect_back(fallback_location: collection_path) }
      end
    end
  end
end

这里要提供两种渠道,一种是响应HTML的,另一种是响应Turbo Streams,这个后台会根据请求头的Accept字段来判断。

accept-header.png

接下来只需要像传统的Rails项目那样提供对应的模板就行,只不过这次的模板的后缀是turbo_stream.erb

<!-- app/views/musk/posts/toggle.turbo_stream.erb -->

<!-- 修改对应资源在表格中的状态,假设资源的id为1,那么就修改id="table_online_1"的单元格 -->
<turbo-stream action="update" target=<%= "table_online_#{post.id}" %>>
  <template>
    <%= boolean_tag(post.online) %>
  </template>
</turbo-stream>

<!-- 修改触发按钮的内容,“上架”按钮,请求结束后会变成“下架”按钮,反之亦然 -->
<turbo-stream action="replace" target=<%= "toggle_link_#{post.id}" %>>
  <template>
    <%= toggle_link(post) %>
  </template>
</turbo-stream>

<!-- 显示flash信息,注意我这里用的是flash.now,因为只需要对当前渲染生效,页面跳转后消失 -->
<turbo-stream action="update" target="alert-area">
  <template>
    <%= alert_area %>
  </template>
</turbo-stream>

简单到有点难以置信吧?篇幅有限,boolean_tag, toggle_link, alert_area这些帮助方法我就不贴了,大家可以根据自己的需要去定制。如果需要参考相关源码可以到笔者的Stone仓库查看。

可见只需要少量的代码,配合服务端渲染,不需要写任何JavaScript代码,就能同时操作页面的不同区域,只不过要事先给待调整的地方做上id唯一标记。这对于熟悉Rails,并不想写太多JavaScript的人来说确实是个不错的工具。我们能够很容易就写出交互性较好的页面。

总结

这篇文章通过案例简单讲解了一下Turbo Streams的基本用法,由于笔者还没有Websocket或者SSE相关的使用场景,这个放在以后再说。作为对Turbo Frames的补充,Turbo Streams只提供了5种(append, prepend, replace, update, remove)基本操作。不过对于大多数场景来说已然足够,或许也是符合80/20的原则?然而作为一个依赖服务端渲染的工具集Turbo Streams依然有它的局限性,毕竟还是有一些场景5种基本操作无法满足的,这个时候我们就需要依赖JavaScript去解决了,接下来的Stimulus框架是Rails社区的首选。