在前端中 WebSocket 是H5新增的对象
主要作用有:实时通讯 长连接 双向传输 后端主动推送数据
websocket实例的主要事件
前端:
直接new 一个实例
const ws = new WebSocket("ws://localhost:9100");
大部分浏览器已经支持 WebSocket 对象 协议格式为ws(不是 file http)
主要事件:
open | 建立链接 |
close | 断开链接 |
error | 发生错误 |
message | 发送消息给后端 |
后端:
使用node 来简单搞一个 本地服务、后端要依赖第三方包使用websocket 这里以ws为例
npm i [email protected]
引入ws 创建服务 配置端口和前端一致
const ws = require('ws')
const wss= new ws.Server({port:9100})
主要事件:
open | 建立链接 |
close | 断开链接 |
error | 发生错误 |
connection message |
有客户端连接上 接收到客户端发送来的消息 一般 message事件在 connection里面 |
案例流程梳理
- 首先登录 选择 头像 起昵称(不能和聊天室内已经有的的重复)
- 进入聊天室 发送欢迎信息 显示在线人数+1
- 发消息时 后端会自动转发给其他连接上的客户端
- 退出的时候 释放 昵称的命名空间
登录页面展示:
样式部分省略...
使用双向绑定获取 用户输入的昵称和 选择的头像 头像v-for渲染数据 点击时currentIndex修改为当前的索引 根据索引 增加高亮边框和 选择此数据传给下一步
<input type="text" placeholder="请输入发言昵称" v-model="nickname" id="input" />
<ul class="avatar">
<li v-for="(aa,index) in avatar_list"
:key="index"
:class="{curr : currentIndex===index}"
@click="bianse(index)">
<img :src="aa" alt="">
</li>
验证输入用户的长度要在1-9位
想服务器发起请求 存放昵称(禁止其他人使用)
收集数据保存到 localStorage 中 进行下一步
methods:{
defind(){
if (this.nickname.length < 1) {
return alert("请输入昵称");
}
if (this.nickname.length > 9) {
return alert("输入昵称过长");
}
this.$http.get(`/login/${this.nickname}`).then(res => {
if(res.data.status === 1){
localStorage.setItem("nickname", this.nickname);
localStorage.setItem("avatar", this.avatar_list[this.currentIndex]);
this.$router.push('/about')
}else{
return alert("昵称已被占用");
}
})
},
bianse(index){
this.currentIndex = index
}
}
聊天室页面
预留组件中需要的数据 进入组件和挂载元素 生命周期中进行对应的操作
进入到页面 简易判断 前一步保存到 localStorage 的数据是否存在 如果不存在自动返回登录页面重新设置 防止用户跳过登录直接进入到 聊天室 没有昵称导致的一系列错误 这一步也可以使用 导航守卫来实现
data(){
return {
nickname:'', // 用户的昵称
message:'', // 用户发送的消息
record:[], // 消息记录数组
ws:null, // ws 实例 预留变量
user_list:[] // 实时在线人数列表
}
},
created() {
this.nickname = localStorage.getItem("nickname")
if (!this.nickname) {
return this.$router.push('/');
}
},
mounted 生命周期中 创建 WebSocket实例
添加 ws相关事件 这里只需要
open 连接上ws服务器端了 发送欢迎消息
message 接收到服务器返回来的数据 渲染到页面 聊天信息部分 并判断是新进入的用户发送的消息 还是老用户发送的消息 如果是新用户 就添加到左侧在线列表 老用户此步骤忽略
业务流程:连接上后端发送欢迎消息 =》后端接收消息 返回给每个客户端 =》 客户端接收到服务端发来的消息 渲染到 消息列表 并且根据条件 渲染左侧在线列表
mounted() {
this.ws = new WebSocket("ws://localhost:9100");
this.ws.addEventListener("open", () => {
this.ws.send(
JSON.stringify({
user: this.nickname,
avatar:localStorage.getItem('avatar'),
dateTime: this.nowTimeFormatChinese(new Date()),
message:'欢迎 ' +this.nickname + ' 来到聊天室',
})
);
});
this.ws.addEventListener("message", (e) => {
const data = JSON.parse(e.data)
this.record.push(data);
const flag = this.user_list.filter(x => x.user === data.user)
if(flag.length === 0){
this.user_list.push(data);
}
});
},
点击发送按钮 组织好数据 ws.send 发送给服务端
发送消息 返回渲染之后 滚动跳滚到最新消息处
this.$refs.lists.scrollTop += 100
// window.scrollTo(0, document.body.scrollHeight);
还有格式化时间的方法
methods:{
send(){
if (!this.message.trim().length) {
return alert("请输入内容");
}
this.ws.send(
JSON.stringify({
user: this.nickname,
avatar:localStorage.getItem('avatar'),
dateTime: this.nowTimeFormatChinese(new Date()),
message: this.message,
})
);
this.message = "";
setTimeout(()=>{
this.$refs.lists.scrollTop += 100
// window.scrollTo(0, document.body.scrollHeight);
},100)
},
padZero(n){
return n > 9 ? n : "0" + n;
},
nowTimeFormatChinese(riqi){
let hour = this.padZero(riqi.getHours()),
min = this.padZero(riqi.getMinutes()),
sec = this.padZero(riqi.getSeconds())
return hour + "时" + min + "分" + sec + "杪";
}
},
离开页面(销毁组件)时 清楚自己的昵称 ---左侧的在线列表 和 服务器端的命名空间
deactivated(){
const index = this.user_list.findIndex(x => x.user === this.nickname)
this.user_list.splice(index, 1)
this.$http.get(`/loginout/${this.nickname}`)
},
聊天室页面完整模板(样式省略):
渲染消息列表时 判断是不是自己所发的消息 返回来的user === 自己的nickname
是的话添加 meSay 样式 右侧显示 作为区分
<template>
<div class="about">
<ul id="list" ref="lists">
<li v-for="(n,index) in record" :key="index" :class="{meSay : n.user === nickname}">
<div>
<div class="cow">
<img :src="n.avatar" alt="" class="avatar">
<p class="ppp">
<span>{
{n.user}}</span>
<br>
<span>{
{n.dateTime}}</span>
</p>
</div>
<div class="nei">
{
{n.message}}
</div>
</div>
</li>
</ul>
<div class="bottom">
<div class="people">
<h3>当前在线人数:{
{this.user_list.length}}</h3>
<div class="user_list" v-for="n in user_list" :key="n.user">
<img :src="n.avatar" alt="">
{
{n.user}}
</div>
</div>
<textarea id="message" v-model="message" @keyup.enter="send"></textarea>
<button id="send" @click="send">发送</button>
</div>
</div>
</template>
后端完整代码
ws部分比较简单 监听链接 和 接收消息的事件 出发了就遍历所有链接的客户端 把数据原封不动的发送出去
const ws = require('ws')
const wss= new ws.Server({port:9100})
wss.on('connection',(client)=>{ // clent 这个客户端链接了
client.on('message',(msg)=>{ // 并且发来了数据
const radio = msg.toString() // 数据转换格式防止乱码
wss.clients.forEach(e =>{ // 遍历再原封不动发送给每个链接的客户端
e.send(radio)
})
})
})
管理昵称命名空间的端口
引入express 快速搭建本地服务器
npm i express@4
使用中间件 解决跨域问题
创建 activeUser 数组储存 已在线的用户昵称、登录时发送请求携带 nickname req.params 获取路径中的形参 判断在 activeUser 中是否存在 如果存在添加失败 不存在 添加进去 这样保证昵称不重复、 注销时发送请求 携带nickname 在activeUser 查找到 并且 删除它
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 这样也可以解决跨域问题
app.use(function(req,res,next){
// 第二个 * 代表通配符 也可以指定具体的网站 http://www.wsg3096.com
const contentType = 'application/json; charset=utf-8'
res.setHeader('Content-Type',contentType)
res.setHeader('Access-Control-Allow-Origin','*')
// 后面的也可以用通配符
res.setHeader('Access-Control-Allow-Methods','OPTIONS,GET,PUT,POST,DELETE')
// 设置其他的请求头
res.setHeader('Access-Control-Allow-Headers','Content-Type','X-Custom-Header')
next()
})
const activeUser = []
// 登录的 API 接口
app.get('/api/login/:nickname', (req, res) => {
const nickname = req.params.nickname
const find = activeUser.find(x => x=== nickname)
if(find){
return res.send({
status:0,
msg:'用户昵称已经被占用'
})
}else{
activeUser.push(nickname)
return res.send({
activeUser,
status:1,
msg:'成功进入聊天室队列'
})
}
})
// 注销的接口
app.get('/api/loginout/:nickname', (req, res) => {
const nickname = req.params.nickname
const index = activeUser.findIndex(x => x=== nickname)
activeUser.splice(index,1)
return res.send({
activeUser,
status:200,
msg: `成功释放${nickname}的命名空间`
})
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(7777, function () {
console.log('Express server running at http://127.0.0.1:7777')
})