Rails 白话文讲 Turbo Frames-附代码示例

lanzhiheng · June 08, 2021 · Last by mizuhashi replied at June 28, 2021 · 1164 hits

上一篇文章以白话文的形式简单讲了一下 Turbo Drive,这篇文章简单讲讲 Turbo Frames,我会针对一些较为常见的应用场景,提供代码示例。原文链接:https://www.lanzhiheng.com/posts/turbo-frames-simple-talking

Turbo Drive 的问题

上一篇文章讲过 Turbo Drive 能够自动拦截所有的点击事件,并异步获取页面,从而避免全页面刷新的尴尬局面。不过它比较大的问题就是每次请求都需要获取整个页面数据,而在有些场景下我们并不需要获取这么多数据。拿表单提交举个例子,假设表单有以下的特征

  1. 表单提交会往服务端发送 PUT 或者 POST 请求,并携带相关的数据。
  2. 表单提交成功之后表单消失,并遗留一段提交成功的文字。

常规的做法大致有两种

  1. 为提交成功设计一个专用页面,并配上对应路由,表单提交成功之后重定向到该页面。这种场景需要获取很多不必要的页面数据,因为提交前后的页面布局大致一样的,只有部分元素有差异,页面布局没必要重复向服务器获取。
  2. 使用 JavaScript 来实现,手动发送异步请求,请求完成之后使用 DOM 操作把表单替换成目标内容。不过要写一堆的 JavaScript 代码。

Turbo Frames的存在能够让我们更方便地去解决这类问题。

Turbo Frames allow predefined parts of a page to be updated on request. Any links and forms inside a frame are captured, and the frame contents automatically updated after receiving a response.

简单来说借助该技术我们可以把页面分割成一个个 Frames,然后对这些 Frames 进行更细粒度的操作。Turbo Frames 会根据请求响应的内容自动替换指定 Frame 包裹的页面结构。我们不再需要写一堆 JavaScript 来实现 DOM 接点的替换,服务端只需要针对特定 Frame 所包裹的内容提供替换的数据,Turbo Frames 会处理剩下的事情。

简单的表单提交

官方没有完整的案例,我这里提供一个 Turbo Frames 版本的表单实现。

设想一个简单的表单提交

# config/routes.rb

Rails.application.routes.draw do
  resources :messages, only: [:create, :new]
end
<!-- /app/views/messages/new.html.erb -->

<div class="contact-form">
  <h1 class="title">Contact Me</h1>
  <turbo-frame id="message-collector">
    <%= form_with model: @message, method: 'POST' do |f| %>
      <div class="field">
        <%= f.label(:name, "Name") %>
        <%= f.text_field(:name, placeholder: "Your Name") %>
      </div>
      <div class="field">
        <%= f.label(:email, "Email:") %>
        <%= f.text_field(:email, placeholder: "Your Email", type: "email") %>
      </div>
      <div class="field">
        <%= f.label(:content, "Content:") %>
        <%= f.text_area(:content, placeholder: "...", rows: 6) %>
      </div>
      <button class="btn" type="submit">SEND</button>
    <% end %>
  </turbo-frame>
</div>

这个表单主要是利用 POST 请求来收集用户信息,要注意的是我们的from标签被turbo-frame标签包裹着,这个时候可以利用 Rails 请求的响应体来提供需要更换的内容。

# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def new
    @message = Message.new
  end

  def create
    @message = Message.new(permitted_message)
    if @message.save
      render :success, status: :created
    else
      render :new
    end
  end

  private

  def permitted_message
    params.require(:message).permit(:name, :email, :content)
  end
end
<!-- /app/views/messages/success.html.erb -->

<turbo-frame id="message-collector">
  <h1 class="success-message">提交成功</h1>
</turbo-frame>

引入了 Turbo 后项目的逻辑,与传统的 Rails 应用稍微有些不同

数据保存成功之后并不会做 3xx 的页面跳转,而是渲染一个新的模板success.html.erb,它包含turbo-frame标签,这个标签会包裹着需要替换的内容,渲染成功之后 Turbo Frames 会把原先页面的turbo-frame标签里面包裹的东西替换成新的内容。这样带来两个好处

  1. 服务端不需要返回一些无关的数据。
  2. Turbo Frames 根据turbo-frame及其id来识别目标并替换内容,不需要写 JS 代码。

下面演示一下表单提交的效果

form-submit.gif

不用写一句 JavaScript 代码能达到这个的交互效果还是蛮不错的。针对一些较为简单的交互场景,俨然已经十分足够。

额外拓展

1. 摆脱“框架”的束缚

Turbo Frames 虽然做一些交互比较方便,然而它的限制也不少,超出了turbo-frame包裹的区域,就显得有点力不从心了。在包裹区域内原则上所有的跳转都会被拦截

<turbo-frame id="wrapper">
  <a href="<%= root_path %>" >
    被Turbo Frames拦截
  </a>
</turbo-frame>

frame-blocking.gif

也就是说它连简单的跳转到首页都做不到,要想跳出turbo-frame的束缚,你需要给它设定一个目标data-turbo-frame="_top"

<turbo-frame id="wrapper">
  <a href="<%= root_path %>" data-turbo-frame="_top">
    我自由了
  </a>
</turbo-frame>

frame-top.gif

这样它就能脱离束缚,完成向首页跳转的任务了,给人的感觉就很像 iframe。

2. 懒加载

还有一个比较实用的场景就是实现页面的懒加载,为了减少首次页面加载的体积,一些不太重要的页面部件可以采用懒加载的方式,用 Turbo Frames 实现起来超级方便。首先为这个部件定义对应的接口

# config/routes.rb

Rails.application.routes.draw do
  resources :posts do
    collection do
      get :fetch_more
    end
  end
end
# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def fetch_more
  end
end
<turbo-frame id="set_aside_tray">
  <h1>Hello Hotwire</h1>
</turbo-frame>

接下来在你想摆放这个部件的地方,放下这个代码片段即可,Turbo Frames 会帮你完成加载并渲染的事情

<ul class="post-list">
  <% unless @posts.size.zero? %>
    <!-- .... -->
    <turbo-frame id="set_aside_tray" src="<%= fetch_more_posts_path %>">
    </turbo-frame>
  <% else %>
    <p class="empty">Empty.</p>
  <% end %>
</ul>

效果如下,你可以看到一个异步的 Fetch 请求。并且Hello Hotwire也被加载出来了。

fetch-more.gif

总结

这篇文章主要围绕Turbo Frames展开讨论,首先会聊到Turbo Drive的一些局限性,然后会以实际例子展示如何借用 Turbo Frames 来把交互做得更出色,它可能只需要请求更少的数据,就能完成对应的任务,而且不需要写一句 JavaScript 代码。我还顺便列举了一些常见的应用场景的代码示例

  1. 表单提交。
  2. 页面懒加载。
  3. 摆脱 Turbo Frames 的页面跳转。

当然 Turbo Frames 也有它自身的局限性,它每次只能更新页面某一个区域的内容,如果想要进一步增强对页面交互的控制力度,还需要借助后面会谈到的Turbo Streams

老哥,我在使用 turbo frame (webpacker 方式) 的时候出现了这么个问题:

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

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

我观察了下,是没有任何请求发出,感觉像是 js 的问题

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

我将有问题的代码推到了 github 上, https://github.com/jicheng1014/hotwire-rails-demo-chat

官方的代码仅需在我的代码基础上回退一次提交即可 他的地址是 https://github.com/hotwired/hotwire-rails-demo-chat

Reply to jicheng1014

我找时间研究一下。我用你的 Demo 重现了。

@jicheng1014 应该跟 @Rei 说的currentURL有关。你用 asset pipline 导入的是这个版本的Turbo.js,这是以前的版本,还没有 currentURL这个概念,当时用的是loadingURL

如果你用 webpacker 导入的会是最新的Turbo.js问题出在这一行,多次点击之后

this.sourceURL != this.currentURL  # => false

导致 if 里面的代码无法执行,所以得想办法重制 sourceURL 的值。我看官方的 PR 就是改的这里。

Reply to jicheng1014

https://github.com/hotwired/turbo/pull/263 已经 merge 了,升到 beta7 会包含这个修复。

Reply to Rei

升级 @hotwired/turbo-rails 至 beta7 后 bug fix

turboframe 在用 src 加载的时候,会读取对应 url 的整个模板,但是只会用到其中 frame 的部分,这样会造成无用的渲染。但是现在 turboframe 并没有专用的 format,不知道以后会不会加上,虽然直接在 param 里面区分一下应该也可以。

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