Phoenix render 迷思

darkbaby123

前言

最近在学习用 Elixir 的 MVC 框架 Phoenix 写一个 Chatroom 。有一个问题是在 channel 中渲染模板,虽然我用 Phoenix.View.render 方法顺利解决了。但这让我开始思考另外几个问题:

  1. Phoenix 中有哪些 render 方法?

  2. 它们分别是干什么用的?

  3. 它们有内部联系吗?

render 的使用场景

在研究有哪些 render 方法之前,我们先看看 Phoenix 的几个使用 render 的场景。

在 controller 中使用 render

def foo(conn, _params) do
  render conn, "foo.html"
end

在 template 中使用 render

<!-- 渲染同一视图中的另一个模板 -->
<%= render "foo.html" %>
<%= render "foo.html", some_model: some_model %>

<!-- 渲染另外一个视图中的模板 -->
<%= render YourApp.OtherView, "bar.html" %>
<%= render YourApp.OtherView, "bar.html", some_model: some_model %>

在其他地方使用 render ,多用于 channel 或者 iex 调试:

Phoenix.View.render(YourApp.CustomView, "foo.html")

除了最后一个例子可以清楚地看到 render 方法来自 Phoenix.View 模块之外,其他几个地方的 render 都不知道出处。这些 render 来自哪里?

如果查一下 Phoenix 的文档,可以发现两个模块定义了 render 方法,它们是 Phoenix.ControllerPhoenix.View ,我们可以猜测前者为所有 controller 提供 render ,后者为所有 view 和 template 提供 render 。不过这还需要验证一下。

controller 的 render

先看看 controller ,Phoenix 的 controller 定义非常简单:

defmodule YourApp.SomeController do
  use YourApp.Web, :controller
end

显然一个空的模块是没有实现 render 方法的,那关键就在 YourApp.Web 里。其实这个模块就在项目的 web/web.ex 文件里。大概像下面这样:

defmodule YourApp.Web do
  # def model ...

  def controller do
    quote do
      use Phoenix.Controller

      alias YourApp.Repo
      import Ecto
      import Ecto.Query, only: [from: 1, from: 2]

      import YourApp.Router.Helpers
      import YourApp.Gettext
    end
  end

  # def view ...
end

YourApp.Web 模块的职责是为其他模块加入一些通用的功能,基本上就是执行一些 alias, import, use 。
controller 中的 use 那一行代码会调用 YourApp.Webcontroller 方法,这里我们看到执行了 use Phoenix.Controller

先大致解释一下 use 。它是一个 Elixir 的 macro ,一般用来为模块附加额外的特性。当模块 A use 模块 B 时,B 的 __using__ 回调会被调用,我们可以在里面写代码为模块 A 附加一些东西。

Phoenix.Controller__using__ 大概如下所示,看不懂语法和 API 不要紧,明白意思就行:

defmacro __using__(opts) do
  quote bind_quoted: [opts: opts] do
    import Phoenix.Controller
    # ...
  end
end

这里我们可以看到 import Phoenix.Controller ,结合开头 controller 中的 use YourApp.Web, :controller ,其实 Phoenix.Controller 用这种形式被 import 到了所有 controller 中,这意味着 Phoenix.Controller 的所有公有方法都可以在 controller 内部使用,其中也包括 render 方法。注意这是 内部使用 ,像 YourApp.SomeController.render 这种调用是不可行的。

template 的 render

在 template 中调用 <%= render %> 应该属于哪个模块的呢?要回答这个问题,我们得先了解下 view 和 template 的关系。

Phoenix 的视图层分为两个部分:view 和 template ,view 是一个 Elixir 模块,template 是一个 EEx 模板文件。一个 view 管理多个 template 。举个例子,一个 YourApp.RoomView 下面可以定义 index.htmlshow.html 等几个不同 template 。要渲染 show.html ,我们可以用 Phoenix.View.render(YourApp.RoomView, "show.html")

template 本质上是一个函数,接收动态数据作为参数,组合静态内容并返回结果。对服务器端渲染而言,结果大多是一个字符串。我们经常使用的模板文件,实际上只是把静态内容存放在文件系统里而已。Phoenix 在编译期间会把 template 编译成函数放在 view 中。模板渲染最终会调用 view 中相应的函数。因此在 template 里调用的方法全都来自于 view 。template 里的 <%= render %> 等于调用相对应的 view 的 render 方法。

一个典型的 view 定义如下:

defmodule YourApp.RoomView do
  use YourApp.Web, :view
end

跟 controller 非常类似的代码。具体源码追溯过程我就不写了,通过同样追溯方法我们最终可以在 Phoenix.View__using__ 中看到同样的 import ,如下所示:

defmacro __using__(options) do
  # ...
  quote do
    import Phoenix.View
    use Phoenix.Template, root: ...
  end
end

看来 view 中的 render 方法应该来自于 Phoenix.View 。不过先别下结论,我们来对比一下方法签名。Phoenix.View.render 的方法签名是 render(module, template, assigns) ,注意其中有 三个参数,并且都是不能省略的

再回顾一下 template 中的 render

<!-- 渲染同一视图中的另一个模板 -->
<%= render "foo.html" %>
<%= render "foo.html", some_model: some_model %>

<!-- 渲染另外一个视图中的模板 -->
<%= render YourApp.OtherView, "bar.html" %>
<%= render YourApp.OtherView, "bar.html", some_model: some_model %>

可见这个 render 可以接受 一个到三个参数 。这跟 Phoenix.Viewrender 明显不一样。这是怎么回事?

答案在 Phoenix.Template 中。回顾一下上面的代码,Phoenix.View__using__ 中还有一行 use Phoenix.Template ,让我们看看 Phoenix.Template__using__

defmacro __using__(options) do
  quote do
    @doc """
    Renders the given template locally.
    """
    def render(template, assigns \\ ${})

    def render(template, assigns) when is_list(assigns) do
      render(template, Enum.into(assigns, %{}))
    end

    def render(module, template) when is_atom(module) do
      Phoenix.View.render(module, template, %{})
    end

    # ...
  end
end

它居然为 view 定义了 render 方法!这下事情明白了,当我们在 template 中使用 render 时,如果传入一个或两个参数,其实我们调用的是 Phoenix.Template 为 view 生成的 render 方法;如果传入三个参数,则是调用 Phoenix.View 中的 render 方法。因为这个 render 方法是在 __using__ 中定义的,所以 Phoenix 文档是查不到的 。

注:Elixir 允许为一个方法定义不同的变种,这些方法并不会互相覆盖。当方法被调用时 Elixir 会通过 pattern match 和 guard 自动去寻找最匹配的方法执行,合理利用可以省不少 if/else

有一点值得提醒,跟 Phoenix.Viewimport 不同,Phoenix.Template 是为 view 动态地定义方法(其实是编译期做的),而且这个方法是公有的。这意味着我们可以在其他模块里调用 YourApp.SomeView.render 去渲染 template 。

view 的 render

这里想说的有两种,一是在 view 中调用 render ,二是在其他模块中调用 Phoenix.View.render

在 view 里面,Phoenix.View 的所有公有方法都可以使用。Phoenix.Template 给了 view 三个 render 方法,但没有把它自己 import 进去 ,所以它的方法是不能在 view 里直接用的。不过大部分情况下 Phoenix.View 提供的方法已经足够了。

在其他模块中,如果要渲染某个 template ,我们其实有两种办法,它们是等价的:

Phoenix.View.render(YourApp.SomeView, "some_template.html")
YourApp.SomeView.render("some_template.html")

虽然第二种方式更简洁,但第一种方式,也就是 Phoenix.View.render 比较推荐,因为它可以设置 layout ,而且也算是 view 渲染的标准入口函数。比如 Phoenix.Controllerrender 内部调用的就是它,而且 Phoenix.View 的其他几个方法比如 render_to_iodatarender_to_string 调用的也是它。源码追溯过程我就不放上来了,有兴趣的可以自己挖掘。

另外 Phoenix.View.render 渲染某个 view 的 template 的时候,它会在内部调用 view 的 render 方法(Phoenix.Template 提供的方法)。

小结

现在我们可以回答开篇的几个问题作为总结:

Phoenix 中有哪些 render 方法?

Phoenix 文档中可以查到两个 render 方法,但实际上有三个 render 方法,前两个在 Phoenix.ControllerPhoenix.View 中定义并被 import 到相应的模块中使用,第三个在 Phoenix.Template__using__ 中被定义,并在编译时附加给 view 。

它们分别是干什么用的?

Phoenix.Controller.render 在 controller 中使用,它关注的是内容协商,即根据客户端的要求来决定渲染类型(HTML/JSON/XML 等)。具体渲染细节会代理给 Phoenix.View.render 去处理。

Phoenix.View.render 可以算是通用的 view 渲染入口,既用在 view 和 template 中,也被其他需要渲染 view 的模块调用。比如 controller 。它处理视图层的渲染细节。

Phoenix.Template 提供的 render 是作为 view 的 render 方法的补充,让 view 的 render 方法变得更灵活多变。

它们有内部联系吗?

Phoenix.Controller.render 内部会调用 Phoenix.View.renderPhoenix.View.render 内部会调用传入的 view 模块的 render 方法,而这个方法是 Phoenix.Template 为每个 view 生成的。

参考资料

Phoenix.Controller.render
Phoenix.View.render
Phoenix.Template
Phoenix Guide: Views
Content negotiation

阅读 3.4k

David Chen 的编程大杂烩
前端,后端,编程技巧,各种被坑经验,无所不包(其实就是想什么放什么)
1.3k 声望
66 粉丝
0 条评论
你知道吗?

1.3k 声望
66 粉丝
宣传栏