查看源代码 状态
要求:本指南假定您已阅读过 入门指南 并且已成功运行 Phoenix 应用程序。
要求:本指南假定您已阅读过 通道指南。
Phoenix 状态是一个功能,允许您在主题上注册进程信息并在集群中透明地复制它。它既是服务器端库,也是客户端库的结合,这使得实现起来非常简单。一个简单的用例是显示哪些用户当前在线。
Phoenix 状态具有很多特殊之处。它没有单点故障,没有单一的事实来源,完全依赖于标准库,没有操作依赖关系,并且可以自我修复。
设置
我们将使用状态来跟踪哪些用户已连接到服务器,并在用户加入和离开时向客户端发送更新。我们将通过 Phoenix 通道传递这些更新。因此,让我们创建一个 RoomChannel
,就像我们在通道指南中做的那样
$ mix phx.gen.channel Room
按照生成器后面的步骤操作,您就可以开始跟踪状态了。
状态生成器
要开始使用状态,我们首先需要生成一个状态模块。我们可以使用 mix phx.gen.presence
任务完成此操作
$ mix phx.gen.presence
* creating lib/hello_web/channels/presence.ex
Add your new module to your supervision tree,
in lib/hello/application.ex:
children = [
...
HelloWeb.Presence,
]
You're all set! See the Phoenix.Presence docs for more details:
https://hexdocs.erlang.ac.cn/phoenix/Phoenix.Presence.html
如果我们打开 lib/hello_web/channels/presence.ex
文件,我们将看到以下行
use Phoenix.Presence,
otp_app: :hello,
pubsub_server: Hello.PubSub
这将为状态设置模块,定义我们跟踪状态所需的函数。如生成器任务中所述,我们应该在 application.ex
中将此模块添加到我们的监督树中
children = [
...
HelloWeb.Presence,
]
与通道和 JavaScript 一起使用
接下来,我们将创建用于通信状态的通道。在用户加入后,我们可以将状态列表推送到通道,然后跟踪连接。我们还可以提供一个额外的信息映射来跟踪。
defmodule HelloWeb.RoomChannel do
use Phoenix.Channel
alias HelloWeb.Presence
def join("room:lobby", %{"name" => name}, socket) do
send(self(), :after_join)
{:ok, assign(socket, :name, name)}
end
def handle_info(:after_join, socket) do
{:ok, _} =
Presence.track(socket, socket.assigns.name, %{
online_at: inspect(System.system_time(:second))
})
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end
end
最后,我们可以使用 phoenix.js
中包含的客户端状态库来管理状态和从套接字传来的状态差异。它监听 "presence_state"
和 "presence_diff"
事件,并提供一个简单的回调,让您在事件发生时处理这些事件,使用 onSync
回调。
onSync
回调允许您轻松地对状态更改做出反应,这通常会导致重新渲染更新的活动用户列表。您可以使用 list
方法来格式化和返回每个单独的状态,具体取决于应用程序的需求。
要迭代用户,我们使用 presences.list()
函数,它接受一个回调。回调将针对每个状态项调用,并带有 2 个参数,状态 ID 和一个元数据列表(每个状态项一个)。我们使用它来显示用户及其在线设备的数量。
通过在 assets/js/app.js
中添加以下内容,我们可以看到状态正在工作
import {Socket, Presence} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
let channel = socket.channel("room:lobby", {name: window.location.search.split("=")[1]})
let presence = new Presence(channel)
function renderOnlineUsers(presence) {
let response = ""
presence.list((id, {metas: [first, ...rest]}) => {
let count = rest.length + 1
response += `<br>${id} (count: ${count})</br>`
})
document.querySelector("main").innerHTML = response
}
socket.connect()
presence.onSync(() => renderOnlineUsers(presence))
channel.join()
我们可以通过打开 3 个浏览器标签来确保它正常工作。如果我们在两个浏览器标签中导航到 http://localhost:4000/?name=Alice 以及在另一个浏览器标签中导航到 http://localhost:4000/?name=Bob,那么我们应该看到
Alice (count: 2)
Bob (count: 1)
如果我们关闭一个 Alice 标签,则计数应降至 1。如果我们关闭另一个标签,则用户应完全从列表中消失。
使其安全
在我们最初的实现中,我们正在将用户的名称作为 URL 的一部分传递。但是,在许多系统中,您希望只允许登录的用户访问状态功能。为此,您应该设置令牌身份验证,如通道指南中的令牌身份验证部分所述。
使用令牌身份验证,您应该访问 socket.assigns.user_id
(在 UserSocket
中设置),而不是从参数设置的 socket.assigns.name
。
与 LiveView 一起使用
虽然 Phoenix 确实附带了用于处理状态的 JavaScript API,但也可以扩展 HelloWeb.Presence
模块以支持 LiveView。
处理 LiveView 时要记住的一点是,每个 LiveView 都是一个有状态的进程,因此如果我们将状态保存在 LiveView 中,则每个 LiveView 进程都将在内存中包含完整的在线用户列表。相反,我们可以跟踪 Presence
进程中的在线用户,并将单独的事件传递给 LiveView,LiveView 可以使用流来更新在线列表。
首先,我们需要更新 lib/hello_web/channels/presence.ex
文件,以向 HelloWeb.Presence
模块添加一些可选回调。
首先,我们添加 init/1
回调。这使我们能够跟踪进程中的状态。
def init(_opts) do
{:ok, %{}}
end
状态模块还允许 fetch/2
回调,这使得可以修改从状态中获取的数据,从而允许我们定义响应的形状。在这种情况下,我们正在添加一个 id
和一个 user
映射。
def fetch(_topic, presences) do
for {key, %{metas: [meta | metas]}} <- presences, into: %{} do
# user can be populated here from the database here we populate
# the name for demonstration purposes
{key, %{metas: [meta | metas], id: meta.id, user: %{name: meta.id}}}
end
end
最后要添加的是 handle_metas/4
回调。此回调根据用户离开和加入更新我们在 HelloWeb.Presence
中跟踪的状态。
def handle_metas(topic, %{joins: joins, leaves: leaves}, presences, state) do
for {user_id, presence} <- joins do
user_data = %{id: user_id, user: presence.user, metas: Map.fetch!(presences, user_id)}
msg = {__MODULE__, {:join, user_data}}
Phoenix.PubSub.local_broadcast(Hello.PubSub, "proxy:#{topic}", msg)
end
for {user_id, presence} <- leaves do
metas =
case Map.fetch(presences, user_id) do
{:ok, presence_metas} -> presence_metas
:error -> []
end
user_data = %{id: user_id, user: presence.user, metas: metas}
msg = {__MODULE__, {:leave, user_data}}
Phoenix.PubSub.local_broadcast(Hello.PubSub, "proxy:#{topic}", msg)
end
{:ok, state}
end
您可以看到我们正在广播加入和离开的事件。这些将由 LiveView 进程监听。您还会注意到,在广播加入和离开时,我们使用了“代理”通道。这是因为我们不希望 LiveView 进程直接接收状态事件。我们可以添加一些辅助函数,以便将此特定实现细节从 LiveView 模块中抽象出来。
def list_online_users(), do: list("online_users") |> Enum.map(fn {_id, presence} -> presence end)
def track_user(name, params), do: track(self(), "online_users", name, params)
def subscribe(), do: Phoenix.PubSub.subscribe(Hello.PubSub, "proxy:online_users")
现在我们已经设置了状态模块并广播了事件,我们可以创建一个 LiveView。创建一个新文件 lib/hello_web/live/online/index.ex
,内容如下
defmodule HelloWeb.OnlineLive do
use HelloWeb, :live_view
def mount(params, _session, socket) do
socket = stream(socket, :presences, [])
socket =
if connected?(socket) do
HelloWeb.Presence.track_user(params["name"], %{id: params["name"]})
HelloWeb.Presence.subscribe()
stream(socket, :presences, HelloWeb.Presence.list_online_users())
else
socket
end
{:ok, socket}
end
def render(assigns) do
~H"""
<ul id="online_users" phx-update="stream">
<li :for={{dom_id, %{id: id, metas: metas}} <- @streams.presences} id={dom_id}><%= id %> (<%= length(metas) %>)</li>
</ul>
"""
end
def handle_info({HelloWeb.Presence, {:join, presence}}, socket) do
{:noreply, stream_insert(socket, :presences, presence)}
end
def handle_info({HelloWeb.Presence, {:leave, presence}}, socket) do
if presence.metas == [] do
{:noreply, stream_delete(socket, :presences, presence)}
else
{:noreply, stream_insert(socket, :presences, presence)}
end
end
end
如果我们将此路由添加到 lib/hello_web/router.ex
中
live "/online/:name", OnlineLive, :index
然后,我们可以在一个标签中导航到 http://localhost:4000/online/Alice,并在另一个标签中导航到 http://localhost:4000/online/Bob,您将看到状态正在跟踪,以及每个用户的状态数。使用各种用户打开和关闭标签将实时更新状态列表。