更多資訊請參考 rubyonrails.org:

1 指南假設

本指南專為想要從頭開始建立 Rails 應用程式的初學者而設計。它假設您沒有任何 Rails 的先前經驗。

Rails 是在 Ruby 程式語言上執行的 Web 應用程式框架。如果您沒有任何 Ruby 的先前經驗,直接投入 Rails 會發現學習曲線非常陡峭。這裡有一些經過整理的線上資源列表,可供學習 Ruby

請注意,某些資源雖然仍然很棒,但涵蓋的是舊版本的 Ruby,可能不包含您在日常 Rails 開發中會看到的一些語法。

2 什麼是 Rails?

Rails 是以 Ruby 程式語言編寫的 Web 應用程式開發框架。它旨在透過假設每個開發人員都需要開始使用的內容,讓程式設計 Web 應用程式變得更容易。與許多其他語言和框架相比,它允許您編寫更少的程式碼,同時完成更多的工作。經驗豐富的 Rails 開發人員也表示,它使 Web 應用程式開發更有趣。

Rails 是有主見的軟體。它假設有一種「最佳」的方法來做事,並且旨在鼓勵這種方法 - 在某些情況下會阻止其他替代方案。如果您學習「Rails 的方式」,您可能會發現生產力大幅提高。如果您堅持將其他語言的舊習慣帶入您的 Rails 開發中,並嘗試使用您在其他地方學到的模式,您可能會感到不太愉快。

Rails 哲學包含兩個主要的指導原則

  • 不要重複自己: DRY 是軟體開發的原則,它指出「每個知識都必須在系統中具有單一、明確、權威的表示」。透過不重複編寫相同的資訊,我們的程式碼更易於維護、更具擴充性且錯誤更少。
  • 慣例優於設定: Rails 對於在 Web 應用程式中執行許多事情的最佳方式有自己的想法,並且預設使用這組慣例,而不是要求您透過無止境的設定檔來指定細節。

3 建立新的 Rails 專案

您可以使用預先設定的開發容器開發環境來建立新的 Rails 應用程式。這是開始使用 Rails 的最快方法。如需說明,請參閱開始使用開發容器

閱讀本指南的最佳方式是逐步執行。所有步驟對於執行此範例應用程式至關重要,並且不需要額外的程式碼或步驟。

透過遵循本指南,您將建立一個名為 blog 的 Rails 專案,這是一個(非常)簡單的網路日誌。在您可以開始建構應用程式之前,您需要確保已安裝 Rails 本身。

下面的範例使用 $ 來表示類 UNIX 作業系統中的終端提示,儘管它可能已自訂為以不同的方式顯示。如果您使用的是 Windows,您的提示會類似於 C:\source_code>

3.1 安裝 Rails

在您安裝 Rails 之前,您應檢查以確保您的系統已安裝正確的先決條件。這些包括

  • Ruby
  • SQLite3

3.1.1 安裝 Ruby

開啟命令列提示。在 macOS 上開啟 Terminal.app;在 Windows 上,從「開始」功能表選擇「執行」,然後輸入 cmd.exe。任何以錢號 $ 開頭的命令都應在命令列中執行。確認您已安裝最新版本的 Ruby

$ ruby --version
ruby 3.2.0

Rails 需要 Ruby 3.2.0 或更高版本。建議使用最新的 Ruby 版本。如果傳回的版本號碼小於該數字(例如 2.3.7 或 1.8.7),則需要安裝新的 Ruby 副本。

若要在 Windows 上安裝 Rails,您必須先安裝Ruby 安裝程式

如需大多數作業系統的更多安裝方法,請查看ruby-lang.org

3.1.2 安裝 SQLite3

您還需要安裝 SQLite3 資料庫。許多常用的類 UNIX 作業系統都隨附可接受的 SQLite3 版本。其他使用者可以在SQLite3 網站找到安裝說明。

確認它已正確安裝並在您的載入 PATH

$ sqlite3 --version

程式應報告其版本。

3.1.3 安裝 Rails

若要安裝 Rails,請使用 RubyGems 提供的 gem install 命令

$ gem install rails

若要確認您已正確安裝所有內容,您應能夠在新終端中執行以下命令

$ rails --version
Rails 8.0.0

如果它顯示類似「Rails 8.0.0」的內容,則表示您已準備好繼續。

3.2 建立部落格應用程式

Rails 隨附許多稱為產生器的指令碼,這些指令碼旨在透過建立開始處理特定任務所需的一切內容來簡化您的開發生活。其中一個是新的應用程式產生器,它將為您提供新的 Rails 應用程式的基礎,因此您不必自己編寫。

若要使用此產生器,請開啟終端,導覽至您有權建立檔案的目錄,然後執行

$ rails new blog

這將在 blog 目錄中建立一個名為 Blog 的 Rails 應用程式,並使用 bundle install 安裝 Gemfile 中已提及的 gem 相依性。

您可以透過執行 rails new --help 來查看 Rails 應用程式產生器接受的所有命令列選項。

在您建立部落格應用程式後,切換到其資料夾

$ cd blog

blog 目錄將包含許多已產生的檔案和資料夾,這些檔案和資料夾組成了 Rails 應用程式的結構。本教學課程中的大部分工作將在 app 資料夾中進行,但以下是 Rails 預設建立的每個檔案和資料夾的功能的基本概述

檔案/資料夾 用途
app/ 包含應用程式的控制器、模型、視圖、輔助方法、郵件程式、作業和資源。您將在本指南的其餘部分中專注於此資料夾。
bin/ 包含啟動應用程式的 rails 指令碼,並且可以包含您用來設定、更新、部署或執行應用程式的其他指令碼。
config/ 包含應用程式路由、資料庫等的設定。更多詳細資訊請參閱設定 Rails 應用程式
config.ru 用於啟動應用程式的基於 Rack 的伺服器的 Rack 設定。有關 Rack 的更多資訊,請參閱Rack 網站
db/ 包含您目前的資料庫綱要以及資料庫遷移。
Dockerfile Docker 的設定檔。
Gemfile
Gemfile.lock
這些檔案可讓您指定 Rails 應用程式所需的 gem 相依性。這些檔案由 Bundler gem 使用。有關 Bundler 的更多資訊,請參閱Bundler 網站
lib/ 應用程式的擴充模組。
log/ 應用程式記錄檔。
public/ 包含靜態檔案和編譯後的資產。當您的應用程式執行時,此目錄將原封不動地公開。
Rakefile 此檔案會尋找並載入可從命令列執行的任務。任務定義定義在 Rails 的各個元件中。您應該將檔案新增至應用程式的 lib/tasks 目錄來新增您自己的任務,而不是變更 Rakefile
README.md 這是您應用程式的簡短說明手冊。您應該編輯此檔案,告訴其他人您的應用程式的功能、設定方式等等。
script/ 包含一次性或通用腳本基準測試
storage/ 用於磁碟服務的 Active Storage 檔案。這會在Active Storage 總覽中涵蓋。
test/ 單元測試、fixture 和其他測試工具。這些會在測試 Rails 應用程式中涵蓋。
tmp/ 暫存檔案(如快取和 pid 檔案)。
vendor/ 所有第三方程式碼的放置位置。在典型的 Rails 應用程式中,這包含已供應的 gem。
.dockerignore 此檔案會告知 Docker 不應將哪些檔案複製到容器中。
.gitattributes 此檔案定義 git 儲存庫中特定路徑的中繼資料。git 和其他工具可以使用此中繼資料來增強其行為。有關更多資訊,請參閱gitattributes 文件
.github/ 包含 GitHub 特定檔案。
.gitignore 此檔案會告知 git 應忽略哪些檔案(或模式)。有關忽略檔案的更多資訊,請參閱GitHub - 忽略檔案
.rubocop.yml 此檔案包含 RuboCop 的設定。
.ruby-version 此檔案包含預設的 Ruby 版本。

4 你好,Rails!

首先,讓我們快速在螢幕上顯示一些文字。為此,您需要讓 Rails 應用程式伺服器執行。

4.1 啟動 Web 伺服器

您實際上已經有一個可用的 Rails 應用程式。若要查看它,您需要在開發機器上啟動 Web 伺服器。您可以在 blog 目錄中執行下列命令來完成此操作

$ bin/rails server

如果您使用的是 Windows,則必須將 bin 資料夾下的腳本直接傳遞給 Ruby 直譯器,例如 ruby bin\rails server

JavaScript 資產壓縮需要您的系統上可用的 JavaScript 執行階段,如果沒有執行階段,您會在資產壓縮期間看到 execjs 錯誤。通常 macOS 和 Windows 都會安裝 JavaScript 執行階段。therubyrhino 是 JRuby 使用者建議的執行階段,並且預設會新增至 JRuby 下產生的應用程式中的 Gemfile。您可以在ExecJS 檢視所有支援的執行階段。

這會啟動 Puma,這是預設隨附於 Rails 的 Web 伺服器。若要查看您的應用程式實際運作,請開啟瀏覽器視窗並導覽至https://127.0.0.1:3000。您應該會看到 Rails 預設資訊頁面

Rails startup page screenshot

當您想要停止 Web 伺服器時,請在執行該伺服器的終端機視窗中按下 Ctrl+C。在開發環境中,Rails 通常不需要您重新啟動伺服器;您在檔案中所做的變更會由伺服器自動接收。

Rails 啟動頁面是新 Rails 應用程式的冒煙測試:它會確保您已正確設定軟體,足以提供頁面。

4.2 說「你好」,Rails

若要讓 Rails 說「你好」,您至少需要建立路由、具有動作控制器檢視。路由會將要求對應至控制器動作。控制器動作會執行處理要求所需的作業,並為檢視準備任何資料。檢視會以所需的格式顯示資料。

在實作方面:路由是使用 Ruby DSL(網域特定語言)撰寫的規則。控制器是 Ruby 類別,其公開方法是動作。檢視是範本,通常以 HTML 和 Ruby 的混合撰寫。

讓我們從在路由檔案 config/routes.rb 中,在 Rails.application.routes.draw 區塊的頂端新增路由開始

Rails.application.routes.draw do
  get "/articles", to: "articles#index"

  # For details on the DSL available within this file, see https://rails-guides.dev.org.tw/routing.html
end

上面的路由宣告 GET /articles 要求對應至 ArticlesControllerindex 動作。

若要建立 ArticlesController 和其 index 動作,我們將執行控制器產生器(使用 --skip-routes 選項,因為我們已經有適當的路由)

$ bin/rails generate controller Articles index --skip-routes

Rails 會為您建立數個檔案

create  app/controllers/articles_controller.rb
invoke  erb
create    app/views/articles
create    app/views/articles/index.html.erb
invoke  test_unit
create    test/controllers/articles_controller_test.rb
invoke  helper
create    app/helpers/articles_helper.rb
invoke    test_unit

其中最重要的是控制器檔案 app/controllers/articles_controller.rb。讓我們來看看它

class ArticlesController < ApplicationController
  def index
  end
end

index 動作是空的。當動作未明確呈現檢視 (或以其他方式觸發 HTTP 回應) 時,Rails 會自動呈現符合控制器和動作名稱的檢視。慣例勝於設定!檢視位於 app/views 目錄中。因此,index 動作會預設呈現 app/views/articles/index.html.erb

讓我們開啟 app/views/articles/index.html.erb,並將其內容取代為

<h1>Hello, Rails!</h1>

如果您先前停止 Web 伺服器來執行控制器產生器,請使用 bin/rails server 重新啟動它。現在請造訪https://127.0.0.1:3000/articles,並查看顯示的文字!

4.3 設定應用程式首頁

目前,https://127.0.0.1:3000 仍然顯示具有 Ruby on Rails 標誌的頁面。讓我們也在 https://127.0.0.1:3000 顯示我們的「你好,Rails!」文字。為此,我們將新增一個將應用程式的根路徑對應至適當控制器和動作的路由。

讓我們開啟 config/routes.rb,並在 Rails.application.routes.draw 區塊的頂端新增下列 root 路由

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
end

現在,當我們造訪 https://127.0.0.1:3000 時,可以看到我們的「你好,Rails!」文字,這確認了 root 路由也會對應至 ArticlesControllerindex 動作。

若要深入瞭解路由,請參閱從外部看 Rails 路由

5 自動載入

Rails 應用程式使用 require 來載入應用程式程式碼。

您可能已注意到 ArticlesController 繼承自 ApplicationController,但 app/controllers/articles_controller.rb 沒有任何類似的內容

require "application_controller" # DON'T DO THIS.

應用程式類別和模組隨處可用,您不需要且不應該使用 require 載入 app 下的任何內容。此功能稱為自動載入,您可以在自動載入和重新載入常數中瞭解更多資訊。

您只需要 require 呼叫來處理兩種使用案例

  • 載入 lib 目錄下的檔案。
  • 載入在 Gemfile 中具有 require: false 的 gem 相依性。

6 MVC 和您

到目前為止,我們已討論路由、控制器、動作和檢視。所有這些都是遵循 MVC (模型-檢視-控制器) 模式的 Web 應用程式的典型部分。MVC 是一種設計模式,可劃分應用程式的責任,使其更容易推理。Rails 依照慣例遵循此設計模式。

由於我們有一個控制器和檢視可供使用,讓我們產生下一個部分:模型。

6.1 產生模型

模型是用來表示資料的 Ruby 類別。此外,模型可以透過 Rails 的一個功能 (稱為 Active Record) 與應用程式的資料庫互動。

若要定義模型,我們將使用模型產生器

$ bin/rails generate model Article title:string body:text

模型名稱是單數,因為具現化的模型代表單一資料記錄。為了協助您記住此慣例,請想想您會如何呼叫模型的建構函式:我們想要撰寫 Article.new(...)而不是 Articles.new(...)

這會建立數個檔案

invoke  active_record
create    db/migrate/<timestamp>_create_articles.rb
create    app/models/article.rb
invoke    test_unit
create      test/models/article_test.rb
create      test/fixtures/articles.yml

我們將重點關注的兩個檔案是遷移檔案 (db/migrate/<timestamp>_create_articles.rb) 和模型檔案 (app/models/article.rb)。

6.2 資料庫遷移

遷移是用來變更應用程式資料庫結構的方式。在 Rails 應用程式中,遷移是以 Ruby 撰寫,以便它們可以與資料庫無關。

讓我們來看看我們新的遷移檔案的內容

class CreateArticles < ActiveRecord::Migration[8.0]
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

create_table 的呼叫會指定應如何建構 articles 資料表。依預設,create_table 方法會新增 id 資料行作為自動遞增的主索引鍵。因此,資料表中的第一筆記錄的 id 會是 1,下一筆記錄的 id 會是 2,依此類推。

create_table 的區塊內,定義了兩個資料行:titlebody。這些是由產生器新增的,因為我們在我們的產生命令中包含了它們 (bin/rails generate model Article title:string body:text)。

在區塊的最後一行是對 t.timestamps 的呼叫。此方法會定義兩個名為 created_atupdated_at 的額外資料行。正如我們將看到的,Rails 會為我們管理這些資料行,並在我們建立或更新模型物件時設定值。

讓我們使用下列命令執行遷移

$ bin/rails db:migrate

此命令會顯示輸出,指出已建立資料表

==  CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0018s
==  CreateArticles: migrated (0.0018s) ==========================

若要深入瞭解遷移,請參閱Active Record 遷移

現在,我們可以使用我們的模型與資料表互動。

6.3 使用模型與資料庫互動

若要稍微使用我們的模型,我們將使用 Rails 的一個功能 (稱為主控台)。主控台是一個互動式程式碼撰寫環境,就像 irb 一樣,但它也會自動載入 Rails 和我們的應用程式程式碼。

讓我們使用此命令啟動主控台

$ bin/rails console

您應該會看到像這樣的 rails 主控台提示

Loading development environment (Rails 8.0.0)
blog(dev)>

在此提示中,我們可以初始化新的 Article 物件

blog(dev)> article = Article.new(title: "Hello Rails", body: "I am on Rails!")

請務必注意,我們只是初始化此物件。此物件完全未儲存至資料庫。它目前僅在主控台中可用。若要將物件儲存至資料庫,我們必須呼叫save

blog(dev)> article.save
(0.1ms)  begin transaction
Article Create (0.4ms)  INSERT INTO "articles" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Hello Rails"], ["body", "I am on Rails!"], ["created_at", "2020-01-18 23:47:30.734416"], ["updated_at", "2020-01-18 23:47:30.734416"]]
(0.9ms)  commit transaction
=> true

上面的輸出顯示了一個 INSERT INTO "articles" ... 資料庫查詢。這表示文章已經被插入到我們的資料表中。如果我們再次查看 article 物件,會發現一些有趣的事情。

blog(dev)> article
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

物件的 idcreated_atupdated_at 屬性現在都被設定了。Rails 在我們儲存物件時為我們完成了這些。

當我們想要從資料庫中提取這篇文章時,我們可以在模型上呼叫 find,並將 id 作為參數傳遞。

blog(dev)> Article.find(1)
=> #<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">

當我們想要從資料庫中提取所有文章時,我們可以在模型上呼叫 all

blog(dev)> Article.all
=> #<ActiveRecord::Relation [#<Article id: 1, title: "Hello Rails", body: "I am on Rails!", created_at: "2020-01-18 23:47:30", updated_at: "2020-01-18 23:47:30">]>

此方法會返回一個 ActiveRecord::Relation 物件,您可以將其視為一個超強大的陣列。

要了解更多關於模型的信息,請參閱Active Record 基礎Active Record 查詢介面

模型是 MVC 拼圖的最後一塊。接下來,我們將把所有部分連接在一起。

6.4 顯示文章列表

讓我們回到 app/controllers/articles_controller.rb 中的控制器,並將 index 動作更改為從資料庫中提取所有文章。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
end

視圖可以存取控制器實例變數。這意味著我們可以在 app/views/articles/index.html.erb 中引用 @articles。讓我們打開該檔案,並將其內容替換為

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= article.title %>
    </li>
  <% end %>
</ul>

上面的程式碼是 HTML 和 ERB 的混合。ERB 是 Embedded Ruby 的縮寫,是一個範本系統,可以評估嵌入在文件中的 Ruby 程式碼。在這裡,我們可以看到兩種 ERB 標籤:<% %><%= %><% %> 標籤表示「評估封閉的 Ruby 程式碼」。<%= %> 標籤表示「評估封閉的 Ruby 程式碼,並輸出它返回的值」。任何您可以在常規 Ruby 程式中編寫的內容都可以放入這些 ERB 標籤中,但為了可讀性,最好讓 ERB 標籤的內容保持簡短。

由於我們不想輸出 @articles.each 返回的值,我們將該程式碼封閉在 <% %> 中。但是,由於我們*確實*想要輸出 article.title(對於每篇文章)返回的值,我們將該程式碼封閉在 <%= %> 中。

我們可以透過訪問 https://127.0.0.1:3000 來查看最終結果。(請記住,bin/rails server 必須正在運行!)以下是我們這樣做時會發生的情況

  1. 瀏覽器發出請求:GET https://127.0.0.1:3000
  2. 我們的 Rails 應用程式接收到此請求。
  3. Rails 路由器將根路由映射到 ArticlesControllerindex 動作。
  4. index 動作使用 Article 模型來提取資料庫中的所有文章。
  5. Rails 自動渲染 app/views/articles/index.html.erb 視圖。
  6. 視圖中的 ERB 程式碼會被評估以輸出 HTML。
  7. 伺服器將包含 HTML 的回應發送回瀏覽器。

我們已經將所有 MVC 部分連接在一起,並且我們有了第一個控制器動作!接下來,我們將繼續第二個動作。

7 應該使用 CRUD 的地方

幾乎所有的 Web 應用程式都涉及 CRUD(建立、讀取、更新和刪除) 操作。您甚至可能會發現,應用程式所做的大部分工作都是 CRUD。Rails 意識到了這一點,並提供了許多功能來幫助簡化執行 CRUD 的程式碼。

讓我們從向我們的應用程式添加更多功能開始探索這些功能。

7.1 顯示單篇文章

我們目前有一個視圖,列出資料庫中的所有文章。讓我們添加一個新的視圖,顯示單篇文章的標題和內容。

首先,我們添加一個新的路由,該路由映射到一個新的控制器動作(我們將在接下來添加)。打開 config/routes.rb,並插入此處顯示的最後一個路由。

Rails.application.routes.draw do
  root "articles#index"

  get "/articles", to: "articles#index"
  get "/articles/:id", to: "articles#show"
end

新的路由是另一個 get 路由,但它的路徑中還有一些額外的東西::id。這指定了一個路由參數。路由參數會捕獲請求路徑的一部分,並將該值放入 params 哈希中,控制器動作可以存取該哈希。例如,當處理像 GET https://127.0.0.1:3000/articles/1 這樣的請求時,1 將被捕獲為 :id 的值,然後可以在 ArticlesControllershow 動作中以 params[:id] 的形式存取。

現在,讓我們在 app/controllers/articles_controller.rb 中的 index 動作下方添加該 show 動作。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end
end

show 動作使用路由參數捕獲的 ID 呼叫 Article.find如先前所述)。返回的文章儲存在 @article 實例變數中,因此視圖可以存取它。預設情況下,show 動作將渲染 app/views/articles/show.html.erb

讓我們建立 app/views/articles/show.html.erb,其內容如下

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

現在,當我們訪問 https://127.0.0.1:3000/articles/1 時,我們可以看到這篇文章了!

為了完成這部分,讓我們添加一種方便的方式來前往文章的頁面。我們將在 app/views/articles/index.html.erb 中將每篇文章的標題連結到其頁面。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="/articles/<%= article.id %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

7.2 資源路由

到目前為止,我們已經介紹了 CRUD 的「R」(讀取)。我們最終將介紹「C」(建立)、「U」(更新)和「D」(刪除)。您可能已經猜到,我們將透過添加新的路由、控制器動作和視圖來完成此操作。每當我們有這樣一個路由、控制器動作和視圖的組合,它們協同工作來對一個實體執行 CRUD 操作時,我們就將該實體稱為一個資源。例如,在我們的應用程式中,我們會說一篇文章是一個資源。

Rails 提供了一個名為 resources 的路由方法,該方法會映射資源集合(例如文章)的所有傳統路由。因此,在我們繼續介紹「C」、「U」和「D」部分之前,讓我們將 config/routes.rb 中的兩個 get 路由替換為 resources

Rails.application.routes.draw do
  root "articles#index"

  resources :articles
end

我們可以透過執行 bin/rails routes 命令來檢查映射了哪些路由。

$ bin/rails routes
      Prefix Verb   URI Pattern                  Controller#Action
        root GET    /                            articles#index
    articles GET    /articles(.:format)          articles#index
 new_article GET    /articles/new(.:format)      articles#new
     article GET    /articles/:id(.:format)      articles#show
             POST   /articles(.:format)          articles#create
edit_article GET    /articles/:id/edit(.:format) articles#edit
             PATCH  /articles/:id(.:format)      articles#update
             PUT    /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy

resources 方法還設定了 URL 和路徑輔助方法,我們可以利用它們來防止程式碼依賴於特定的路由配置。上面「Prefix」列中的值加上 _url_path 的後綴形成了這些輔助方法的名稱。例如,當給定一篇文章時,article_path 輔助方法會返回 "/articles/#{article.id}"。我們可以利用它來整理 app/views/articles/index.html.erb 中的連結。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <a href="<%= article_path(article) %>">
        <%= article.title %>
      </a>
    </li>
  <% end %>
</ul>

但是,我們將使用 link_to 輔助方法更進一步。link_to 輔助方法會渲染一個連結,其第一個參數是連結的文字,第二個參數是連結的目的地。如果我們傳遞一個模型物件作為第二個參數,link_to 將呼叫適當的路徑輔助方法將該物件轉換為路徑。例如,如果我們傳遞一篇文章,link_to 將呼叫 article_path。因此,app/views/articles/index.html.erb 變成

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

太棒了!

若要深入瞭解路由,請參閱從外部看 Rails 路由

7.3 建立新文章

現在我們繼續介紹 CRUD 的「C」(建立)。通常,在 Web 應用程式中,建立新資源是一個多步驟的過程。首先,使用者請求填寫表單。然後,使用者提交表單。如果沒有錯誤,則會建立資源並顯示某種確認訊息。否則,表單會重新顯示並帶有錯誤訊息,並且該過程會重複進行。

在 Rails 應用程式中,這些步驟通常由控制器的 newcreate 動作處理。讓我們在 app/controllers/articles_controller.rbshow 動作的下方添加這些動作的典型實作。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(title: "...", body: "...")

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end
end

new 動作會實例化一個新的文章,但不會儲存它。當在視圖中建立表單時,將使用這篇文章。預設情況下,new 動作將渲染 app/views/articles/new.html.erb,我們接下來將建立它。

create 動作會實例化一個新的文章,其中包含標題和內容的值,並嘗試儲存它。如果文章儲存成功,則該動作會將瀏覽器重新導向到文章的頁面,網址為 "https://127.0.0.1:3000/articles/#{@article.id}"。否則,該動作會透過渲染 app/views/articles/new.html.erb 並帶有狀態碼 422 Unprocessable Entity 來重新顯示表單。此處的標題和內容是虛擬值。在我們建立表單後,我們將返回並更改這些值。

redirect_to 將導致瀏覽器發出新的請求,而 render 會為目前的請求渲染指定的視圖。在變更資料庫或應用程式狀態後,務必使用 redirect_to。否則,如果使用者重新整理頁面,瀏覽器將發出相同的請求,並且變更將會重複進行。

7.3.1 使用表單建立器

我們將使用 Rails 的一個稱為表單建立器的功能來建立我們的表單。使用表單建立器,我們可以用最少的程式碼輸出一个完全配置並遵循 Rails 慣例的表單。

讓我們建立 app/views/articles/new.html.erb,其內容如下

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

form_with 輔助方法會實例化一個表單建立器。在 form_with 區塊中,我們在表單建立器上呼叫 labeltext_field 等方法來輸出適當的表單元素。

我們呼叫 form_with 的結果輸出將如下所示

<form action="/articles" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="...">

  <div>
    <label for="article_title">Title</label><br>
    <input type="text" name="article[title]" id="article_title">
  </div>

  <div>
    <label for="article_body">Body</label><br>
    <textarea name="article[body]" id="article_body"></textarea>
  </div>

  <div>
    <input type="submit" name="commit" value="Create Article" data-disable-with="Create Article">
  </div>
</form>

要了解更多關於表單建立器的資訊,請參閱Action View 表單輔助方法

7.3.2 使用強參數

提交的表單資料會與捕獲的路由參數一起放入 params 哈希中。因此,create 動作可以透過 params[:article][:title] 存取提交的標題,並透過 params[:article][:body] 存取提交的內容。我們可以單獨將這些值傳遞給 Article.new,但這樣會很冗長且容易出錯。而且,當我們添加更多欄位時,情況會變得更糟。

取而代之的是,我們將傳遞一個包含值的單一雜湊(Hash)。然而,我們仍然必須指定該雜湊中允許哪些值。否則,惡意使用者可能會提交額外的表單欄位並覆寫私有資料。事實上,如果我們將未經過濾的 params[:article] 雜湊直接傳遞給 Article.new,Rails 會引發一個 ForbiddenAttributesError 來警告我們這個問題。因此,我們將使用 Rails 的一個稱為「強參數」(Strong Parameters)的功能來過濾 params。可以把它想像成 params強型別

讓我們在 app/controllers/articles_controller.rb 的底部新增一個名為 article_params 的私有方法,該方法會過濾 params。並讓我們修改 create 以使用它。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

若要瞭解更多關於強參數的資訊,請參閱 Action Controller 概觀 § 強參數

7.3.3 驗證與顯示錯誤訊息

如我們所見,建立資源是一個多步驟的過程。處理無效的使用者輸入是該過程的另一個步驟。Rails 提供了一個稱為「驗證」(validations)的功能,以幫助我們處理無效的使用者輸入。「驗證」是在儲存模型物件之前檢查的規則。如果任何檢查失敗,則儲存操作將會中止,並且會將適當的錯誤訊息新增至模型物件的 errors 屬性。

讓我們在 app/models/article.rb 中的模型中新增一些驗證。

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

第一個驗證宣告 title 值必須存在。由於 title 是一個字串,這表示 title 值必須至少包含一個非空白字元。

第二個驗證宣告 body 值也必須存在。此外,它還宣告 body 值必須至少有 10 個字元長。

您可能想知道 titlebody 屬性是在哪裡定義的。Active Record 會自動為每個資料表欄位定義模型屬性,因此您無需在模型檔案中宣告這些屬性。

在我們的驗證就緒後,讓我們修改 app/views/articles/new.html.erb 以顯示 titlebody 的任何錯誤訊息。

<h1>New Article</h1>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% @article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %><br>
    <% @article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

full_messages_for 方法會傳回指定屬性的使用者友善錯誤訊息陣列。如果該屬性沒有錯誤,則該陣列將為空。

為了理解這一切是如何協同運作的,讓我們再次看一下 newcreate 控制器動作。

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

當我們訪問 https://127.0.0.1:3000/articles/new 時,GET /articles/new 請求會被對應到 new 動作。new 動作不會嘗試儲存 @article。因此,不會檢查驗證,也不會有錯誤訊息。

當我們提交表單時,POST /articles 請求會被對應到 create 動作。create 動作確實會嘗試儲存 @article。因此,檢查驗證。如果任何驗證失敗,則不會儲存 @article,並且會使用錯誤訊息呈現 app/views/articles/new.html.erb

若要瞭解更多關於驗證的資訊,請參閱 Active Record 驗證。若要瞭解更多關於驗證錯誤訊息的資訊,請參閱 Active Record 驗證 § 使用驗證錯誤

7.3.4 完成

我們現在可以透過訪問 https://127.0.0.1:3000/articles/new 來建立文章。為了完成,讓我們從 app/views/articles/index.html.erb 的底部連結到該頁面。

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <li>
      <%= link_to article.title, article %>
    </li>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

7.4 更新文章

我們已經介紹了 CRUD 中的「CR」。現在讓我們繼續介紹「U」(更新)。更新資源與建立資源非常相似。它們都是多步驟的過程。首先,使用者請求一個表單來編輯資料。然後,使用者提交表單。如果沒有錯誤,則更新資源。否則,會重新顯示帶有錯誤訊息的表單,並重複該過程。

這些步驟通常由控制器的 editupdate 動作處理。讓我們在 app/controllers/articles_controller.rbcreate 動作下方新增這些動作的典型實作。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

請注意 editupdate 動作與 newcreate 動作的相似之處。

edit 動作會從資料庫中提取文章,並將其儲存在 @article 中,以便在建構表單時使用。預設情況下,edit 動作會呈現 app/views/articles/edit.html.erb

update 動作會(重新)從資料庫中提取文章,並嘗試使用 article_params 過濾的提交表單資料來更新它。如果沒有驗證失敗且更新成功,則該動作會將瀏覽器重新導向到文章的頁面。否則,該動作會透過呈現 app/views/articles/edit.html.erb 重新顯示帶有錯誤訊息的表單。

7.4.1 使用局部範本來共用檢視程式碼

我們的 edit 表單看起來會與我們的 new 表單相同。即使程式碼也會相同,這要歸功於 Rails 表單建構器和資源路由。表單建構器會根據模型物件是否已先前儲存,自動配置表單以發出適當種類的請求。

因為程式碼會相同,所以我們將把它提取到一個稱為局部範本的共用檢視中。讓我們建立一個包含以下內容的 app/views/articles/_form.html.erb

<%= form_with model: article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
    <% article.errors.full_messages_for(:title).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.label :body %><br>
    <%= form.textarea :body %><br>
    <% article.errors.full_messages_for(:body).each do |message| %>
      <div><%= message %></div>
    <% end %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

上面的程式碼與我們在 app/views/articles/new.html.erb 中的表單相同,只是所有出現的 @article 都被替換為 article。由於局部範本是共用程式碼,因此最佳實務是不讓它們依賴於控制器動作設定的特定實例變數。相反地,我們會將文章作為局部變數傳遞給局部範本。

讓我們更新 app/views/articles/new.html.erb 以透過 render 使用局部範本。

<h1>New Article</h1>

<%= render "form", article: @article %>

局部範本的檔案名稱必須底線作為前綴,例如 _form.html.erb。但是,在呈現時,它會不帶底線地被引用,例如 render "form"

現在,讓我們建立一個非常相似的 app/views/articles/edit.html.erb

<h1>Edit Article</h1>

<%= render "form", article: @article %>

若要瞭解更多關於局部範本的資訊,請參閱 Rails 中的版面配置和呈現 § 使用局部範本

7.4.2 完成

我們現在可以透過訪問其編輯頁面來更新文章,例如 https://127.0.0.1:3000/articles/1/edit。為了完成,讓我們從 app/views/articles/show.html.erb 的底部連結到編輯頁面。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
</ul>

7.5 刪除文章

最後,我們到了 CRUD 中的「D」(刪除)。刪除資源比建立或更新更簡單。它只需要一個路由和一個控制器動作。而我們的資源路由 (resources :articles) 已提供路由,該路由將 DELETE /articles/:id 請求對應到 ArticlesControllerdestroy 動作。

因此,讓我們在 app/controllers/articles_controller.rbupdate 動作下方新增一個典型的 destroy 動作。

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.expect(article: [:title, :body])
    end
end

destroy 動作會從資料庫中提取文章,並對其呼叫 destroy。然後,它會使用狀態碼 303 See Other 將瀏覽器重新導向到根路徑。

我們選擇重新導向到根路徑,因為那是我們存取文章的主要入口點。但是,在其他情況下,您可能會選擇重新導向到例如 articles_path

現在,讓我們在 app/views/articles/show.html.erb 的底部新增一個連結,以便我們可以從自己的頁面刪除文章。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

在上面的程式碼中,我們使用 data 選項來設定「刪除」連結的 data-turbo-methoddata-turbo-confirm HTML 屬性。這兩個屬性都連結到 Turbo,預設情況下,Turbo 會包含在新的 Rails 應用程式中。data-turbo-method="delete" 會導致該連結發出 DELETE 請求而不是 GET 請求。data-turbo-confirm="確定要刪除嗎?" 會導致在按一下連結時出現確認對話方塊。如果使用者取消對話方塊,則會中止請求。

就是這樣!我們現在可以列出、顯示、建立、更新和刪除文章了!太棒了!

8 新增第二個模型

是時候為應用程式新增第二個模型了。第二個模型將處理文章的留言。

8.1 產生模型

我們將看到與建立 Article 模型時相同的產生器。這次我們將建立一個 Comment 模型來保存對文章的參考。在您的終端機中執行此命令。

$ bin/rails generate model Comment commenter:string body:text article:references

此命令將產生四個檔案。

檔案 用途
db/migrate/<時間戳記>_create_comments.rb 在您的資料庫中建立留言資料表的遷移。
app/models/comment.rb 留言模型
test/models/comment_test.rb 留言模型的測試工具。
test/fixtures/comments.yml 用於測試的範例留言。

首先,看一下 app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :article
end

這與您先前看到的 Article 模型非常相似。不同之處在於 belongs_to :article 這行,它會建立一個 Active Record 關聯。您將在本指南的下一節中瞭解一些關於關聯的資訊。

在 Shell 命令中使用的 (:references) 關鍵字是模型的特殊資料類型。它會在您的資料庫資料表上建立一個新的資料行,並在該資料行附加提供的模型名稱和可以保存整數值的 _id。若要更好地理解,請在執行遷移後分析 db/schema.rb 檔案。

除了模型之外,Rails 還進行了遷移以建立相應的資料庫資料表。

class CreateComments < ActiveRecord::Migration[8.0]
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :article, null: false, foreign_key: true

      t.timestamps
    end
  end
end

t.references 行會建立一個名為 article_id 的整數資料行、一個索引以及指向 articles 資料表 id 資料行的外來鍵約束。繼續執行遷移。

$ bin/rails db:migrate

Rails 非常聰明,只會執行尚未針對目前資料庫執行的遷移,因此在此情況下,您只會看到。

==  CreateComments: migrating =================================================
-- create_table(:comments)
   -> 0.0115s
==  CreateComments: migrated (0.0119s) ========================================

8.2 關聯模型

Active Record 關聯可讓您輕鬆宣告兩個模型之間的關係。就留言和文章而言,您可以用這種方式寫出關係。

  • 每個留言都屬於一個文章。
  • 一個文章可以有很多留言。

事實上,這非常接近 Rails 用於宣告此關聯的語法。您已經看到在 Comment 模型 (app/models/comment.rb) 內的程式碼行,它讓每個留言都屬於一個文章。

class Comment < ApplicationRecord
  belongs_to :article
end

您需要編輯 app/models/article.rb 以新增關聯的另一端。

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

這兩個宣告啟用了一些自動行為。例如,如果您有一個包含文章的實例變數 @article,則可以使用 @article.comments 以陣列形式擷取屬於該文章的所有留言。

若要瞭解更多關於 Active Record 關聯的資訊,請參閱 Active Record 關聯指南。

8.3 新增留言的路由

articles 控制器一樣,我們需要新增路由,以便 Rails 知道我們想導覽到哪裡查看 comments。再次開啟 config/routes.rb 檔案,並按照如下方式編輯它。

Rails.application.routes.draw do
  root "articles#index"

  resources :articles do
    resources :comments
  end
end

這會建立 comments 作為 articles 內的巢狀資源。這是捕捉文章和留言之間存在之階層關係的另一部分。

若要瞭解更多關於路由的資訊,請參閱 Rails 路由指南。

8.4 產生控制器

有了模型後,您可以將注意力轉移到建立相符的控制器。同樣,我們將使用之前使用的同一個產生器。

$ bin/rails generate controller Comments

這會建立三個檔案和一個空目錄

檔案/目錄 用途
app/controllers/comments_controller.rb 留言控制器
app/views/comments/ 控制器的視圖存放於此
test/controllers/comments_controller_test.rb 控制器的測試
app/helpers/comments_helper.rb 一個視圖輔助檔案

如同任何部落格,我們的讀者會在閱讀文章後直接建立留言,並且一旦他們新增了留言,就會被送回文章的顯示頁面,看到他們剛剛列出的留言。因此,我們的 CommentsController 存在目的是提供建立留言的方法,以及在垃圾留言出現時刪除它們的方法。

首先,我們將連結文章顯示範本 (app/views/articles/show.html.erb),讓我們可以新增留言。

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

這會在 Article 顯示頁面新增一個表單,該表單會透過呼叫 CommentsControllercreate 動作來新增留言。這裡的 form_with 呼叫使用了一個陣列,這將建立一個巢狀路由,例如 /articles/1/comments

讓我們在 app/controllers/comments_controller.rb 中連結 create

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  private
    def comment_params
      params.expect(comment: [:commenter, :body])
    end
end

你會在這裡看到比文章控制器更複雜的部分。這是你所設定的巢狀結構的副作用。每個留言請求都必須追蹤留言所附加的文章,因此首先呼叫 Article 模型的 find 方法來取得相關文章。

此外,程式碼利用了關聯的一些可用方法。我們在 @article.comments 上使用 create 方法來建立並儲存留言。這將自動連結留言,使其屬於該特定文章。

一旦我們新增了留言,我們使用 article_path(@article) 輔助函式將使用者送回原始文章。正如我們已經看到的,這會呼叫 ArticlesControllershow 動作,進而呈現 show.html.erb 範本。這就是我們希望顯示留言的地方,所以讓我們將其新增到 app/views/articles/show.html.erb

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

現在,您可以將文章和留言新增到您的部落格,並讓它們顯示在正確的位置。

Article with Comments

9 重構

現在我們已經讓文章和留言運作,請查看 app/views/articles/show.html.erb 範本。它變得又長又笨拙。我們可以使用局部範本來清理它。

9.1 呈現局部集合

首先,我們將建立一個留言局部範本,以提取文章的所有留言顯示。建立檔案 app/views/comments/_comment.html.erb 並將以下內容放入其中

<p>
  <strong>Commenter:</strong>
  <%= comment.commenter %>
</p>

<p>
  <strong>Comment:</strong>
  <%= comment.body %>
</p>

然後,您可以將 app/views/articles/show.html.erb 變更為如下所示

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

現在,它將針對 @article.comments 集合中的每個留言,呈現 app/views/comments/_comment.html.erb 中的局部範本一次。當 render 方法迭代 @article.comments 集合時,它會將每個留言指派給一個與局部範本名稱相同的局部變數,在此例中為 comment,然後該變數可在局部範本中供我們顯示。

9.2 呈現局部表單

讓我們也將新的留言區塊移到它自己的局部範本。再次,您建立一個包含以下內容的檔案 app/views/comments/_form.html.erb

<%= form_with model: [ article, article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.textarea :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

然後,您讓 app/views/articles/show.html.erb 看起來像這樣

<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
                    turbo_method: :delete,
                    turbo_confirm: "Are you sure?"
                  } %></li>
</ul>

<h2>Comments</h2>
<%= render @article.comments %>

<h2>Add a comment:</h2>
<%= render "comments/form", article: @article %>

第二個 render 僅定義我們要呈現的局部範本,comments/form。Rails 足夠聰明,可以發現該字串中的斜線,並意識到您想要呈現 app/views/comments 目錄中的 _form.html.erb 檔案。

9.3 使用關注點 (Concerns)

關注點是一種使大型控制器或模型更容易理解和管理的方法。當多個模型(或控制器)共享相同的關注點時,它也具有可重用性的優勢。關注點是使用模組來實現的,這些模組包含表示模型或控制器負責的功能的明確定義的切片的方法。在其他語言中,模組通常稱為混入 (mixins)。

您可以在控制器或模型中使用關注點,就像使用任何模組一樣。當您第一次使用 rails new blog 建立您的應用程式時,在 app/ 內除了其他部分之外,還建立了兩個資料夾

app/controllers/concerns
app/models/concerns

在下面的範例中,我們將為我們的部落格實作一個新的功能,該功能將受益於使用關注點。然後,我們將建立一個關注點,並重構程式碼以使用它,使程式碼更 DRY 且更易於維護。

一篇部落格文章可能有多種狀態 - 例如,它可以對所有人可見(即 public),或僅對作者可見(即 private)。它也可能對所有人隱藏但仍然可以檢索(即 archived)。留言也可能以類似的方式隱藏或可見。這可以使用每個模型中的 status 欄位來表示。

首先,讓我們執行以下遷移來將 status 新增到 ArticlesComments

$ bin/rails generate migration AddStatusToArticles status:string
$ bin/rails generate migration AddStatusToComments status:string

接下來,讓我們使用產生的遷移更新資料庫

$ bin/rails db:migrate

若要為現有文章和留言選擇狀態,您可以將 default: "public" 選項新增到產生的遷移檔案中,並再次啟動遷移。您也可以在 rails 主控台中呼叫 Article.update_all(status: "public")Comment.update_all(status: "public")

若要深入瞭解遷移,請參閱Active Record 遷移

我們還必須允許 :status 鍵作為強參數的一部分,在 app/controllers/articles_controller.rb


  private
    def article_params
      params.expect(article: [:title, :body, :status])
    end

以及在 app/controllers/comments_controller.rb


  private
    def comment_params
      params.expect(comment: [:commenter, :body, :status])
    end

article 模型中,在使用 bin/rails db:migrate 命令執行遷移以新增 status 欄位之後,您將新增

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }

  VALID_STATUSES = [ "public", "private", "archived" ]

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == "archived"
  end
end

Comment 模型中

class Comment < ApplicationRecord
  belongs_to :article

  VALID_STATUSES = [ "public", "private", "archived" ]

  validates :status, inclusion: { in: VALID_STATUSES }

  def archived?
    status == "archived"
  end
end

然後,在我們的 index 動作範本 (app/views/articles/index.html.erb) 中,我們將使用 archived? 方法來避免顯示任何封存的文章

<h1>Articles</h1>

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

同樣地,在我們的留言局部視圖 (app/views/comments/_comment.html.erb) 中,我們將使用 archived? 方法來避免顯示任何封存的留言

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

但是,如果您再次查看我們的模型,您會發現邏輯是重複的。如果將來我們增加部落格的功能 - 例如,包括私人訊息 - 我們可能會發現自己再次重複邏輯。這就是關注點派上用場的地方。

關注點僅負責模型責任的重點子集;我們關注點中的方法都將與模型的可見性相關。讓我們將我們的新關注點(模組)稱為 Visible。我們可以在 app/models/concerns 內建立一個名為 visible.rb 的新檔案,並儲存模型中重複的所有狀態方法。

app/models/concerns/visible.rb

module Visible
  def archived?
    status == "archived"
  end
end

我們可以將狀態驗證新增到關注點,但由於驗證是在類別層級呼叫的方法,因此這稍微複雜一些。ActiveSupport::Concern ( API 指南) 為我們提供了一種更簡單的方法來包含它們

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = [ "public", "private", "archived" ]

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  def archived?
    status == "archived"
  end
end

現在,我們可以從每個模型中移除重複的邏輯,而是包含我們新的 Visible 模組

app/models/article.rb

class Article < ApplicationRecord
  include Visible

  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

以及在 app/models/comment.rb

class Comment < ApplicationRecord
  include Visible

  belongs_to :article
end

類別方法也可以新增到關注點。如果我們想在我們的主頁上顯示公開文章或留言的計數,我們可以將類別方法新增到 Visible,如下所示

module Visible
  extend ActiveSupport::Concern

  VALID_STATUSES = [ "public", "private", "archived" ]

  included do
    validates :status, inclusion: { in: VALID_STATUSES }
  end

  class_methods do
    def public_count
      where(status: "public").count
    end
  end

  def archived?
    status == "archived"
  end
end

然後在視圖中,您可以像呼叫任何類別方法一樣呼叫它

<h1>Articles</h1>

Our blog has <%= Article.public_count %> articles and counting!

<ul>
  <% @articles.each do |article| %>
    <% unless article.archived? %>
      <li>
        <%= link_to article.title, article %>
      </li>
    <% end %>
  <% end %>
</ul>

<%= link_to "New Article", new_article_path %>

最後,我們將在表單中新增一個下拉式方塊,並讓使用者在建立新文章或張貼新留言時選擇狀態。我們也可以選擇物件的狀態,如果尚未設定,則選擇預設值 public。在 app/views/articles/_form.html.erb 中,我們可以新增

<div>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: article.status || 'public' %>
</div>

以及在 app/views/comments/_form.html.erb

<p>
  <%= form.label :status %><br>
  <%= form.select :status, Visible::VALID_STATUSES, selected: 'public' %>
</p>

10 刪除留言

部落格的另一個重要功能是能夠刪除垃圾留言。為此,我們需要在視圖中實作某種連結,並在 CommentsController 中實作 destroy 動作。

首先,讓我們在 app/views/comments/_comment.html.erb 局部範本中新增刪除連結

<% unless comment.archived? %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <p>
    <%= link_to "Destroy Comment", [comment.article, comment], data: {
                  turbo_method: :delete,
                  turbo_confirm: "Are you sure?"
                } %>
  </p>
<% end %>

點擊這個新的「刪除留言」連結將向我們的 CommentsController 發送一個 DELETE /articles/:article_id/comments/:id,然後它可以使用它來找到我們想要刪除的留言,因此讓我們在我們的控制器中新增一個 destroy 動作 (app/controllers/comments_controller.rb)

class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  def destroy
    @article = Article.find(params[:article_id])
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    redirect_to article_path(@article), status: :see_other
  end

  private
    def comment_params
      params.expect(comment: [:commenter, :body, :status])
    end
end

destroy 動作將找到我們正在查看的文章,找到 @article.comments 集合中的留言,然後將其從資料庫中移除,並將我們送回文章的顯示動作。

10.1 刪除關聯物件

如果您刪除一篇文章,其關聯的留言也需要刪除,否則它們只會佔用資料庫中的空間。Rails 允許您使用關聯的 dependent 選項來實現此目的。修改文章模型 app/models/article.rb,如下所示

class Article < ApplicationRecord
  include Visible

  has_many :comments, dependent: :destroy

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

11 安全性

11.1 基本身份驗證

如果您在線上發布您的部落格,任何人都可以新增、編輯和刪除文章或刪除留言。

Rails 提供了一個 HTTP 身份驗證系統,可以在這種情況下很好地運作。

ArticlesController 中,我們需要一種方法來阻止對各種動作的存取,如果該人未通過身份驗證。在這裡,我們可以使用 Rails 的 http_basic_authenticate_with 方法,如果該方法允許,則允許存取請求的動作。

若要使用身份驗證系統,我們在 ArticlesController 的頂部指定它,位於 app/controllers/articles_controller.rb 中。在我們的案例中,我們希望使用者在除 indexshow 之外的每個動作中都進行身份驗證,所以我們寫下

class ArticlesController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]

  def index
    @articles = Article.all
  end

  # snippet for brevity
end

我們也希望只允許通過身份驗證的使用者刪除留言,所以在 CommentsController (app/controllers/comments_controller.rb) 中,我們寫下

class CommentsController < ApplicationController
  http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy

  def create
    @article = Article.find(params[:article_id])
    # ...
  end

  # snippet for brevity
end

現在,如果您嘗試建立新文章,您將看到一個基本的 HTTP 身份驗證挑戰

Basic HTTP Authentication Challenge

輸入正確的使用者名稱和密碼後,您將保持身份驗證,直到需要不同的使用者名稱和密碼或瀏覽器關閉。

其他身份驗證方法可用於 Rails 應用程式。Rails 的兩個熱門身份驗證附加元件是 Devise rails 引擎和 Authlogic gem,以及許多其他附加元件。

11.2 其他安全性注意事項

安全性,尤其是在 Web 應用程式中,是一個廣泛而詳細的領域。您的 Rails 應用程式中的安全性在 Ruby on Rails 安全性指南 中有更深入的介紹。

12 接下來是什麼?

既然您已經看過您的第一個 Rails 應用程式,您應該可以自由更新它並自行實驗。

請記住,您不必在沒有協助的情況下完成所有事情。當您需要協助啟動和執行 Rails 時,請隨時查閱這些支援資源

13 設定陷阱

使用 Rails 的最簡單方法是將所有外部資料儲存為 UTF-8。如果您不這樣做,Ruby 函式庫和 Rails 通常能夠將您的原生資料轉換為 UTF-8,但這並不總是可靠地運作,因此您最好確保所有外部資料都是 UTF-8。

如果您在此方面犯了錯誤,最常見的症狀是在瀏覽器中出現一個帶有問號的黑色菱形。另一個常見的症狀是出現「ü」之類的字元,而不是「ü」。Rails 採取許多內部步驟來減輕可以自動偵測和更正的這些問題的常見原因。但是,如果您有未儲存為 UTF-8 的外部資料,則偶爾會導致 Rails 無法自動偵測和更正的這些問題。

兩個非常常見的非 UTF-8 資料來源

  • 你的文字編輯器:大多數的文字編輯器(例如 TextMate)預設會將檔案儲存為 UTF-8 格式。如果你的文字編輯器沒有這麼做,你可能會在瀏覽器中看到,你在範本中輸入的特殊字元(例如 é)會顯示為內含問號的菱形。這也適用於你的 i18n 翻譯檔案。大多數不預設為 UTF-8 的編輯器(例如某些版本的 Dreamweaver)都提供將預設值變更為 UTF-8 的方法。請務必進行變更。
  • 你的資料庫:Rails 預設會在邊界將資料庫中的資料轉換為 UTF-8。但是,如果你的資料庫內部並未使用 UTF-8,則可能無法儲存使用者輸入的所有字元。例如,如果你的資料庫內部使用 Latin-1,而你的使用者輸入俄語、希伯來語或日語字元,則資料一旦進入資料庫就會永久遺失。如果可以,請使用 UTF-8 作為資料庫的內部儲存格式。


回到頂端