关于 liveview-patterns
这是一项面向 Phoenix LiveView 1.0 和 1.1 开发的参考技能,涵盖异步数据加载、streams、表单、导航和组件选择,并说明如何避免常见的生命周期与内存问题。
主要功能
- 01异步赋值与数据加载模式
- 02使用 streams 处理列表
- 03遵循生命周期的 PubSub 订阅
- 04表单验证与错误处理
- 05导航和组件选择指南
适用场景
- 01在 LiveView mount 中安全加载数据
- 02使用 streams 渲染大型列表
- 03避免重复订阅 PubSub
- 04选择导航与组件模式
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ oliver-kriska/ claude-elixir-phoenix --skill liveview-patterns安装后在 Claude Code 中输入/liveview-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/async-streams.mdchannels-presence.mdcomponents.mdforms-uploads.mdjs-interop.mdpubsub-navigation.md详细介绍
LiveView 模式参考
Ash 项目:使用
ash-frameworkskill 处理AshPhoenix.Form。生命周期:在phx-change上调用AshPhoenix.Form.validate/3,提交时调用AshPhoenix.Form.submit/2,在 HEEx 中使用to_form/1。不要使用Ecto.Changeset.cast/3。
Phoenix LiveView 1.0/1.1 构建参考。
铁律 — 绝不违反
- 禁止在 MOUNT 中执行无条件数据库查询 — Mount 会运行两次。默认使用
assign_async。SEO 路由:使用connected?守卫,并在断开连接的分支中使用缓存(爬虫会读取该 HTML) - 列表始终使用 Streams — 普通 assigns = 每位用户 O(n) 内存。Streams = O(1)
- 订阅前检查 connected?/1 — 防止重复订阅
- 在 assign_async 闭包外提取变量 — 闭包会复制所有被引用的变量
- 在 mount/3 中加载主要数据,在 handle_params/3 中处理分页 — handle_params 会在每次 URL 变更时运行
- 绝不将 socket 传给业务逻辑 — 调用 contexts 前先提取数据
- 调试 UI 前先检查 changeset 错误 — 表单静默保存失败时,先检查
{:error, changeset},而不是视口/JS - 所有必填的嵌入字段都要使用隐藏输入框 — 如果无法直接编辑,嵌入式 schema 中的每个必填字段都必须有一个
hidden_input - 生命周期值绝不使用
assign_new— 如果键已存在,assign_new会跳过函数。对于 locale、当前用户或任何每次 mount 都要刷新的值,请使用assign/3 - 显式匹配
{:error, %Ecto.Changeset{}}— 裸写{:error, _}会合并 changeset 错误和非 changeset 错误,导致表单静默地无法重新渲染验证错误。请单独处理其他错误
内存影响
| 模式 | 3K 项 | 10K 用户 × 10K 项 |
|---|---|---|
| 普通 assigns | ~5.1 MB | ~10+ GB |
| Streams | ~1.1 MB | 极少(O(1)) |
决策:超过 100 项的列表 → 使用 streams,而不是 assigns
快速模式
Async Assigns(关键)
def mount(%{"slug" => slug}, _session, socket) do
# Extract needed values BEFORE the closure
scope = socket.assigns.current_scope
{:ok,
socket
|> assign_async(:org, fn -> {:ok, %{org: fetch_org(scope, slug)}} end)}
end
列表使用 Streams
def mount(_params, _session, socket) do
{:ok, stream(socket, :items, Items.list_items())}
end
# Insert/update/delete
stream_insert(socket, :items, item, at: 0)
stream_delete(socket, :items, item)
SEO 死渲染(使用缓存的断开连接分支)
对于公开/SEO 可见的路由(营销页面、文章、产品列表),断开连接时的渲染就是爬虫看到的 HTML。在该分支中从缓存获取数据,连接后再获取真实数据:
def mount(_params, _session, socket) do
products =
if connected?(socket),
do: Catalog.list_products(),
else: Cache.get_products() || []
{:ok, assign(socket, products: products)}
end
列表为空时 → 使用兼容 <noscript> 的骨架屏。缓存 → :persistent_term、ETS 或 Cachex。这样既符合铁律 #1,也能让 Googlebot/GPTBot 正常访问。
使用 connected? 检查 PubSub
def mount(_params, _session, socket) do
if connected?(socket), do: Chat.subscribe(room_id)
{:ok, socket}
end
导航决策树
Same LiveView, different params? → patch / push_patch
Different LiveView, same live_session? → navigate / push_navigate
Different live_session or non-LiveView? → href / redirect
组件决策树
Does component need BOTH internal state AND event handling?
│
├── YES → Does it encapsulate APPLICATION logic (not just DOM)?
│ ├── YES → Use LiveComponent ✅
│ └── NO → Refactor to function component with parent handling
│
└── NO → Use Function Component ✅
官方建议:“优先使用函数组件,而不是 LiveComponent”
常见反模式
| 错误做法 | 正确做法 |
|---|---|
未使用 assign_async 执行数据库查询 | Mount 查询使用 assign_async(SEO 路由:使用 connected? + 缓存的死渲染) |
对列表使用 assign(socket, items: list) | 使用 stream(socket, :items, list) |
未检查 connected? 就订阅 PubSub | if connected?(socket), do: subscribe() |
| 将 socket 传给 context 函数 | 先提取 socket.assigns |
在 handle_event 中编写业务逻辑 | 委托给 context |
在 hooks 中使用 assign_new 设置 locale/user | 使用 assign/3(必须在每次 mount 时运行) |
参考资料
详细模式请参见:
${CLAUDE_SKILL_DIR}/references/async-streams.md- assign_async、stream_async、streams${CLAUDE_SKILL_DIR}/references/forms-uploads.md- 表单、验证、文件上传${CLAUDE_SKILL_DIR}/references/components.md- 函数组件、LiveComponents${CLAUDE_SKILL_DIR}/references/pubsub-navigation.md- PubSub、导航、JS 命令${CLAUDE_SKILL_DIR}/references/js-interop.md- 第三方 JS 库、phx-update="ignore"、hooks${CLAUDE_SKILL_DIR}/references/channels-presence.md- Phoenix Channels、Presence、令牌身份验证
评论
支持 Markdown