phoenix-framework

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Phoenix Framework Development

Phoenix Framework 开发

This skill activates when working with Phoenix web applications, including setup, development, LiveView, contexts, controllers, and channels.
本技能适用于Phoenix Web应用相关工作场景,包括项目搭建、开发、LiveView实现、上下文设计、控制器开发以及Channels配置等。

When to Use This Skill

适用场景

Activate this skill when:
  • Creating or modifying Phoenix applications
  • Implementing LiveView components or pages
  • Working with Phoenix contexts and business logic
  • Building real-time features with channels or LiveView
  • Configuring Phoenix routers, plugs, or endpoints
  • Troubleshooting Phoenix-specific issues
在以下场景中激活本技能:
  • 创建或修改Phoenix应用
  • 实现LiveView组件或页面
  • 处理Phoenix上下文与业务逻辑
  • 基于Channels或LiveView构建实时功能
  • 配置Phoenix路由、Plugs或Endpoints
  • 排查Phoenix相关问题

Phoenix Project Structure

Phoenix项目结构

Follow Phoenix conventions:
lib/
  my_app/           # Business logic and contexts
    accounts/       # Domain contexts
    repo.ex
  my_app_web/       # Web interface
    controllers/
    live/           # LiveView modules
    components/     # Function components
    router.ex
    endpoint.ex
遵循Phoenix的约定:
lib/
  my_app/           # Business logic and contexts
    accounts/       # Domain contexts
    repo.ex
  my_app_web/       # Web interface
    controllers/
    live/           # LiveView modules
    components/     # Function components
    router.ex
    endpoint.ex

Context-Driven Design

上下文驱动设计

Organize business logic into contexts (bounded domains):
将业务逻辑组织到上下文(限界领域)中:

Creating Contexts

创建上下文

Generate contexts with related schemas:
bash
mix phx.gen.context Accounts User users email:string name:string
Structure contexts to encapsulate business logic:
elixir
defmodule MyApp.Accounts do
  @moduledoc """
  The Accounts context - manages user accounts and authentication.
  """

  alias MyApp.Repo
  alias MyApp.Accounts.User

  def list_users do
    Repo.all(User)
  end

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

  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end
end
生成包含相关Schema的上下文:
bash
mix phx.gen.context Accounts User users email:string name:string
结构化上下文以封装业务逻辑:
elixir
defmodule MyApp.Accounts do
  @moduledoc """
  The Accounts context - manages user accounts and authentication.
  """

  alias MyApp.Repo
  alias MyApp.Accounts.User

  def list_users do
    Repo.all(User)
  end

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

  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

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

Context Best Practices

上下文最佳实践

  • Keep contexts focused on a single domain
  • Avoid cross-context dependencies when possible
  • Use public API functions, not direct Repo access in web layer
  • Name contexts after business domains, not technical layers
  • 保持上下文聚焦于单一领域
  • 尽可能避免跨上下文依赖
  • 在Web层使用公开API函数,而非直接访问Repo
  • 以业务领域命名上下文,而非技术层

LiveView Development

LiveView开发

LiveView enables rich, real-time experiences without writing JavaScript.
LiveView无需编写JavaScript即可实现丰富的实时交互体验。

LiveView Lifecycle

LiveView生命周期

Understand the mount → handle_event → render cycle:
elixir
defmodule MyAppWeb.UserLive.Index do
  use MyAppWeb, :live_view

  alias MyApp.Accounts

  @impl true
  def mount(_params, _session, socket) do
    # Runs on initial page load and live connection
    {:ok, assign(socket, :users, list_users())}
  end

  @impl true
  def handle_params(params, _url, socket) do
    # Runs after mount and on live patch
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    user = Accounts.get_user!(id)
    {:ok, _} = Accounts.delete_user(user)

    {:noreply, assign(socket, :users, list_users())}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.table rows={@users} id="users">
        <:col :let={user} label="Name"><%= user.name %></:col>
        <:col :let={user} label="Email"><%= user.email %></:col>
        <:action :let={user}>
          <.button phx-click="delete" phx-value-id={user.id}>Delete</.button>
        </:action>
      </.table>
    </div>
    """
  end

  defp list_users do
    Accounts.list_users()
  end
end
理解mount → handle_event → render的循环流程:
elixir
defmodule MyAppWeb.UserLive.Index do
  use MyAppWeb, :live_view

  alias MyApp.Accounts

  @impl true
  def mount(_params, _session, socket) do
    # Runs on initial page load and live connection
    {:ok, assign(socket, :users, list_users())}
  end

  @impl true
  def handle_params(params, _url, socket) do
    # Runs after mount and on live patch
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    user = Accounts.get_user!(id)
    {:ok, _} = Accounts.delete_user(user)

    {:noreply, assign(socket, :users, list_users())}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.table rows={@users} id="users">
        <:col :let={user} label="Name"><%= user.name %></:col>
        <:col :let={user} label="Email"><%= user.email %></:col>
        <:action :let={user}>
          <.button phx-click="delete" phx-value-id={user.id}>Delete</.button>
        </:action>
      </.table>
    </div>
    """
  end

  defp list_users do
    Accounts.list_users()
  end
end

LiveView Best Practices

LiveView最佳实践

  • Use
    mount/3
    for initial data loading
  • Handle route changes in
    handle_params/3
  • Keep renders fast - compute in event handlers, not render
  • Use
    assign_new/3
    for expensive computations
  • Prefer LiveView over JavaScript for interactive UIs
  • Use
    phx-debounce
    and
    phx-throttle
    for frequent events
  • 使用
    mount/3
    加载初始数据
  • handle_params/3
    中处理路由变更
  • 保持渲染速度——在事件处理器中计算,而非渲染阶段
  • 使用
    assign_new/3
    处理昂贵的计算操作
  • 对于交互式UI,优先使用LiveView而非JavaScript
  • 对频繁触发的事件使用
    phx-debounce
    phx-throttle

Function Components

函数组件

Create reusable components:
elixir
defmodule MyAppWeb.Components.UserCard do
  use Phoenix.Component

  attr :user, :map, required: true
  attr :class, :string, default: ""

  def user_card(assigns) do
    ~H"""
    <div class={"card " <> @class}>
      <h3><%= @user.name %></h3>
      <p><%= @user.email %></p>
    </div>
    """
  end
end
Use with
<.user_card user={@current_user} />
in templates.
创建可复用组件:
elixir
defmodule MyAppWeb.Components.UserCard do
  use Phoenix.Component

  attr :user, :map, required: true
  attr :class, :string, default: ""

  def user_card(assigns) do
    ~H"""
    <div class={"card " <> @class}>
      <h3><%= @user.name %></h3>
      <p><%= @user.email %></p>
    </div>
    """
  end
end
在模板中通过
<.user_card user={@current_user} />
使用组件。

Form Handling

表单处理

Use changesets for validation:
elixir
@impl true
def mount(_params, _session, socket) do
  changeset = Accounts.change_user(%User{})
  {:ok, assign(socket, form: to_form(changeset))}
end

@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
  changeset =
    %User{}
    |> Accounts.change_user(user_params)
    |> Map.put(:action, :validate)

  {:noreply, assign(socket, form: to_form(changeset))}
end

@impl true
def handle_event("save", %{"user" => user_params}, socket) do
  case Accounts.create_user(user_params) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "User created successfully")
       |> push_navigate(to: ~p"/users/#{user}")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

def render(assigns) do
  ~H"""
  <.form for={@form} phx-change="validate" phx-submit="save">
    <.input field={@form[:name]} label="Name" />
    <.input field={@form[:email]} label="Email" type="email" />
    <.button>Save</.button>
  </.form>
  """
end
使用Changeset进行验证:
elixir
@impl true
def mount(_params, _session, socket) do
  changeset = Accounts.change_user(%User{})
  {:ok, assign(socket, form: to_form(changeset))}
end

@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
  changeset =
    %User{}
    |> Accounts.change_user(user_params)
    |> Map.put(:action, :validate)

  {:noreply, assign(socket, form: to_form(changeset))}
end

@impl true
def handle_event("save", %{"user" => user_params}, socket) do
  case Accounts.create_user(user_params) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "User created successfully")
       |> push_navigate(to: ~p"/users/#{user}")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

def render(assigns) do
  ~H"""
  <.form for={@form} phx-change="validate" phx-submit="save">
    <.input field={@form[:name]} label="Name" />
    <.input field={@form[:email]} label="Email" type="email" />
    <.button>Save</.button>
  </.form>
  """
end

Routing

路由

Route Organization

路由组织

Structure routes logically:
elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    live "/", HomeLive, :index
    live "/users", UserLive.Index, :index
    live "/users/new", UserLive.Index, :new
    live "/users/:id", UserLive.Show, :show
  end

  scope "/api", MyAppWeb do
    pipe_through :api

    resources "/users", UserController, except: [:new, :edit]
  end
end
按逻辑结构配置路由:
elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    live "/", HomeLive, :index
    live "/users", UserLive.Index, :index
    live "/users/new", UserLive.Index, :new
    live "/users/:id", UserLive.Show, :show
  end

  scope "/api", MyAppWeb do
    pipe_through :api

    resources "/users", UserController, except: [:new, :edit]
  end
end

LiveView Routes

LiveView路由

Use live actions for modal/overlay states:
elixir
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id/edit", UserLive.Index, :edit
Then handle in
handle_params/3
:
elixir
defp apply_action(socket, :edit, %{"id" => id}) do
  socket
  |> assign(:page_title, "Edit User")
  |> assign(:user, Accounts.get_user!(id))
end

defp apply_action(socket, :new, _params) do
  socket
  |> assign(:page_title, "New User")
  |> assign(:user, %User{})
end

defp apply_action(socket, :index, _params) do
  socket
  |> assign(:page_title, "Listing Users")
  |> assign(:user, nil)
end
使用Live Actions实现模态框/覆盖层状态:
elixir
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id/edit", UserLive.Index, :edit
随后在
handle_params/3
中处理:
elixir
defp apply_action(socket, :edit, %{"id" => id}) do
  socket
  |> assign(:page_title, "Edit User")
  |> assign(:user, Accounts.get_user!(id))
end

defp apply_action(socket, :new, _params) do
  socket
  |> assign(:page_title, "New User")
  |> assign(:user, %User{})
end

defp apply_action(socket, :index, _params) do
  socket
  |> assign(:page_title, "Listing Users")
  |> assign(:user, nil)
end

Channels and PubSub

Channels与PubSub

Phoenix Channels

Phoenix Channels

For custom real-time protocols:
elixir
defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  @impl true
  def join("room:" <> room_id, _payload, socket) do
    if authorized?(socket, room_id) do
      {:ok, assign(socket, :room_id, room_id)}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  @impl true
  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast!(socket, "new_msg", %{body: body, user: socket.assigns.user})
    {:noreply, socket}
  end
end
适用于自定义实时协议场景:
elixir
defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  @impl true
  def join("room:" <> room_id, _payload, socket) do
    if authorized?(socket, room_id) do
      {:ok, assign(socket, :room_id, room_id)}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  @impl true
  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast!(socket, "new_msg", %{body: body, user: socket.assigns.user})
    {:noreply, socket}
  end
end

Phoenix PubSub

Phoenix PubSub

For LiveView updates and process communication:
elixir
undefined
适用于LiveView更新与进程间通信:
elixir
undefined

Subscribe in mount

Subscribe in mount

def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(MyApp.PubSub, "users") end
{:ok, assign(socket, :users, list_users())} end
def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(MyApp.PubSub, "users") end
{:ok, assign(socket, :users, list_users())} end

Handle broadcasts

Handle broadcasts

def handle_info({:user_created, user}, socket) do {:noreply, update(socket, :users, fn users -> [user | users] end)} end
def handle_info({:user_created, user}, socket) do {:noreply, update(socket, :users, fn users -> [user | users] end)} end

Broadcast from context

Broadcast from context

def create_user(attrs) do with {:ok, user} <- do_create_user(attrs) do Phoenix.PubSub.broadcast(MyApp.PubSub, "users", {:user_created, user}) {:ok, user} end end
undefined
def create_user(attrs) do with {:ok, user} <- do_create_user(attrs) do Phoenix.PubSub.broadcast(MyApp.PubSub, "users", {:user_created, user}) {:ok, user} end end
undefined

Testing Phoenix Applications

Phoenix应用测试

Controller Tests

控制器测试

elixir
defmodule MyAppWeb.UserControllerTest do
  use MyAppWeb.ConnCase, async: true

  test "GET /users", %{conn: conn} do
    conn = get(conn, ~p"/users")
    assert html_response(conn, 200) =~ "Listing Users"
  end
end
elixir
defmodule MyAppWeb.UserControllerTest do
  use MyAppWeb.ConnCase, async: true

  test "GET /users", %{conn: conn} do
    conn = get(conn, ~p"/users")
    assert html_response(conn, 200) =~ "Listing Users"
  end
end

LiveView Tests

LiveView测试

elixir
defmodule MyAppWeb.UserLiveTest do
  use MyAppWeb.ConnCase

  import Phoenix.LiveViewTest

  test "displays users", %{conn: conn} do
    user = insert(:user)

    {:ok, view, html} = live(conn, ~p"/users")

    assert html =~ user.name
    assert has_element?(view, "#user-#{user.id}")
  end

  test "creates user", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/users/new")

    assert view
           |> form("#user-form", user: %{name: "Alice", email: "alice@example.com"})
           |> render_submit()

    assert_patch(view, ~p"/users")
  end
end
elixir
defmodule MyAppWeb.UserLiveTest do
  use MyAppWeb.ConnCase

  import Phoenix.LiveViewTest

  test "displays users", %{conn: conn} do
    user = insert(:user)

    {:ok, view, html} = live(conn, ~p"/users")

    assert html =~ user.name
    assert has_element?(view, "#user-#{user.id}")
  end

  test "creates user", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/users/new")

    assert view
           |> form("#user-form", user: %{name: "Alice", email: "alice@example.com"})
           |> render_submit()

    assert_patch(view, ~p"/users")
  end
end

Channel Tests

Channel测试

elixir
defmodule MyAppWeb.RoomChannelTest do
  use MyAppWeb.ChannelCase

  test "broadcasts are pushed to the client", %{socket: socket} do
    {:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{})

    broadcast_from!(socket, "new_msg", %{body: "test"})
    assert_broadcast "new_msg", %{body: "test"}
  end
end
elixir
defmodule MyAppWeb.RoomChannelTest do
  use MyAppWeb.ChannelCase

  test "broadcasts are pushed to the client", %{socket: socket} do
    {:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{})

    broadcast_from!(socket, "new_msg", %{body: "test"})
    assert_broadcast "new_msg", %{body: "test"}
  end
end

Common Patterns

常见模式

Loading Associations

关联数据加载

Preload associations efficiently:
elixir
def list_posts do
  Post
  |> preload([:author, comments: :author])
  |> Repo.all()
end
高效预加载关联数据:
elixir
def list_posts do
  Post
  |> preload([:author, comments: :author])
  |> Repo.all()
end

Pagination

分页

Use Scrivener or custom pagination:
elixir
def list_users(page \\ 1) do
  User
  |> order_by(desc: :inserted_at)
  |> Repo.paginate(page: page, page_size: 20)
end
使用Scrivener或自定义分页实现:
elixir
def list_users(page \\ 1) do
  User
  |> order_by(desc: :inserted_at)
  |> Repo.paginate(page: page, page_size: 20)
end

File Uploads

文件上传

Handle uploads in LiveView:
elixir
def mount(_params, _session, socket) do
  {:ok,
   socket
   |> assign(:uploaded_files, [])
   |> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
end

def handle_event("save", _params, socket) do
  uploaded_files =
    consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
      dest = Path.join("priv/static/uploads", Path.basename(path))
      File.cp!(path, dest)
      {:ok, "/uploads/" <> Path.basename(dest)}
    end)

  {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end
在LiveView中处理文件上传:
elixir
def mount(_params, _session, socket) do
  {:ok,
   socket
   |> assign(:uploaded_files, [])
   |> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
end

def handle_event("save", _params, socket) do
  uploaded_files =
    consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
      dest = Path.join("priv/static/uploads", Path.basename(path))
      File.cp!(path, dest)
      {:ok, "/uploads/" <> Path.basename(dest)}
    end)

  {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end

Performance Optimization

性能优化

Database Query Optimization

数据库查询优化

  • Use
    preload/2
    to avoid N+1 queries
  • Add database indexes for frequently queried fields
  • Use
    select/3
    to load only needed fields
  • Consider using
    Repo.stream/2
    for large datasets
  • 使用
    preload/2
    避免N+1查询问题
  • 为频繁查询的字段添加数据库索引
  • 使用
    select/3
    仅加载所需字段
  • 处理大型数据集时考虑使用
    Repo.stream/2

LiveView Performance

LiveView性能优化

  • Move expensive computations to
    handle_event
    or background jobs
  • Use
    assign_new/3
    for computed values
  • Implement
    handle_continue/2
    for async operations after mount
  • Use temporary assigns for large lists:
    assign(socket, :items, temporary: true)
  • 将昂贵的计算操作移至事件处理器或后台任务中
  • 使用
    assign_new/3
    处理计算值
  • 实现
    handle_continue/2
    以在mount后执行异步操作
  • 对大型列表使用临时赋值:
    assign(socket, :items, temporary: true)

Caching

缓存

Use Cachex or ETS for caching:
elixir
def get_user!(id) do
  Cachex.fetch(:users, id, fn ->
    {:commit, Repo.get!(User, id)}
  end)
end
使用Cachex或ETS实现缓存:
elixir
def get_user!(id) do
  Cachex.fetch(:users, id, fn ->
    {:commit, Repo.get!(User, id)}
  end)
end

Security Best Practices

安全最佳实践

  • Always validate and sanitize user input through changesets
  • Use CSRF protection (enabled by default)
  • Implement rate limiting for APIs
  • Use
    put_secure_browser_headers
    plug
  • Validate file uploads (type, size, content)
  • Use prepared statements (Ecto does this automatically)
  • Implement proper authentication and authorization
  • 始终通过Changeset验证和清理用户输入
  • 使用CSRF保护(默认已启用)
  • 为API实现速率限制
  • 使用
    put_secure_browser_headers
    插件
  • 验证文件上传的类型、大小和内容
  • 使用预编译语句(Ecto会自动处理)
  • 实现完善的身份认证与授权机制

Tidewave MCP Dev Tools

Tidewave MCP开发工具

Tidewave connects AI coding assistants to running Phoenix applications via MCP, exposing runtime introspection tools (Ecto schemas, code execution, docs, logs, SQL queries).
Tidewave通过MCP将AI代码助手与运行中的Phoenix应用连接,提供运行时内省工具(Ecto Schema、代码执行、文档、日志、SQL查询等)。

When to Use Tidewave

Tidewave适用场景

  • Introspecting a running Phoenix app (schemas, modules, logs)
  • Executing Elixir code or SQL queries against a live dev server
  • Looking up documentation for project dependencies at runtime
  • Debugging LiveView components with source annotations
  • 内省运行中的Phoenix应用(Schema、模块、日志)
  • 在运行的开发服务器上执行Elixir代码或SQL查询
  • 在运行时查找项目依赖的文档
  • 通过源码注解调试LiveView组件

Quick Setup

快速搭建

  1. Add dependency to
    mix.exs
    :
elixir
{:tidewave, "~> 0.5", only: :dev}
  1. Add plug to
    endpoint.ex
    (before
    code_reloading?
    ):
elixir
if Mix.env() == :dev do
  plug Tidewave
end
  1. Connect Claude Code to the MCP server:
bash
claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp
  1. mix.exs
    中添加依赖:
elixir
{:tidewave, "~> 0.5", only: :dev}
  1. endpoint.ex
    中添加插件(在
    code_reloading?
    之前):
elixir
if Mix.env() == :dev do
  plug Tidewave
end
  1. 将Claude Code连接到MCP服务器:
bash
claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp

Key MCP Tools

核心MCP工具

ToolPurpose
project_eval
Execute Elixir code in the running app
execute_sql_query
Run SQL queries against the database
get_ecto_schemas
List schemas with fields and associations
get_docs
Retrieve module/function documentation
get_source_location
Find source file paths and line numbers
get_logs
Access server logs
工具用途
project_eval
在运行中的应用中执行Elixir代码
execute_sql_query
针对数据库运行SQL查询
get_ecto_schemas
列出包含字段和关联的Schema
get_docs
获取模块/函数的文档
get_source_location
查找源文件路径和行号
get_logs
访问服务器日志

Security

安全注意事项

Tidewave is dev-only. Always guard with
Mix.env() == :dev
and never deploy to production. It only accepts localhost requests by default.
For full setup details, configuration options, LiveView debug annotations, and troubleshooting, see
references/tidewave.md
.
Tidewave仅适用于开发环境。始终通过
Mix.env() == :dev
进行防护,切勿部署到生产环境。默认情况下仅接受本地请求。
如需完整的搭建细节、配置选项、LiveView调试注解以及故障排除指南,请参阅
references/tidewave.md

Key Principles

核心原则

  • Context boundaries: Keep business logic in contexts, not controllers/LiveViews
  • LiveView first: Prefer LiveView over JavaScript for interactive features
  • Changesets for validation: Always validate through Ecto changesets
  • Pub/Sub for communication: Use Phoenix.PubSub for cross-process updates
  • Test at boundaries: Test contexts, controllers, and LiveViews separately
  • Follow conventions: Use Phoenix generators and follow established patterns
  • 上下文边界:将业务逻辑置于上下文中,而非控制器/LiveView中
  • 优先使用LiveView:对于交互式功能,优先使用LiveView而非JavaScript
  • Changeset验证:始终通过Ecto Changeset进行验证
  • Pub/Sub通信:使用Phoenix.PubSub实现跨进程更新
  • 边界测试:分别测试上下文、控制器和LiveView
  • 遵循约定:使用Phoenix生成器并遵循既定模式