上一篇文章以白话文的形式简单讲了一下 Turbo Drive,这篇文章简单讲讲 Turbo Frames,我会针对一些较为常见的应用场景,提供代码示例。原文链接:https://www.lanzhiheng.com/posts/turbo-frames-simple-talking
上一篇文章讲过 Turbo Drive 能够自动拦截所有的点击事件,并异步获取页面,从而避免全页面刷新的尴尬局面。不过它比较大的问题就是每次请求都需要获取整个页面数据,而在有些场景下我们并不需要获取这么多数据。拿表单提交举个例子,假设表单有以下的特征
常规的做法大致有两种
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
标签里面包裹的东西替换成新的内容。这样带来两个好处
turbo-frame
及其id
来识别目标并替换内容,不需要写 JS 代码。下面演示一下表单提交的效果
不用写一句 JavaScript 代码能达到这个的交互效果还是蛮不错的。针对一些较为简单的交互场景,俨然已经十分足够。
Turbo Frames 虽然做一些交互比较方便,然而它的限制也不少,超出了turbo-frame
包裹的区域,就显得有点力不从心了。在包裹区域内原则上所有的跳转都会被拦截
<turbo-frame id="wrapper">
<a href="<%= root_path %>" >
被Turbo Frames拦截
</a>
</turbo-frame>
也就是说它连简单的跳转到首页都做不到,要想跳出turbo-frame
的束缚,你需要给它设定一个目标data-turbo-frame="_top"
<turbo-frame id="wrapper">
<a href="<%= root_path %>" data-turbo-frame="_top">
我自由了
</a>
</turbo-frame>
这样它就能脱离束缚,完成向首页跳转的任务了,给人的感觉就很像 iframe。
还有一个比较实用的场景就是实现页面的懒加载,为了减少首次页面加载的体积,一些不太重要的页面部件可以采用懒加载的方式,用 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
也被加载出来了。
这篇文章主要围绕Turbo Frames展开讨论,首先会聊到Turbo Drive的一些局限性,然后会以实际例子展示如何借用 Turbo Frames 来把交互做得更出色,它可能只需要请求更少的数据,就能完成对应的任务,而且不需要写一句 JavaScript 代码。我还顺便列举了一些常见的应用场景的代码示例
当然 Turbo Frames 也有它自身的局限性,它每次只能更新页面某一个区域的内容,如果想要进一步增强对页面交互的控制力度,还需要借助后面会谈到的Turbo Streams。