Phoenix views有着两个主要工作。第一,渲染模板(包括layouts)。渲染时调用的核心函数render/3
,就是定义于Phoenix.View
模块中的。views也提供一些函数,能获取raw数据,并让其更容易被模板使用。如果你很熟悉decorators或facade模式,这很类似。
Phoenix遵从一个强大的命名惯例,从控制器到views到它们渲染的模板。PageController
要求一个PageView
去渲染web/templates/page
目录中的模板。
如果我们愿意,可以修改Phoenix认定的模板根目录。Phoenix在web/web.ex
中定义的HelloPhoenix.Web
模块中提供了一个view/0
函数。它的第一行允许我们通过修改:root
键的值来改变我们的根目录。
一个新创建的Phoenix应用有三个view模块 - ErrorView
, LayoutView
, 和 PageView
- 它们都在web/views
目录中。
让我们来看一下LayoutView
。
defmodule HelloPhoenix.LayoutView do
use HelloPhoenix.Web, :view
end
它够简单了。只有一行, use HelloPhoenix.Web, :view
。这一行调用了我们刚才看到的view/0
函数。除了让我们改变模板根目录,view/0
还让Phoenix.View
模块中的__using__
宏发挥了作用。它也可以处理任何模块import,或alias我们应用的view模块。
在教程的开头,我们提到views是用来存放在模板中要使用的函数的地方。让我们来试试看。
让我们打开我们应用的layout模板,templates/layout/app.html.eex
,改变这一行,
<title>Hello Phoenix!</title>
到调用title/0
函数,像这样。
<title><%= title %></title>
现在为我们的LayoutView
添加一个title/0
函数。
defmodule HelloPhoenix.LayoutView do
use HelloPhoenix.Web, :view
def title do
"Awesome New Title!"
end
end
当重载欢迎页面,我们会看到新标题。
<%=
和 %>
来自于ElixirEEx 项目。它们将Elixir代码嵌入到模板中。
注意我们不需要用HelloPhoenix.LayoutView
来全名调用title/0
,因为正是LayoutView
在进行渲染。
当我们use HelloPhoenix.Web, :view
,我们可以得到其它的方便。由于view/0
函数import了HelloPhoenix.Router.Helpers
,我们不需要完整写出path helpers在模板中。让我们看看它是如何为我们的欢迎页面改变模板的。
让我们打开templates/page/index.html.eex
,定位到这一节。
<div class="jumbotron">
<h2>Welcome to Phoenix!</h2>
<p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
</div>
让我们添加一行带有一个到本页的链接。(作用是查看path helpers 在模板中如何响应,并不添加任何功能。)
<div class="jumbotron">
<h2>Welcome to Phoenix!</h2>
<p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
<p><a href="<%= page_path @conn, :index %>">Link back to this page</a></p>
</div>
现在我们可以重载页面并看看我们有了怎样的源文件。
<a href="/">Link back to this page</a>
很好,page_path/2
返回了/
,和我们预期一样,而且我们不需要用HelloPhoenix.View
来修饰它。
关于Views的更多
你也许想知道views是如何能够这样近地操作模板的。
Phoenix.View
模块获得了访问模板行为的途径,是通过use Phoenix.Template
这一行中的__using__/1
宏。Phoenix.Template
提供了很多处理模板的便捷操作 - 搜索,提取名字和路径,等等。
让我们做个小实验,对web/views/page_view.ex
。我们会添加一个message/0
函数到其中。
defmodule HelloPhoenix.PageView do
use HelloPhoenix.Web, :view
def message do
"Hello from the view!"
end
end
现在让我们创建一个新模板,web/templates/page/test.html.eex
。
This is the message: <%= message %>
我们可以在iex中渲染它,运行iex -S mix
。
iex(1)> Phoenix.View.render(HelloPhoenix.PageView, "test.html", %{})
{:safe, [["" | "This is the message: "] | "Hello from the view!"]}
如你所见,我们调用了render/3
,伴随着单独的view负责我们的测试模板,测试模板的名字,和一个空映射代表着我们想要传送的任何数据。
返回值是一个元组,以原子:safe
开头,后面是插值过的模板执行的结果字符串。
这里的“Safe”意思是Phoenix已经escape了我们渲染的模板的内容。Phoenix定义了它自己的Phoenix.HTML.Safe
协议,包含对原子,位串,列表,整数,浮点数,和元组的实现,来为我们处理这个escape,当我们的模板被渲染成字符串时。
如果我们给render/3
的第三个参数赋一些键值对,会发生什么?我们需要小小地修改一下模板。
I came from assigns: <%= @message %>
This is the message: <%= message %>
注意@
,现在如果我们改变函数调用,我们会在重新编译PageView
模块之后看到不同的渲染。
iex(2)> r HelloPhoenix.PageView
web/views/page_view.ex:1: warning: redefining module HelloPhoenix.PageView
{:reloaded, HelloPhoenix.PageView, [HelloPhoenix.PageView]}
iex(3)> Phoenix.View.render(HelloPhoenix.PageView, "test.html", message: "Assigns has an @.")
{:safe,
[[[["" | "I came from assigns: "] | "Assigns has an @."] |
"\nThis is the message: "] | "Hello from the view!"]}
让我们测试一下HTML escaping。
iex(4)> Phoenix.View.render(HelloPhoenix.PageView, "test.html", message: "<script>badThings();</script>")
{:safe,
[[[["" | "I came from assigns: "] |
"<script>badThings();</script>"] |
"\nThis is the message: "] | "Hello from the view!"]}
如果我们只需要渲染字符串,而不是整个元组,我们可以使用render_to_iodata/3
。
iex(5)> Phoenix.View.render_to_iodata(HelloPhoenix.PageView, "test.html", message: "Assigns has an @.")
[[[["" | "I came from assigns: "] | "Assigns has an @."] |
"\nThis is the message: "] | "Hello from the view!"]
Layouts 杂谈
Layouts只是模板,和其它模板一样,有view。在刚生成的app中,它是web/views/layout_view.ex
。你可能想知道字符串是如何从一个layout中的渲染过的view中生成的。这是个好问题!
在web/templates/layout/app.html.eex
中,大约在<body>
中间的地方,我们会看到。
<%= render @view_module, @view_template, assigns %>
这就是从控制器中来的view模块和它的模板被渲染成字符串并存放在layout中的地方。
ErrorView
Phoenix有一个叫做ErrorView
的view,在web/views/error_view.ex
。它的目的是处理两个最常见的错误 - 404 not found
和 500 internal error
- 通常是由中央位置发出的。让我们来看看。
defmodule HelloPhoenix.ErrorView do
use HelloPhoenix.Web, :view
def render("404.html", _assigns) do
"Page not found"
end
def render("500.html", _assigns) do
"Server internal 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
首先,让我们到浏览器中看看404 not found
信息长什么样。在开发环境下,Phoenix会默认调试错误,给我们一个非常消息化的调试页面。我们要看的是这个页面在生产环境下的样子。所以我们需要在config/dev.exs
中设置debug_errors: false
。
use Mix.Config
config :hello_phoenix, HelloPhoenix.Endpoint,
http: [port: 4000],
debug_errors: false,
code_reloader: true,
. . .
改动完配置文件后,我们需要重启服务器来应用改动。重启后,打开http://localhost:4000/such/a/wrong/path 看看发生了什么。
好吧,并不exciting。我们得到了裸露的字符串"Page not found",没有任何装饰与风格。
让我们想象如何让错误页面变有趣。
第一个问题是,这个字符串从哪来的?答案是ErrorView
。
def render("404.html", _assigns) do
"Page not found"
end
好的,我们有一个render/2
,参数是一个模板和一个assigns
映射,我们忽略了它。这个render/2
函数在那里被调用?
答案是在Phoenix.Endpoint.RenderErrors
模块中定义的render/5
函数中。这个模块唯一的目的就是捕获错误,并用一个view渲染它们,在这里,是HelloPhoenix.ErrorView
。
现在,让我们做一个更好的错误页面吧。
Phoenix为我们生成了ErrorView
,但没有给我们一个web/templates/error
目录。让我们创建一个。在目录内,创建一个模板,not_found.html.eex
,并给它一些装饰 - 混合了我们的应用layout和一个带有我们给用户的信息的新div
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Welcome to Phoenix!</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<div class="container">
<div class="header">
<ul class="nav nav-pills pull-right">
<li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
</ul>
<span class="logo"></span>
</div>
<div class="jumbotron">
<p>Sorry, the page you are looking for does not exist.</p>
</div>
<div class="footer">
<p><a href="http://phoenixframework.org">phoenixframework.org</a></p>
</div>
</div> <!-- /container -->
<script src="/js/app.js"></script>
</body>
</html>
现在我们可以使用刚才看到的render/2
函数做个试验。
我们的render/2
函数现在应该是这样。
def render("404.html", _assigns) do
render("not_found.html", %{})
end
返回http://localhost:4000/such/a/wrong/path ,我们会看到一个好得多的错误页面。
如果我们不通过应用的layout来渲染not_found.html.eex
模板,那就毫无意义。因为我们希望错误页面和网站的其他部分保持一致。主要原因是全局化地处理错误很容易导致边界问题。
如果我们想最小化我们应用的layout和not_found.html.eex
模板之间的重复,我们可以共用头部和尾部的模板。请到Template Guide 获取更多信息。
当然,我们可以对用ErrorView
中的def render("500.html", _assigns) do
从句做同样的步骤 。
我们也可以使用传送给ErrorView
中任何render/2
从句的assigns
映射,而不是弃用它,为了在模板中显示更多信息。
渲染JSON
view的工作除了渲染模板,还要渲染JSON。Phoenix使用Poison 来将Maps编码成JSON,所以我们需要做的就是在views中将我们需要响应的数据用Map格式表示,然后Phoenix会完成剩下的工作。
直接从控制器中跳过View,返回JSON作为响应,是可能的。如果我们认为控制器有责任接受请求,并获取需要返回的数据,那就包含数据操作和格式化。而一个view是负责格式化和数据操作的模块。
让我们进入PageController
,看看当我们以JSON形态的静态页面映射作为响应,而非HTML,时,会发生什么。
defmodule HelloPhoenix.PageController do
use HelloPhoenix.Web, :controller
def show(conn, _params) do
page = %{title: "foo"}
render conn, "show.json", page: page
end
def index(conn, _params) do
pages = [%{title: "foo"}, %{title: "bar"}]
render conn, "index.json", pages: pages
end
end
这里,我们让show/2
和 index/2
返回静态页面数据。我们用"show.json"
替代了"show.html"
,作为模板名,传递给render/2
。这样,我们就可以让负责渲染HTML和JSON的view模式匹配到不同的文件类型。
defmodule HelloPhoenix.PageView do
use HelloPhoenix.Web, :view
def render("index.json", %{pages: pages}) do
%{data: render_many(pages, HelloPhoenix.PageView, "show.json")}
end
def render("show.json", %{page: page}) do
%{data: render_one(page, HelloPhoenix.PageView, "page.json")}
end
def render("page.json", %{page: page}) do
%{title: page.title}
end
end
在view中,我们看到render/2
函数模式匹配"index.json"
, "show.json"
,和"page.json"
。在我们控制器中的show/2
函数,render conn, "show.json", page: page
会模式匹配成功,然后render/3
函数中延伸。
也就是说,render conn, "index.json", pages: pages
将会调用render("index.json", %{pages: pages})
。
render_many/3
函数的参数是我们想要返回的数据(pages
),一个View
,以及一个用于模式匹配View
中所定义的render/3
函数的字符串。它会映射pages
中所有元素,并将它们传送给View
中与文件字符串相匹配的render/3
函数。
render_one/3
也是一样的。
render/3
匹配了 "index.json"
,会以你所期望的JSON作为回应:
{
"data": [
{
"title": "foo"
},
{
"title": "bar"
},
]
}
而匹配"show.json"
的render/3
:
{
"data": {
"title": "foo"
}
}
以这种方式构建views很有用,因为它们可以被组合。假设我们的Page
与Author
有一个has_many
的关系,依照请求,我们可能想要使用page
来返回author
数据,我们可以使用新的render/3
轻松完成它:
defmodule HelloPhoenix.PageView do
use HelloPhoenix.Web, :view
def render("page_with_authors.json", %{page: page}) do
%{title: page.title,
authors: render_many(page.authors, HelloPhoenix.AuthorView, "show.json")}
end
def render("page.json", %{page: page}) do
%{title: page.title}
end
end