iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 21
1
Software Development

函數式編程: 從 Elixir & Phoenix 入門。系列 第 21

沒有很 thin 的 Controller

前情提要

經由 Endpoint 的 pipeline 處理,並依 URI 進行分派之後,connection 將會傳到的 Controller 的個別函式中。我們把這些函式稱為 action,因為他們分別對應了 CRUD 的其中一個行為。

試著把 scope 及 resource 的結果展開一條來當範例,當我們在 Router 中寫了這行 :

get "/post", PostController, :index

mix phx.routes 就會顯示:

post_path GET /posts HelloPhxWeb.PostController :index

而符合這個 URI 的 HTTP 請求,在通過 Endpoint 之後,就會將 connection 傳到 PostControllerindex 函式。

Controller 的職責

與 Rails 倡導的 fat model, thin controller 不同,在 Phoenix Controller 的每個 action 裡,會主動調用各個 Context 的取值與寫值函式,並在蒐集完資料後,算繪 (render) 出最終的 HTML 或是 JSON 回應,存入 connection 後回傳。簡而言之,Phoenix 的 Controller 是負責存取資料及分發的流程控制中心

進行到這一篇,當我說 Phoenix 的 Controller 其實也是個 plug 時,你應該已經不會覺得太訝異了。

Controller 裡的每個 action 函式,也符合 plug 的規範,接收兩個參數並回傳一個新的 connection。第一個參數當然是我們反覆提到的 connection,%Plug.Conn{}。而第二個參數,則是 HTTP 請求時所帶上的參數 (雖然這些值在 connection 裡也翻得到),會以一個用字串當 key 的 Map 傳入。

Action 解析

我們來看一下 PostController 裡的幾個重點函式。

index

def index(conn, _params) do
  posts = Blog.list_posts()
  render(conn, "index.html", posts: posts)
end

由於用不到 URI 傳入的參數 (根本沒有什麼參數吧),用上了 _params 表示出我們的不在乎。在函式本體的第一行,調用了 Blog context 的 list_posts 取得 post 清單的資料。接著用 connection、template 名稱 "index.html" 及取得的清單做為參數,呼叫 render 算繪 HTML 內容並回傳。

new

def new(conn, _params) do
  changeset = Blog.change_post(%Post{})
  render(conn, "new.html", changeset: changeset)
end

new 用來顯示想新增 post 時要填的空白表格,因此我們也不在乎傳入的 URI 參數。在用 Blog.change_post(%Post{}) 做出一個空白的 changeset* 之後,以 connection,template 名稱 "new.html" 及 changeset 做為參數呼叫 render/3 來算繪顯示的表格頁面。

create

def create(conn, %{"post" => post_params}) do
  case Blog.create_post(post_params) do
    {:ok, post} ->
      conn
      |> put_flash(:info, "Post created successfully.")
      |> redirect(to: post_path(conn, :show, post))
    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

這個 action 需要將新的 post 資料寫入資料庫中。參數裡的 post_params 會給我們已經整理好的資料,所以這裡調用了 Blog.create_post 來保存資料。

我們用 case 來比對保存行為的結果是成功的 {:ok, post} 或是失敗的 {:error, changeset}。如果是成功的話,用 put_flash 放入新增成功的提示訊息,並重導到已建好的 post 頁面。如果是失敗的情況,則用帶有錯誤訊息的 changeset 資料來重新顯示新增 post 表格頁面。

edit 及 update

def edit(conn, %{"id" => id}) do
  post = Blog.get_post!(id)
  changeset = Blog.change_post(post)
  render(conn, "edit.html", post: post, changeset: changeset)
end

def update(conn, %{"id" => id, "post" => post_params}) do
  post = Blog.get_post!(id)

  case Blog.update_post(post, post_params) do
    {:ok, post} ->
      conn
      |> put_flash(:info, "Post updated successfully.")
      |> redirect(to: post_path(conn, :show, post))
    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "edit.html", post: post, changeset: changeset)
  end
end

概念與 new -> create 相同,是用來更新已保存的資料。所以當顯示 edit 表格時,我們需要想更正的資料的 id 做為參數,並取出資料庫相應的記錄來顯示表格。而 update 則與 create 相仿,試著將將自表格頁面傳來的新內容寫入資料庫中。

delete

def delete(conn, %{"id" => id}) do
  post = Blog.get_post!(id)
  {:ok, _post} = Blog.delete_post(post)

  conn
  |> put_flash(:info, "Post deleted successfully.")
  |> redirect(to: post_path(conn, :index))
end

刪除 post 就只需要該筆記錄的 id,成功之後將刪除成功的訊息放入 connection,並重導到 index 頁面。

注意在這裡如果刪除失敗時,會導致 MatchError,所以如果你的應用程式變得較複雜,有可能發生刪除失敗的情況,需要改寫這段以防止應用程式錯誤。

小結

歸納來看,在 Controller 的各個 action 裡,會依序發生這些事:

  1. 取得 connection 及參數
  2. 進一步蒐集更多資料
  3. 呼叫 Context 函式更新資料庫 (create, update, delete)
  4. 算繪回傳的結果或重導頁面

那個 before_action 呢?

Rails 框架的 controller 有提供 before_action、after_action 及 around_action,用於宣告此 Controller 每個 action 都需要進行的前置及後置處理。

仔細想一下,我們之前在 Router 裡好像有個很類似的概念,叫做 pipeline。而既然 Controller 也是個 plug,所以他也有自己的 pipeline。但與 Router 的 pipeline 不同的有兩點: 1. 一個 controller 只有一條 pipeline。2. 可以使用 guard

用起來大概像是這樣:

defmodule UserController do
    use MyAppWeb, :controller

    plug :authenticate, [usernames: ["j", "e"]] when action in [:show, :edit]
    plug :authenticate, [usernames: ["admin"]] when not action in [:index]

   # actions

   defp authenticate(conn, options) do
     if get_session(conn, :username) in options[:usernames] do
      conn
     else
      conn |> redirect(to: "/") |> halt()
     end
   end
end

在 controller 的 pipeline,可以用於 guard 判斷的變數有 connactioncontroller (會拿到目前的 controller 名稱)。

但由於每個 action 都要主動控制流程,而不是像 Rails controller action 的方法本體可以只指派模組變數,Phoenix 的 Controller 沒有提供 after_action 及 around_action,而是在流程中適當的地方 pipe 過自行宣告的函式。

Controller 會做的其它事情

在後續的章節裡,將解釋 Controller 跟 Context、Schema 及 View 互動的細節,及 1.3 版新增的 Action Fallback 機制。除此之外,controller 還會做一些其它的事情:

重新導向

在剛才新增失敗的例子裡,就有看到這樣的用例:

conn
|> redirect(to: post_path(conn, :index))

redirect 接收的 keyword list 參數中,to: 的值會是重導向目標路徑的字串。而 post_path(conn, :index) 會回傳該字串。這部份的細節,請參考上一篇的 Path Helper 部份。

也可以用 external: 來重新導向到外部連結:

def index(conn, _params) do
  redirect conn, external: "https://elixir-lang.org/"
end

中止所有後續操作

當流程中有經過 halt/1 時,結束這個呼叫後,會中止所有後續的流程 (通常是 render),直接回傳 connection。大多用於處理錯誤發生的情況。

 conn
  |> send_resp(404, "Not found")
  |> halt()

除此之外,controller 還可以設定 content-type,HTTP status 及用 send_resp 直接回傳 HTTP 回應等等。詳細的說明也請參考 Phoenix.Controller 文件吧。

重點整理

  • Router 將處理過後的 connection 分配到 Controller 的 action 裡
  • Controller 是資料流程的控制中心
  • 明示優於隱含
  • Controller 也是個 plug,裡面的每個 action 也大致符合 plug 的定義
  • 七個 action 分別對應 CRUD 所需的行為
  • Controller 會主動與 Context 及 View 接洽
  • 用 plug 來處理 before_action 般的行為
  • Controller 需要決定是否重新導向或中止所有後續操作

下一篇要來解說 Controller 所呼叫的 Context 了。
Happy hacking! 明天見。


上一篇
Router.part_2
下一篇
再會, model
系列文
函數式編程: 從 Elixir & Phoenix 入門。31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言