使用vue3和nodejs搭建一个websocket聊天

310 阅读2分钟

首先创建一个server.js文件

const WebSocket = require('ws')
const express = require('express')
// cors是Express的一个第三方中间件。通过安装和配置cors中间件,可以很方便地解决跨域问题(https://blog.csdn.net/qq_46311811/article/details/128088175)。
const cors = require('cors')
const mock = require('./mock')
const app = express()

app.use(cors())

// 创建一个服务,端口为8080
const server = app.listen(8080, () => {
  console.log('服务启动成功,端口为:8080')
})

const wss = new WebSocket.Server({ server })

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const message = JSON.parse(data)

    wss.clients.forEach((client) => {
      // 如果不是当前客户端,并且客户端的连接状态是OPEN,则发送消息
      if (client.readyState === WebSocket.OPEN) {
        // 初始化的消息不广播
        if (message.type !== 'init') {
          const data = { ...message, isMyselt: client == ws }
          client.send(JSON.stringify(data))
        }
      }
    })
  })

  // 关闭连接回调
  wss.on('close', function () {
    console.log('连接已关闭')
  })
})

创建一个socket.js文件,定义一个名为Socket的类,方便项目中多次调用

  • new WebSocket(this.url):传入url,实例化WebSocket对象
  • ws.onopen(()=>{}):开启连接
  • ws.onmessage(()=>{}):接收服务器返回的消息
  • ws.close():关闭连接( 会触发onclose )
  • ws.onclose(()=>{}):关闭连接时的回调
  • ws.onerror(()=>{}):连接报错
  • ws.send():给服务器发送消息
  • 心跳机制:创建一个定时器,在一定时间间隔内给服务器发送一段固定信息,判断当前的连接状态
export default class Socket {
  /**
   * @Descripttion:初始化实例属性,保存参数
   * @url {String} ws链接地址
   * @callback {Function} 服务器返回的信息后的回调
   * @name {String}
   * @ws {Object} WebSocket实例化对象
   * @status {'open'|'close'} 连接状态
   * @pingInterval {Function} 心跳机制定时器
   * @_timeout {Number} 心跳检测频率
   * @isHeart {Boolean} 是否心跳
   * @isReconnection {Boolean} 断开后是否重连
   */
  constructor(options) {
    this.url = options.url
    this.callback = options.received
    this.name = options.name || 'default'
    this.ws = null
    this.status = null
    this.pingInterval = null
    // 心跳检测频率
    this._timeout = options.timeout || 10000
    this.isHeart = options.isHeart
    this.isReconnection = options.isReconnection
    // 建立连接 - 创建实例时会自动调用该方法
    this.connect({ type: 'init' })
  }
  
  connect(data) {
    this.ws = new WebSocket(this.url)

    // 建立连接
    this.ws.onopen = (e) => {
      this.status = 'open'
      console.log('连接成功', e)
      if (this.isHeart) {
        // 开启心跳机制
        this._heartCheck()
      }
      // 给后台发送消息
      if (data !== undefined) {
        return this.ws.send(JSON.stringify({ type: 'init' }))
      }
    }

    // 接收服务器返回的消息
    this.ws.onmessage = (e) => {
      if (typeof this.callback === 'function') {
        return this.callback(e.data)
      } else {
        console.log('参数的类型必须为函数')
      }
    }

    // 关闭连接
    this.ws.onclose = (e) => {
      console.log('onclose', e)
      this._closeSocket(e)
    }

    // 报错
    this.onerror = (e) => {
      console.log('onerror', e)
      this._closeSocket(e)
    }
  }

  // 给后台发送消息
  sendMsg(data) {
    let msg = JSON.stringify(data)
    return this.ws.send(msg)
  }

  // 清除心跳机制定时器
  _resetHeart() {
    clearInterval(this.pingInterval)
    return this
  }

  // 开启心跳机制
  _heartCheck() {
    this.pingInterval = setInterval(() => {
      if (this.ws.readyState === 1) {
        this.ws.send(JSON.stringify({ type: 'ping' }))
      }
    }, this._timeout)
  }

  _closeSocket(e) {
    this._resetHeart()
    if (this.status !== 'close') {
      console.log('断开,重连', e)
      if (this.isReconnection) {
        // 重连
        this.connect()
      }
    } else {
      console.log('手动关闭了', e)
    }
  }

  close() {
    this.status = 'close'
    this._resetHeart()
    return this.ws.close()
  }
}

创建一个websocket.vue文件

<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import Socket from './socket'

interface ListType {
id: number
text: string
type: string
isMyselt: boolean
}

const list = ref<ListType[]>([])
const inputVal = ref('')
const ws = ref()

const socketFn = () => {
// 初始化
ws.value = new Socket({
  // url: 'wss://echo.websocket.org', // 阮一峰老师教程链接
  url: 'ws://localhost:8080',
  name: '', // name
  timeout: 15000,
  isHeart: true, // 是否心跳
  isReconnection: true, // 是否断开重连
  // 监听服务器返回信息
  received: (data) => handleMessage(data)
})

// 手动关闭
// ws.value.close()
}

const handleMessage = (data: string) => {
const msgData = JSON.parse(data)
if (msgData?.type == 'ping') return
list.value.push(msgData)
}

const sendMessage = () => {
if (!inputVal.value) return
const message = {
  id: Date.now(),
  text: inputVal.value,
  type: 'sendMsg'
}

// ws 处于连接状态才能发送消息
if (ws.value.status === 'open') {
  // 发送消息
  ws.value.sendMsg(message)
  inputVal.value = ''
} else {
  console.error('WebSocket未连接,消息未发送')
}
}

onMounted(() => {
socketFn()
})
</script>

<template>
<el-card class="my-card">
  <div class="content">
    <div>
      <h2>消息列表</h2>
      <hr />
      <div class="msg">
        <div v-for="message in list" :key="message.id">
          <div class="opposite" v-if="message.isMyselt">
            {{ message.text }}
            <el-tag type="success"></el-tag>
          </div>

          <div v-if="!message.isMyselt">
            <el-tag type="warning">对方</el-tag>
            {{ message.text }}
          </div>
        </div>
      </div>
    </div>

    <div style="margin-top: 20px">
      <el-input
        style="width: 170px"
        size="large"
        type="text"
        v-model="inputVal"
        @keyup.enter="sendMessage()"
      />
      <el-button style="margin-left: 10px" size="large" type="primary" @click="sendMessage()"
        >发送</el-button
      >
      <el-button size="large" type="danger" @click="() => ws.value.close()">关闭聊天</el-button>
    </div>
  </div>
</el-card>
</template>

<style scoped lang="scss">
.my-card {
width: 400px;
min-height: 600px;
.content {
  min-height: 600px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  .msg {
    height: 500px;
    padding: 20px 0 0 20px;
    overflow-y: auto;
    > div {
      margin-top: 12px;
    }
    .opposite {
      display: flex;
      justify-content: end;
    }
  }
}
}

.el-tag.el-tag--success {
margin-left: 4px;
}
.el-tag.el-tag--warning {
margin-right: 4px;
}
</style>


运行

  • 此时我们运行server.js文件:node .\server.js
  • 运行vue项目,同时运行在两个浏览器窗口,就可以互相聊天了

Snipaste_2023-09-06_09-31-57.jpg