目录结构 #

一、项目根目录 #

1.1 根目录结构 #

text
hello/
├── _build/              # 编译输出
├── assets/              # 前端资源
├── config/              # 配置文件
├── deps/                # 依赖包
├── lib/                 # 源代码
├── priv/                # 私有资源
├── test/                # 测试文件
├── .formatter.exs       # 格式化配置
├── .gitignore           # Git忽略文件
├── mix.exs              # 项目定义
├── mix.lock             # 依赖锁定
└── README.md            # 项目说明

1.2 核心文件说明 #

文件/目录 说明
mix.exs 项目定义、依赖配置
mix.lock 依赖版本锁定
config/ 环境配置文件
lib/ 应用源代码
priv/ 数据库迁移、静态资源
test/ 测试文件

二、lib目录详解 #

2.1 lib目录结构 #

text
lib/
├── hello/                    # 业务逻辑层
│   ├── application.ex        # OTP应用
│   ├── repo.ex              # Ecto仓库
│   ├── accounts/            # Accounts Context
│   │   ├── accounts.ex      # Context模块
│   │   ├── user.ex          # User Schema
│   │   └── user_token.ex    # UserToken Schema
│   └── mailer.ex            # 邮件发送
│
└── hello_web/               # Web层
    ├── controllers/         # 控制器
    │   ├── page_controller.ex
    │   ├── page_html.ex
    │   └── page_html/
    │       └── home.html.heex
    ├── components/          # 组件
    │   ├── core_components.ex
    │   └── layouts.ex
    ├── endpoint.ex          # HTTP入口
    ├── router.ex            # 路由定义
    ├── telemetry.ex         # 监控指标
    └── gettext.ex           # 国际化

2.2 业务逻辑层 (lib/hello) #

text
lib/hello/
├── application.ex           # OTP应用定义
├── repo.ex                 # Ecto仓库配置
├── accounts/               # 用户账户Context
│   ├── accounts.ex         # 公共API
│   ├── user.ex             # 用户Schema
│   └── user_token.ex       # 令牌Schema
└── mailer.ex               # 邮件配置

application.ex

elixir
defmodule Hello.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      HelloWeb.Telemetry,
      Hello.Repo,
      {DNSCluster, query: Application.get_env(:hello, :dns_cluster_query) || :ignore},
      {Phoenix.PubSub, name: Hello.PubSub},
      HelloWeb.Endpoint
    ]

    opts = [strategy: :one_for_one, name: Hello.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

repo.ex

elixir
defmodule Hello.Repo do
  use Ecto.Repo,
    otp_app: :hello,
    adapter: Ecto.Adapters.Postgres
end

2.3 Web层 (lib/hello_web) #

text
lib/hello_web/
├── controllers/            # 控制器目录
│   ├── page_controller.ex  # 页面控制器
│   ├── page_html.ex        # 页面视图模块
│   └── page_html/          # 页面模板目录
│       └── home.html.heex  # 首页模板
│
├── components/             # 组件目录
│   ├── core_components.ex  # 核心组件
│   └── layouts.ex          # 布局组件
│
├── endpoint.ex             # Endpoint定义
├── router.ex               # 路由定义
├── telemetry.ex            # 监控配置
├── gettext.ex              # 国际化配置
└── error_html.ex           # 错误页面

三、config目录详解 #

3.1 配置文件结构 #

text
config/
├── config.exs              # 基础配置
├── dev.exs                 # 开发环境配置
├── runtime.exs             # 运行时配置
└── test.exs                # 测试环境配置

3.2 配置文件说明 #

config.exs - 基础配置

elixir
import Config

config :hello,
  ecto_repos: [Hello.Repo]

config :hello, HelloWeb.Endpoint,
  url: [host: "localhost"],
  render_errors: [
    formats: [html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON],
    layout: false
  ],
  pubsub_server: Hello.PubSub,
  live_view: [signing_salt: "your_salt"]

config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

import_config "#{config_env()}.exs"

dev.exs - 开发环境配置

elixir
import Config

config :hello, HelloWeb.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 4000],
  check_origin: false,
  code_reloader: true,
  debug_errors: true,
  secret_key_base: "dev_secret_key"

config :hello, HelloWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
      ~r"lib/hello_web/(controllers|live|components)/.*(ex|heex)$"
    ]
  ]

config :hello, Hello.Repo,
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  database: "hello_dev",
  stacktrace: true,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

runtime.exs - 运行时配置

elixir
import Config

if config_env() == :prod do
  database_url =
    System.get_env("DATABASE_URL") ||
      raise """
      environment variable DATABASE_URL is missing.
      """

  config :hello, Hello.Repo,
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

  secret_key_base =
    System.get_env("SECRET_KEY_BASE") ||
      raise """
      environment variable SECRET_KEY_BASE is missing.
      """

  config :hello, HelloWeb.Endpoint,
    http: [
      ip: {0, 0, 0, 0},
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: secret_key_base
end

四、priv目录详解 #

4.1 priv目录结构 #

text
priv/
├── repo/
│   ├── migrations/          # 数据库迁移文件
│   │   ├── 20240101000001_create_users.exs
│   │   └── 20240101000002_create_posts.exs
│   └── seeds.exs           # 种子数据
│
└── static/                 # 编译后的静态资源
    ├── assets/
    ├── images/
    └── favicon.ico

4.2 迁移文件示例 #

elixir
defmodule Hello.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string, null: false
      add :hashed_password, :string, null: false
      add :confirmed_at, :naive_datetime

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end

4.3 种子数据 #

elixir
alias Hello.Accounts

for email <- ["alice@example.com", "bob@example.com"] do
  Accounts.register_user(%{email: email, password: "password123"})
end

五、assets目录详解 #

5.1 assets目录结构 #

text
assets/
├── css/
│   ├── app.css             # 主样式文件
│   └── phoenix.css         # Phoenix默认样式
│
├── js/
│   └── app.js              # 主JavaScript文件
│
├── vendor/
│   └── topbar.js           # 第三方库
│
└── tailwind.config.js      # Tailwind配置

5.2 CSS文件示例 #

css
defmodule HelloWeb.Layouts do
  use HelloWeb, :html

  embed_templates "layouts/*"
end

5.3 JavaScript文件 #

javascript
import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken}
})

topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

liveSocket.connect()
window.liveSocket = liveSocket

六、test目录详解 #

6.1 test目录结构 #

text
test/
├── hello/                  # 业务逻辑测试
│   └── accounts_test.exs
│
├── hello_web/              # Web层测试
│   ├── controllers/
│   │   └── page_controller_test.exs
│   └── live/
│       └── user_live_test.exs
│
├── support/                # 测试辅助
│   ├── fixtures/
│   │   └── accounts_fixtures.ex
│   ├── conn_case.ex
│   └── data_case.ex
│
└── test_helper.exs         # 测试配置

6.2 测试文件示例 #

elixir
defmodule HelloWeb.PageControllerTest do
  use HelloWeb.ConnCase

  test "GET /", %{conn: conn} do
    conn = get(conn, ~p"/")
    assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
  end
end

七、Context设计模式 #

7.1 Context结构 #

text
lib/hello/
├── accounts/               # Accounts Context
│   ├── accounts.ex         # 公共API
│   ├── user.ex             # User Schema
│   ├── user_token.ex       # UserToken Schema
│   └── accounts.ex         # 业务逻辑
│
├── blog/                   # Blog Context
│   ├── blog.ex             # 公共API
│   ├── post.ex             # Post Schema
│   └── comment.ex          # Comment Schema
│
└── shop/                   # Shop Context
    ├── shop.ex             # 公共API
    ├── product.ex          # Product Schema
    └── order.ex            # Order Schema

7.2 Context模块示例 #

elixir
defmodule Hello.Accounts do
  alias Hello.Repo
  alias Hello.Accounts.User

  def list_users do
    Repo.all(User)
  end

  def get_user!(id), do: Repo.get!(User, id)

  def get_user_by_email(email) do
    Repo.get_by(User, email: email)
  end

  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end

  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end

  def delete_user(%User{} = user) do
    Repo.delete(user)
  end

  def change_user(%User{} = user, attrs \\ %{}) do
    User.changeset(user, attrs)
  end
end

八、编译输出目录 #

8.1 _build目录 #

text
_build/
├── dev/
│   └── lib/
│       ├── hello/
│       ├── phoenix/
│       └── ecto/
│
└── test/
    └── lib/

8.2 deps目录 #

text
deps/
├── phoenix/
├── phoenix_ecto/
├── ecto/
├── ecto_sql/
├── postgrex/
├── phoenix_live_view/
└── ...

九、文件命名规范 #

9.1 命名约定 #

类型 文件名 模块名
控制器 user_controller.ex UserController
视图 user_html.ex UserHTML
模板 index.html.heex -
Schema user.ex User
Context accounts.ex Accounts
迁移 20240101_create_users.exs CreateUsers
测试 user_test.exs UserTest

9.2 目录命名 #

text
lib/hello/accounts/         # 小写下划线
lib/hello_web/controllers/  # 小写下划线
test/hello_web/             # 小写下划线

十、总结 #

10.1 核心目录 #

目录 说明
lib/hello/ 业务逻辑层
lib/hello_web/ Web层
config/ 配置文件
priv/repo/ 数据库迁移
assets/ 前端资源
test/ 测试文件

10.2 设计原则 #

  • 业务逻辑与Web分离: lib/hello/ vs lib/hello_web/
  • Context模式: 按领域组织代码
  • 约定优于配置: 遵循Phoenix约定
  • 模块化: 每个文件一个模块

10.3 下一步 #

现在你已经了解了Phoenix的目录结构,接下来让我们学习 路由基础,深入了解Phoenix的路由系统!

最后更新:2026-03-28