Ruby on Rails 7: Hotwire va Turbo cho Ung Dung Phan Hoi
Huong dan day du ve Hotwire va Turbo trong Rails 7. Xay dung ung dung phan hoi khong can JavaScript voi Turbo Drive, Frames va Streams.

Rails 7 da cach mang hoa viec phat trien web bang cach tich hop Hotwire mac dinh. Stack nay cho phep xay dung cac ung dung co tinh phan hoi cao ma khong can viet bat ky dong JavaScript tuy chinh nao. Turbo Drive, Turbo Frames va Turbo Streams thay the cac cach tiep can SPA truyen thong bang triet ly "HTML qua day".
Hotwire giam manh do phuc tap cua frontend. Khong can React hay Vue cho giao dien dong: server gui HTML san sang su dung va Turbo tu dong xu ly cac cap nhat DOM.
Hieu ve Kien Truc Hotwire
Hotwire bao gom ba cong nghe bo sung hoat dong cung nhau de mang lai trai nghiem nguoi dung muot ma khong co do phuc tap dien hinh cua cac framework JavaScript.
Turbo Drive tang toc dieu huong bang cach chan cac click lien ket va gui bieu mau. Thay vi tai lai toan bo trang, chi noi dung <body> duoc thay the, giu nguyen ngu canh JavaScript va CSS.
Turbo Frames chia cac trang thanh cac phan doc lap. Moi frame co the duoc cap nhat rieng biet, cho phep tuong tac co muc tieu ma khong anh huong den phan con lai cua trang.
Turbo Streams cho phep cap nhat thoi gian thuc qua WebSocket hoac dap ung cac yeu cau HTTP. Tam hanh dong CRUD co san de thao tac DOM khai bao.
# Gemfile
# Installing Turbo Rails (included by default in Rails 7+)
gem 'turbo-rails'Doi voi cac du an hien co, viec cai dat chi can mot lenh duy nhat.
# installation.sh
# Installing Turbo in an existing Rails project
rails turbo:install
# Verify the JavaScript import is present
cat app/javascript/application.js
# Should contain: import "@hotwired/turbo-rails"Cau Hinh Ban Dau Turbo Drive
Turbo Drive duoc bat mac dinh trong Rails 7. Tat ca dieu huong tu dong tro thanh "turbo" ma khong can cau hinh them. Hanh vi co the tuy chinh thong qua cac thuoc tinh data.
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<meta name="turbo-cache-control" content="no-preview">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<!-- Progress bar during navigation -->
<div class="turbo-progress-bar"></div>
<%= yield %>
</body>
</html>Turbo Drive co the duoc tat tren cac lien ket hoac bieu mau cu the khi can thiet.
<!-- app/views/pages/examples.html.erb -->
<!-- Link with Turbo enabled (default) -->
<%= link_to "Dashboard", dashboard_path %>
<!-- Disable Turbo for a specific link -->
<%= link_to "Download PDF", export_path, data: { turbo: false } %>
<!-- Disable Turbo for a form (file upload for example) -->
<%= form_with model: @document, data: { turbo: false } do |f| %>
<%= f.file_field :attachment %>
<%= f.submit "Upload" %>
<% end %>
<!-- Turbo with confirmation -->
<%= link_to "Delete", item_path(@item),
data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>Turbo Drive luu cache cac trang da truy cap. Thuoc tinh data-turbo-track="reload" tren tai nguyen buoc tai lai day du neu cac file CSS/JS thay doi.
Turbo Frames cho Cap Nhat Co Muc Tieu
Turbo Frames dinh nghia cac vung trang duoc cap nhat doc lap. Moi frame co mot dinh danh duy nhat va chi phan hoi cac response chua frame trung khop.
<!-- app/views/messages/index.html.erb -->
<h1>Messages</h1>
<!-- Frame for the message list -->
<turbo-frame id="messages_list">
<div id="messages">
<%= render @messages %>
</div>
<%= link_to "Load more", messages_path(page: @next_page) %>
</turbo-frame>
<!-- Frame for the creation form -->
<turbo-frame id="new_message">
<%= render "form", message: Message.new %>
</turbo-frame>Phan hoi tu server phai chua frame voi cung dinh danh de cap nhat hoat dong.
<!-- app/views/messages/_message.html.erb -->
<!-- Partial for an individual message -->
<turbo-frame id="<%= dom_id(message) %>">
<div class="message">
<p><%= message.content %></p>
<span class="metadata">
By <%= message.author.name %> - <%= time_ago_in_words(message.created_at) %>
</span>
<!-- These links stay within the frame -->
<%= link_to "Edit", edit_message_path(message) %>
<%= button_to "Delete", message_path(message), method: :delete %>
</div>
</turbo-frame>Lazy Loading voi Turbo Frames
Cac frame co the tai noi dung bat dong bo su dung thuoc tinh src. Noi dung ban dau duoc hien thi trong khi tai.
<!-- app/views/dashboard/show.html.erb -->
<h1>Dashboard</h1>
<!-- Statistics loaded asynchronously -->
<turbo-frame id="stats" src="<%= dashboard_stats_path %>">
<div class="loading-placeholder">
<p>Loading statistics...</p>
</div>
</turbo-frame>
<!-- Notifications loaded when visible (lazy loading) -->
<turbo-frame id="notifications"
src="<%= notifications_path %>"
loading="lazy">
<p>Loading notifications...</p>
</turbo-frame>
<!-- Recent activity with periodic refresh -->
<turbo-frame id="recent_activity"
src="<%= activity_path %>"
data-controller="auto-refresh"
data-auto-refresh-interval-value="30000">
<%= render "activity/placeholder" %>
</turbo-frame>Controller phan hoi voi view chi chua frame duoc yeu cau.
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
def stats
@user_count = User.count
@message_count = Message.count
@active_today = User.where("last_seen_at > ?", 24.hours.ago).count
# Partial rendering for the frame
render partial: "dashboard/stats"
end
end<!-- app/views/dashboard/_stats.html.erb -->
<turbo-frame id="stats">
<div class="stats-grid">
<div class="stat-card">
<h3><%= @user_count %></h3>
<p>Users</p>
</div>
<div class="stat-card">
<h3><%= @message_count %></h3>
<p>Messages</p>
</div>
<div class="stat-card">
<h3><%= @active_today %></h3>
<p>Active today</p>
</div>
</div>
</turbo-frame>Sẵn sàng chinh phục phỏng vấn Ruby on Rails?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Turbo Streams cho Cap Nhat Thoi Gian Thuc
Turbo Streams cung cap tam hanh dong de thao tac DOM: append, prepend, replace, update, remove, before, after va morph. Cac hanh dong nay co the duoc kich hoat qua phan hoi HTTP hoac WebSocket.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
@message = current_user.messages.build(message_params)
respond_to do |format|
if @message.save
# Turbo Stream response to add the message
format.turbo_stream do
render turbo_stream: [
turbo_stream.prepend("messages", @message),
turbo_stream.update("message_count", partial: "messages/count"),
turbo_stream.replace("new_message", partial: "messages/form",
locals: { message: Message.new })
]
end
format.html { redirect_to messages_path, notice: "Message created" }
else
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"new_message",
partial: "messages/form",
locals: { message: @message }
)
end
format.html { render :new, status: :unprocessable_entity }
end
end
end
def destroy
@message = Message.find(params[:id])
@message.destroy
respond_to do |format|
# Removal with animation
format.turbo_stream { render turbo_stream: turbo_stream.remove(@message) }
format.html { redirect_to messages_path, notice: "Message deleted" }
end
end
endTemplate Turbo Stream Chuyen Dung
Doi voi cac phan hoi phuc tap hon, template .turbo_stream.erb mang lai su linh hoat hon.
<!-- app/views/messages/create.turbo_stream.erb -->
<!-- Prepend the new message to the list -->
<%= turbo_stream.prepend "messages" do %>
<%= render @message %>
<% end %>
<!-- Update the counter -->
<%= turbo_stream.update "message_count" do %>
<span><%= Message.count %> messages</span>
<% end %>
<!-- Reset the form -->
<%= turbo_stream.replace "new_message" do %>
<%= render "form", message: Message.new %>
<% end %>
<!-- Display a flash notification -->
<%= turbo_stream.prepend "flash_messages" do %>
<div class="flash flash-success" data-controller="auto-dismiss">
Message sent successfully!
</div>
<% end %>Cac phan hoi Turbo Stream phai co content-type text/vnd.turbo-stream.html. Rails xu ly dieu nay tu dong voi respond_to va dinh dang turbo_stream.
Broadcasting Thoi Gian Thuc voi Action Cable
Turbo Streams thuc su toa sang voi broadcasting WebSocket. Nguoi dung nhan cap nhat tuc thi ma khong can polling.
# app/models/message.rb
class Message < ApplicationRecord
belongs_to :room
belongs_to :author, class_name: "User"
# Automatic broadcast after creation
after_create_commit do
broadcast_append_to(
room,
target: "messages",
partial: "messages/message",
locals: { message: self }
)
end
# Broadcast after update
after_update_commit do
broadcast_replace_to(
room,
target: dom_id(self),
partial: "messages/message",
locals: { message: self }
)
end
# Broadcast after deletion
after_destroy_commit do
broadcast_remove_to(room, target: dom_id(self))
end
endView dang ky stream tuong ung voi helper turbo_stream_from.
<!-- app/views/rooms/show.html.erb -->
<h1><%= @room.name %></h1>
<!-- Subscribe to the WebSocket stream -->
<%= turbo_stream_from @room %>
<!-- Container for messages -->
<div id="messages">
<%= render @room.messages.order(created_at: :asc) %>
</div>
<!-- New message form -->
<%= form_with model: [@room, Message.new], id: "new_message" do |f| %>
<%= f.text_area :content, placeholder: "Your message..." %>
<%= f.submit "Send" %>
<% end %>Broadcasting Bat Dong Bo cho Thao Tac Nang
De tranh chan cac yeu cau, broadcasting co the duoc thuc hien o che do nen.
# app/models/report.rb
class Report < ApplicationRecord
belongs_to :user
after_create_commit :generate_async
private
def generate_async
GenerateReportJob.perform_later(self)
end
end# app/jobs/generate_report_job.rb
class GenerateReportJob < ApplicationJob
queue_as :default
def perform(report)
# Simulating a long operation
report.update!(status: "processing")
# Notify user of start
report.broadcast_replace_to(
report.user, :reports,
target: dom_id(report),
partial: "reports/report"
)
# Generate the report
result = ReportGenerator.new(report).generate
report.update!(content: result, status: "completed")
# Notify user of completion
report.broadcast_replace_to(
report.user, :reports,
target: dom_id(report),
partial: "reports/report"
)
end
endCac Mau Nang Cao voi Turbo
Chinh Sua Bieu Mau Inline
Mot mau pho bien lien quan den viec thay the noi dung tinh bang bieu mau chinh sua inline.
<!-- app/views/tasks/_task.html.erb -->
<turbo-frame id="<%= dom_id(task) %>">
<div class="task">
<span class="task-content"><%= task.content %></span>
<div class="task-actions">
<%= link_to "Edit", edit_task_path(task) %>
<%= button_to "Delete", task_path(task), method: :delete,
data: { turbo_confirm: "Delete this task?" } %>
</div>
</div>
</turbo-frame><!-- app/views/tasks/edit.html.erb -->
<turbo-frame id="<%= dom_id(@task) %>">
<%= form_with model: @task do |f| %>
<%= f.text_field :content, autofocus: true %>
<div class="form-actions">
<%= f.submit "Save" %>
<%= link_to "Cancel", task_path(@task) %>
</div>
<% end %>
</turbo-frame>Dieu Huong Ngoai Frame Cha
Mac dinh, cac lien ket trong frame se o lai trong frame do. Thuoc tinh data-turbo-frame cho phep nham den frame khac hoac toan bo trang.
<!-- app/views/search/_results.html.erb -->
<turbo-frame id="search_results">
<ul>
<% @results.each do |result| %>
<li>
<!-- This link navigates the entire page, not the frame -->
<%= link_to result.title, result_path(result),
data: { turbo_frame: "_top" } %>
</li>
<% end %>
</ul>
</turbo-frame><!-- app/views/layouts/_sidebar.html.erb -->
<aside>
<!-- This frame loads content and updates main -->
<turbo-frame id="sidebar_nav" target="main_content">
<%= render "navigation" %>
</turbo-frame>
</aside>
<main>
<turbo-frame id="main_content">
<%= yield %>
</turbo-frame>
</main>Trong moi truong phat trien, mo console JavaScript va go Turbo.setProgressBarDelay(0) de thay thanh tien trinh ngay lap tuc. Turbo.session.drive = false tam thoi tat Turbo Drive.
Xu Ly Loi va Trang Thai Tai
UX tot doi hoi xu ly cac trang thai tai va loi mang.
<!-- app/views/shared/_form_with_loading.html.erb -->
<%= form_with model: @model,
data: {
controller: "form-loading",
action: "turbo:submit-start->form-loading#disable turbo:submit-end->form-loading#enable"
} do |f| %>
<%= yield f %>
<button type="submit" data-form-loading-target="submit">
<span data-form-loading-target="text">Save</span>
<span data-form-loading-target="spinner" class="hidden">
<svg class="animate-spin h-5 w-5"><!-- spinner SVG --></svg>
</span>
</button>
<% end %>import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["submit", "text", "spinner"]
disable() {
this.submitTarget.disabled = true
this.textTarget.classList.add("hidden")
this.spinnerTarget.classList.remove("hidden")
}
enable() {
this.submitTarget.disabled = false
this.textTarget.classList.remove("hidden")
this.spinnerTarget.classList.add("hidden")
}
}Xu Ly Loi Server
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound do |exception|
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
"main_content",
partial: "shared/not_found"
)
end
format.html { render "errors/not_found", status: :not_found }
end
end
endToi Uu Hoa Hieu Suat
Nhieu ky thuat dam bao cac ung dung Turbo co hieu suat tot.
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
# Avoid N+1 queries with eager loading
def index
@messages = Message.includes(:author, :room)
.order(created_at: :desc)
.page(params[:page])
end
# Fragment caching for static elements
def show
@message = Message.find(params[:id])
fresh_when @message
end
end<!-- app/views/messages/_message.html.erb -->
<!-- Partial-level caching -->
<% cache message do %>
<turbo-frame id="<%= dom_id(message) %>">
<div class="message">
<%= message.content %>
<small>By <%= message.author.name %></small>
</div>
</turbo-frame>
<% end %>Ket Luan
Hotwire va Turbo bien doi viec phat trien Rails bang cach loai bo do phuc tap cua cac framework JavaScript truyen thong. Turbo Drive tang toc dieu huong, Turbo Frames cho phep cap nhat co muc tieu, va Turbo Streams cung cap kha nang thoi gian thuc manh me.
Danh Sach Kiem Tra de Bat Dau voi Hotwire
- Su dung Rails 7+ de tich hop Hotwire san co
- Hieu ba thanh phan: Drive, Frames va Streams
- Xac dinh cac vung trang se huong loi tu Turbo Frames
- Su dung
respond_tovoiformat.turbo_streamtrong controller - Trien khai broadcasting cho tinh nang thoi gian thuc
- Ket hop voi Stimulus cho tuong tac JavaScript don gian
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Cach tiep can "HTML qua day" nay cho phep xay dung cac ung dung hien dai, phan hoi trong khi duy tri su don gian va nang suat ma Ruby on Rails tro nen manh me. Ket qua: it ma nguon can bao tri hon, hieu suat tuyet voi va trai nghiem nha phat trien toi uu.
Thẻ
Chia sẻ