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.

Huong dan Hotwire va Turbo cho Ruby on Rails 7

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".

Tai sao Hotwire?

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.

ruby
# 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.

bash
# 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.

erb
<!-- 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.

erb
<!-- 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?" } %>
Cache Turbo Drive

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.

erb
<!-- 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.

erb
<!-- 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.

erb
<!-- 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.

ruby
# 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
erb
<!-- 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.

ruby
# 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
end

Template Turbo Stream Chuyen Dung

Doi voi cac phan hoi phuc tap hon, template .turbo_stream.erb mang lai su linh hoat hon.

erb
<!-- 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 %>
Dinh Dang Phan Hoi

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.

ruby
# 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
end

View dang ky stream tuong ung voi helper turbo_stream_from.

erb
<!-- 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.

ruby
# 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
ruby
# 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
end

Cac 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.

erb
<!-- 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>
erb
<!-- 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.

erb
<!-- 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>
erb
<!-- 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>
Debug Turbo

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.

erb
<!-- 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 %>
app/javascript/controllers/form_loading_controller.jsjavascript
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

ruby
# 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
end

Toi Uu Hoa Hieu Suat

Nhieu ky thuat dam bao cac ung dung Turbo co hieu suat tot.

ruby
# 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
erb
<!-- 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_to voi format.turbo_stream trong 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ẻ

#ruby on rails
#hotwire
#turbo
#turbo frames
#turbo streams

Chia sẻ