Phoenix官方教程 (六) view

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: "] |
     "&lt;script&gt;badThings();&lt;/script&gt;"] |
    "\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 found500 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/2index/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很有用,因为它们可以被组合。假设我们的PageAuthor有一个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

转载于:https://my.oschina.net/ljzn/blog/733757

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值