How does Plug.Builder work?

(本文涉及的源码需要有一些对macro的了解才比较容易看懂)

上回说到,我们可以利用Plug,搭配Cowboy这个web server来写一个简单的web app。而实际中我们不可能把所有的处理逻辑都放在一个Plug中,代码既不容易维护,也不能重复利用Plug。

正如Plug的名字(插头)一样,我们可以把一个个的Plug连起来,组成一个功能强大的Plug pipeline。就像你到国外旅行,电源适配器接口不对应,就可以买个转换器(其实就是一个插头),一头插着你的电源,另一头插在酒店的插座上。 再比如Plug实现了Plug.Logger,把它“插在”在你的整个Plug的pipeline中,就帮你加入了日志的功能。

Plug.Builder的目的就是为了让你方便地写出Plug pipeline,我们还是先来看文档里的例子? :D

defmodule MyApp do
  use Plug.Builder

  plug Plug.Logger
  plug :hello, upper: true

  def hello(conn, opts) do
    body = if opts[:upper], do: "WORLD", else: "world"
    send_resp(conn, 200, body)
  end
end

MyApp就是一个组合了几个Plug的Plug pipeline,按顺序有Plug.Loggerhellosend_resp是从Conn引入的,plug这个macro显然是从Plug.Builder引入的,那具体是如何引入,以及为什么这样就可以定义一个Plug pipeline,秘密都在use Plug.Builder这行代码中。

Define plugs one by one #

当以一个Module为参数调用了use这个macro(是的,use也是一个macro)时,其实最终只是调用了那个Module的__use__ macro而已。是的,还是macro ╮(╯▽╰)╭

我们来看Plug.Builder的__use__

@behaviour Plug          # Adopt behaviour of Plug

def init(opts) do        # Implement of the init callback
   opts
end

def call(conn, opts) do  # Implement of the call callback
   plug_builder_call(conn, opts)
end

import Plug.Conn
import Plug.Builder, only: [plug: 1, plug: 2]  # import plug

Module.register_attribute(__MODULE__, :plugs, accumulate: true)
@before_compile Plug.Builder

Plug这个Module定义了一个behaviour,并限定了所有采用了Plug behaviour的Module必须实现init/1call/2函数,也就是上篇博客中提到的这两个关键的函数。

我们先跳过call函数里的具体内容,直接看倒数第二行。

Module.register_attribute(__MODULE__, :plugs, accumulate: true)为MyApp注册了plugs这个属性,使我们可以通过@plugs来访问到它,或者通过@plugs foo来把一个foo添加到这个@plugs这个list的头上。

现在看plug,就会发现它其实就只是把一个个的Plug放到@plugs里,比如MyApp中就有这样的等价关系

plug Plug.Logger  # => @plugs {Plug.Logger, [], true}
plug :hello, upper: true # => @plugs {:hello, [upper: true], true}

于是@plugs现在就是[{ :hello, [upper: true], true }, { Plug.Logger, [], true }],这个由一个个plug的tuple组成的list(注意:与plug执行的顺序相反)。

Compile all plugs to one function #

来到最后一行,@before_compile Plug.Builder会在MyApp编译前调用Plug.Builder里的__before_compile__

在这里会利用各种macro,把@plugs“compile”成一个函数plug_builder_call/2,而这个函数就是之前被__using__里的call函数调用的唯一一行代码。这个compile的过程我就不在此展开,只说一下最后大概是什么样的代码:

def plug_builder_call(conn, _) do
  case(Plug.Logger.call(conn, [])) do
    %Plug.Conn{halted: true} = conn -> conn
    %Plug.Conn{} = conn ->
      case(hello(conn, upper: true)) do
        %Plug.Conn{halted: true} = conn -> conn
        %Plug.Conn{} = conn -> conn
        _ ->
          raise("expected :hello/2 to return a Plug.Conn")
      end
    _ ->
      raise("expected Plug.Logger.call/2 to return a Plug.Conn")
  end
end

可以看到,call函数基本上就是按顺序一个个地,以conn和plug调用时的opts作为参数,调用我们加入的plug的call方法。这里的顺序的保证,是因为@plugs是逆序,而compile函数会把@plugs再做逆序。

Wrap up #

当你用Plug.Builder去定义一个Plug pipeline的时候,它会帮你把所有Plug都compile成一个函数,并在call里按你加入的顺序调用。当然,你的这个Plug pipeline也是一个Plug,可以放在其他的plug pipeline里被调用,这就让我们可以写出模块化、可复用的web server。

Tips

不想一点点看compile函数的代码就想看到最后的结果?没问题,有一种简单的方法可以做到,在iex中执行:

IO.puts Macro.to_string(
  Plug.Builder.compile([
    {:hello, [upper: true], true},
    {Plug.Logger, [], true}
  ])
)

Refer

http://elixir-lang.org/docs/stable/elixir/Kernel.html#use/2
https://github.com/elixir-lang/plug/blob/master/lib/plug/builder.ex#L98
http://elixir-lang.org/getting-started/typespecs-and-behaviours.html
http://elixir-lang.org/docs/v1.0/elixir/Module.html

http://elixir-lang.org/docs/stable/elixir/Kernel.html#defoverridable/1
http://elixir-lang.org/docs/stable/elixir/Module.html#register_attribute/3
http://elixir-lang.org/docs/v1.0/elixir/Macro.html#to_string/2

 
14
Kudos
 
14
Kudos

Now read this

Kubernetes 源码解读 - 由一次 debug 学到的

在最近公司办的第一次 Open L 活动中,我们分享了为什么我们要用 Kubernetes,其中吸引我们的一方面就是 autoscaling,它能够根据 CPU 等指标动态调整 pod 的个数,以此提高机器的利用率。 但最近却发现它并不能按预期正常地工作,deployment 的 Horizontal Pod Autoscaler(HPA) 显示的 CPU 并不能反应实际情况,所以也就不能正常地对 pod 的数量进行调整。查了 log 又 Google 一番后,... Continue →