liveview-lifecycle
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLiveView Rendering Lifecycle
LiveView 渲染生命周期
Critical Understanding
关键知识点
LiveView renders happen in TWO phases:
-
Static/Disconnected Render - Initial HTTP request, server-side HTML
- No WebSocket connection
- No live functionality yet
- returns
connected?(socket)false
-
Connected Render - WebSocket established, full LiveView active
- Live updates work
- Events are handled
- returns
connected?(socket)true
LiveView 的渲染分为两个阶段:
-
静态/断开连接渲染 - 初始HTTP请求,服务端生成HTML
- 无WebSocket连接
- 暂不具备实时功能
- 返回
connected?(socket)false
-
连接后渲染 - WebSocket已建立,LiveView完全激活
- 支持实时更新
- 可处理事件
- 返回
connected?(socket)true
The Problem: Uninitialized Assigns
问题:未初始化的Assign
During static render, socket assigns may not be fully initialized:
elixir
undefined在静态渲染期间,socket的assign可能尚未完全初始化:
elixir
undefined❌ DANGEROUS - Can crash during static render
❌ 危险 - 静态渲染期间可能崩溃
def render(assigns) do
user_name = assigns.current_user.name # KeyError if not set!
~H"<p>Hello <%= user_name %></p>"
end
**Error:** `KeyError: key :current_user not found`def render(assigns) do
user_name = assigns.current_user.name # 若未设置会触发KeyError!
~H"<p>Hello <%= user_name %></p>"
end
**错误:** `KeyError: key :current_user not found`The Solution: Defensive Access
解决方案:防御式访问
Always use safe access patterns:
elixir
undefined请始终使用安全的访问模式:
elixir
undefined✅ SAFE - Works in both render phases
✅ 安全 - 在两个渲染阶段都能正常工作
def render(assigns) do
user_name = Map.get(assigns, :current_user, %{name: "Guest"}).name
~H"<p>Hello <%= user_name %></p>"
end
def render(assigns) do
user_name = Map.get(assigns, :current_user, %{name: "Guest"}).name
~H"<p>Hello <%= user_name %></p>"
end
✅ BETTER - Initialize in mount
✅ 更优方案 - 在mount中初始化
@impl true
def mount(_params, session, socket) do
socket = assign(socket, :current_user, get_user(session))
{:ok, socket}
end
undefined@impl true
def mount(_params, session, socket) do
socket = assign(socket, :current_user, get_user(session))
{:ok, socket}
end
undefinedBest Practices
最佳实践
1. Initialize All Assigns in mount/3
1. 在mount/3中初始化所有Assign
Always initialize every assign you'll use:
elixir
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:user, nil)
|> assign(:loading, false)
|> assign(:data, [])
|> assign(:error, nil)
{:ok, socket}
end请始终初始化所有需要用到的assign:
elixir
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:user, nil)
|> assign(:loading, false)
|> assign(:data, [])
|> assign(:error, nil)
{:ok, socket}
end2. Use Map.get for Optional Assigns
2. 对可选Assign使用Map.get
When accessing assigns that might not exist:
elixir
undefined当访问可能不存在的assign时:
elixir
undefined❌ BAD
❌ 不良写法
defp render_user(socket) do
socket.assigns.current_user.name
end
defp render_user(socket) do
socket.assigns.current_user.name
end
✅ GOOD
✅ 正确写法
defp render_user(socket) do
case Map.get(socket.assigns, :current_user) do
nil -> "Guest"
user -> user.name
end
end
defp render_user(socket) do
case Map.get(socket.assigns, :current_user) do
nil -> "Guest"
user -> user.name
end
end
✅ ALSO GOOD
✅ 同样正确的写法
defp render_user(socket) do
Map.get(socket.assigns, :current_user, %{name: "Guest"}).name
end
undefineddefp render_user(socket) do
Map.get(socket.assigns, :current_user, %{name: "Guest"}).name
end
undefined3. Use Pattern Matching Safely
3. 安全使用模式匹配
elixir
undefinedelixir
undefined❌ BAD - Crashes if not a map with :name
❌ 不良写法 - 若不是包含:name的map会崩溃
defp format_user(%{name: name}), do: name
defp format_user(%{name: name}), do: name
✅ GOOD - Handles nil case
✅ 正确写法 - 处理nil情况
defp format_user(%{name: name}), do: name
defp format_user(_), do: "Unknown"
defp format_user(%{name: name}), do: name
defp format_user(_), do: "Unknown"
✅ ALSO GOOD - Check first
✅ 同样正确的写法 - 先进行检查
defp format_user(user) when is_map(user), do: Map.get(user, :name, "Unknown")
defp format_user(_), do: "Unknown"
undefineddefp format_user(user) when is_map(user), do: Map.get(user, :name, "Unknown")
defp format_user(_), do: "Unknown"
undefined4. Use assigns_to_attributes for Components
4. 对组件使用assigns_to_attributes
elixir
undefinedelixir
undefinedComponent with dynamic assigns
带动态assign的组件
def card(assigns) do
~H"""
<div class="card" {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
enddef card(assigns) do
~H"""
<div class="card" {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
endUsage
使用示例
<.card id="my-card" data-role="admin">
Content
</.card>
undefined<.card id="my-card" data-role="admin">
Content
</.card>
undefinedConnected Check
连接状态检查
Use for operations that only work with WebSocket:
connected?/1elixir
@impl true
def mount(_params, _session, socket) do
socket =
if connected?(socket) do
# Only run when WebSocket is connected
Phoenix.PubSub.subscribe(MyApp.PubSub, "updates")
schedule_refresh()
socket
else
# Static render - skip expensive operations
socket
end
socket = assign(socket, :data, load_initial_data())
{:ok, socket}
endWhy? PubSub subscriptions, timers, and live updates only work when connected.
对于仅在WebSocket连接下才能正常工作的操作,请使用:
connected?/1elixir
@impl true
def mount(_params, _session, socket) do
socket =
if connected?(socket) do
# 仅当WebSocket连接建立后才执行
Phoenix.PubSub.subscribe(MyApp.PubSub, "updates")
schedule_refresh()
socket
else
# 静态渲染 - 跳过耗时操作
socket
end
socket = assign(socket, :data, load_initial_data())
{:ok, socket}
end原因: PubSub订阅、定时器和实时更新只有在连接建立后才能正常工作。
Handle Params Considerations
Handle Params注意事项
handle_params/3elixir
@impl true
def handle_params(%{"id" => id}, _uri, socket) do
# This runs during static AND connected render
post = Posts.get_post!(id) # OK - database queries work in both phases
if connected?(socket) do
# Only subscribe when connected
Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{id}")
end
{:noreply, assign(socket, :post, post)}
endhandle_params/3elixir
@impl true
def handle_params(%{"id" => id}, _uri, socket) do
# 此代码会在静态渲染和连接后渲染阶段都执行
post = Posts.get_post!(id) # 没问题 - 数据库查询在两个阶段都能执行
if connected?(socket) do
# 仅在连接建立后才订阅
Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{id}")
end
{:noreply, assign(socket, :post, post)}
endCommon Lifecycle Mistakes
常见生命周期错误
❌ Mistake 1: Assuming Assigns Exist
❌ 错误1:假设Assign已存在
elixir
def render(assigns) do
~H"""
<p>Count: <%= @count %></p> <!-- Crash if @count not initialized -->
"""
endelixir
def render(assigns) do
~H"""
<p>Count: <%= @count %></p> <!-- 若@count未初始化会崩溃 -->
"""
end✅ Fix: Initialize in mount
✅ 修复方案:在mount中初始化
elixir
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
def render(assigns) do
~H"""
<p>Count: <%= @count %></p> <!-- Safe now -->
"""
endelixir
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
def render(assigns) do
~H"""
<p>Count: <%= @count %></p> <!-- 现在安全了 -->
"""
end❌ Mistake 2: Subscribing in Both Phases
❌ 错误2:在两个阶段都执行订阅
elixir
@impl true
def mount(_params, _session, socket) do
# BAD - Subscribes during static render (doesn't work)
Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
{:ok, socket}
endelixir
@impl true
def mount(_params, _session, socket) do
# 不良写法 - 在静态渲染阶段执行订阅(无法正常工作)
Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
{:ok, socket}
end✅ Fix: Check connected?
✅ 修复方案:检查连接状态
elixir
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
end
{:ok, socket}
endelixir
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
end
{:ok, socket}
end❌ Mistake 3: Expensive Operations in Static Render
❌ 错误3:在静态渲染阶段执行耗时操作
elixir
@impl true
def mount(_params, _session, socket) do
# BAD - Runs expensive query twice (static + connected)
data = run_expensive_query()
{:ok, assign(socket, :data, data)}
endelixir
@impl true
def mount(_params, _session, socket) do
# 不良写法 - 耗时查询会执行两次(静态渲染+连接后渲染)
data = run_expensive_query()
{:ok, assign(socket, :data, data)}
end✅ Fix: Defer to connected phase
✅ 修复方案:延迟到连接后阶段执行
elixir
@impl true
def mount(_params, _session, socket) do
socket =
if connected?(socket) do
# Only run expensive operations when connected
assign(socket, :data, run_expensive_query())
else
# Use placeholder data for static render
assign(socket, :data, [])
end
{:ok, socket}
endelixir
@impl true
def mount(_params, _session, socket) do
socket =
if connected?(socket) do
# 仅在连接建立后才执行耗时操作
assign(socket, :data, run_expensive_query())
else
# 静态渲染使用占位数据
assign(socket, :data, [])
end
{:ok, socket}
endLifecycle Flow
生命周期流程
1. HTTP Request arrives
↓
2. mount/3 called (connected? = false)
↓
3. handle_params/3 called (connected? = false)
↓
4. render/1 called (STATIC HTML generated)
↓
5. HTML sent to browser
↓
6. Browser connects WebSocket
↓
7. mount/3 called AGAIN (connected? = true)
↓
8. handle_params/3 called AGAIN (connected? = true)
↓
9. render/1 called (sent over WebSocket)
↓
10. LiveView now active and reactive1. HTTP 请求到达
↓
2. 调用mount/3(connected? = false)
↓
3. 调用handle_params/3(connected? = false)
↓
4. 调用render/1(生成静态HTML)
↓
5. 将HTML发送至浏览器
↓
6. 浏览器建立WebSocket连接
↓
7. 再次调用mount/3(connected? = true)
↓
8. 再次调用handle_params/3(connected? = true)
↓
9. 调用render/1(通过WebSocket发送)
↓
10. LiveView 现在已激活并具备响应式能力Debugging Tips
调试技巧
Check if LiveView is connected
检查LiveView是否已连接
elixir
def render(assigns) do
~H"""
<div data-connected={@connected?}>
<!-- Shows connection state -->
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :connected?, connected?(socket))}
endelixir
def render(assigns) do
~H"""
<div data-connected={@connected?}>
<!-- 显示连接状态 -->
</div>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :connected?, connected?(socket))}
endLog render phases
记录渲染阶段
elixir
@impl true
def mount(_params, _session, socket) do
IO.puts("Mount called - Connected: #{connected?(socket)}")
{:ok, socket}
end
@impl true
def handle_params(params, _uri, socket) do
IO.puts("Handle params - Connected: #{connected?(socket)}")
{:noreply, socket}
endelixir
@impl true
def mount(_params, _session, socket) do
IO.puts("调用Mount - 已连接: #{connected?(socket)}")
{:ok, socket}
end
@impl true
def handle_params(params, _uri, socket) do
IO.puts("调用Handle params - 已连接: #{connected?(socket)}")
{:noreply, socket}
endSafe Assign Access Helpers
安全Assign访问工具函数
Create helper functions for safe access:
elixir
defp get_assign(socket, key, default \\ nil) do
Map.get(socket.assigns, key, default)
end
defp has_assign?(socket, key) do
Map.has_key?(socket.assigns, key)
end可以创建工具函数来实现安全访问:
elixir
defp get_assign(socket, key, default \\ nil) do
Map.get(socket.assigns, key, default)
end
defp has_assign?(socket, key) do
Map.has_key?(socket.assigns, key)
endUsage
使用示例
def some_function(socket) do
if has_assign?(socket, :current_user) do
user = get_assign(socket, :current_user)
# Do something with user
end
end
undefineddef some_function(socket) do
if has_assign?(socket, :current_user) do
user = get_assign(socket, :current_user)
# 处理用户数据
end
end
undefinedTesting Both Phases
测试两个阶段
elixir
test "renders correctly in both phases", %{conn: conn} do
# Static render (disconnected)
{:ok, _view, html} = live(conn, "/page")
assert html =~ "Expected content"
# Now connected
# Can test live interactions
end
test "initializes assigns in mount", %{conn: conn} do
{:ok, view, _html} = live(conn, "/page")
# Check assigns are set
assert view.assigns.count == 0
assert view.assigns.user != nil
endelixir
test "在两个阶段都能正确渲染", %{conn: conn} do
# 静态渲染(断开连接状态)
{:ok, _view, html} = live(conn, "/page")
assert html =~ "预期内容"
# 现在处于连接状态
# 可以测试实时交互
end
test "在mount中初始化assign", %{conn: conn} do
{:ok, view, _html} = live(conn, "/page")
# 检查assign是否已设置
assert view.assigns.count == 0
assert view.assigns.user != nil
endQuick Reference
快速参考
Safe Patterns
安全模式
elixir
undefinedelixir
undefined✅ Initialize in mount
✅ 在mount中初始化
assign(socket, :key, default_value)
assign(socket, :key, default_value)
✅ Use Map.get for optional
✅ 对可选值使用Map.get
Map.get(socket.assigns, :key, default)
Map.get(socket.assigns, :key, default)
✅ Check connected for side effects
✅ 对副作用操作检查连接状态
if connected?(socket), do: subscribe()
if connected?(socket), do: subscribe()
✅ Pattern match with fallback
✅ 带降级处理的模式匹配
def helper(%{name: name}), do: name
def helper(_), do: "default"
undefineddef helper(%{name: name}), do: name
def helper(_), do: "default"
undefinedUnsafe Patterns
不安全模式
elixir
undefinedelixir
undefined❌ Direct access without initialization
❌ 未初始化直接访问
socket.assigns.key
socket.assigns.key
❌ Subscribe without checking
❌ 未检查连接状态就执行订阅
Phoenix.PubSub.subscribe(...)
Phoenix.PubSub.subscribe(...)
❌ Expensive ops in both phases
❌ 在两个阶段都执行耗时操作
mount(...) do
data = expensive_query()
end
mount(...) do
data = expensive_query()
end
❌ Pattern match without fallback
❌ 无降级处理的模式匹配
def helper(%{name: name}), do: name
undefineddef helper(%{name: name}), do: name
undefined