顯示原始程式碼 存在

需求:本指導預設您已完成入門指導,並建立了一個已啟動且執行中的 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.dev.org.tw/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 函式庫來管理連線區段傳入的狀態和 Presence 差異。它會監聽 "presence_state""presence_diff" 事件,並提供一個簡單的回呼讓您處理這些事件,並使用 onSync 回呼。

onSync 回呼讓您可以輕鬆地對 Presence 狀態的變更做出反應,這通常會導致重新繪製已更新的活躍用戶清單。您可以使用 list 方法來根據應用程式的需求格式化並傳回每個個別 Presence。

若要反覆處理用戶,我們使用會接受回呼的 presences.list() 函式。回呼會針對每個 Presence 項目呼叫,並提供 2 個引數,也就是 Presence id 和元資料清單(每一個 Presence id 的一個 Presence)。我們使用此方法顯示用戶和他們使用中的裝置數量

我們可以透過將下列程式碼新增至 assets/js/app.js 讓 Presence 開始運作

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 個瀏覽器分頁來確認此功能運作正常。如果我們在兩個瀏覽器分頁上導覽至 https://127.0.0.1:4000/?name=Alice,然後在另一個分頁上導覽至 https://127.0.0.1:4000/?name=Bob,我們將會看到

Alice (count: 2)
Bob (count: 1)

如果我們關閉其中一個 Alice 分頁,則人數應減少 1 位。如果我們關閉另一個分頁,則該用戶應從清單中完全消失。

讓它更安全

在我們的初始實作中,我們使用 URL 提供使用者名稱。但是,在許多系統中,您只想允許已登入的使用者存取 Presence 功能。若要執行此動作,您應該設定 token 驗證,如頻道指南的 token 驗證區段所述

透過 token 驗證,您應該存取 socket.assigns.user_id,這是 UserSocket 中設定的,而不是使用來自參數的 socket.assigns.name

與 LiveView 一同使用

儘管 Phoenix 確實隨附了可處理 Presence 的 JavaScript API,但也可以擴充 HelloWeb.Presence 模組以支援 LiveView

處理 LiveView 時有一點需要注意,每個 LiveView 都是一個有狀態程序,所以如果我們在 LiveView 中保留 Presence 狀態,則每個 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

接著,我們可以在一個標籤中導航至 https://127.0.0.1:4000/online/Alice,在另一個標籤中導航至 https://127.0.0.1:4000/online/Bob,你會看到線上狀態已追蹤,並有使用者線上人數。使用不同的使用者開啟與關閉標籤,會即時更新線上名單。