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

lanzhiheng · 2021年06月08日 · 最后由 mizuhashi 回复于 2021年06月28日 · 1275 次阅读

上一篇文章以白话文的形式简单讲了一下 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

jicheng1014 回复

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

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

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

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

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

jicheng1014 回复

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

Rei 回复

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

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

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