檢視原始碼 請求生命週期

需求:本指南假定您已閱讀 入門指南,而且已經 安裝並執行 Phoenix 應用程式。

本指南的目標是要說明 Phoenix 的請求生命週期。本指南將採取實作方式來學習:我們會在 Phoenix 專案中新增兩個新頁面,並在這過程中說明各部分如何組合在一起。

讓我們從新增第一個 Phoenix 新頁面開始吧!

新增網頁

瀏覽器存取 https://127.0.0.1:4000/ 時,會傳送 HTTP 請求至在該位址上執行的服務(在這個情況下,是我們的 Phoenix 應用程式)。HTTP 請求由動詞和路徑組成。例如,下列的瀏覽器請求會轉換成

瀏覽器網址列動詞路徑
https://127.0.0.1:4000/GET/
https://127.0.0.1:4000/helloGET/hello
https://127.0.0.1:4000/hello/worldGET/hello/world

還有其他 HTTP 動詞。例如,提交表單通常會使用 POST 動詞。

網頁應用程式會透過將每個動詞/路徑對應到應用程式中的特定部分來處理請求。在 Phoenix 中,路由器會進行此配對。例如,我們可以將 "/articles" 對應到我們應用程式中顯示所有文章的部分。因此,要新增網頁,我們的第一個任務就是新增一個新的路由。

一個新的路由

路由器會將唯一的 HTTP 動詞/路徑對應到控制項/處理這些對應項的動作。在 Phoenix 中,控制項只是 Elixir 模組。動作是在這些控制項中定義的函數。

Phoenix 會在新應用程式中幫我們產生一個路由器檔案,路徑為 lib/hello_web/router.ex。我們將在本節中使用此檔案。

先前 啟動與執行指南 中 "Welcome to Phoenix!" 頁面的路由如下所示。

get "/", PageController, :home

讓我們來了解這個路由告訴我們哪些事。當造訪 https://127.0.0.1:4000/ 時,會向根目錄路徑發出 HTTP GET 請求。像這樣的所有請求都會由 HelloWeb.PageController 模組中,定義在 lib/hello_web/controllers/page_controller.exhome/2 函數來處理。

當我們瀏覽器指向 https://127.0.0.1:4000/hello 時,我們即將建立的頁面會顯示「你好,世界,來自 Phoenix!」。

我們首先需要為新頁面建立頁面路由。讓我們在文字編輯器中開啟 lib/hello_web/router.ex。對於全新應用程式,看起來像這樣

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :home
  end

  # Other scopes may use custom stacks.
  # scope "/api", HelloWeb do
  #   pipe_through :api
  # end

  # ...
end

現在,我們會忽略管線與 scope 的使用方法,專注於新增路由。我們將在 路由指南 中討論這些內容。

讓我們在路由器中加入一個新路由,它會將 GET 要求(對於 /hello)對應到我們即將建立的 HelloWeb.HelloController 中的 index 動作(在路由器的 scope "/" do 區塊中)

scope "/", HelloWeb do
  pipe_through :browser

  get "/", PageController, :home
  get "/hello", HelloController, :index
end

新的控制器

控制器是 Elixir 模組,而動作是在其中定義的 Elixir 函數。動作的目的是收集資料並執行渲染所需的工作。我們的路由指定我们需要一個包含 index/2 函數的 HelloWeb.HelloController 模組。

讓我們建立一個 lib/hello_web/controllers/hello_controller.ex 檔案,讓 index 動作發生,並且讓它看起來像下列範例

defmodule HelloWeb.HelloController do
  use HelloWeb, :controller

  def index(conn, _params) do
    render(conn, :index)
  end
end

我們會將 use HelloWeb, :controller 的討論留給 控制器指南。現在,讓我們先專注於 index 動作。

所有控制動作都使用兩個引數。第一個是 conn,一個包含大量要求資料的結構。第二個是 params,也就是要求參數。在這裡,我們不會使用 params,並且透過在前面加上 _ 來避免編譯器警告。

這個動作的核心是 render(conn, :index)。它告訴 Phoenix 渲染 index 範本。負責渲染的模組稱為檢視。預設情況下,Phoenix 檢視會以控制器(HelloController)和格式(在本例中為 HTML)命名,因此 Phoenix 會預期存在 HelloWeb.HelloHTML 並定義 index/1 函數。

新的檢視

Phoenix 檢視充當表示層。例如,我們預期渲染 index 的輸出是一個完整的 HTML 頁面。為了讓自己的執行更容易,我們常常使用範本來建立這些 HTML 頁面。

讓我們建立一個新的檢視。建立 lib/hello_web/controllers/hello_html.ex,讓它看起來像這樣

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html
end

要將範本新增到這個檢視中,我們可以在模組或個別檔案中將它們定義為函數元件。

讓我們先定義一個函式元件

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  def index(assigns) do
    ~H"""
    Hello!
    """
  end
end

我們定義了一個接收 assigns 作為引數的函式,並使用 符號 ~H 放入我們要呈現的內容。在符號 ~H 中,我們使用了一種稱之為 HEEx 的範本語言,代表「HTML+EEx」。EEx 是嵌入 Elixir 的函式庫,隨 Elixir 自身附帶。「HTML+EEx」是 EEx 的 Phoenix 擴充,它具備 HTML 辨識能力,並支援 HTML 驗證、元件,以及值的自動跳脫。後者保護您免於跨網站腳本撰寫等安全漏洞的侵害,而無需您額外費心。

範本檔案以相同方式運作。函式元件對於較小的範本來說很棒,而單獨的檔案在您有許多的標記或函式開始難以管控時是一個好選擇。

讓我們透過在一個自己的檔案中定義範本來嘗試一下。首先移除以上我們的函式 def index(assigns),並用 embed_templates 宣告替換它

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  embed_templates "hello_html/*"
end

在此我們指示 Phoenix.Component 將在同層 hello_html 目錄中找到的所有 .heex 範本嵌入我們的模組中,做為函式定義。

接著,我們需要將檔案加入到 lib/hello_web/controllers/hello_html 目錄中。

請注意,控制器名稱 (HelloController)、視圖名稱 (HelloHTML) 和範本目錄 (hello_html) 都遵循相同的命名慣例,並且是根據彼此來命名。它們在目錄樹中也被並排放置

注意:我們可以將 hello_html 目錄重新命名為任何我們想要的,並將它放在 lib/hello_web/controllers 的子目錄中,只要我們相應地更新 embed_templates 設定即可。然而,最好還是維持相同的命名慣例,避免產生任何混淆。

lib/hello_web
 controllers
    hello_controller.ex
    hello_html.ex
    hello_html
|        index.html.heex

範本檔案具有下列結構: NAME.FORMAT.TEMPLATING_LANGUAGE。就我們的例子中,我們在 lib/hello_web/controllers/hello_html/index.html.heex 中建立一個 index.html.heex 檔案

<section>
  <h2>Hello World, from Phoenix!</h2>
</section>

範本檔案在模組中被編譯為函式元件本身,這兩種樣式之間沒有執行時間或效能的差別。

現在我們已經有了路由、控制器、視圖和範本,我們應該能夠在瀏覽器中指向 https://127.0.0.1:4000/hello,並從 Phoenix 中看到我們的問候語。(如果您中途停止伺服器,重新啟動它的任務是 mix phx.server。)

Phoenix Greets Us

我們剛才做的事有幾件有趣之處。在我們做出這些變更時,我們無需停止並重新啟動伺服器。是的,Phoenix 有暖重載碼功能!此外,即使我們的 index.html.heex 檔僅包含一個 section 標籤,我們取得的頁面仍是完整的 HTML 文件。我們的索引範本實際上會呈現為配置:它首先呈現 lib/hello_web/components/layouts/root.html.heex,而該程式碼會呈現 lib/hello_web/components/layouts/app.html.heex,最後包含我們的內容。如果你開啟這些檔案,你會在底部看到如下列一行的程式碼

<%= @inner_content %>

它會將我們的範本注入配置,然後再將 HTML 傳送至瀏覽器。我們將在控制項指南中進一步說明配置。

關於暖重載碼的說明:某些編輯器及其自動 linter 可能會阻止暖重載碼運作。如果在你的電腦上不起作用,請參閱 此議題 中的討論。

從端點返回檢視

建立我們的首個頁面時,我們可以開始瞭解請求生命週期如何被整合在一起。現在讓我們深入探討它。

所有 HTTP 請求都從我們的應用程式端點啟動。你可以把它視為一個稱為 HelloWeb.Endpoint 的模組,位置位於 lib/hello_web/endpoint.ex 中。開啟端點檔案後,你會看見與路由器類似,端點也有許多呼叫 plugPlug 是將 Web 應用程式拼接在一起的函式庫和規範。這是 Phoenix 處理請求的必要部分,我們將在接下來的 Plug 指南 中詳細說明它。

目前來說,足夠說明的是每個 plug 都定義了一段處理請求的程式片段。你將在端點中找到類似這樣的骨幹程式碼

defmodule HelloWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello

  plug Plug.Static, ...
  plug Plug.RequestId
  plug Plug.Telemetry, ...
  plug Plug.Parsers, ...
  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, ...
  plug HelloWeb.Router
end

這些 plug 各自有我們未來將會學習到的特定責任。最後一個 plug 就是 HelloWeb.Router 模組。這讓端點可以將所有後續請求處理委派給路由器。就像我們現在知道的,它的主要責任是將動詞/路徑配對至控制項。接著,控制項會告知檢視去呈現範本。

此刻,你可能會認為執行這麼多步驟才能呈現一個頁面,未免也太繁瑣了吧。然而,隨著我們的應用程式越來越複雜,我們將會了解到每一個層次都有不同的目的

  • 端點 (Phoenix.Endpoint) - 端點包含所有請求都會經過的常見初始路徑。如果你想讓某個事項發生在所有請求中,請將其加入端點。

  • 路由器 (Phoenix.Router) - 路由器負責將動詞/路徑傳送至控制項。路由器也讓我們可以定義範圍功能。例如,你的應用程式中的某些頁面可能需要使用者驗證,而另一些則可能不需要。

  • 控制器 (Phoenix.Controller) - 控制器的任務是擷取要求資訊、與您的業務網域對話並為展示層準備資料。

  • 檢視 - 檢視會處理來自控制器的結構化資料,並將其轉換成展示內容以供使用者檢視。檢視名稱通常會以其正在呈現的內容格式命名。

讓我們快速回顧最後三個組件如何透過新增另一個頁面來共同運作。

其他新頁面

讓我們為應用程式新增一些小小複雜度。我們將新增一個新頁面,它會辨識 URL 的一些片段,將其標示為「Messenger」,然後透過控制器傳遞到範本,以便我們的 Messenger 可以向您問候。

如同上次一樣,我們要執行的第一件事情是建立一個新路由。

另一個新路由

針對此練習,我們將重新使用在 上一步驟 所建立的 HelloController,並新增一個 show 動作。我們會在最後一個路由下方新增一列,如下所示

scope "/", HelloWeb do
  pipe_through :browser

  get "/", PageController, :home
  get "/hello", HelloController, :index
  get "/hello/:messenger", HelloController, :show
end

請注意,我們在路徑中使用了 :messenger 語法。Phoenix 會將出現在 URL 中此位置的任何值轉換為參數。例如,如果我們將瀏覽器指向:https://127.0.0.1:4000/hello/Frank"messenger" 的值會是 "Frank"

其他新動作

對我們新路由的要求將由 HelloWeb.HelloController show 動作處理。我們的控制器已經在 lib/hello_web/controllers/hello_controller.ex 中,所以我們只需要編輯該控制器並為其新增 show 動作。這次,我們需要從參數中擷取 Messenger,以便我們可以將它(Messenger)傳遞到範本中。為此,我們將這個 show function 新增到控制器中

def show(conn, %{"messenger" => messenger}) do
  render(conn, :show, messenger: messenger)
end

show 動作的主體中,我們也傳遞第三個引數給 render function,它是 :messenger 是 key,而 messenger 變數會傳遞為 value 的 key-value pair。

如果動作的主體需要存取繫結到 params 變數的完整參數映射,除了繫結的 Messenger 變數之外,我們可以這樣定義 show/2

def show(conn, %{"messenger" => messenger} = params) do
  ...
end

切勿忘記,params 對應的鍵值永遠會是字串,且等號並不代表賦值,而是在這表示模式比對正則。

另一個新的範本

為了拼湊出這最後一塊拼圖,我們需要一個新的範本。由於它是 HelloControllershow 動作,因此它會在 lib/hello_web/controllers/hello_html 目錄中,並命名為 show.html.heex。它會和我們的 index.html.heex 範本異常相似,不同之處僅在於我們需要顯示訊息傳遞者的名稱。

為了做到這件事,我們將使用 HEEx 專門用於執行 Elixir 表達式的標籤:<%= %>。請注意,初始標籤上像這樣,帶有等號:<%=。這表示在這些標籤之間的所有 Elixir 程式碼都會執行,且執行結果會取代 HTML 輸出中的標籤。如果沒有等號,程式碼也還是會執行,但值不會顯示在頁面上。

請記住,我們的範本是用 HEEx(HTML+EEx)撰寫的。HEEx 是 EEx 的超集,這就是為什麼它們會共用 <%= %> 語法。

以下就是範本的程式碼

<section>
  <h2>Hello World, from <%= @messenger %>!</h2>
</section>

我們的訊息傳遞者顯示為 @messenger

我們從控制器傳遞到檢視的值統稱為「配置」。我們可以使用 assigns.messenger 存取訊息傳遞者值,但是透過一點元程式設計,Phoenix 為我們提供了更簡潔的 @ ,以便在範本中使用。

完成了。如果您將瀏覽器指向 https://127.0.0.1:4000/hello/Frank,您應該會看到一個類似這樣的網頁

Frank Greets Us from Phoenix

玩玩看。不論您在 /hello/ 後面輸入什麼訊息,都會出現在網頁上,並顯示為您的訊息傳遞者。