phoenix-thinking

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Phoenix Thinking

Phoenix 思考

Mental shifts for Phoenix applications. These insights challenge typical web framework patterns.
针对Phoenix应用的思维转变。这些见解挑战了典型的Web框架模式。

The Iron Law

铁律

NO DATABASE QUERIES IN MOUNT
mount/3 is called TWICE (HTTP request + WebSocket connection). Queries in mount = duplicate queries.
elixir
def mount(_params, _session, socket) do
  # NO database queries here! Called twice.
  {:ok, assign(socket, posts: [], loading: true)}
end

def handle_params(params, _uri, socket) do
  # Database queries here - once per navigation
  posts = Blog.list_posts(socket.assigns.scope)
  {:noreply, assign(socket, posts: posts, loading: false)}
end
mount/3 = setup only (empty assigns, subscriptions, defaults) handle_params/3 = data loading (all database queries, URL-driven state)
No exceptions: Don't query "just this one small thing" in mount. Don't "optimize later". LiveView lifecycle is non-negotiable.
NO DATABASE QUERIES IN MOUNT
mount/3会被调用两次(HTTP请求 + WebSocket连接)。在mount中执行查询会导致重复查询。
elixir
def mount(_params, _session, socket) do
  # 此处禁止数据库查询!会被调用两次。
  {:ok, assign(socket, posts: [], loading: true)}
end

def handle_params(params, _uri, socket) do
  # 数据库查询放在这里——每次导航执行一次
  posts = Blog.list_posts(socket.assigns.scope)
  {:noreply, assign(socket, posts: posts, loading: false)}
end
mount/3 = 仅用于初始化(空赋值、订阅、默认设置) handle_params/3 = 数据加载(所有数据库查询、URL驱动的状态)
无例外情况: 不要在mount中查询“哪怕这一件小事”。不要“以后再优化”。LiveView的生命周期是不可协商的。

Scopes: Security-First Pattern (Phoenix 1.8+)

作用域:安全优先模式(Phoenix 1.8+)

Scopes address OWASP #1 vulnerability: Broken Access Control. Authorization context is threaded automatically—no more forgetting to scope queries.
elixir
def list_posts(%Scope{user: user}) do
  Post |> where(user_id: ^user.id) |> Repo.all()
end
作用域解决了OWASP排名第一的漏洞:访问控制失效。授权上下文会自动传递——再也不会忘记对查询添加作用域。
elixir
def list_posts(%Scope{user: user}) do
  Post |> where(user_id: ^user.id) |> Repo.all()
end

PubSub Topics Must Be Scoped

PubSub主题必须添加作用域

elixir
def subscribe(%Scope{organization: org}) do
  Phoenix.PubSub.subscribe(@pubsub, "posts:org:#{org.id}")
end
Unscoped topics = data leaks between tenants.
elixir
def subscribe(%Scope{organization: org}) do
  Phoenix.PubSub.subscribe(@pubsub, "posts:org:#{org.id}")
end
未添加作用域的主题会导致租户之间的数据泄露。

External Polling: GenServer, Not LiveView

外部轮询:使用GenServer,而非LiveView

Bad: Every connected user makes API calls (multiplied by users). Good: Single GenServer polls, broadcasts to all via PubSub.
错误做法: 每个已连接用户都发起API调用(调用次数随用户数量倍增)。 正确做法: 单个GenServer进行轮询,通过PubSub向所有用户广播结果。

Components Receive Data, LiveViews Own Data

组件接收数据,LiveView拥有数据

  • Functional components: Display-only, no internal state
  • LiveComponents: Own state, handle own events
  • LiveViews: Full page, owns URL, top-level state
  • 函数式组件: 仅用于展示,无内部状态
  • LiveComponents: 拥有自身状态,处理自身事件
  • LiveViews: 完整页面,拥有URL,顶级状态

Async Data Loading

异步数据加载

Use
assign_async/3
for data that can load after mount:
elixir
def mount(_params, _session, socket) do
  {:ok, assign_async(socket, :user, fn -> {:ok, %{user: fetch_user()}} end)}
end
使用
assign_async/3
加载mount之后可获取的数据:
elixir
def mount(_params, _session, socket) do
  {:ok, assign_async(socket, :user, fn -> {:ok, %{user: fetch_user()}} end)}
end

Gotchas from Core Team

核心团队指出的陷阱

LiveView terminate/2 Requires trap_exit

LiveView的terminate/2需要捕获退出信号

terminate/2
only fires if you're trapping exits—which you shouldn't do in LiveView.
Fix: Use a separate GenServer that monitors the LiveView process via
Process.monitor/1
, then handle
:DOWN
messages to run cleanup.
terminate/2
仅在捕获退出信号时才会触发——但你不应在LiveView中这么做。
修复方案: 使用单独的GenServer,通过
Process.monitor/1
监控LiveView进程,然后处理
:DOWN
消息来执行清理操作。

start_async Duplicate Names: Later Wins

start_async重复名称:后调用的会覆盖

Calling
start_async
with the same name while a task is in-flight: the later one wins, the previous task's result is ignored.
Fix: Call
cancel_async/3
first if you want to abort the previous task.
在任务执行过程中使用相同名称调用
start_async
后调用的会生效,之前任务的结果会被忽略。
修复方案: 如果要中止之前的任务,先调用
cancel_async/3

Channel Intercept Socket State is Stale

Channel拦截中的Socket状态已过期

The socket in
handle_out
intercept is a snapshot from subscription time, not current state.
Why: Socket is copied into fastlane lookup at subscription time for performance.
Fix: Use separate topics per role, or fetch current state explicitly.
handle_out
拦截中的socket是订阅时的快照,并非当前状态。
原因: 为了性能,socket会在订阅时被复制到快速通道查找表中。
修复方案: 为每个角色使用单独的主题,或显式获取当前状态。

CSS Class Precedence is Stylesheet Order

CSS类优先级由样式表顺序决定

When merging classes on components, precedence is determined by stylesheet order, not HTML order. If
btn-primary
appears later in the compiled CSS than
bg-red-500
, it wins regardless of HTML order.
Fix: Use variant props instead of class merging.
在组件上合并类时,优先级由样式表顺序决定,而非HTML顺序。如果
btn-primary
在编译后的CSS中比
bg-red-500
靠后,无论HTML顺序如何,它都会生效。
修复方案: 使用变体属性而非类合并。

Upload Content-Type Can't Be Trusted

Upload的Content-Type不可信

The
:content_type
in
%Plug.Upload{}
is user-provided. Always validate actual file contents (magic bytes) and rewrite filename/extension.
%Plug.Upload{}
中的
:content_type
是用户提供的。始终验证实际文件内容(魔术字节)并重写文件名/扩展名。

Read Body Before Plug.Parsers for Webhooks

针对Webhooks,在Plug.Parsers之前读取请求体

To verify webhook signatures, you need the raw body. But Plug.Parsers consumes it.
elixir
{:ok, body, conn} = Plug.Conn.read_body(conn)
verify_signature!(conn, body)
%{conn | body_params: JSON.decode!(body)}
Don't use
preserve_req_body: true
—it keeps the entire body in memory for ALL requests.
要验证Webhook签名,你需要原始请求体。但Plug.Parsers会消耗它。
elixir
{:ok, body, conn} = Plug.Conn.read_body(conn)
verify_signature!(conn, body)
%{conn | body_params: JSON.decode!(body)}
不要使用
preserve_req_body: true
——它会将整个请求体保留在内存中,影响所有请求。

Red Flags - STOP and Reconsider

危险信号——立即停止并重新考虑

  • Database query in mount/3
  • Unscoped PubSub topics in multi-tenant app
  • LiveView polling external APIs directly
  • Using terminate/2 for cleanup (won't fire without trap_exit)
  • Calling start_async with same name without cancel_async first
  • Relying on socket.assigns in Channel intercepts (stale!)
  • CSS class merging for component customization (use variants)
  • Trusting
    %Plug.Upload{}.content_type
    for security
Any of these? Re-read The Iron Law and the Gotchas section.
  • 在mount/3中执行数据库查询
  • 多租户应用中使用未添加作用域的PubSub主题
  • LiveView直接轮询外部API
  • 使用terminate/2执行清理操作(不捕获退出信号则不会触发)
  • 未先调用cancel_async就使用相同名称调用start_async
  • 在Channel拦截中依赖socket.assigns(已过期!)
  • 使用CSS类合并进行组件定制(使用变体)
  • 信任
    %Plug.Upload{}.content_type
    以保障安全
出现以上任何一种情况?重新阅读《铁律》和《陷阱》部分。