從點一個 URL 到看到頁面中間發生了什麼事?(下集)

終於,我們進入 Ruby on Rails 應用程式中。

上一集在這裡

三、應用程式處理請求,打包成 HTTP 回應

關鍵字:Routing, MVC, ORM, Action Pack, Active Record

請求流向:routing → controller action → model ( ←→ database) → controller → view → controller → server

Rails 的設計遵守 MVC (Model, View, Controller)* 架構,會先由路由設定判斷這個請求應該要轉發給哪一個 controller action。Controller 接收了請求之後,會依照需求與資料庫 (體現在 model 中) 互動,並且從 view 拿 html template,打包成 http response 傳回瀏覽器。

Rails 使用 Action PackActive Record 來實作這些機制,下面也會一併介紹。

*補充:最近剛好看到一篇說明 Rails(與其他大家認為是 MVC 的後端框架)其實不是 MVC 的文章,耐人尋味:MVC 是一個巨大誤會

路由 (Routing)

路由是請求進入應用程式的把關人,負責辨識請求的 URL,將請求轉給對應的 controller action,還會順便處理網址內的參數,把參數放進 params 中一起交給 controller 處理,是個盡責的守門人來著。可以把路由想像成 2.0 版的霍格華滋分類帽,除了依照每個新進學生的特性分派他們進入不同學院,還會把學生的身家背景一併整理交給學院長。

Rails 中是由 routes.rb 檔案管理應用程式的路由設定,值得一提的是有預設的 Resource Routing

Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index, show, new, edit, create, updateand destroy actions, a resourceful route declares them in a single line of code.

摘錄自 Rails Routing from the Outside In

一般常見的 http 請求有 GET, POST, PATCH, PUT and DELETE這幾種,每一種方法都是要對請求的對象(resource)進行特定操作。既然這些操作如此常見,那麼每次都分別定義就太不符合 DRY (Don’t Repeat Yourself) 的原則了。所以,一個 resourceful route 提供這些 http verbs 與 controller action 之間的對應關係,而通常這些 controller action 們也會直接對應到資料庫的 CRUD (Create, Read, Update, Delete) 操作。

比如說,今天在檔案內容是

# config/routes.rb
Rails.application.routes.draw do
resources :posts
end

在終端機打 $rails routes 會出現這個應用程式的所有路由,這時候你會看到單單這一行宣告會出現下面這些:

Prefix     Verb   URI Pattern                  Controller#Action
root       GET    /                            posts#index
posts      GET    /posts(.:format)             posts#index
           POST   /posts(.:format)             posts#create
new_post   GET    /posts/new(.:format)         posts#new
edit_post  GET    /posts/:id/edit(.:format)    posts#edit
post       GET    /posts/:id(.:format)         posts#show
           PATCH  /posts/:id(.:format)         posts#update
           PUT    /posts/:id(.:format)         posts#update
           DELETE /posts/:id(.:format)         posts#destroy

當一個請求的 URL 是 posts/3, method=GET 時,Rails 路由器看到 resources :posts,就會把請求送到 posts#show,並將 params[:id] 中的參數設為 3。如果沒有使用 resourceful routing,則我們必須單獨定義這個動作,例如 get 'posts/:id', :to => 'posts#show'

想看更完整又深入淺出的解釋與精美示意圖的話可以來看看這篇 為你自己學 Ruby on Rails

所以現在我們的 https://jennycodes.herokuapp.com 請求到了路由,在 routes.rb 檔案中有一行是root 'posts#index',代表路由會將對首頁的請求送到 PostController 的 index action,於是我們現在來看 controller。

控制器 (Controller)

Controller 在應用程式中,大概是一個風光程度僅次於分類帽(路由)的角色。請求被送過來之後,到產生回應送回去這中間的所有過程都歸它管了。Controller 會負責去操作資料庫、或是串接外部 API (跟外站要資料)、 跟 View 要 http template,然後進行打包 http response 的工作。

Rails 中每個 controller 是一個 ruby class,裡面的 method 就是一個個 controller method。在我們的例子中,請求現在已經被轉到 PostController 中的 index 了,先來看看裡面長什麼樣子。

# app/controllers/posts_controller.rb
class PostsController < ApplicationController

def index
@posts = Post.published.order('created_at DESC')
end
  ...(略)
end

(完整版可以看這裡

在這個簡單範例中,index 做了兩件事情:1. 去資料庫拿 Post 資料表的資料,2. 存進實例變數 @posts 中。一與二分別是跟 Model 與 View 的互動,於是我們現在先來看看 Model。

模型 (Model)

Rails 的模型是資料庫的代表,一般來說,每個資料表都會有自己對應的模型,就像各國大使一樣,我們要在應用程式中操作資料表,或是設計資料表之間的關聯性(associations),都是交付它。Rails 怎麼做到的呢?

物件關聯對應 Object Relational Mapping (ORM)

物件關聯對映是一種程式設計技術,用於實現物件導向程式語言裡不同類型系統的資料之間的轉換。

節錄自維基百科

好的,我們還是不懂。

要理解 ORM,就要先理解現在我們大多數使用的語言都是物件導向語言(Object Oriented Programming)(Ruby 的特色就是所有東西都會被衡量成物件!),而資料庫(這邊討論的是關聯式資料庫)的特徵雖然跟 OOP 的物件很像,但是卻有本質上的不同。ORM 的出現就像是翻譯人員,提供一個抽象的介面讓彼此溝通,這樣做有幾個優點:

  1. 開發人員可以直接用 OOP (在本例中,是 Ruby) 操作資料庫,而不用接觸到資料庫(SQL)語言,省時省力。
  2. 因為不是直接寫資料庫語言,所以如果需要更換資料庫(比如說從 SQLite 換成 MySQL),原本的程式碼也不用改。
  3. 比較容易理解。

其實還有其他優點,現在只是舉我自己目前最有感的。Rails 的 ORM 讓我們只要定義好模型類別,就可以在程式碼中使用,比如現在我們的資料庫裡有 posts這張表,而我們在專案資料夾路徑 app/models/ 新增一個 post.rb檔案:

# app/models/post.rb
class Post < ApplicationRecord
end

在 controller 中,就可以呼叫 Post.all 來取得 posts 資料表的所有資料了,或是可以在 post.rb中增加一些定義或是方法,來對資料庫做更進階的操作,像剛剛的例子 @posts = Post.published.order(‘created_at DESC’)等式右半邊可以翻譯成「找到狀態是已上架 (published) 的文章,並以創立日期 (created_at) 由新往舊排 (DESC)」。

而這個查詢結果,會被存進 @posts,然後帶進 View 中。

畫面 (View)

隨著心理學的不斷推展,View 的重要性,也與日俱增。怎麼說呢?傳統俗諺告訴我們人不可貌相,西洋文化也有 Don’t judge a book by its cover 的諄諄教誨,但是心理學的實驗在在證實了我們就是會被事物外表影響,並且一個印象的建立只是幾個瞬間的事情

而 view 就是決定一個網頁會不會在使用者打開 0.5 秒之後就馬上被關掉的決定性因素。

View 負責的是資料的呈現,提供樣板(template)來動態產生內容,通常由 HTML, CSS 與 JavaScript 組成。每一個 controller action 都會有自己分別的 view 檔案,比如說 posts#index 對應到的 view 慣例上會放在 app/views/posts/index.html.erb。檔名中的 .erb 是 Rails 預設用來產生 template 的方式,可以自己換成別的,像是許多人(包含我)習慣使用的 slim。我們在 controller 建立的實例變數 @posts 可以傳進 view 中被呼叫,所以這時候只要寫個迴圈就可以把 @posts 列出來。

# app/views/posts/index.html.slim
...(略)
- @posts.each do |post|
.post-preview
h2.post-title
= link_to post.title, post_path(post)
h3.post-subtitle
= post.description
p.post-meta
= post.created_at.strftime('%B %-d, %Y')
hr/

這邊只列出最相關的部分,可以看到 @posts 可以直接在裡面被呼叫。

順帶一提,現在前端技術發展越來越成熟,很多前端框架出現了,這時候呈現資料的方法就變得很多元,像是把 Rails 當作是一個 API ,不去 view 要 template,而是拿到瀏覽器再渲染 (client-side rendering),又分了像是 SPA (Single Page Application), SSR (server-side rendering) 等等,可以看看這篇優質好文,當初解了我不少惑。

View 的部分完成了,controller 的工作算是功德圓滿,此時它將會送出一個有 status + header + body 三部分組成的 http response,而這個 response 就會沿著前面那些路線一路被送回瀏覽器。

所以說這些分派請求、MVC 三部分各自的角色與彼此的溝通,都是怎麼做到的?示範的程式碼明明就看起來很簡單啊(甚至還有一個類別是空的)?這時候就要請默默在背後幫你把事情都打點好的 Action Pack 與 Active Record 出場了。

Action Pack

Action Pack is a framework for handling and responding to web requests. It provides mechanisms for routing (mapping request URLs to actions), defining controllers that implement actions, and generating responses by rendering views, which are templates of various formats.

節錄自 Rails 官方文件

看到上面粗體的幾個關鍵字:routing, controllers 與 views,沒耐心看的大概也猜得出來,這些角色之所以能行雲流水的運作,就是靠著 Action Pack 這個框架。

Action Pack 底下有兩個模組,Action Dispatch 與 Action Controller。

Action Dispatch 負責解析請求、處理開發人員設定好的路由(就是我們剛剛在 routes.rb 中定義的),並且做一些更進階的處理像是 POST, PUT, PATCH 的 http 的請求參數、http 快取邏輯、cookies 和 sessions。

Action Controller 提供一個 base class 讓眾 controller 們繼承,我們的 PostsController 也是如此。回顧一下剛剛的檔案:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
...(略)
end

而 application_controller.rb 檔案長這樣:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
end

ActionController::Base 的 API 文件 在這裡。

在 Ruby on Rails 的框架中,開發人員會直接接觸到的只會有 Action Controller 這個模組。Action Dispatch ,與之後的 Action View 會自動被啟動。而在一個簡單專案中(如這個),我們做的事情也只是讓 controller 繼承 ActionController::Base ,剩下的一切就會都幫我們做好。

Active Record

Active Record connects classes to relational database tables to establish an almost zero-configuration persistence layer for applications. The library provides a base class that, when subclassed, sets up a mapping between the new class and an existing table in the database.

節錄自 Rails 官方文件

沒錯,Active Record 就是 Rails 實作出 ORM 的地方,將資料庫的表對應到 Ruby 類別(在 Rails 情境中就是 model)。只要讓你的 class 繼承ActiveRecord::Base,並且遵循命名規範,你就可以幾乎零設定就開始用 Ruby 程式碼操作資料庫了。回頭看一下剛剛的例子:

# app/models/post.rb
class Post < ApplicationRecord
end

Post 這個類別是繼承自ApplicationRecord,在 application_record.rb 中:

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

ApplicationRecord的確是繼承自 ActiveRecord。有興趣的同樣也可以去看看 Rails 的 API 文件,這邊就先附上連結不多說了。

介紹完了 Rails 的 MVC 機制,使用者在螢幕那端大約快要不耐煩了(她還沒看到畫面哪),現在 response 已經被產生,接下來它會沿著 request 進來時的路徑,從路由、中介軟體一路被送出去。

第四步:瀏覽器收到回應,渲染頁面,完成!

這邊直接列點描述會經過的步驟:

  1. 判斷狀態碼:2XX 表示可以放行,3XX 表示你要找的路不在這在另一邊,4XX 與 5XX 表示你要找的路不在這但不知道在哪邊
  2. 多國語言的編碼解析: 常常看到 I18N 卻不知道是什麼意思嗎?是 Internationalization
  3. 根據 html 建構 DOM Tree:如果遇到 <stylesheet> 會先保存給之後渲染,如果遇到 <script> 標記則會送到 Javascript 引擎
  4. 製造 Render Tree:根據 html 與 css 製造的,這邊會把被 css 隱藏的部分留下,不會放進樹中。
  5. 渲染頁面,頁面加載完成。

事到如今,網頁終於呈現出來了。雖然經過了兩大篇的篇幅,但是這段從輸入 URL 到看到回應的過程實際上只會發生在幾秒之間,是不是偉哉網路。

後記

噫!我寫完了!

本來只是覺得好玩,想要挑戰一下自己,沒想到邊寫邊查資料的過程中也順道為自己釐清了很多原本模糊或是似懂非懂的觀念。講得出來才是真正理解的開始啊!除了釐清觀念之外,同時也發現了很多有趣的東西,但考量到放進來的話篇幅就會太長而且發散,所以以後有空慢慢來介紹。

雖然已盡能力所及地確保資料的正確性,但我恐怕還是會有不對/不精確的觀念或用字,如果願意指正我的話我會非常感激!

參考資料

Rails Routing from the Outside In

Rails from Request to Response

Examining The Internals Of The Rails Request/Response Cycle

浏览器的工作原理:新式网络浏览器幕后揭秘

文章同步發表於 Medium


  • Find me at