[elixir! #0001] 初探channel,理解最简单的聊天频道

Channel

Channel 是 Phoenix 框架中的一种高级抽象,也是Phoenix中最激动人心的部分。它可以方便地实现客户端之间的软实时通信,今天我们就来用它来构建一个最简单的聊天频道。主要目的是理解 Channel 的使用方式。

目标

[elixir! #0001] 初探channel,理解最简单的聊天频道_第1张图片

图中的圆圈代表客户端,圆角矩形代表一个 Channel ,room:lobby 是Channel的名称,又叫 topic ,以 topic:subtopic 的形式表示。在这个 Channel 中,每个客户端发出的消息都可以被所有成员看到。

层次

[elixir! #0001] 初探channel,理解最简单的聊天频道_第2张图片

首先,客户端发送一个请求加入频道的信息给服务器。服务器中的 socket handler 套接字处理器接受了这个请求,并根据服务器中所定义的 Channel Route 频道路径来进入相应的频道。Transport 层代表频道中具体的通信手段,默认是 Web Socket。PubSub 层中定义了 Channel 里的各种行为,例如关注某个话题,取消关注,发布广播等,一般不会修改这一层。

流程

[elixir! #0001] 初探channel,理解最简单的聊天频道_第3张图片

首先,我们使用 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! #0001] 初探channel,理解最简单的聊天频道_第4张图片

这一期的elixir! 就到这里了,如果你觉得有哪里不太明白的,或者发现了错误的地方,请务必留言!下一期我们将继续扩展这个聊天室的功能。

你可能感兴趣的