我的目标是使用 Phoenix 创建一个遵循最佳实践的简单聊天应用程序。为了共谋,该应用程序是围绕聊天“房间”构建的,其中每个房间都包含一个消息列表。每个房间都有一个关联的标识符(一个六个字符长的字母数字字符串),以便其他用户可以输入并加入。
目前,我将其建模为两个模块:一个用于处理用户输入并显示房间中消息列表的 LiveView,以及一个用于跟踪房间状态的模块。代码大致是这样的
defmodule Chat.ChatServer do
use GenServer
# Client
def start_link(room_id) do
GenServer.start_link(__MODULE__, %{room_id: room_id, messages: []}, name: via_tuple(room_id))
end
def whereis(room_id) do
GenServer.whereis(via_tuple(room_id))
end
defp via_tuple(room_id) do
{:via, Registry, {Registry.Chat, room_id}}
end
def add_message(room_id, message) do
GenServer.cast(via_tuple(room_id), {:add_message, message})
end
def get_messages(room_id) do
GenServer.call(via_tuple(room_id), :get_messages)
end
def subscribe(room_id) do
Phoenix.PubSub.subscribe(Chat.PubSub, "chat:#{room_id}")
end
# Server
def init(messages) do
{:ok, messages}
end
def handle_cast({:add_message, new_message}, %{messages: messages} = state) do
new_messages = [new_message | messages]
{:noreply, %{state | messages: new_messages}, {:continue, :notify_subscribers}}
end
def handle_call(:get_messages, _from, %{messages: messages} = state) do
{:reply, messages, state}
end
def handle_continue(:notify_subscribers, %{room_id: room_id} = state) do
Phoenix.PubSub.broadcast(Chat.PubSub, "chat:#{room_id}", {:updated_messages})
{:noreply, state}
end
end
defmodule ChatWeb.ChatLive do
use ChatWeb, :live_view
alias Chat.{ChatServer}
def mount(%{"id" => room_id}, _session, socket) do
if connected?(socket) do
ChatServer.subscribe(room_id)
end
assigns = [
messages: ChatServer.get_messages(room_id),
room_id: room_id
]
{:ok, assign(socket, assigns)}
end
def handle_event("add_message", _, %{assigns: %{room_id: room_id, messages: messages}} = socket) do
ChatServer.add_message(room_id, "Example message")
{:noreply, socket}
end
def handle_info({:updated_messages}, %{assigns: %{room_id: room_id}} = socket) do
messages = ChatServer.get_messages(room_id)
{:noreply, assign(socket, :messages, messages)}
end
end
如您所见,我已将聊天室建模为 GenServer。这样做而不是将所有房间保留在单个 GenServer 进程中是否有任何缺点?
这是正确使用 GenServer 吗?使用一个 GenServer 来包装所有房间,而不是每个房间一个 GenServer 进程是不是更好的方法?
我观察到但不知道如何解决的一些问题是,截至目前,每个进程都将自身的 name
保持在其状态。唯一的原因是它只能向订阅该特定房间的进程广播。这是我对观察者模式的尝试。是否有一种简单的方法可以在不保持 name
状态的情况下执行此操作?
最后,避免 LiveView 在新消息附加到状态之前从 GenServer 重新获取消息。这是对 handle_continue/2
的滥用吗?