查看源代码 请求生命周期
本指南的目的是讨论 Phoenix 的请求生命周期。本指南将采用实用方法,通过实践来学习:我们将为 Phoenix 项目添加两个新页面,并在此过程中解释各部分是如何衔接的。
让我们开始创建第一个 Phoenix 页面!
添加新页面
当您的浏览器访问http://localhost:4000/时,它会向该地址上运行的服务发送 HTTP 请求,在本例中是我们的 Phoenix 应用程序。HTTP 请求由一个动词和一个路径组成。例如,以下浏览器请求会转换为
浏览器地址栏 | 动词 | 路径 |
---|---|---|
http://localhost:4000/ | GET | / |
http://localhost:4000/hello | GET | /hello |
http://localhost:4000/hello/world | GET | /hello/world |
还存在其他 HTTP 动词。例如,提交表单通常使用 POST 动词。
Web 应用程序通常通过将每个动词/路径对映射到应用程序的特定部分来处理请求。在 Phoenix 中,这种匹配由路由器完成。例如,我们可以将 "/articles" 映射到应用程序中显示所有文章的部分。因此,要添加一个新页面,我们的第一步是添加一个新的路由。
新路由
路由器将唯一的 HTTP 动词/路径对映射到控制器/动作对,这些对将处理它们。Phoenix 中的控制器只是 Elixir 模块。动作是在这些控制器中定义的函数。
Phoenix 在新的应用程序中为我们生成了一个路由器文件,位于 lib/hello_web/router.ex
。本节我们将在这个文件中进行操作。
我们之前快速入门指南中构建的 "欢迎来到 Phoenix!" 页面的路由如下所示。
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
目前,我们将忽略管道和 scope
的使用,只关注添加路由。我们将在路由指南中讨论这些内容。
让我们在路由器中添加一个新路由,将 /hello
的 GET
请求映射到即将创建的 HelloWeb.HelloController
的 index
动作,位于路由器的 scope "/" do
块中
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
get "/hello", HelloController, :index
end
新控制器
控制器是 Elixir 模块,动作是在其中定义的 Elixir 函数。动作的目的是收集数据并执行渲染所需的步骤。我们的路由指定我们需要一个具有 index/2
函数的 HelloWeb.HelloController
模块。
要使 index
动作生效,让我们创建一个新的 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
,在本例中) 命名,因此 Phoenix 预计 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 的库,它作为 Elixir 本身的一部分提供。"HTML+EEx" 是 Phoenix 对 EEx 的扩展,它支持 HTML,并支持 HTML 验证、组件和值的自动转义。后者可以保护您免受跨站脚本 (XSS) 攻击,而无需您额外操作。
模板文件的工作方式相同。函数组件非常适合小型模板,而当您有大量标记或您的函数变得难以管理时,单独的文件是一个不错的选择。
让我们尝试在单独的文件中定义一个模板。首先,删除我们上面的 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
中创建一个 index.html.heex
文件
<section>
<h2>Hello World, from Phoenix!</h2>
</section>
模板文件会被编译成模块本身的函数组件,这两种风格之间没有运行时或性能差异。
现在我们有了路由、控制器、视图和模板,我们应该能够将浏览器指向 http://localhost:4000/hello 并看到来自 Phoenix 的问候!(如果您在此过程中停止了服务器,则重新启动服务器的任务是 mix phx.server
。)
我们刚刚做的事情有一些有趣的地方。我们不需要在进行这些更改时停止和重新启动服务器。是的,Phoenix 具有热代码重载功能!此外,即使我们的 index.html.heex
文件只包含一个 section
标签,我们得到的页面也是一个完整的 HTML 文档。我们的 index 模板实际上被渲染到布局中:首先它渲染 lib/hello_web/components/layouts/root.html.heex
,然后渲染 lib/hello_web/components/layouts/app.html.heex
,最后包含我们的内容。如果您打开这些文件,您将在底部看到一行代码,如下所示
<%= @inner_content %>
这将在将 HTML 发送到浏览器之前将我们的模板注入到布局中。我们将在控制器指南中详细介绍布局。
关于热代码重载的说明:一些具有自动 linter 的编辑器可能会阻止热代码重载工作。如果它对您不起作用,请参阅此问题中的讨论。
从端点到视图
在我们构建第一个页面时,我们开始理解请求生命周期是如何组成的。现在让我们更全面地了解一下。
所有 HTTP 请求都从应用程序端点开始。您可以在 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
模块。这使得端点可以将所有后续请求处理委托给路由器。正如我们现在所知,它的主要职责是将动词/路径对映射到控制器。然后控制器告诉视图渲染一个模板。
此时,您可能认为为了简单地渲染一个页面,需要执行很多步骤。但是,随着应用程序复杂度的增加,我们将看到每个层都具有不同的目的
端点 (
Phoenix.Endpoint
) - 端点包含所有请求都会经过的公共路径和初始路径。如果您希望在所有请求上执行某些操作,则应将其添加到端点中。路由器 (
Phoenix.Router
) - 路由器负责将动词/路径分派给控制器。路由器还允许我们对功能进行范围界定。例如,应用程序中的某些页面可能需要用户身份验证,而其他页面则不需要。控制器 (
Phoenix.Controller
) - 控制器的任务是检索请求信息、与您的业务领域进行交互,并为表示层准备数据。视图 - 视图处理来自控制器的结构化数据,并将其转换为向用户展示的表示形式。视图通常以它们渲染的内容格式命名。
让我们通过添加另一个页面来快速回顾一下最后三个组件是如何协同工作的。
另一个新页面
让我们为我们的应用程序添加一点复杂性。我们将添加一个新页面,它将识别 URL 的一部分,将其标记为“messenger”,并将其通过控制器传递到模板中,以便我们的 messenger 可以打招呼。
和上次一样,我们要做的第一件事是创建一个新的路由。
另一个新路由
对于此练习,我们将重用在 上一步 创建的 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,以便我们可以将其(messenger)传递给模板。为此,我们将此 show 函数添加到控制器中
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
在 show
操作的主体中,我们还向渲染函数传递了第三个参数,这是一个键值对,其中 :messenger
是键,messenger
变量作为值传递。
如果操作的主体需要访问绑定到 params
变量的完整参数映射,除了绑定 messenger 变量之外,我们可以这样定义 show/2
def show(conn, %{"messenger" => messenger} = params) do
...
end
请记住,params
映射的键始终是字符串,等号并不代表赋值,而是 模式匹配 断言。
另一个新模板
对于这个谜题的最后一块,我们需要一个新的模板。由于它是针对 HelloController
的 show
操作的,因此它将进入 lib/hello_web/controllers/hello_html
目录,并被称为 show.html.heex
。它看起来与我们的 index.html.heex
模板非常相似,只是我们需要显示 messenger 的姓名。
为此,我们将使用用于执行 Elixir 表达式的特殊 HEEx 标记:<%= %>
。请注意,初始标记有一个等号,如下所示:<%=
。这意味着任何位于这些标记之间的 Elixir 代码都将被执行,并且生成的值将替换 HTML 输出中的标记。如果缺少等号,代码仍然会被执行,但值不会出现在页面上。
请记住,我们的模板是用 HEEx(HTML+EEx)编写的。HEEx 是 EEx 的超集,这就是它共享 <%= %>
语法的原因。
模板应该如下所示
<section>
<h2>Hello World, from <%= @messenger %>!</h2>
</section>
我们的 messenger 显示为 @messenger
。
我们从控制器传递给视图的值统称为“分配”。我们可以通过 assigns.messenger
访问我们的 messenger 值,但通过一些元编程,Phoenix 为我们提供了更简洁的 @
语法供在模板中使用。
我们完成了。如果将浏览器指向 http://localhost:4000/hello/Frank,您应该会看到一个页面,如下所示
玩一会儿。您在 /hello/
之后输入的任何内容都将在页面上显示为您的 messenger。