我们做一个“城邦调度台”。
defmodule CityWeb.DispatchLive.Index do
use CityWeb, :live_view
alias City.Dispatch
@impl true
def mount(_params, _session, socket) do
scope = socket.assigns.current_scope
topic = "org:#{scope.user.organization_id}:dispatches"
if connected?(socket) do
Phoenix.PubSub.subscribe(City.PubSub, topic)
end
{:ok,
socket
|> assign(:form, to_form(%{"title" => ""}, as: :dispatch))
|> stream(:dispatches, Dispatch.list_dispatches(scope))}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<section>
<h1>实时调度台</h1>
<.form for={@form} phx-submit="save">
<.input field={@form[:title]} label="新事件标题" />
<.button>登记事件</.button>
</.form>
<ul id="dispatches" phx-update="stream">
<li :for={{dom_id, dispatch} <- @streams.dispatches} id={dom_id}>
<strong>{dispatch.title}</strong>
<span>{dispatch.status}</span>
</li>
</ul>
</section>
</Layouts.app>
"""
end
@impl true
def handle_event("save", %{"dispatch" => params}, socket) do
case Dispatch.create_dispatch(socket.assigns.current_scope, params) do
{:ok, _dispatch} ->
{:noreply, assign(socket, :form, to_form(%{"title" => ""}, as: :dispatch))}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset, as: :dispatch))}
end
end
@impl true
def handle_info({:dispatch_created, dispatch}, socket) do
{:noreply, stream_insert(socket, :dispatches, dispatch, at: 0)}
end
end这段代码应该怎么读?
#1. mount/3 是中控室开机
- 初始化表单
- 拉首批数据
- 在连接建立后订阅广播主题
为什么 connected?(socket) 很重要?
因为 LiveView 首次是“断开态静态渲染”,连接建立后才进入“持续值守态”。
很多需要订阅、定时器、外部监听的事,都应该在连上之后再做。
#2. handle_event/3 是市民按下按钮
按钮上的 phx-submit="save" 会把事件发到服务器。
LiveView 不要求你在浏览器里维护一套复杂状态机;它让服务器端进程直接接这件事。
#3. handle_info/2 是城内广播到达
当 Context 通过 PubSub 广播 {:dispatch_created, dispatch} 时,这个 LiveView 收到消息,更新自己的流。
#4. stream/3 和 stream_insert/4 是大名单优化
它的意义不是“另一个 list assign”,而是:
让大集合主要留在客户端 DOM 里,服务器不必永远抱着整份列表。
这对实时列表、时间线、通知流、消息流非常重要。