iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0
自我挑戰組

富士大顆系列 第 29

vol. 29 Rails 的補充: 前端的革命英雄 Hotwire

  • 分享至 

  • xImage
  •  

你好,我是富士大顆 Aiko

想要少寫 JavaScript 嗎?(誤)
看這篇就對了!
會談到下列幾點:

  • Hotwire 的介紹
  • Stimulus 的介紹
  • 實際使用

Hotwire 是什麼?

Hotwire 是由 Basecamp 開發的前端框架,希望能讓開發者更有效率地製作網頁應用程式。核心理念是 "HTML over the wire" 也就是只用 HTML 來動態更新頁面,而不是大量的 JavaScript。所以會讓開發過程更簡單,也更能提高效能與維護性。

Hotwire 如何運作?

運作方式建立在將 HTML 內容從伺服器動態推送到客戶端的基礎上,達到實時更新和互動。Hotwire 主要由 Turbo 組成,時常會搭配 Stimulus。

為什麼要用 Hotwire?

  • 簡化開發:不需要寫很多的 JS 操作 DOM
  • 提高效能:因為只更新必要的頁面,減少了網路負載和渲染時間
  • 保持同步:更新頁面由伺服器執行,因此可以使客戶端和伺服器狀況同步,減少資料不一致的風險、提高安全性,而且實時更新

重要的 Hotwire 元素:Turbo

Turbo Drive

當 user 想在不同頁面間移動,Turbo Drive 會攔截這些請求,然後只更新需要改變的頁面區塊,而不是重新 loading 整個頁面。

透過 AJAX 來讀取新頁面的 HTML,只替換變動的部分。

Turbo Frames

在一個頁面中部分更新 HTML。

可以用 turbo-frame 標籤來包一個 HTML 區塊。當該區塊需要更新時,只需要從伺服器傳送新的 turbo-frame 內容過來即可。

Turbo Streams

使用 WebSocket 或其他實時通訊協定來進行實時更新的機制。

伺服器可以發送特定的 Turbo Stream 指令(例如,新增、更新或刪除某個元素)到客戶端,客戶端會根據這些指令來更新頁面。(也就是新增、更新或刪除某個元素後的畫面各不相同)

Hotwire 的好友:Stimulus

Stimulus 是什麼?

雖然不是 Hotwire 的一部分,但 Stimulus 經常與 Turbo 一起使用。是一個輕量級的 JavaScript 框架,主要用於基本的前端邏輯,例如 response user 的動作(如點擊或鍵盤事件)。

Stimulus 使用標記和 JavaScript 代碼來控制客戶端行為。

Stimlulus 跟 Turbo Frame 有什麼差別?

兩者都是用於互動式的網頁應用程式,但著重的面向不同:

  • Stimulus 主要處理的是客戶端邏輯和事件;而 Turbo Frames 則專注於由伺服器驅動的局部頁面更新。
  • Stimulus 主要與客戶端 JavaScript 互動; Turbo Frames 則更依賴於伺服器端的 HTML 生成。

Stimulus Reflex

用於 Ruby on Rails 的現代前端框架,旨在簡化實時互動功能的開發。Stimulus Reflex 結合了 Stimulus.js(一個小型 JavaScript 框架)和 WebSocket(透過 ActionCable)來創造互動性的使用介面,無需編寫大量的客戶端 JavaScript 代碼。

相對 Stimulus 而言,Stimulus Reflex 通常用於更複雜的專案,Stimulus 提供基礎的客戶端互動,而 Stimulus Reflex 則是在這個基礎上加了由伺服器驅動的實時功能。兩者經常一起使用。

Stimulus Relfex 如何運作?

1. 事件觸發:user 進行了某種操作,比如點擊按鈕。

2. 伺服器反應:該事件會被發送到伺服器,伺服器執行相應的邏輯。

3. DOM 更新:伺服器會計算出需要更新哪些 DOM 元素,並將這些更新發送回客戶端。

4. 客戶端渲染:客戶端收到更新後,會實時更新 DOM。

假設有一個計數器:

class CounterReflex < ApplicationReflex
  def increment
    @count = element.dataset[:count].to_i + 1
  end
end

CounterReflex 是一個 Reflex 類別,當 user 點擊一個按鈕來增加計數器時,會執行 increment 方法。dataset 通常用於儲存在 HTML 元素的自定資料屬性(data attributes),而這些屬性的值在 HTML 中總是字串所以要轉成數字存到 @count 變數,然後這個更新會透過 ActionCable (Rails 的 WebSocket 框架)自動廣播到客戶端。

舉個例子:To-do List

設置環境

new 一個新 Rails 專案時就安裝(Stimulus 也會一起安)

$rails new my_new_project --hot

或者在現有專案安裝 Hotwire

$bundle add hotwire-rails

建立基本 CRUD

建立 Model 們:

$rails g model Model'sname

建立資料庫,這是我的 schema:

ActiveRecord::Schema[7.0].define(version: 2023_10_14_093446) do
  create_table "categories", force: :cascade do |t|
    t.string "name", null: false
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_categories_on_user_id"
  end

  create_table "todos", force: :cascade do |t|
    t.string "title", null: false
    t.text "content"
    t.integer "status", default: 0, null: false
    t.datetime "due_date"
    t.datetime "start_at"
    t.datetime "end_at"
    t.integer "category_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["category_id"], name: "index_todos_on_category_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

  add_foreign_key "categories", "users"
  add_foreign_key "todos", "categories"
end

建立 Controller(這是我的 categories controller):

class CategoriesController < ApplicationController
  before_action :set_category, except: [:index, :new, :create]

  def index
    @categories = Category.all
  end

  def new
    @category = Category.new
  end

  def create
    @category = Category.new(params_category)
    if @category.save
      redirect_to categories_url, notice: 'Category was successfully created.'
    else
      render :new
    end
  end

  def show
  end

  def edit
  end

  def update
    if @category.update(params_category)
      redirect_to @category, notice: 'Category was successfully updated.'
    else
      render :edit
    end
  end
  
  def destroy
    @category.destroy
    redirect_to categories_url, notice: 'Category was successfully destroyed.'
  end

  private

  def set_category
    @category = Category.find_by(id: params[:id])
    redirect_to categories_path, alert: "Category not found" unless @category
  end

  def params_category
    params.require(:category).permit(:name)
  end
end

設定 Routes:

Rails.application.routes.draw do
  devise_for :users
  root "categories#index"

  resources :categories do
    resources :todos
  end  
end

記得要建視圖!包括 index, show, new, edit(後面這兩個應該可以共用表單)

使用 Turbo

安裝

gem 'turbo-rails'
$bundle install

import

確認 application.js 有:

import "@hotwired/turbo-rails

Turbo Drive

通常不用另外設定,但某些情況會需要設定 data:{ turbo: false }

  • Turbo Drive 在干擾 JS,有時候傳統的 AJAX 或 Fetch API 比較適合
  • 有些第三方 JS 套件跟 TD 不相容
  • 測試的時候,可能 TD 就是問題:D
  • 需要全頁面重新載入,不是只更新部分的頁面

Turbo Frame

turbo Stream


Hotwire 官方文件
Turbo 官方文件


上一篇
vol. 28 你還沒用過 Zeabur 部署?那你有點落伍了
下一篇
vol. 30 上完課了那然後呢?
系列文
富士大顆30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言