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.Logger
和hello
。send_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/1
和call/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