首先 create 新的 project rumbl
$ mix phoenix.new rumbl --database mysql
修改 config/dev.exs 的 DB 設定,然後 create database
$ mix ecto.create
啟動 server
mix phoenix.server
或是
iex -S mix phoenix.server
調整首頁 /web/templates/page/index.html.eex
<div class="jumbotron">
<h2>Welcome to Rumbl.io</h2>
<p class="lead">Rumble out loud.</p>
</div>
Create User
系統通常會以 User 開發為起點。首先定義 Rumbl.User module,包含欄位 id, name, username, password
/web/models/user.ex
defmodule Rumbl.User do
defstruct [:id, :name, :username, :password]
end
為什麼要使用 struct 而不是 map 定義 User model?
使用 map,在撰寫 model 定義,並不會在編譯時發現有打錯字的狀況,另外也不能為所有欄位都填上預設值。
在 project folder 直接測試
$ iex -S mix
Generated rumbl app
# 可以使用 User
iex(1)> alias Rumbl.User
Rumbl.User
# 以 Map 定義 user
iex(2)> user = %{usernmae: "jose", password: "elixir"}
%{password: "elixir", usernmae: "jose"}
# 因為剛剛定義 user map 時,把 username 打錯了
iex(3)> user.username
** (KeyError) key :username not found in: %{password: "elixir", usernmae: "jose"}
# 如果用 User struct 就不會發生這個問題,還將其他欄位都填上預設值 nil
iex(3)> jose = %User{name: "Jose"}
%Rumbl.User{id: nil, name: "Jose", password: nil, username: nil}
iex(4)> jose.name
"Jose"
# 編譯時就會檢查欄位名稱
iex(5)> chris = %User{nmae: "chris"}
** (KeyError) key :nmae not found in: %Rumbl.User{id: nil, name: nil, password: nil, username: nil} ....
# struct 就跟 map 一樣,差別只在於 struct 是一個擁有 __struct__ key 的 map
iex(5)> jose.__struct__
Rumbl.User
Working with Repositories
Repository 是一組儲存資料的 API,可快速建立測試資料的 data interface,待完成 view, template 後,就可以替換為完整的 database-backend repository。換句話說,可以將 data model 跟 database 完全分離。
修改 /lib/rumbl/repo.ex
defmodule Rumbl.Repo do
# use Ecto.Repo, otp_app: :rumbl
# 所有 User 的資料
def all(Rumbl.User) do
[
%Rumbl.User{id: "1", name: "Jose", username: "josie", password: "elixir"},
%Rumbl.User{id: "2", name: "Bruce", username: "bruce", password: "pass"},
%Rumbl.User{id: "3", name: "Chris", username: "chris", password: "phx"}
]
end
def all(_module), do: []
# get user by id
def get(module, id) do
Enum.find all(module), fn map -> map.id == id end
end
# get user by a custom attribute
def get_by(module, params) do
Enum.find all(module), fn map ->
Enum.all?(params, fn {key, val} -> Map.get(map, key) == val end)
end
end
end
將 /lib/rumbl.ex,關掉跟 Ecto 相關的 supervisor
# supervisor(Rumbl.Repo, []),
檢查 User 的測試資料庫
$ iex -S mix
iex(1)> alias Rumbl.User
Rumbl.User
iex(2)> alias Rumbl.Repo
Rumbl.Repo
iex(3)> Repo.all User
[%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"},
%Rumbl.User{id: "2", name: "Bruce", password: "pass", username: "bruce"},
%Rumbl.User{id: "3", name: "Chris", password: "phx", username: "chris"}]
iex(4)> Repo.all Rumbl.Other
[]
iex(5)> Repo.get User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}
iex(6)> Repo.get_by User, name: "Bruce"
%Rumbl.User{id: "2", name: "Bruce", password: "pass", username: "bruce"}
撰寫 Controller & View
修改 /web/router.ex,將 /users 及 /users/:id 對應到 UserController 的 :index 及 :show 兩個 functions
scope "/", Rumbl do
pipe_through :browser # Use the default browser stack
get "/users", UserController, :index
get "/users/:id", UserController, :show
get "/", PageController, :index
end
新增 /web/controllers/user_controller.ex
defmodule Rumbl.UserController do
use Rumbl.Web, :controller
def index(conn, _params) do
users = Repo.all(Rumbl.User)
render conn, "index.html", users: users
end
end
/web/views/user_view.ex
defmodule Rumbl.UserView do
use Rumbl.Web, :view
alias Rumbl.User
def first_name(%User{name: name}) do
name
|> String.split(" ")
|> Enum.at(0)
end
end
/web/templates/user/index.html.eex
<h1>Listing Users</h1>
<table class="table">
<%= for user <- @users do %>
<tr>
<td><b><%= first_name(user) %></b> (<%= user.id %>)</td>
<td><%= link "View", to: user_path(@conn, :show, user.id) %></td>
</tr>
<% end %>
</table>
因為剛剛有修改 /lib/rumbl/Repo.ex,因為 lib 目錄更新程式後不會自動 reload,記得要重新啟動 phoenix.server,修改後的程式才會有作用。
mix phoenix.server
Using Helpers
link function 封裝了很多有用的 functions,可用來處理很多 HTML structures。
link 的第二個參數是 keyword list
iex(1)> Phoenix.HTML.Link.link("Home", to: "/")
{:safe, [60, "a", [[32, "href", 61, 34, "/", 34]], 62, "Home", 60, 47, "a", 62]}
iex(2)> Phoenix.HTML.Link.link("Delete", to: "/", method: "delete")
{:safe,
[60, "a",
[[32, "data-csrf", 61, 34,
"DSMgIAMOCzQqQTczRR0/ElEUFDIJJgAA8KZynKSYp9CZ0/MYaNCwDA==", 34],
[32, "data-method", 61, 34, "delete", 34], [32, "data-to", 61, 34, "/", 34],
[32, "href", 61, 34, "#", 34], [32, "rel", 61, 34, "nofollow", 34]], 62,
"Delete", 60, 47, "a", 62]}
HTML helper 會放在每個 view 的最上層,Phoenix.HTML 負責處理 HTML functions in views
/web/web.ex 裡面 view 的區塊內容為
def view do
quote do
use Phoenix.View, root: "web/templates"
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import Rumbl.Router.Helpers
import Rumbl.ErrorHelpers
import Rumbl.Gettext
end
end
ref: Phoenix.HTML Helpers for working with HTML strings and templates.
不能把自己做的 function 直接寫在 web.ex,要用 import 的方式處理
Showing a User
剛剛在 /web/router.ex 有定義這個 url route
get "/users/:id", UserController, :show
修改 /web/controllers/user_controller.ex,增加 show function,注意參數的部分為 %{"id" => id}
,這是從網址來的
defmodule Rumbl.UserController do
use Rumbl.Web, :controller
def index(conn, _params) do
users = Repo.all(Rumbl.User)
render conn, "index.html", users: users
end
def show(conn, %{"id" => id}) do
user = Repo.get(Rumbl.User, id)
render conn, "show.html", user: user
end
end
新增 /web/templates/user/show.html.eex
<h1>Showing User (<%= @user.id %>)</h1>
<b><%= first_name(@user) %></b> (<%= @user.id %>)'s username is <b><%= @user.username %></b>
Naming Conventions
當 Phoenix 在 controller 要 render templates 時,他會使用 controler module 名稱 Rumbl.UserController 參考到 view module 的名稱為 Rumbl.UserView,template 目錄在 /web/templates/user/。
在後面可知道如何 customize 這些 conventions
Nesting Templates,共用 template
新增 /web/templates/user/user.html.eex
<b><%= first_name(@user) %></b> (<%= @user.id %>)'s username is <b><%= @user.username %></b>
修改 /web/templates/user/show.html.eex
<h1>Showing User (<%= @user.id %>)</h1>
<%= render "user.html", user: @user %>
修改 /web/templates/user/index.html.eex
<h1>Listing Users</h1>
<table class="table">
<%= for user <- @users do %>
<tr>
<td><%= render "user.html", user: user %></td>
<td><%= link "View", to: user_path(@conn, :show, user.id) %></td>
</tr>
<% end %>
</table>
<%= render "user.html", user: user %>
這就是共用的部分
以 iex -S mix
測試 view
$ iex -S mix
iex(1)> user = Rumbl.Repo get Rumbl.User, "1"
** (SyntaxError) iex:1: syntax error before: get
iex(1)> user = Rumbl.Repo.get Rumbl.User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}
iex(2)> view = Rumbl.UserView.render("user.html", user: user)
{:safe,
[[[[[[["" | "<b>"] | "Jose"] | "</b> ("] | "1"] | ")'s username is <b>"] |
"josie"] | "</b>"]}
iex(3)> Phoenix.HTML.safe_to_string(view)
"<b>Jose</b> (1)'s username is <b>josie</b>"
每個 template 最後都會變成 render(template_name, assigns),因此 rendering template 就是 template name 及 function 的 pattern matching。
我們可以跳過整個 template 機制,自訂自己的 render clause,這也是 Rumbl.ErrorView 提供的自訂錯誤頁面的方式。
/web/views/error_view.ex
defmodule Rumbl.ErrorView do
use Rumbl.Web, :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
Phoenix.View module 也提供了 render view 的 functions,包含 a function to render 及 轉換 rendered template 為 string的 function。
Phoenix.View 會呼叫 view 的 render functions。
iex(5)> user = Rumbl.Repo.get Rumbl.User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}
iex(6)> Phoenix.View.render(Rumbl.UserView, "user.html", user: user)
{:safe,
[[[[[[["" | "<b>"] | "Jose"] | "</b> ("] | "1"] | ")'s username is <b>"] |
"josie"] | "</b>"]}
iex(7)> Phoenix.View.render_to_string(Rumbl.UserView, "user.html", user: user)
"<b>Jose</b> (1)'s username is <b>josie</b>"
Layouts
在 controller 呼叫 render 時,controller 會先 render the layout view,然後再依照 predefined markup 去 render the actual template
因為 layout 也是 view with templates,每個 template 會收到一些特定的變數,
查看 /web/templates/layout/app.html.eex 的內容有一段 <%= render @view_module, @view_template, assigns %>
1
layouts 就是 HTML templates that embed an action’s HTML
<body>
<div class="container">
<header class="header">
<nav role="navigation">
<ul class="nav nav-pills pull-right">
<li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
</ul>
</nav>
<span class="logo"></span>
</header>
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<main role="main">
<%= render @view_module, @view_template, assigns %>
</main>
</div> <!-- /container -->
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
沒有留言:
張貼留言