elixir-phoenix

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Phoenix Project Setup

Phoenix项目搭建

Standard patterns for Phoenix projects using Bun, Tailwind v4, devenv, and PostgreSQL.
使用Bun、Tailwind v4、devenv和PostgreSQL的Phoenix项目标准配置方案。

1. Create the Phoenix Project

1. 创建Phoenix项目

bash
undefined
bash
undefined

Standard project

标准项目

mix phx.new my_app --database postgres
mix phx.new my_app --database postgres

Umbrella app

Umbrella应用

mix phx.new my_app --umbrella --database postgres

After generation, replace the default esbuild config with bun.
mix phx.new my_app --umbrella --database postgres

项目生成后,将默认的esbuild配置替换为Bun。

2. Replace esbuild with Bun

2. 用Bun替换esbuild

In
mix.exs
deps, replace
{:esbuild, ...}
with:
elixir
{:bun, "~> 1.4", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
In
config/config.exs
, replace the
config :esbuild
block with:
elixir
config :bun,
  version: "1.3.4",
  my_app: [
    args: ~w(build assets/js/app.js --outdir=priv/static/assets --external /fonts/* --external /images/*),
    cd: Path.expand("../", __DIR__)
  ]

config :tailwind,
  version: "4.1.11",
  my_app: [
    args: ~w(--input=assets/css/app.css --output=priv/static/assets/app.css),
    cd: Path.expand("../", __DIR__)
  ]
For umbrella apps, use
Path.expand("../apps/my_app", __DIR__)
for
cd:
.
In
config/dev.exs
, update watchers:
elixir
watchers: [
  tailwind: {Tailwind, :install_and_run, [:my_app, ~w(--watch)]},
  bun: {Bun, :install_and_run, [:my_app, ~w(--sourcemap=inline --watch)]}
]
Update
mix.exs
aliases:
elixir
"assets.deploy": [
  "phx.digest.clean",
  "tailwind my_app --minify",
  "bun my_app --minify",
  "phx.digest"
]
mix.exs
的依赖中,将
{:esbuild, ...}
替换为:
elixir
{:bun, "~> 1.4", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
config/config.exs
中,将
config :esbuild
代码块替换为:
elixir
config :bun,
  version: "1.3.4",
  my_app: [
    args: ~w(build assets/js/app.js --outdir=priv/static/assets --external /fonts/* --external /images/*),
    cd: Path.expand("../", __DIR__)
  ]

config :tailwind,
  version: "4.1.11",
  my_app: [
    args: ~w(--input=assets/css/app.css --output=priv/static/assets/app.css),
    cd: Path.expand("../", __DIR__)
  ]
对于umbrella应用,
cd:
使用
Path.expand("../apps/my_app", __DIR__)
config/dev.exs
中,更新监控器:
elixir
watchers: [
  tailwind: {Tailwind, :install_and_run, [:my_app, ~w(--watch)]},
  bun: {Bun, :install_and_run, [:my_app, ~w(--sourcemap=inline --watch)]}
]
更新
mix.exs
中的别名:
elixir
"assets.deploy": [
  "phx.digest.clean",
  "tailwind my_app --minify",
  "bun my_app --minify",
  "phx.digest"
]

3. Set NODE_PATH for Phoenix Dependency Resolution

3. 设置NODE_PATH以解析Phoenix依赖

Set
NODE_PATH
to the project root's
deps/
directory so Bun resolves Node-style packages from Phoenix's
deps/
directory. Imports like
import {Socket} from "phoenix"
and
import "phoenix_html"
will resolve to the Elixir dependency packages without needing
node_modules/
.
In
devenv.nix
, this is already configured using an absolute path:
nix
env.NODE_PATH = "${config.git.root}/deps";
For non-devenv setups, export it in your shell or build scripts:
bash
export NODE_PATH="$(pwd)/deps"
No
npm install
or symlinks needed — Phoenix ships JS alongside its Elixir source in
deps/<package>/priv/static/
.
NODE_PATH
设置为项目根目录的
deps/
文件夹,这样Bun就能从Phoenix的
deps/
目录解析Node风格的包。像
import {Socket} from "phoenix"
import "phoenix_html"
这样的导入会直接解析到Elixir依赖包,无需
node_modules/
devenv.nix
中,已经通过绝对路径配置了这一变量:
nix
env.NODE_PATH = "${config.git.root}/deps";
对于非devenv环境,在shell或构建脚本中导出该变量:
bash
export NODE_PATH="$(pwd)/deps"
无需执行
npm install
或创建符号链接——Phoenix会将JS文件与Elixir源码一起放在
deps/<package>/priv/static/
目录中。

5. Configure runtime.exs for Binary Paths

5. 配置runtime.exs以设置二进制文件路径

In
config/runtime.exs
, read env vars so devenv-provided binaries are used instead of downloading copies. Both packages require explicit configuration:
elixir
if System.get_env("MIX_BUN_PATH") do
  config :bun, path: System.get_env("MIX_BUN_PATH")
end

if System.get_env("MIX_TAILWIND_PATH") do
  config :tailwind, path: System.get_env("MIX_TAILWIND_PATH")
end
When
path
is set, the hex packages skip downloading and use the provided binary directly. devenv sets these env vars via
lib.getExe
to point at the Nix store paths (see step 4).
config/runtime.exs
中,读取环境变量,从而使用devenv提供的二进制文件,而非下载副本。这两个包都需要显式配置:
elixir
if System.get_env("MIX_BUN_PATH") do
  config :bun, path: System.get_env("MIX_BUN_PATH")
end

if System.get_env("MIX_TAILWIND_PATH") do
  config :tailwind, path: System.get_env("MIX_TAILWIND_PATH")
end
当设置了
path
时,hex包会跳过下载步骤,直接使用提供的二进制文件。devenv通过
lib.getExe
设置这些环境变量,指向Nix存储路径(见步骤4)。

6. Set Up devenv

6. 设置devenv

See references/devenv-template.md for the full
devenv.yaml
and
devenv.nix
templates.
Key points:
  • lib.getExe
    resolves Nix store paths for
    MIX_BUN_PATH
    and
    MIX_TAILWIND_PATH
  • PostgreSQL runs via Unix socket only (
    listen_addresses = ""
    ) — no port conflicts
  • DATABASE_URL
    uses
    ?socket=
    parameter pointing to devenv's state directory
  • PGHOST
    is set so
    psql
    and Ecto both find the socket automatically
完整的
devenv.yaml
devenv.nix
模板请参考references/devenv-template.md
核心要点:
  • lib.getExe
    解析
    MIX_BUN_PATH
    MIX_TAILWIND_PATH
    对应的Nix存储路径
  • PostgreSQL仅通过Unix套接字运行(
    listen_addresses = ""
    )——避免端口冲突
  • DATABASE_URL
    使用
    ?socket=
    参数指向devenv的状态目录
  • 设置
    PGHOST
    以便
    psql
    和Ecto都能自动找到套接字

7. Configure Ecto for URL-based Connection

7. 配置Ecto以支持基于URL的连接

In
config/dev.exs
, support both Unix socket (devenv) and TCP (manual setup):
elixir
db_config =
  [
    username: System.get_env("POSTGRES_USER", "my_app_dev"),
    password: System.get_env("POSTGRES_PASSWORD", "my_app_dev"),
    database: System.get_env("POSTGRES_DB", "my_app_dev"),
    show_sensitive_data_on_connection_error: true,
    pool_size: 10
  ]
  |> then(fn config ->
    case System.get_env("PGHOST") do
      nil ->
        config ++ [hostname: System.get_env("POSTGRES_HOST", "localhost"),
                   port: String.to_integer(System.get_env("POSTGRES_PORT", "5432"))]
      pghost when is_binary(pghost) ->
        if String.starts_with?(pghost, "/") do
          config ++ [socket_dir: pghost]
        else
          config ++ [hostname: pghost,
                     port: String.to_integer(System.get_env("POSTGRES_PORT", "5432"))]
        end
    end
  end)

config :my_app, MyApp.Repo, db_config
This auto-detects whether
PGHOST
is a Unix socket path or a hostname.
config/dev.exs
中,同时支持Unix套接字(devenv环境)和TCP(手动配置)两种方式:
elixir
db_config =
  [
    username: System.get_env("POSTGRES_USER", "my_app_dev"),
    password: System.get_env("POSTGRES_PASSWORD", "my_app_dev"),
    database: System.get_env("POSTGRES_DB", "my_app_dev"),
    show_sensitive_data_on_connection_error: true,
    pool_size: 10
  ]
  |> then(fn config ->
    case System.get_env("PGHOST") do
      nil ->
        config ++ [hostname: System.get_env("POSTGRES_HOST", "localhost"),
                   port: String.to_integer(System.get_env("POSTGRES_PORT", "5432"))]
      pghost when is_binary(pghost) ->
        if String.starts_with?(pghost, "/") do
          config ++ [socket_dir: pghost]
        else
          config ++ [hostname: pghost,
                     port: String.to_integer(System.get_env("POSTGRES_PORT", "5432"))]
        end
    end
  end)

config :my_app, MyApp.Repo, db_config
该配置会自动检测
PGHOST
是Unix套接字路径还是主机名。

8. Production Database via URL

8. 通过URL配置生产环境数据库

In
config/runtime.exs
for prod:
elixir
if config_env() == :prod do
  database_url =
    System.get_env("DATABASE_URL") ||
      raise "DATABASE_URL is not set"

  config :my_app, MyApp.Repo,
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE", "10"))
end
在生产环境的
config/runtime.exs
中:
elixir
if config_env() == :prod do
  database_url =
    System.get_env("DATABASE_URL") ||
      raise "DATABASE_URL is not set"

  config :my_app, MyApp.Repo,
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE", "10"))
end

9. End-to-End Tests

9. 端到端测试

E2e tests live in
e2e_test/
alongside the standard
test/
folder. They are expensive and must never run automatically — only on explicit request.
Add the
test.e2e
alias in
mix.exs
:
elixir
defp aliases do
  [
    # ... existing aliases ...
    "test.e2e": ["run --no-halt", "cmd mix test e2e_test/"]
  ]
end
Configure
e2e_test/
as an extra test path in
mix.exs
project config so Mix finds the helpers, but exclude it from the default
mix test
run:
elixir
def project do
  [
    # ...
    test_paths: ["test"],
    elixirc_paths: elixirc_paths(Mix.env()),
  ]
end
Run e2e tests explicitly:
bash
mix test.e2e
Never add
test.e2e
to CI pipelines, pre-commit hooks, or any automated alias that runs as part of a normal build or test cycle.
端到端测试位于
e2e_test/
目录,与标准的
test/
文件夹同级。这类测试资源消耗较大,绝不能自动运行——仅在明确请求时执行。
mix.exs
中添加
test.e2e
别名:
elixir
defp aliases do
  [
    # ... 现有别名 ...
    "test.e2e": ["run --no-halt", "cmd mix test e2e_test/"]
  ]
end
mix.exs
的项目配置中,将
e2e_test/
设置为额外的测试路径,以便Mix能找到辅助函数,但将其排除在默认的
mix test
运行范围之外:
elixir
def project do
  [
    # ...
    test_paths: ["test"],
    elixirc_paths: elixirc_paths(Mix.env()),
  ]
end
显式运行端到端测试:
bash
mix test.e2e
切勿
test.e2e
添加到CI流水线、预提交钩子或任何作为正常构建或测试周期一部分自动运行的别名中。

Quick Reference

快速参考

ToolHex PackageEnv Vardevenv Source
Bun
{:bun, "~> 1.4"}
MIX_BUN_PATH
lib.getExe pkgs.bun
Tailwind
{:tailwind, "~> 0.2"}
MIX_TAILWIND_PATH
lib.getExe pkgs.tailwindcss_4
PostgreSQL
{:ecto_sql, ...}
+
{:postgrex, ...}
DATABASE_URL
/
PGHOST
services.postgres
工具Hex包环境变量devenv来源
Bun
{:bun, "~> 1.4"}
MIX_BUN_PATH
lib.getExe pkgs.bun
Tailwind
{:tailwind, "~> 0.2"}
MIX_TAILWIND_PATH
lib.getExe pkgs.tailwindcss_4
PostgreSQL
{:ecto_sql, ...}
+
{:postgrex, ...}
DATABASE_URL
/
PGHOST
services.postgres