Erlang/Elixir Phoenix render 迷思

darkbaby123 · 2015年12月28日 · 最后由 davidgao 回复于 2016年02月02日 · 7423 次阅读
本帖已被管理员设置为精华贴

前言

原文放在 SegmentFault

最近在学习用 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

看来群里没啥搞 Elixir/Phoenix 的人啊,求同好~

#1 楼 @darkbaby123 在关注 Phoenix 耍了一下 感觉基础不够成熟欠缺还比较多 社区也比较小 像是发邮件和文件日志这种比较基础的东西都要自己实现或者借助第三方包 在 twitter 上讨论的时候 作者也表示贡献者太少了 只能专注一些更重要的特性发开 像 ecto 这种基础库 2.0 也还会有较大改动 不过玩的话还是很有意思的 很有吸引力 最近对函数式也比较感兴趣 Erlang 感觉前景也很棒《Programming Erlang》看到前言就被 Joe Armstrong 安利了

#2 楼 @zhang_soledad 发邮件有库的 标准库就有日志模块

#3 楼 @yukihiro_matz 是有库 但是没有ActionMailer这样框架集成

默认 backend 只有 console 只会输出到 console 要输出到文件 就要自己实现 backend 或者用库

功能肯定都可以实现 像官方 guides 推荐的 mail 解决方案是mailgun 而不是mailman这样的 让我觉得很不好

一只没搞明白 Phoenix 为什么要一个 view 还要一个 Template

#6 楼 @hanhor 我是把 view 理解成 Rails 的 helper。

@zhang_soledad @yukihiro_matz 新兴语言和框架的生态圈不够好,这是大部分新兴技术的问题。相比起来 Elixir 其实还算好点的,因为 Elixir 可以直接用 Erlang 的包。后者发展了这么多年,再怎么说生态圈也不会太差。

我觉得 Elixir 的语言特性足够吸引人,也看好它的前景。语言特性几年之内非常难以改变,这甚至决定了一门语言能够发展的上限。相比而言生态圈则容易扩展得多,只要 key feature 足够吸引人。虽然谁也说不好 Elixir 能走多远,但它确实也是个挺有意思的选择。

BTW hex 的库里有 log 的 file storage,并且 mailman 也是搜索结果前三。就像你说的,功能肯定都是可以实现的。并且我相信,非常大众的需求往往都已经有了解决方案。至于是否框架内置,我倒挺喜欢 Web 框架就只干 Web 的事情,不过那是另一个话题了。

@hanhor @alex_marmot 这个问题 Phoenix 文档可以告诉你:

Phoenix views have two main jobs. First and foremost, they render templates (this includes layouts). The core function involved in rendering, render/3, is defined in Phoenix itself in the Phoenix.View module. Views also provide functions which take raw data and make it easier for templates to consume. If you are familiar with decorators or the facade pattern, this is similar.

我的理解是,Phoenix View 一是负责 render,二是负责隐藏 view 层的逻辑,让 template 变得更简单(类似 logicless template)的理念。相比而言,Rails 用 renderer 对象去负责 render,但默认不提供 presenter/decorator/facade 这类对象去隐藏 view 层的逻辑。helper 跟 presenter 并不一样,这点就不说了。

#8 楼 @darkbaby123 谢谢大神,受教了。。

#3 楼 @yukihiro_matz 是 matz?会说中文?

@yukihiro_matz @tangmonk 哈哈,又有一个被骗了 😄

好文,最近同样在看这块。

挺详细的,Phoenix 还是很不错的。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号