Request life-cycle
先说好,看完前两章再看这章。
如题,这章讲请求的生命周期。(不知道请求的话,可能得先了解了解HTTP协议)。
这个指南采用一种实用的学习方法,那就是在实践中学习(learn by doing)。这里我们将会在 Phoenix 项目中添加两个新页面,并解释各个部分是如何组合在一起的。
增加一个页面 Adding a new page
当浏览器访问 http://localhost:4000 ,浏览器会发送一个请求到这个地址的一个服务上,在这里是我们的 Phoenix 应用。请求包含一个请求动作和一个路径。比如下面的转换:
浏览器地址栏 请求动作 路径
http://localhost:4000/ GET /
http://localhost:4000/hello GET /
http://localhost:4000/hello/world GET /
当然,还存在其他的请求动作。比如,提交表单时常用的 POST 动作。
web 应用通常将请求动作和请求地址一起映射到应用程序中的某部分处理。在 Phoenix 中是通过 router 来完成这个映射。比如,将 '/articles' 映射到显示所有文章的程序上。因此,要增加一个页面,首先得增加一个路由。
一个新的 route (路由)
路由将唯一的 HTTP 请求(verb/path组合)映射到 controller/action 来处理。Controller 仅仅就是 Elixir 模块。action 就是这些模块中的方法。
Phoenix 默认生成的路由文件在 lib/hello_web/router.ex。本节就来认识下这个文件。
默认的欢迎页面的路由是:
get "/", PageController, :index
来分解一下这条路由的信息。访问 http://localhost:4000/ 会向根路径发出 HTTP GET 请求。像这样的请求全部将由 lib/hello_web/controllers/page_controller.ex 中定义的 HelloWeb.PageController 模块中的 index/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, {HelloWeb.LayoutView, :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, :index
end
# Other scopes may use custom stacks.
# scope "/api", HelloWeb do
# pipe_through :api
# end
# ...
end
先不管 pipeline,先看看 scope,先关注怎么添加一个路由。其他的我们后续章节再表。
现在我们要添加一个 GET 请求,地址是 /hello,映射到即将要创建的 HelloWeb.HelloController 模块中的 index action。在 scope "/" do 块编辑:
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :index
get "/hello", HelloController, :index
end
一个新的 controller (控制器)
Controllers 就是 Elixir 模块,actions 就是定义在其中的 Elixir 函数。actions 的目的是收集数据并执行渲染所需的任务。从router可以看出,我们需要一个 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.html")
end
end
use HelloWeb, :controller 压后再表。先来看看 index action。
所有控制器 action 都有两个参数。第一个参数是 conn,一个包含大量请求数据的结构体。第二个是 params,请求参数。暂时先不用 params,为了避免编译器的警告,在参数前加上 _。
这个 action 的主要内容是 render(conn, "index.html")。就是告诉 Phoenix 去渲染 "index.html"。 负责渲染的模块称为 views。默认情况下,Phoenix views 以 controller 的名字命名,所以 Phoenix 期望存在一个 HelloWeb.HelloView view 并为我们处理 "index.html"。
注意:使用原子(atom)作为模板名字
render(conn, :index),也是同样的效果。在这些情况下,会根据 headers 中的 Accept 字段来选择模板,比如 "index.html" 或者 "index.json"。
一个新的 view (视图)
Phoenix 中的 views 为表现层。例如,我们期望渲染“index.html”的输出是一个完整的 HTML 页面。为了让我们的生活更轻松,我们经常使用模板来创建这些 HTML 页面。
创建 lib/hello_web/templates/hello/index.html.heex,编辑如下内容:
<section class="phx-hero">
<h2>Hello World, from Phoenix!</h2>
</section>
现在我们已经有了路由、controller、view和template,我们到浏览器访问 http://localhost:4000/hello ,就能看到来自 Phoenix 的问候! (如果您在此过程中停止了服务器,重新启动命令是 mix phx.server。)
有一些点需要了解一下。我们做修改时不需要关闭再重新启动服务,是的,Phoenix 可以热更新。然后,我们的 index.html.heex 只包含了一个 section 标签,但我们得到了一个完整的 HTML 页面。我们的 index 模板被渲染到 lib/hello_web/templates/layout/app.html.heex layout 中。如果打开它,您会看到如下所示的一行:
<%= @inner_content %>
在将 HTML 发送到浏览器之前,它将我们的模板注入到布局中。
关于热代码重新加载的说明:一些带有自动 linter 的编辑器可能会阻止热代码重新加载工作。
从 endpoint 到 views
我们已经构建好了第一个页面,现在可以来了解一下 请求 的生命周期了。
所有的 HTTP 请求开始于程序中的 endpoint(端点)。在 lib/hello_web/endpoint.ex 中的 HelloWeb.Endpoint 模块内。打开这个文件,你会看到很多 plug。Plug 是一个用于将 Web 应用程序拼接在一起的库和规范。它是 Phoenix 如何处理请求的重要部分,我们将在接下来的 Plug 指南中详细讨论它。
现在,可以说每个插件定义了一个请求处理片段。来看看代码的结构:
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 将所有进一步的请求处理委托给 router 。正如我们现在所知,它的主要职责是将 动词/路径对 映射到控制器。然后控制器告诉视图渲染模板。
此时,您可能会认为这可能需要很多步骤来简单地呈现一个页面。然而,随着我们的应用程序变得越来越复杂,我们将看到每一层都有不同的用途:
- endpoint(
Phoenix.Endpoint)-端点包含所有请求通过的公共和初始路径。如果您希望所有请求都发生某些事情,它会转到端点。 - router(
Phoenix.Router)-router负责将动词/路径分派给控制器。路由器还允许我们确定功能范围。例如,您的应用程序中的某些页面可能需要用户身份验证,而其他页面可能不需要。 - controller (
Phoenix.Controller) - 控制器的工作是检索请求信息,与您的业务域对话,并为表示层准备数据。 - view (
Phoenix.View) - 视图处理来自控制器的结构化数据并将其转换为呈现给用户的表现层。
再来个例子。
另一个新页面
来增加一点程序的复杂性。增加一个页面,使用 URL 中的一小片段作为 "messenger",通过 controller 传递到模板中。和上次一样,我们要做的第一件事就是创建一条新的路由。
另一个新路由
继续沿用之前的 HelloController,增加一个 show action。然后在路由文件中增加一个新的路由:
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :index
get "/hello", HelloController, :index
get "/hello/:messenger", HelloController, :show
end
注意,我们在路径中使用了 :messenger 语法。Phoenix 将采用 URL 中该位置出现的任何值并将其转换为参数。例如,如果我们将浏览器指向:http://localhost:4000/hello/Frank ,“messenger”的值将是“Frank”。
另一个新的 action
对我们新路由的请求将由 HelloWeb.HelloController 中的 show action 处理。我们已经在 lib/hello_web/controllers/hello_controller.ex 中拥有了控制器,所以我们需要做的就是编辑该控制器并为其添加一个 show action。这一次,我们需要从参数中提取 messenger ,以便我们可以将它传递给模板。为此,我们将这个 show 函数添加到控制器中:
def show(conn, %{"messenger" => messenger}) do
render(conn, "show.html", messenger: messenger)
end
在 show action 的主体中,我们还将第三个参数传递给渲染函数,这是一个键值对,其中 :messenger 是键,而 messenger 变量作为值传递。
如果 action 的主体需要访问绑定到 params 变量的完整参数,除了绑定的 messenger 变量,我们可以像这样定义 show/2:
def show(conn, %{"messenger" => messenger} = params) do
...
end
请记住,params map 的键始终是字符串,并且等号不代表赋值,而是模式匹配断言。
另一个新的 template
最后一块,我们需要一个新模板。由于是针对 HelloController 的 show action,所以会进入lib/hello_web/templates/hello 目录,名为 show.html.heex。它看起来与我们的 index.html.heex 模板惊人地相似,只是我们需要显示我们的 messenger 的名称。
为此,我们将使用特殊的 EEx 标签来执行 Elixir 表达式:<%= %>。请注意,初始标签有一个等号,如下所示: <%= 。这意味着在这些标签之间的任何 Elixir 代码都将被执行,并且结果值将替换 HTML 中的标签。如果等号缺失,代码仍会执行,但该值不会出现在页面上。
这就是模板的样子:
<section class="phx-hero">
<h2>Hello World, from <%= @messenger %>!</h2>
</section>
我们的 messenger 显示为 @messenger。从控制器传递给视图的值我们称为 “assigns”。它是一种特殊的元编程语法,代表 assigns.messenger。这样做更容易理解,也更容易在模板中使用。
OK,搞掂。如果你访问 http://localhost:4000/hello/Frank , 你会看到如下页面:
可以随意玩一玩。无论你在 /hello/ 后面放什么,都会作为你的 messenger 出现在页面上。
完
本章主要是对整个请求的生命周期有个整体上的初步了解,这章可以多看几遍,等学完后面几章,还是可以再来看几遍。