查看源代码 状态

要求:本指南假定您已阅读过 入门指南 并且已成功运行 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.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,您将看到状态正在跟踪,以及每个用户的状态数。使用各种用户打开和关闭标签将实时更新状态列表。