之前幾次在 五倍紅寶石的 React 講座 裡,我都會這樣說:
人類在開發各式 Web 框架的過程中,意識到了其實 View 分為兩個部份,一是 view template,你想顯示的內容的樣板,另一個則是 view logic,用來處理樣板裡判斷與重覆 (迴圈) 等邏輯的部份。
之前的許多框架、包含 PHP、ASP、JSP 及 Rails 的 ERB 等等,都是以 template 為 View 的主體,並試著設計一些 template 的語法、讓它能做一些判斷與迴圈。不然就是會發展出像是 presenter、decorator 等模式,試圖把複雜的邏輯拆分出去。
但 React 不同,它是以 View logic ,也就是 JavaScript 程式碼為本體,用函數式編程的想法,把需要邏輯的部份拆成一個個的函式。在最上層的 render 函式中放最大塊的 template,並在需要的地方呼叫函式或嵌入變數。
這樣做的優秀之處,在於JavaScript 是圖靈完備的。
而 Phoenix 的 View,也是運用了同樣的概念。我們先來看之前產生出來的 post_view.ex
:
# lib/hello_phx_web/post_view.ex
defmodule HelloPhxWeb.PostView do
use HelloPhxWeb, :view
end
看起來什麼都沒有,那為什麼在 post_controller 裡可以 render(conn, "index.html", posts: posts)
呢?
我們來做個實驗,先在 post_view
裡加入一個函式,讓他變這樣:
# lib/hello_phx_web/post_view.ex
defmodule HelloPhxWeb.PostView do
use HelloPhxWeb, :view
def render("hello.html", assigns) do
"I'm just a function! I got #{inspect Map.key(assigns)}"
end
end
接著將 post_controller
的 index/2
函式改成這樣:
# lib/hello_phx_web/controllers/post_controller
# ...
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "hello.html", posts: posts) # 改第二個參數
end
# ...
接著用 mix phx.server
將應用程式跑起來,接著用瀏覽器打開 http://localhost:4000/posts
,就會看到這樣的畫面:
原來當我們在 controller 裡呼叫了 render(conn, “string.html”, opts)
時,他會去試著用第二個參數 “string.html”,去 pattern matching 同名 View 裡的每個 render/2
函式,若找到符合的,就回傳該函式呼叫的結果。
而既然 render/2
是個函式,那麼像是把裡面的複雜區塊拆出去變成回傳字串的小函式,把複雜邏輯的部份用高階函式處理等等,所有函式可以做的事它都可以照做。
不過在修改之前, post_controller
裡沒有定義任何函式。我們還是不知道那個 render(conn, “index.html”, assigns)
為什麼可以正常運作。看一下 lib/hello_phx_web/templates
資料夾:
lib/hello_phx_web/templates
├── layout
│ └── app.html.eex
├── page
│ └── index.html.eex
└── post
├── edit.html.eex
├── form.html.eex
├── index.html.eex
├── new.html.eex
└── show.html.eex
當 Phoenix 在編譯階段時,發現 templates
資料夾下的 檔名.html.eex
檔案,就會在對應的 View 裡預先編譯成 render("檔名.html", assigns)
的函式區塊,它的回傳值就是 template 檔案的內容。
所以在 Phoenix 的應用程式執行期,所有的 template 內容都會被載入記憶體裡,執行時不用去重新讀檔案,也就不需要複雜的檔案快取機制,重新組合 template 的各個小區塊等等,這麼一來,效能上會有極大的進步。
但相較之下,效能只是這個設計裡額外附帶的好處而己。最棒的地方,在於不需要去找套件或是自己組出 presenter 或是 decorator pattern,也不用跟團隊協調如何用一樣的方式,把塞了一大堆複雜邏輯的神妙 template 語法弄乾淨一點,新人加入團隊時,也不用重新訓練一次。View 就只是個函式,就用一般函式的方式把事做好。
當你在 template 裡寫了 <%= foo %>
時,會被視為 View 模組內的函式呼叫 foo()
,但是許多時候,我們希望能從 controller 直接指派變數讓 template 嵌入,或是在 template 裡取得目前 conn
的值。而這就是第二個參數 assign
派上用場的地方了。
當你在 controller 裡呼叫 render/3
時,最後一個參數 keyword list,可以在 template 用 @名稱
來取值。例如我們原本 post_controller
裡的 index/2
,我們在最後 render/3
傳入了 [posts: posts]
,那麼在 template 裡,就可以用 @posts
拿到 controller 給我們的所有 posts 資料。
而除了自行傳進去的值之外,每個 template 都還會拿到 @conn
、@view_model
及 @view_template
三個變數。
在 lib/hello_phx_web/view
裡,有一張 error_view.ex
,內容長這樣:
defmodule HelloPhxWeb.ErrorView do
use HelloPhxWeb, :view
def render("404.html", _assigns) do
"Page not found"
end
def render("500.html", _assigns) do
"Internal server error"
end
# In case no render clause matches or no
# template is found, let's render it as 500
def template_not_found(_template, assigns) do
render "500.html", assigns
end
end
不過我們先來試試一個錯誤的頁面,用瀏覽器打開 http://localhost:4000/not_exist
,咦,怎麼不是那個 “Page not found?”
其實是因為我們在開發模式下,Phoenix 會顯示內建的 debug 頁面幫助我們開發。但當我們要自訂網站的錯誤頁面時,就要先改一下設定,打開 config/dev.exs
,將設定改成這樣:
config :hello_phx, HelloPhxWeb.Endpoint,
http: [port: 4000],
debug_errors: false, # <= 改這行
code_reloader: true,
check_origin: false,
watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
cd: Path.expand("../assets", __DIR__)]]
更動設定後,要回到正在跑 mix phx.server
的 shell 裡重新啟動 server。重新整理瀏覽器後,終於看到了我們的簡單字串:
嗯,雖然成功了,但是這頁面實在太沒有誠意了。嗯…在你動手改寫函式之前請先等一等。要用純字串寫 HTML 得要考慮 escape 跟換行,相當麻煩啊。
我們有更好的選擇,刪掉 error_view.ex
裡的 render("404.html", _assigns)
函式那三行。然後建立 lib/hello_phx_web/templates/error
資料夾,並在裡面加一個 404.html.eex
檔案:
<%# lib/hello_phx_web/templates/error/404.html.eex %>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<div class="container">
<div class="jumbotron">
<p>網頁不存在!</p>
</div>
</div>
</body>
</html>
好的,接下來就是你發揮的空間了。
Elixir 的 template 語法模組叫 EEx,與 Ruby 的 erb 非常類似,都是用 <% %>
來括住函式呼叫,而用 <%= %>
來印出內容。但要注意的是如果要想印出內層表示式的內容時,外層也要使用 <%= %>
,例如在迴圈的情況下:
<%= for post <- @posts do %>
<tr>
<td><%= post.title %></td>
<td><%= post.content %></td>
<td><%= post.draft %></td>
</tr>
<% end %>
注意到那個 for
也有加上 =
了嗎?
Rails 做的最棒的,就屬各種好用的 view helper 了,像是表單的 form_for
、link_to
等等。而 Phoenix 基本上抄致敬了絕大多數的 Rails View Helper,甚至當你找不到 Phoenix 的範例時,直接把網路上查到 Rails View 裡的做法直接貼上去,都有很高的機率能正常運作。
詳細的功能,就請參考 Phoenix.HTML 模組的文件說明了。
<% foo %>
就是呼叫 View 模組裡定義的 foo/0
函式@
可以讓你在 template 裡取得 controller 傳進來的值,及 @conn
等等=
Phoenix.HTML
裡有一大堆像是 Rails View Helpers 的東西~~,相似度高達 87成~~下一篇要來介紹 Phoenix 裡的好東西:文件與測試。(我是認真的)
Happy hacking! 明天見。