October 2, 2023

Viiisit [Ruby on Rails] - Controller, Action, View!

#ruby on rails

建立 Controller 與定義 Action

在先前介紹路徑時,以「文章列表」的路徑為例子:

1
get '/articles', to: 'articles#index'

可以發現,在建立路徑的同時,to: 後方就是 controller 與 action!

Controller 的命名會根據 Route 是使用複數的 resources 還是單數 resource 方法而定。
這種命名幫助我們更好地組織與管理 controller 與 action,程式碼也更易於理解和維護。
同時也符合 Rails 的慣例優於設定(Convention over Configuration)的原則。

在這裡我們使用的是 resources,所以我們可以建立一個 ArticlesController!

rails g controller Articles

透過 rails generate controller Articles or rails g controller Articles 指令,
生成叫做 Articles 的 controller!

Remark:
controller 建立, rails generate controller 自定義名字 -> rails g controller 自定義名字
controller 移除, rails destroy controller 自定義名字 -> rails d controller 自定義名字

剛剛的指令可以幫我們生成一些所需要的檔案,而 Action 就會被定義在 controller

透過 controller.rb 定義 action

app/controllers/articles_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class ArticlesController < ApplicationController

# Rails 在讀取以下 action 之前,可以先調用 private 方法,透過 only 限定哪些 action 需要。
# except: [...] 是另外一種寫法,可以根據要限定的 action 多寡來決定要用哪個。
before_action :set_article, only: [:show, :edit, :update, :destroy]

# Read
def index
# 設定一個實體變數,將所有新增的文章呈現在 index 裡
# 預設是升冪排序 (id: :asc)
@articles = Article.order(id: :desc)
end

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

# Create
def new
# 在 controller 生成 "@實體變數" , view 呈現!
@article = Article.new
end

def create # 不需獨立建 view 可以直接借別人的畫面使用
# 在這裡用實體變數 @article view 就可以一起連動所有有這個變數的檔案,呈現畫面!
@article = Article.new(article_params)

# Save in 資料庫
if @article.save
redirect_to "/articles", notice: "文章新增成功"
else
# 拿 new.html.erb,將驗證過後,失敗的話,留下原始有填寫的欄位留下來
render :new
end
end

# Update
def edit
# @article = Article.find(params[:id]) # 直接調用剛剛 show 所寫找出想對應文章
end

def update # 不需獨立建 view 可以直接借別人的畫面使用
# @article = Article.find(params[:id])

if @article.update(article_params)
# redirect_to "/articles", notice: "文章新增成功"
redirect_to articles_path, notice: "文章更新成功"
else
render :edit
end
end

# Delete
def destroy
# @article = Article.find(params[:id])
@article.destroy
redirect_to articles_path, notice: "文章刪除成功"
end

private
# Strong Parameter
def article_params
params.require(:article).permit(:title, :content, :sub_title)
end

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

註解拿掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ArticlesController < ApplicationController

before_action :set_article, only: [:show, :edit, :update, :destroy]

# Read
def index
@articles = Article.includes(:user).order(id: :desc)
end

def show
@comment = Comment.new
@comments = @article.comments.order(id: :desc)
end

# Create
def new
@article = Article.new
end

def create
@article = current_user.articles.new(article_params)

if @article.save
redirect_to "/articles", notice: "文章新增成功"
else
render :new
end
end

def edit
end

def update
if @article.update(article_params)
redirect_to articles_path, notice: "文章更新成功"
else
render :edit
end
end

def destroy
@article.destroy
redirect_to articles_path, notice: "文章刪除成功"
end

private
def article_params
params.require(:article).permit(:title, :content, :sub_title, :password)
end

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

建立 View

當 Route, Controller 與 Action 都建立好後,就需要建立 View 呈現畫面!
需要手動新增檔案:action 名稱.html.erb,建立 view
(以 index 為例) index.html.erb

app/views/articles/index.html.erb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<h1>Articles 文章列表</h1>
<%# <button><a href="/articles/new" style="text-decoration: none" >Add a new article!</a></button> %>
<button><%= link_to "Add a new article!", new_article_path %></button>
<ul>
<% @articles.each do |article| %>
<li class="article_list">
<%#= link_to article.title, article_path(article.id) %>
<%#= link_to article.title, article_path(article) %>
<%#= article.user&.email %>
<%= link_to article.title, article %>
</li>
<span><%= link_to 'Updated', edit_article_path(article.id) %></span>
<span><%= link_to 'Deleted', article_path(article.id),
data: { turbo_method: 'delete', turbo_confirm: 'Are you sure?' }%></span>
<%# turbo_confirm 可以連結 JS 的 confirm 功能 %>
<% end %>
</ul>

我建立了 POST 路徑,為了新增一篇文章用的,
也確定 Controller, Action & View 都有對應到,
但是顯示這個錯誤訊息
ActionController::InvalidAuthenticityToken in ArticlesController#create
發生什麼事了?!

這裡要額外說明一個知識,就是 CSRF

CSRF

What is CSRF?

CSRF 是指跨站的請求偽造,這種攻擊方法會讓使用者去瀏覽一個曾經認證過的網站並執行惡意的操作,因為已經驗證過該使用者,所以網站就會認為操作來自該使用者,因而接受。
CSRF 之所以成立,是因為使用者的身份已經先被驗證過。
白話一點就像是別人拿你的會員卡去買東西,但剛好因為店家認卡不認人,所以當看到有人拿著你的卡,就相信是你本人,並接受他人使用你的會員卡進行消費。

在 Rails 中,POST 行為具有保護機制。
如果在設定好的 action 中重新整理頁面,並且在沒有建立 view 的情況下,
你會注意到與之前不同的錯誤訊息:
ActionController::InvalidAuthenticityToken in ArticlesController#create
這時候,你需要設定 CSRF token 保護機制,以便在提交表單後能夠通過!

Rails 的 form_with 方法,會自動為每個表單和非 GET 請求生成唯一的 CSRF Token,
並在提交請求時驗證該 Token,以防止 CSRF 攻擊。

Remark:
有種情況下不需要設定 CSRF token:
當你需要跟第三方金流做連線時,需要把這個保護機制關閉,這樣才能互相聯繫。 
非同步交易時,當完成之後,會透過 Notify-URL 去通知對方。

實作 Go! Go!

app/views/articles/new.html.erb
特別以 new.html.erb 來說明 form_with 可以生成所需要的表單並包含 CSRF token,
保護你的應用免受 CSRF 攻擊。

這裡先看一次不藉由 form_with 時,自己加上 CSRF Token 的 input tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<h1>Add a new article!</h1>
<form action="/articles" method="post" data-turbo="false">
<div>
<h2>Title</h2>
<input type="text" name="title" >
</div>
<div>
<h2>Content</h2>
<textarea name="content" id="" cols="30" rows="10" placeholder="Enter your story..."></textarea>
</div>
<%# CSRF token 讓送出之後可以通過 POST 的保護機制 %>
<input name="authenticity_token" value="<%= form_authenticity_token %>" type="hidden">
<button>Submit!</button>
</form>

接著是實際做一次使用 form_with :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<h1>Add a new article!</h1>
<%= form_with(model: article, data: { turbo: false }) do |f| %>
<div>
<h2>Title</h2>
<%= f.text_field :title %>
</div>

<div>
<h2>Subtitle</h2>
<%= f.text_field :sub_title %>
</div>

<div>
<span>password</span>
<%= f.password_field :password %>
</div>

<div>
<h2>Content</h2>
<%= f.text_area :content %>
</div>

<%= f.button :submit, class: 'submit-btn' %>
<% end %>

當使用 form_with 時,可以去檢視網頁的原始碼,
就會發現我們不用自己加上 CSRF Token,HTML 也會生成以下這段:

1
<input type="hidden" name="authenticity_token" value="BTeOTjllGc-FxHfBtsCidE_i1BAotM0RvZHVnr1LmA16OgTE04My-zwySMSVBav6tlOt62iEDUyDCgNANEVCkA" autocomplete="off" />

完整的 HTML 原始碼,CSRF Token 於 line 4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<h1 class="main-title">Add a new article!</h1>

<form data-turbo="false" action="/articles" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="BTeOTjllGc-FxHfBtsCidE_i1BAotM0RvZHVnr1LmA16OgTE04My-zwySMSVBav6tlOt62iEDUyDCgNANEVCkA" autocomplete="off" />
<div>
<h2>Title</h2>
<input type="text" name="article[title]" id="article_title" />
</div>

<div>
<h2>Subtitle</h2>
<input type="text" name="article[sub_title]" id="article_sub_title" />
</div>

<div>
<span>password</span>
<input type="password" name="article[password]" id="article_password" />
</div>

<div>
<h2>Content</h2>
<textarea name="article[content]" id="article_content">
</textarea>
</div>

<button name="article[submit]" type="submit" id="article_submit" class="submit-btn">Create Article</button>
</form>

Summary

自己在實作 CRUD 方式是從 Route -> Controller -> Action -> View 一個一個打造起來,
這篇主要敘述 Controller, Action & View,這三者之間的關聯與遇到 CSRF 錯誤訊息的狀況!


參考資料:
為你自己學 Ruby on Rails - Controller
wikipedia - 跨站請求偽造
ExplainThis