檢視原始程式碼 控制器

需求:本指南假設您已完成入門指南,而且 啟動並執行 Phoenix 應用程式。

需求:本指南假設您已完成請求生命週期指南

Phoenix 控制器作為中介模組。它們的功能,稱為動作,會根據 HTTP 請求從路由器中呼叫。這項動作會收集所有必要資料,並在呼叫檢視層以呈現範本或傳回 JSON 回應前,執行所有必要的步驟。

Phoenix 控制器也會建立於 Plug 套件,本身就是插頭。這些控制器提供函式,幾乎可讓我們在動作中執行任何需要的操作。如果發現 Phoenix 控制器沒有提供我們要尋找的內容,我們可以在 Plug 中找到需要的內容。請參閱 Plug 指南Plug 文件 以取得詳細資料。

新生成的 Phoenix 應用程式會有單一控制器,命名為 PageController,可在 lib/hello_web/controllers/page_controller.ex 中找到,會顯示為

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  def home(conn, _params) do
    render(conn, :home, layout: false)
  end
end

模組定義下方的第一行會呼叫 HelloWeb 模組的 __using__/1 巨集,這個巨集會匯入一些有用的模組。

PageController 會提供 home 動作,它用來呈現與 Phoenix 中路由器所定義的預設路由相關的 Phoenix 歡迎網頁

動作

控制器動作僅是函式。它們可以根據 Elixir 的命名規則,以我們想要的形式來命名。我們必須符合的唯一需求是:動作名稱必須與路由器中定義的路由相符。

例如,在 lib/hello_web/router.ex 中,我們可以在 Phoenix 在新應用程式中提供的預設路由中變更動作名稱,從 home

get "/", PageController, :home

變更為 index

get "/", PageController, :index

只要我們也會把 PageController 中的動作名稱變更為 index,那麼 歡迎網頁 就能如先前般載入。

defmodule HelloWeb.PageController do
  ...

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

當我們可以依我們喜歡的來命名不同的動作時,將會有遵循慣例的動作名稱,只要可能的話皆應遵循。我們之前已在〈routing 指南〉中介紹過,但在此我們將再快速地瀏覽一次。

  • index - 呈現給定資源類型所有項目清單
  • show - 透過 ID 呈現單一項目
  • new - 呈現用於建立新項目的表單
  • create - 接收新項目參數並將其儲存在資料儲存體中
  • edit - 透過 ID 擷取單一項目並在表單中呈現用於編輯
  • update - 接收已編輯項目的參數並將該項目儲存至資料儲存體
  • delete - 接收要刪除項目的 ID 並將其從資料儲存體中刪除

這每個動作使用兩個參數,而 Phoenix 將在幕後提供。

第一個參數總是 conn,一個包含請求資訊的結構,例如主機、路徑元素、連接埠、查詢字串等等。 conn 是透過 Elixir 的 Plug 中介層架構提供給 Phoenix。您能在 Plug.Conn 文件 中找到關於 conn 的更多詳細資訊。

第二個參數是 params。不難理解,這是一個包含傳遞到 HTTP 請求中的任何參數的地圖。在函數簽章中對應參數進行模式比對以提供資料,這是一個良好的做法,而我們可以將其傳遞到渲染,這樣我們就可以提供一個簡潔的套件。我們在 request 生命週期指南 中看過,當時我們將 messenger 參數新增到 lib/hello_web/controllers/hello_controller.ex 中的 show 路由。

defmodule HelloWeb.HelloController do
  ...

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

在某些情況下 — 通常例如在 index 動作中,我們不會在意參數,因為我們的行為並不受這些參數影響。在這種情況下,我們不會使用輸入參數,而僅在變數名稱之前加上底線,將其稱為 _params。這樣編譯器就不會抱怨未使用的變數,同時仍保持正確的元數。

渲染

控制器可以使用多種方式來渲染內容。最簡單的方法是使用 Phoenix 提供的 text/2 函數來渲染一般文字。

例如,我們來重新撰寫 HelloController 中的 show 動作讓它回傳文字。為了達成此目標,我們可以進行以下操作。

def show(conn, %{"messenger" => messenger}) do
  text(conn, "From messenger #{messenger}")
end

現在,您的瀏覽器中的 /hello/Frank 應該以一般文字顯示 From messenger Frank 而沒有任何 HTML。

再進階一點的做法是使用 json/2 函數來渲染純 JSON。我們需要傳遞可以由 Jason 函式庫 解碼為 JSON 的項目,例如一個地圖。(Jason 是 Phoenix 的依存關係之一。)

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

如果我們再次在瀏覽器中拜訪 /hello/Frank,應該會看到一塊 JSON,包含的 id 鍵對映到 "Frank" 字串。

{"id": "Frank"}

對於編寫 API,json/2 函數相當有用,而 html/2 函數則用於呈現 HTML,但在大多數情況下,我們使用 Phoenix 檢視來建構回應。為此,Phoenix 包含 render/3 函數。對 HTML 回應來說這特別重要,因為 Phoenix 檢視提供了效能與安全性優點。

我們將 show 操作還原到我們原本在 請求生命週期指南 中編寫的內容。

defmodule HelloWeb.HelloController do
  use HelloWeb, :controller

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

要讓 render/3 函數能正常運作,控制項與檢視必須共用相同根名稱(本例中為 Hello),而 HelloHTML 模組必須包含 embed_templates 定義,指定其範本在哪裡。預設情況下,控制器、檢視模組和範本會置於同個控制器目錄。換句話說,HelloController 需要 HelloHTML,而 HelloHTML 需要 lib/hello_web/controllers/hello_html/ 目錄存在,該目錄必須包含 show.html.heex 範本。

render/3 也會將 show 操作接收到的 messenger 參數值,作為指派,傳遞給範本。

如果我們在使用 render 時需要將值傳遞給範本,很簡單。我們可以像處理 messenger: messenger 一樣傳遞關鍵字,或是可以使用 Plug.Conn.assign/3,如此一來,會方便地回傳 conn

  def show(conn, %{"messenger" => messenger}) do
    conn
    |> Plug.Conn.assign(:messenger, messenger)
    |> render(:show)
  end

注意:使用 Phoenix.Controller 會載入 Plug.Conn,因此可以簡化對 assign/3 的呼叫。

一次傳遞多個值給範本,只要將 assign/3 函數串接在一起即可。

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

或者,可以改為直接將指派傳遞給 render

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

一般而言,一旦所有指派都設定完畢,我們便會呼叫檢視層。隨後檢視層 (HelloWeb.HelloHTML) 會呈現 show.html 以及配置,並將回應傳回給瀏覽器。

元件和 HEEx 範本 有自己的指南,因此在此不會花太多時間討論這些內容。我們將探討如何從控制器操作中呈現不同的格式。

新的渲染格式

透過範本渲染 HTML 很好,但如果我們需要動態變更渲染格式呢?假設我們有時需要 HTML、有時需要純文字,有時需要 JSON。那該怎麼辦呢?

檢視的工作不只是渲染 HTML 範本。檢視關於資料呈現。給定一袋資料,檢視的目的是以有意義的方式呈現出採用某種格式的資料,不論是 HTML、JSON、CSV 或其他格式。今日的許多網路應用程式將 JSON 傳回給遠端用戶端,而 Phoenix 檢視非常適合用來進行 JSON 渲染。

舉一個範例,我們可以從新產生應用程式的 PageControllerhome 動作開始。系統一開始會使用正確的檢視 PageHTML、來自 (lib/hello_web/controllers/page_html) 的嵌入式範本,以及用來渲染 HTML 的正確範本 (home.html.heex.)

def home(conn, _params) do
  render(conn, :home, layout: false)
end

它沒有的是用來渲染 JSON 的檢視。Phoenix Controller 將範本的渲染交給檢視模組進行,而且是以格式為依據進行交辦。我們已經有一個用於 HTML 格式的檢視,但我們還需要指示 Phoenix 如何渲染 JSON 格式。預設情況下,你可以從 lib/hello_web.ex 中看到你的控制器支援哪些格式。

  def controller do
    quote do
      use Phoenix.Controller,
        formats: [:html, :json],
        layouts: [html: HelloWeb.Layouts]
      ...
    end
  end

因此,預設情況下,Phoenix 會根據要求格式和控制器名稱來尋找 HTMLJSON 檢視模組。我們也可以在控制器中明確地告訴 Phoenix,針對每個格式要使用哪一個檢視。例如,Phoenix 預設執行的作業可以用以下控制器中的動作來明確設定。

plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON

我們將 PageJSON 檢視模組新增至 lib/hello_web/controllers/page_json.ex

defmodule HelloWeb.PageJSON do
  def home(_assigns) do
    %{message: "this is some JSON"}
  end
end

由於 Phoenix 檢視層只是一個由控制器用來渲染的函式,會傳遞連線指定,我們可以定義一個常規的 home/1 函式,並傳回要序列化為 JSON 的對應。

我們只需要再做一些事來讓這項工作生效。由於我們想要從同一個控制器渲染 HTML 和 JSON,我們需要告訴路由器它應該接受 json 格式。我們可以將 json 新增至 :browser 管線中已接受格式清單中來達成目的。我們開啟 lib/hello_web/router.ex,並將 plug :accepts 變更為如下,讓它包含 jsonhtml

defmodule HelloWeb.Router do
  use HelloWeb, :router

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

Phoenix 允許我們用 _format 查詢字串參數動態變更格式。如果我們前往 https://127.0.0.1:4000/?_format=json,我們將會看到 %{"message": "this is some JSON"}

實際上,需要同時呈現兩種格式的應用程式通常會為每種格式使用兩個不同的管線,例如路由器檔案中已定義的 pipeline :api。如要了解更多資訊,請參閱 JSON and APIs 指南

直接傳送回應

如果以上顯示選項都不符合我們的需求,我們可以使用 Plug 提供的部分函式來自行撰寫。假設我們要傳送回應,狀態為「201」,且無任何內文。我們可以使用 Plug.Conn.send_resp/3 函式來執行這項作業。

lib/hello_web/controllers/page_controller.ex 中編輯 PageControllerhome 操作,使其如下所示:

def home(conn, _params) do
  send_resp(conn, 201, "")
end

重新載入 https://127.0.0.1:4000 應會顯示一個完全空白的頁面。瀏覽器開發人員工具的網路索引標籤應會顯示「201」(已建立)的回應狀態。由於未設定內容類型,有些瀏覽器(例如 Safari)會下載回應。

如要明確說明內容類型,我們可以使用 put_resp_content_type/2 結合 send_resp/3

def home(conn, _params) do
  conn
  |> put_resp_content_type("text/plain")
  |> send_resp(201, "")
end

以這種方式使用 Plug 函式,我們就能建立符合需求的回應。

設定內容類型

類似 _format 查詢字串參數,我們可以透過修改 HTTP 內容類型標頭並提供適當的範本來呈現我們想要的任何格式。

如果我們想呈現 home 操作的 XML 版本,我們可以在 lib/hello_web/page_controller.ex 中如此實作這個操作。

def home(conn, _params) do
  conn
  |> put_resp_content_type("text/xml")
  |> render(:home, content: some_xml_content)
end

然後,我們需要提供一個會建立有效 XML 的 home.xml.eex 範本,這樣就完成了。

有關有效內容 MIME 類型的清單,請參閱 MIME 函式庫。

設定 HTTP 狀態

我們還可以設定回應的 HTTP 狀態碼,其方式與設定內容類型的方式類似。Plug.Conn 模組(已匯入所有控制器中)有一個 put_status/2 函式可以用來執行這項作業。

Plug.Conn.put_status/2 使用 conn 作為第一個參數,並將第二個參數設定為整數或「友善名稱」,而「友善名稱」會用作我們要設定的狀態碼的原子。狀態碼原子表示清單可以在 Plug.Conn.Status.code/1 文件中找到。

讓我們在 PageControllerhome 動作中變更狀態。

def home(conn, _params) do
  conn
  |> put_status(202)
  |> render(:home, layout: false)
end

我們提供的狀態碼必須是有效數字。

重新導向

通常,我們需要在要求中段重新導向至新的 URL。舉例來說,成功的 create 動作,通常會重新導向至我們剛剛建立的資源的 show 動作。或者,它可以重新導向至 index 動作以顯示相同類型的東西。還有許多其他案例,重新導向也很有用。

無論文況為何,Phoenix 控制器提供便利的 redirect/2 函數來簡化重新導向。Phoenix 區分重新導向至應用程式內的路徑,以及重新導向至 URL,無論是在應用程式內或是外部。

為嘗試使用 redirect/2,讓我們在 lib/hello_web/router.ex 中建立一個新路徑。

defmodule HelloWeb.Router do
  ...

  scope "/", HelloWeb do
    ...
    get "/", PageController, :home
    get "/redirect_test", PageController, :redirect_test
    ...
  end
end

接著,我們將變更 PageController 控制器中 home 動作,僅執行重新導向至新路徑的操作。

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  def home(conn, _params) do
    redirect(conn, to: ~p"/redirect_test")
  end
end

我們使用 Phoenix.VerifiedRoutes.sigil_p/2 來建構我們的重新導向路徑,這是參考應用程式中任何路徑的首選方式。我們在 路由指引 中了解了已驗證路徑。

最後,我們在同一個檔案中定義我們重新導向到的動作,它僅呈現首頁,但現在使用新的位址

def redirect_test(conn, _params) do
  render(conn, :home, layout: false)
end

當我們重新載入 歡迎頁面 時,我們會發現我們已被重新導向至 /redirect_test,其中顯示了原始的歡迎頁面。這樣做成功了!

如果我們有空,我們可以開啟開發人員工具,按一下網路標籤,並再次造訪我們的根路徑。我們會看到這個頁面有兩個主要要求:一個 / 的 GET,狀態為 302,另一個 /redirect_test 的 GET,狀態為 200

請注意,重新導向函數將 conn 視為應用程式內部的相對路徑字串。基於安全性考量,:to 選項只能重新導向至應用程式內的路徑。如果您想要重新導向至完全限定的路徑或外部 URL,您應該改用 :external

def home(conn, _params) do
  redirect(conn, external: "https://elixir.dev.org.tw/")
end

快閃訊息

有時候,我們需要在動作過程中與使用者溝通。也許有更新架構的錯誤,或者我們只是想再次歡迎他們回到應用程式。正因為這樣,我們有了快閃訊息。

Phoenix.Controller 模組提供 put_flash/3 來設定快閃訊息,作為一個金鑰值對,並將它們置入連接中的 @flash 指派中。我們在 HelloWeb.PageController 中設定兩個快閃訊息來試試看。

為此,我們修改 home 動作如下

defmodule HelloWeb.PageController do
  ...
  def home(conn, _params) do
    conn
    |> put_flash(:error, "Let's pretend we have an error.")
    |> render(:home, layout: false)
  end
end

為了看到我們的快閃訊息,我們需要能夠在範本佈局中擷取並顯示它們。我們可以使用 Phoenix.Flash.get/2 來做到這一點,它會擷取快閃資料和我們關心的金鑰。然後,它會傳回該金鑰的值。

為了我們的方便,一個 flash_group 組件已經可用,並加到我們的 歡迎頁面 開頭處

<.flash_group flash={@flash} />

當我們重新載入 歡迎頁面 時,我們的訊息應該會出現在頁面的右上角。

快閃功能在混用重新導向時很方便。或許您想要重新導向到一個包含一些額外資訊的頁面。如果我們重新使用前一節的重新導向動作,我們可以執行

  def home(conn, _params) do
    conn
    |> put_flash(:error, "Let's pretend we have an error.")
    |> redirect(to: ~p"/redirect_test")
  end

現在,如果您重新載入 歡迎頁面,您將會被重新導向,而且快閃訊息會再一次被顯示。

除了 put_flash/3 之外,Phoenix.Controller 模組還有另一個有用的函式值得認識。clear_flash/1 只接收 conn,並移除任何可能儲存在會話中的快閃訊息。

Phoenix 沒有強制要儲存在快閃中的金鑰。只要我們內部是一致的,一切都會很好。:info:error 卻很常見,而且預設會在我們的範本中處理。

錯誤頁面

Phoenix 有兩個稱為 ErrorHTMLErrorJSON 的檢視,它們存在於 lib/hello_web/controllers/ 中。這些檢視的目的是以一般的途徑來處理來自 HTML 或 JSON 要求的錯誤。與我們在本指南中建立的檢視類似,錯誤檢視可以傳回 HTML 和 JSON 回應。請參閱 自訂錯誤頁面的操作指南 來取得更多資訊。