Channel
Channel 是 Phoenix 框架中的一种高级抽象,也是Phoenix中最激动人心的部分。它可以方便地实现客户端之间的软实时通信,今天我们就来用它来构建一个最简单的聊天频道。主要目的是理解 Channel 的使用方式。
目标
图中的圆圈代表客户端,圆角矩形代表一个 Channel ,room:lobby
是Channel的名称,又叫 topic ,以 topic:subtopic
的形式表示。在这个 Channel 中,每个客户端发出的消息都可以被所有成员看到。
层次
首先,客户端发送一个请求加入频道的信息给服务器。服务器中的 socket handler 套接字处理器接受了这个请求,并根据服务器中所定义的 Channel Route 频道路径来进入相应的频道。Transport 层代表频道中具体的通信手段,默认是 Web Socket。PubSub 层中定义了 Channel 里的各种行为,例如关注某个话题,取消关注,发布广播等,一般不会修改这一层。
流程
首先,我们使用 mix phoenix.new monkey
新建一个名为 monkey 的 Phoenix 应用。安装好依赖之后,打开 monkey 文件夹。
在 lib/monkey/endpoint.ex
文件中,可以看到对应的 socket 已经设置好:
defmodule Monkey.Endpoint do
use Phoenix.Endpoint, otp_app: :monkey
socket "/socket", Monkey.UserSocket
我们打开 web/channels/user_socket.ex
, UserSocket 模块就是在这里定义的。将其中 Channels 下一行的注释取消:
defmodule Monkey.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", Monkey.RoomChannel
这里的 room:*
表示所有 topic 为 room 的频道请求都会调用 RoomChannel 模块。下面我们就来实现这个模块。
RoomChannel
在 web/channels/room_channel.ex
中写入如下内容:
defmodule Monkey.RoomChannel do
use Monkey.Web, :channel
intercept ["new_msg"]
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "unauthorized"}}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast! socket, "new_msg", %{body: body}
{:noreply, socket}
end
def handle_out("new_msg", payload, socket) do
push socket, "new_msg", payload
{:noreply, socket}
end
end
在 web/static/js/socket.js
中做如下修改:
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("room:lobby", {})
let chatInput = document.querySelector("#chat-input")
let messagesContainer = document.querySelector("#messages")
chatInput.addEventListener("keypress", event => {
if(event.keyCode === 13){
channel.push("new_msg", {body: chatInput.value})
chatInput.value = ""
}
})
channel.on("new_msg", payload => {
let messageItem = document.createElement("li");
messageItem.innerText = `[${Date()}] ${payload.body}`
messagesContainer.appendChild(messageItem)
})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket
把 web/static/js/app.js
中 // import socket from "./socket"
这一行的注释取消。
将 web/templates/page/index.html.eex
中的内容修改为:
首先,我们定义了 join/3
函数,用来处理客户端的进入请求。当客户端请求进入聊天大厅 room:lobby
时,返回 {:ok, socket}
, 当 subtopic 为其它值时,返回{:error, %{reason: "unauthorized"}}
。我们在js文件中将频道名默认设置为 room:lobby
,所以这条从句暂时不会起作用。
handle_in/3
函数定义了接收到新消息时的行为,它会调用 broadcast!/3
函数,而该函数又需要通过 handle_out/3
函数。 handle_out/3
的作用相当于一个过滤器,我们可以在其中设置哪些消息不能发送出去。在这里我们没有用到过滤功能,只是直接将接收到的消息push 到socket。
当socket接收到了 "new_msg", payload
,便会在messagesContainer
中新建一个内容为新消息的li标签。
效果
这一期的elixir! 就到这里了,如果你觉得有哪里不太明白的,或者发现了错误的地方,请务必留言!下一期我们将继续扩展这个聊天室的功能。