创建 Phoenix 项目
# 安装 Phoenix 生成器
mix archive.install hex phx_new
# 创建新项目(含 Ecto 数据库 + LiveView)
mix phx.new blog --database postgres
# 只创建 API 项目(无 HTML 模板)
mix phx.new blog_api --no-html --no-assets
# 项目结构
# blog/
# ├── lib/
# │ ├── blog/ — 业务逻辑(Context 层)
# │ └── blog_web/ — Web 层(Router/Controller/View)
# │ ├── router.ex
# │ ├── endpoint.ex
# │ └── controllers/
# ├── priv/repo/migrations/ — 数据库迁移文件
# ├── config/ — 环境配置
# └── mix.exs
# 配置数据库(config/dev.exs)并初始化
mix ecto.create
mix ecto.migrate
# 启动开发服务器
mix phx.server
# 访问 http://localhost:4000
Router:路由定义
# lib/blog_web/router.ex
defmodule BlogWeb.Router do
use BlogWeb, :router
# Plug 管道:定义一组中间件
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, html: {BlogWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
end
# API 路由(通过 :api 管道)
scope "/api", BlogWeb do
pipe_through :api
resources "/posts", PostController, except: [:new, :edit]
# 自动生成:
# GET /api/posts index
# POST /api/posts create
# GET /api/posts/:id show
# PUT /api/posts/:id update
# DELETE /api/posts/:id delete
end
# 认证路由组
scope "/api/admin", BlogWeb do
pipe_through [:api, :require_auth]
get "/stats", AdminController, :stats
end
end
Controller:请求处理
# lib/blog_web/controllers/post_controller.ex
defmodule BlogWeb.PostController do
use BlogWeb, :controller
alias Blog.Posts
def index(conn, params) do
page = Map.get(params, "page", "1") |> String.to_integer()
posts = Posts.list_posts(page: page, per_page: 20)
render(conn, :index, posts: posts)
end
def show(conn, %{"id" => id}) do
case Posts.get_post(id) do
nil -> send_resp(conn, 404, "Not found")
post -> render(conn, :show, post: post)
end
end
def create(conn, %{"post" => post_params}) do
case Posts.create_post(post_params) do
{:ok, post} ->
conn
|> put_status(:created)
|> put_resp_header("location", "/api/posts/#{post.id}")
|> render(:show, post: post)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(:error, changeset: changeset)
end
end
end
Plug:中间件管道
Plug 是 Phoenix 的中间件规范,类似 Rack(Ruby)或 Express 的中间件。整个请求处理就是一条 Plug 管道:
# 自定义 Plug:JWT 认证
defmodule BlogWeb.Plugs.RequireAuth do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts
def call(conn, _opts) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
case Blog.Auth.verify_token(token) do
{:ok, user_id} ->
assign(conn, :current_user_id, user_id)
{:error, _} ->
conn |> send_resp(401, "Unauthorized") |> halt()
end
_ ->
conn |> send_resp(401, "Missing token") |> halt()
end
end
end
Ecto:数据库层
Schema 与 Changeset
# lib/blog/posts/post.ex
defmodule Blog.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
field :body, :text
field :published, :boolean, default: false
field :view_count, :integer, default: 0
belongs_to :author, Blog.Accounts.User
has_many :comments, Blog.Posts.Comment
timestamps() # 自动添加 inserted_at, updated_at
end
@doc "创建/更新 changeset,含验证"
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body, :published])
|> validate_required([:title, :body])
|> validate_length(:title, min: 5, max: 200)
|> validate_length(:body, min: 10)
end
end
数据库迁移
# 生成迁移文件
mix ecto.gen.migration create_posts
# priv/repo/migrations/20240101_create_posts.exs
defmodule Blog.Repo.Migrations.CreatePosts do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :string, null: false
add :body, :text, null: false
add :published, :boolean, default: false
add :view_count, :integer, default: 0
add :author_id, references(:users, on_delete: :delete_all)
timestamps()
end
create index(:posts, [:author_id])
create index(:posts, [:published, :inserted_at])
end
end
mix ecto.migrate
Context 层:业务逻辑
# lib/blog/posts.ex(Context 模块)
defmodule Blog.Posts do
import Ecto.Query
alias Blog.Repo
alias Blog.Posts.Post
def list_posts(opts \\ []) do
page = Keyword.get(opts, :page, 1)
per_page = Keyword.get(opts, :per_page, 20)
Post
|> where(published: true)
|> order_by([desc: :inserted_at])
|> limit(^per_page)
|> offset(^((page - 1) * per_page))
|> preload(:author)
|> Repo.all()
end
def get_post(id), do: Repo.get(Post, id)
def create_post(attrs) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
def update_post(post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
def delete_post(post), do: Repo.delete(post)
end
本章小结:Phoenix 的 MVC 架构清晰分离关注点:Router 定义 URL 到 Controller 的映射;Plug 管道处理中间件;Controller 协调业务逻辑与响应;Context 封装业务逻辑;Ecto Schema + Changeset 提供类型安全的数据库操作与验证。这个架构在大型项目中保持了良好的可维护性。下一章进入 Phoenix LiveView,体验无 JS 的实时 UI。