【Phoenix】请求的生命周期

本文的目的是讨论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

暂时忽略 pipelinescope ,在稍后的教程中再讨论它们。

让我们在 scope "/" do 下添加一个路由,将 GET /hello 请求映射到 HelloWeb.HelloControllerindex 方法。

scope "/", HelloWeb do 
	pipe_through :browser

	get "/", PageController, :home 
	get "/hello", HelloController, :index
end

新建controller

控制器是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】请求的生命周期_第1张图片

这里有些有趣的事情值得我们注意。当我们做这些变更时,不需要停止和重启服务器。没错,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被发送到浏览器之前将模板注入到布局中。关于布局我们会在后面的教程中介绍。

从endpoint到视图

我们已经创建了第一个页面,现在可以看看一个请求的生命周期是如何串联起来的了。

所有的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将所有请求的后续处理都交给路由器。路由器的主要作用就是将请求映射到处理器。最后处理器告诉视图渲染一个模板。

此时,你可能会想,简单地渲染一个页面怎么需要这么多步骤。但是,当应用变得越来越复杂时,我们会看到每一层都有其特殊的作用:

  • 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.HelloControllershow 函数处理。我们已经有了一个控制器 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 ,应该会看到下面的页面:

【Phoenix】请求的生命周期_第2张图片

你可能感兴趣的:(Phoenix,phoenix)