本文的目的是讨论Phoenix请求的生命周期。我们实战添加两个新的页面,并讨论整个过程是如何串起来的。
让我们从添加第一个新页面开始。
web应用通常通过将HTTP方法和路径映射到应用的某个函数来处理请求。Phoenix通过路由器来实现这个匹配。例如将”/articles”映射到显示文章的函数。因此,添加一个页面首先要添加一个新的路由。
控制器和动作通过路由器关联它要处理的HTTP方法和路径。在Phoenix中,控制器对应者Elixir的模块,动作是控制器下定义的方法。
动作本质上就是一个处理请求的函数,在Go语言中,称为处理器函数,Phoenix使用了”action”一词来表述它,翻译为动作确实略显生硬,阅读时可以理解为每个请求对应的动作。但对于其本质一定要拿捏准确。
对于新的应用,Phoenix为我们生成了一个路由器文件 lib/hello_web/router.ex
,它也是本章的主角。
在前面例子中欢迎页的路由如下:
get "/", PageController, :home
让我们看看这个路由干了什么。访问 http://localhost:4000 向跟目录发起一个HTTP GET请求。这个请求会被 lib/hello_web/controllers/page_controller.ex
文件定义的 HelloWeb.PageController
中的 home/2
函数处理。
我们会新建一个页面,当访问 http://localhost:4000/hello 时,输出”Hello World, from Phoenix!”。
我们要做的第一件事是添加一个页面路由。打开 lib/hello_web/router.ex
,对于一个全新的应用,内容如下:
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
end
# Other scopes may use custom stacks.
# scope "/api", HelloWeb do
# pipe_through :api
# end
# ...
end
暂时忽略 pipeline
和 scope
,在稍后的教程中再讨论它们。
让我们在 scope "/" do
下添加一个路由,将 GET /hello
请求映射到 HelloWeb.HelloController
的 index
方法。
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
get "/hello", HelloController, :index
end
控制器是Elixir模块,动作是模块下的Elixir函数。动作的目的是收集数据并执行渲染。我们需要一个 HelloWeb.HelloController
模块以及一个 index/2
函数。那么动手创建一个 lib/hello_web/controllers/hello_controller.ex
文件,并输入下面的代码:
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def index(conn, _params) do
render(conn, :index)
end
end
use HelloWeb
和 :controller
再后面的教程中再详细讨论,先将注意力集中到 index
函数。
一个控制器动作有两个参数,第一个是 conn
,它是一个存储着大量请求数据的结构体;第二个是 params
,它是请求参数。这里为了避免编译器警告,我们在 params
前面加了一个下划线 _
。
函数的核心是 render(conn, :index)
,它告诉Phoenix要渲染 index
模板。表示渲染的模块叫做视图,Phoenix视图默认控制器和视图格式来命名,这里控制器是 HelloController
,视图格式是 HTML
,因此我们需要一个 HelloWeb.HelloHTML
模块并定义个 index/1
函数。
Phoenix视图充当的是展示层。例如,我们希望 index
输出的是完整的HTML页面。为了快乐搬砖,我们常常会用模板创建HTML页面。
让我们来创建一个视图,新建 lib/hello_web/controllers/hello_html.ex
文件,输入以下代码:
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
end
我们可以通过函数或者单独的文件向视图添加模板。
通过函数添加代码如下:
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
def index(assigns) do
~H"""
Hello!
"""
end
end
我们定义了一个接受 assigns
的函数,并使用 ~H
标记添加想要渲染的内容。在 ~H
标记内,我们使用的模板语言叫HEEx,表示”HTML+EEx”。EEx是一个用来嵌入Elixir的库,HTML+EEx是EEx针对HTML的扩展,支持HTML验证,组件,和值的自动转义。后者可使你免受跨站点脚本之类的安全漏洞的影响,而无需额外的工作。
模板文件原理类似。函数方式适用于短小的模板,模板文件适用于有很多标签或当你感觉函数已难以维护时。
让我们试着定义一个模板文件。首先删除 def index(assigns)
函数定义,替换成 embed_templates
声明:
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
embed_templates "hello_html/*"
end
这里我们告诉 Phoenix.Component
将同级目录 hello_html
下的所有 .heex
模板做为函数定义嵌入我们的模块。
接下来我们需要向 lib/hello_web/controllers/hello_html
目录添加文件。
注意看控制器名称 HelloController
,视图名称 HelloHTML
和模板目录 hello_html
都遵循着相同的命名约定,并且它们在目录树中也在一起。
注意:我们可以任意重命名
hello_html
目录并将它放在lib/hello_web/controllers
子目录下,但是需要更新embed_templates
设置。因此建议保持统一的命名约定以避免歧义。
lib/hello_web
├── controllers
│·····├── hello_controller.ex
│·····├── hello_html.ex
│·····├── hello_html
|·············├── index.html.heex
模板文件名格式为 NAME.FORMAT.TEMPLATING_LANGUAGE
,我们在 lib/hello_web/controllers/hello_html/
目录下创建一个名为 index.html.heex
的文件:
<section>
<h2>Hello World, from Phoenix!h2>
section>
模板文件会自行编译为模块下的函数,两种方式没有运行时的性能差异。
现在我们有了路由,控制器,视图和模板,我们可以访问 http://localhost:4000/hello 来看看效果了。
这里有些有趣的事情值得我们注意。当我们做这些变更时,不需要停止和重启服务器。没错,Phoenix具有代码热加载!还有即使我们的 index.html.heex
文件只包含一个简单的 section
标签,我们也得到了一个完整的HTML文档。事实上我们的模板是渲染在一个布局中的:首先渲染的是 lib/hello_web/components/layouts/root.html.heex
,然后它会渲染 lib/hello_web/components/layouts/app.html.heex
,最后是我们的内容。如果你打开这些文件看一看,就会在底部发现这样一行代码:
<%= @inner_content %>
它会在HTML被发送到浏览器之前将模板注入到布局中。关于布局我们会在后面的教程中介绍。
我们已经创建了第一个页面,现在可以看看一个请求的生命周期是如何串联起来的了。
所有的HTTP请求都始于应用的endpoint,其实就是 lib/hello_web/endpoint.ex
文件中的 HelloWeb.Endpoint
模块。当我们打开这个文件查看时,就会发现它里面大量调用了 plug
,跟路由挺像的。Plug是一个库,也是组织web应用的说明书。它是Phoenix处理请求的重要部分,有关细节后面的教程中会讲到。
目前,可以说每个plug都定义了一个处理请求的队列。在endpoint中,你会看到大致如下的框架:
defmodule HelloWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello
plug Plug.Static, ...
plug Plug.RequestId
plug Plug.Telemetry, ...
plug Plug.Parsers, ...
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, ...
plug HelloWeb.Router
end
每个插件都有不同的作用,后面我们会讲到。最后一个插件恰好就是 HelloWeb.Router
模块。它让endpoint将所有请求的后续处理都交给路由器。路由器的主要作用就是将请求映射到处理器。最后处理器告诉视图渲染一个模板。
此时,你可能会想,简单地渲染一个页面怎么需要这么多步骤。但是,当应用变得越来越复杂时,我们会看到每一层都有其特殊的作用:
Phoenix.Endpoint
) - endpoint包含所有请求的公共和初始路径,用来处理所有请求都要做的事情。Phoenix.Router
) - 路由负责将请求分发到控制器,同时也运行我们确定一些功能的范围。比如有些页面需要用户鉴权,有些页面则不需要。Phoenix.Controller
) - 控制器的工作是提取请求信息,调用业务领域,并为表示层准备数据。让我们再添加一个页面,巩固一下最后三个组件是如何协同工作的。
这里我保留了endpoint这个单词,本意为端点、终点,直译不好理解,这里endpoint指的其实就是服务端,或者说是服务所有请求的入口点。
让我们稍微加一点难度,添加一个页面,它会截取URL的一部分并通过控制器传入模板,最后在页面上显示出来。
如前面说的,我们首先要做的是创建一个新的路由。
这里我们复用之前创建的 HelloController
,添加一个新的 show
方法。在之前的路由下添加一行:
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
get "/hello", HelloController, :index
get "/hello/:messenger", HelloController, :show
end
注意我们在路径中用到了 :messenger
语法,Phoenix会将URL中对应位置的值转成一个变量。例如,我们在浏览器输入 http://localhost:4000/hello/Frank ,messenger的值就是Frank。
新路由下的请求会由 HelloWeb.HelloController
的 show
函数处理。我们已经有了一个控制器 lib/hello_web/controllers/hello_controller.ex
,因此我们唯一需要做的就是在控制器下添加一个 show
函数。这一次,我们需要从参数中提取messenger变量,并传递给模板。为此,将下面的函数添加到控制器:
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
我们给 render
函数传递了第三个参数,一个键值对。其中 :messenger
是键,变量 messenger
是值。
如果我们需要访问除messenger之外的请求参数,可以像下面这样定义 show/2
函数:
def show(conn, %{"messenger" => messenger} = params) do
...
end
要记住, params
的键是字符串,等号不是赋值,而是模式匹配。
最后我们需要创建一个新的模板,遵循命名规范,将它放在 lib/hello_web/controllers/hello_html
目录下,命名为 show.html.heex
。唯一与 index.html.heex
不同的是,这次我们需要显示messenger变量。
为此,我们使用特殊的HEEx标签 <%= %>
来求值Elixir表达式。任何出现在标签内的Elixir代码都会被执行,其结果会替换该标签。如果标签内没有等号,代码依然会被执行,但结果不会出现在页面中。
记住我们的模板是用HEEx(HTML+EEx)编写的,HEEx是EEx的超集,因此也继承了 <%= %>
语法。
模板内容如下:
Hello World, from <%= @messenger %>!
我们从控制器传入视图的值统称为”assigns”,我们可以通过 assigns.messenger
来访问messenger,但是通过元编程,Phoenix为我们提供了更加干净的 @
语法。
完成。如果我们用浏览器访问 http://localhost:4000/hello/Frank ,应该会看到下面的页面: