Appearance
简易聊天室
WebSocket 原生 API
js
const ws = new WebSocket("地址"); // 创建 websocket 连接,浏览器自动握手
// 事件:握手完成后触发(异步)
ws.onopen = function () {
console.log("连接到了服务器");
};
// 事件:收到服务器消息后触发
ws.onmessage = function (e) {
console.log(e.data); // e.data:服务器发送的消息
};
// 事件:连接关闭后触发(可以是客户端主动关闭,也可以是服务器主动关闭或者浏览器关闭了)
// 可以通过 wbsocet 实例的 close 方法来关闭连接
ws.onclose = function () {
console.log("连接关闭了");
};
// 发送消息到服务器
ws.send(消息);
// 连接状态:0-正在连接中 1-已连接 2-正在关闭中 3-已关闭
ws.readyState;当使用 live server 插件时,可以从浏览器的 network 选项卡中看到有两个 websocket 连接,这是因为 live server 插件本身就是利用 websocket 来实现了热更新的,通过查看网页的源代码可以看到,live server 插件会在网页中引入一个 js 脚本,其中创建了 websocket 来链接 live server 搭建的服务器。
Socket.io
原生的接口虽然简单,但是在实际应用中会造成很多麻烦
比如一个页面,既有 K 线,也有实时聊天,于是:

上图是一段时间中服务器给客户端推送的数据,你能区分这些数据都是什么意思吗?
这就是问题所在:连接双方可以在任何时候发送任何类型的数据,另一方必须要清楚这个数据的含义是什么。
回忆 HTTP 是如何处理这个问题的?通过不同的路径来区分
你会如何解决这个问题?比如发送消息时,在消息中包含一个类型字段,服务器根据类型字段来判断消息的类型,客户端根据类型字段来判断消息的类型
虽然我们可以自行解决这些问题,但毕竟麻烦
Socket.io帮助我们解决了这些问题,它把消息放到不同的事件中,通过监听和触发事件来实现对不同消息的处理

客户端和服务器双方事先约定好不同的事件,事件由谁监听,由谁触发,就可以把各种消息进行有序管理了
注意,Socket.io 为了实现这些要求,对消息格式进行了特殊处理,因此如果一方要使用 Socket.io,双方必须都使用
在客户端,使用 Socket.io 是非常简单的
参见:https://socket.io/docs/v4/client-installation/
在约定事件名时要注意,Socket.io 有一些预定义的事件名,比如 message、connect 等
为了避免冲突,建议自定义事件名使用一个特殊的前缀,比如
$
除此之外,Socket.io 对低版本浏览器还进行了兼容处理,如果浏览器不支持 WebSocket,Socket.io 将使用长轮询(long polling)处理。另外,Socket.io 还支持使用命名空间来进一步隔离业务,要了解这些高级功能,以及 Socket.io 的更多 API,请参阅其官方文档
接口文档
测试接口
连接地址:ws://localhost:9527
服务器消息:
- 服务器每隔 3 秒钟会发送一个消息给客户端
- 每次收到客户端的消息后,服务器会回应一个消息
聊天室接口
连接地址:ws://localhost:9528
服务器触发的事件/客户端需要监听的事件:
| 事件名 | 触发时机 | 传递的消息 | 传递消息示例 |
|---|---|---|---|
| $updateUser | 有新用户进入 有老用户退出 自己进入 | 当前聊天室的用户数组 | ['张三', '李四'] |
| $name | 自己进入 | 分配的用户名称 | "张三" |
| $history | 自己进入 | 历史聊天记录 | [{ name:"张三", content:"你好", date: 1635484786373 }] |
| $message | 其他人发送消息 | 消息对象 | { name:"张三", content:"你好", date: 1635484786373 } |
客户端触发的事件/服务器需要监听的事件:
| 事件名 | 触发时机 | 传递的消息 | 传递消息示例 |
|---|---|---|---|
| $message | 发送聊天消息 | 消息字符串 | "你好!" |
关键代码实现
vue
<template>
<ChatWindow :me :users :history @chat="handleChat" />
</template>
<script setup>
import ChatWindow from "./components/ChatWindow.vue";
import { ref, onMounted, onBeforeUnmount } from "vue";
import io from "socket.io-client";
let me = ref("AK");
let users = ref([]);
let history = ref([]);
let handleChat = (data) => {
history.value.push(data);
socket.emit("$message", data.content);
};
let socket;
onMounted(() => {
// 异步连接 websocket
socket = io("ws://localhost:9528");
// 监听事件
socket.on("connect", () => {
console.log("连接成功");
});
socket.on("$updateUser", (data) => {
users.value = data;
});
socket.on("$history", (data) => {
console.log(data);
history.value = data;
});
socket.on("$name", (name) => {
me.value = name;
});
socket.on("$message", (data) => {
history.value.push(data);
});
});
onBeforeUnmount(() => {
// 组件销毁时,关闭 websocket 连接
socket.disconnect();
});
</script>vue
<template>
<div class="container">
<!-- 用户区域 -->
<div class="users-area">
<p class="title">聊天室成员</p>
<ul class="users-list">
<li v-for="(name, index) in users" :key="index">{{ name }}</li>
</ul>
</div>
<div class="main">
<!-- 聊天区域 -->
<div class="chat-area" ref="chatArea">
<!-- 每一条对话 -->
<div
class="chat-item"
v-for="item in history"
:key="item.date"
:class="{ mine: item.name === props.me }"
>
<div class="chat-name">{{ item.name }}</div>
<div class="chat-content">{{ item.content }}</div>
<div class="chat-date">{{ formatDate(item.date) }}</div>
</div>
</div>
<!-- 输入框 -->
<div class="input-area">
<textarea v-model="content" @keydown.enter="handleEnter"></textarea>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, useTemplateRef } from "vue";
import moment from "moment";
const props = defineProps({
users: {
type: Array,
default: () => [],
},
history: {
type: Array,
default: () => [],
},
me: {
type: String,
required: true,
},
});
const emit = defineEmits(["chat"]);
let content = ref("");
let chatArea = useTemplateRef("chatArea");
onMounted(() => {
watch(
() => props.history,
() => {
chatArea.value.scroll(0, chatArea.value.scrollHeight);
},
{
immediate: true,
}
);
});
let formatDate = (date) => {
date = moment(date);
return date.fromNow().replace(/\s/g, " ");
};
let handleEnter = () => {
const value = content.value.trim();
if (value) {
emit("chat", { name: props.me, content: value, date: Date.now() });
content.value = "";
}
};
</script>