我们的目标是给我们的 Phoenix
项目增加两个新的页面,一个是纯静态页面,另一个则从 url
里面截取一部分作为输入,
然后传递给模板显示。从这两个简单的例子中我们将会熟悉一个 Phoenix
项目的基本构成: 路由,控制器,视图以及模板。
当我们用命令产生一个 Phoenix
项目后,默认的目录结构如下:
├── _build
├── assets
├── config
├── deps
├── lib
│ └── hello
│ └── hello_web
│ └── hello.ex
│ └── hello_web.ex
├── priv
├── test
我们这里涉及的大部分内容都在 lib/hello_phoenix/web
目录下,也就是和 web 相关的
部分,它的结构展开如下图:
├── channels
│ └── user_socket.ex
├── controllers
│ └── page_controller.ex
├── templates
│ ├── layout
│ │ └── app.html.eex
│ └── page
│ └── index.html.eex
└── views
│ ├── error_helpers.ex
│ ├── error_view.ex
│ ├── layout_view.ex
│ └── page_view.ex
├── endpoint.ex
├── gettext.ex
├── router.ex
在 controllers
, templates
和 views
目录里的所有文件都是用于创建我们之前看到的那个
Welcome to Phenix
欢迎页面的,我们之后将重用这里的一些代码。在开发模式下,每一
次新的请求到达,web
目录都会自动重新编译。
我们应用所需的所有静态资源都在 assets
目录下(按照css,images或者js分类), 编译
过程(brunch或其他前端工具)会把这里对应的 js, css 文件分别编译到 priv/static
,
我们现在不会深入,只是留个印象。
├── assets
│ ├── css
│ │ └── app.css
│ ├── js
│ │ └── app.js
│ └── static
│ └── node_modules
│ └── vendor
我还需要了解一下 "非 web 相关的部分"。我的应用文件(负责启动我们的 Elixir 应用和
监测树)位于 lib/hello/application.ex
. 以及 Ecto Repo
lib/hello_phoenix/repo.ex
负责和数据库交互. 我们会在后面的章节介绍 Ecto.
lib
├── hello
| ├── application.ex
| └── repo.ex
├── hello_web
| ├── channels
| ├── controllers
| ├── templates
| ├── views
| ├── endpoint.ex
| ├── gettext.ex
| └── router.ex
我们的 web 相关的文件 -- 路由,控制器,模板,通道等都在 lib/hello_web
目录下,而其他的部分都在 lib/hello/
下,你可以像其他任何的 Elixir 应用
一样来组织他们。
唠叨的差不多了,现在让我们编写一个新的 Phoenix
页面吧!
路由将给定并唯一的 HTTP 动词/路径(verb/path)
映射到处理他们的 controller/action
, Phoenix
的路由
配置信息在 lib/hello_web/router.ex
文件,让我们打开来看看。
欢迎页面对应的路由是这条语句:
get "/", PageController, :index
让我们看看这代表什么意思呢,浏览器访问 http://localhost:4000/ 会给我们网站的根
目录发出一个GET
请求,这个请求会被 HelloPhoenix.pageController
中的 index
函数处理,后者定义在
lib/hello_web/controllers/page_controller.ex
文件中.
我们要建立的页面功能是:当浏览器访问 http://localhost:4000/hello 时,简单 的在页面上显示 'Hello World from Phoenix'.
首先我们要为这个页面增加一个路由,现在lib/hello_phoenix/web/router.ex
看起来如下:
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloWeb do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
end
# Other scopes may use custom stacks.
# scope "/api", HelloWeb do
# pipe_through :api
# end
end
现在,我们先不管 pipelines
和 scope
部分(如果你好奇的话,可以阅读 路由指北)
).
让我们为 /hello
的 GET
请求创建一个路由吧, 在稍后创建的HelloWeb.HelloController
的index 部分。
get "/hello", HelloController, :index
添加后,router.ex
文件的 scope '/'
部分看起来应该是这个样子:
scope "/", HelloPhoenix do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
get "/hello", HelloController, :index
end
控制器
里面其实就是 Elixir 的模块、action
和定义在其中的Elixir
函数,action
的作用是
收集和处理渲染页面所需的数据和某些操作 (gather any data and perform any tasks needed for rendering )。
我们现在要建立一个模块 HelloPhoenix.HelloController
, 里面包含 index/2
这个action ( /2,意思是需要两个参数的意思)。
具体的,我们建立文件 lib/hello_phoenix/web/controllers/hello_controller.ex
, 然后加入以下内容:
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def index(conn, _params) do
render conn, "index.html"
end
end
我们会在 控制器指北
章节中详细讨论 HelloPhoenix.Web, :controller
语句,现在我们先来看看 index/2
函数。
所有的 controller actions
都接受两个参数,第一个是 conn
--一个包含了大量请求(request)
信息的结构体。
第二个是params
--请求参数,我们的例子不需要params
, 所以加一个前缀 _
表示忽略该参数,否则编译过程会产生警告。
这个 action
的核心语句是 render conn, "index.html"
, Phoenix 会根据这条语句寻找一个叫做 index.html.eex
的模板并且渲染出来,查找规则是遵循 controller
的命名,即 lib/hello_web/templates/hello
。
注意 这里用原子(atom)的简写法也是可以的,比如
render conn, :index
,这是渲染机制就会根据请求头的规则自动寻找合适的模板,如index.html
或者index.json
等等。
负责具体渲染的模块是视图(views)
,我们现在就来建立一个。
视图(views)
负责几个重要的工作,它渲染模板,并且作为 controller
里 裸数据 (raw data)
的 展示层(presentation layer)
, 以及一些有用的函数将这些数据处理后供模板使用。
举个栗子,比如我们有一个包含了 first_name
字段和 last_name
的用户数据,然后在模板里,我们想显示这个
用户的全名。我们可以在模板里写函数把它们拼接起来,但更好的办法是在 视图(view)
里写一个函数
去做这件事,然后在模板里调用这个函数,这种解决方案会让模板变得干净且更加灵活。
为了能让我们的 HelloController
渲染模板,我们需要一个 HelloView
。这里的命名同样具有特殊意义-- 'Hello' 要和
控制器的'Hello'相对应,让我们先创建一个
lib/hello_web/views/hello_view.ex
文件(稍后解释细节),内容如下:
defmodule HelloWeb.HelloView do
use HelloWeb, :view
end
Phoenix 默认使用的模板引擎是eex
,意思是
嵌入式 Elixir (Embedded Elixir),
所以我们的模板文件都会带有.eex
后缀。
模板被视图所限定,视图又被控制器所限定,Phoenix 为我们创建了一个 lib/hello_phoenix/web/templates
目录来容纳这些文件,最佳实践就是使用命名空间来组织它们。所以在我们这个hello应用
中,这意味着我们需要在lib/hello_phoenix/web/templates
目录下面建立一个hello文件夹,再在里面建立一
个index.html.eex
文件,内容如下。
<div class="jumbotron">
<h2>Hello World, from Phoenix!</h2>
</div>
现在我们有了路由
,控制器
,视图
和 模板
,我们应该可以通过浏览器访问
http://localhost:4000/hello
看到新的欢迎页面了!(如果你之前停止了服务器可以通过 mix phoenix.server
重新启动)。
这里有些知识点需要注意:首先,改动代码以后,我们并不需要重启服务器,Phoenix
有代码热更新 ( hot code re-loading) 机制。另外,
尽管我们的 index.html.eex
文件只包含了一个div
标签,我们依然得到了一个"完整的"页面,我们的模板被渲染进了应
用布局(application layout) -- lib/hello_web/templates/layout/app.html.eex
。 如
果你打开它,就会发现有这样一行内容<%= render @view_module, @view_template, assigns %>
, 意思在被送往浏览器之前,你自己的模板会被"插入"到这里。
让我们为我们的应用增加一点复杂性。我们建立一个新的页面,她的功能是识别我们url的一部分,将她作为'信息'通过 给控制器传递给模板,然后显示出来。
就像我们之前做的那样,先添加一个路由。
在这个练习中,我们复用之前创建的 HelloController
然后添加一个新的 show
action, 增加的路由如下:
scope "/", HelloWeb do
pipe_through :browser # Use the default browser stack.
get "/", PageController, :index
get "/hello", HelloController, :index
get "/hello/:messenger", HelloController, :show
end
注意 这个原子写法:
:messenger
, Phoenix 会把 messenger 作为键,并把用户输入在这个地址后的任何输入作为值,组成一个Map
传递给 controller。
比如,如果我们在浏览器输入 http://localhost:4000/hello/Frank, ":messenger" 的值就会是 "Frank" 。
新的请求会被 HelloWeb.HelloController
的 show
action 处理,我们复用之前的
lib/hello_web/controllers/hello_controller.ex
文件,
在其中添加 show action 如下(注意其中字典的写法):
def show(conn, %{"messenger" => messenger}) do
render conn, "show.html", messenger: messenger
end
注意 如果你想要得到 params 的所有参数并绑定 messenger 变量,我们可以这样定义(类似js es6 的解构写法):
def show(conn, %{"messenger" => messenger} = params) do
...
end
注意所有的 keys 都是字符串,还有这里的
=
不是赋值符号,而是模式匹配(pattern match) 。
终于到最后一道菜了,一个新的模板。根据命名规则,Phoenix
会去 web/templates/hello
文件夹下寻找
show.html.eex
文件。她和之前的 index.eex
模板长的很像,除了一个 Elixir 表达式:<%= %>
,
注意标签里有个<%=
符号。这表示这个便签里的 Elixir 代码会被执行并且结果会覆盖这个标签,如果我们不
加 =
, 里面的 Elixir 代码依然会执行,但是结果却不会出现在页面上。
show.html.eex
文件的内容如下:
<div class="jumbotron">
<h2>Hello World, from <%= @messenger %>!</h2>
</div>
这里的@messenger并不是什么模块的属性,而是一个元编程的语法糖,表示 Map.get(assigns, :messenger)
。只是这种写法对于模板来说更加友好。
一切就绪,现在让我们打开 http://localhost:4000/hello/Frank 来检验成果吧!
现在无论的在 /hello/
后面输入什么,它都会出现在页面上了。