socket.io实现简易聊天室功能
本文简单介绍使用websocket实现一个简单的聊天室功能,我这里是用vite初始化的vue3项目。
在线体验地址:http://chat.lb0125.com/chat
需要安装的库:
socket.io、socket.io-client等
1、代码
整体代码目录结构如下,分为客户端和服务端代码:
1.1、服务端代码chat_server
a、首先使用 npm init 初始化一个node工程
b、然后npm install socket.io
c、新建一个app.js文件,代码如下:
const { createServer } = require("http"); const { Server } = require("socket.io"); const httpServer = createServer(); const io = new Server(httpServer, { cors: { //解决跨域问题 origin: "*", methods: ["GET", "POST"] } }); io.on("connection", (socket, data) => { // 接受消息 socket.on('sendMsg', (data) => { // io表示广播出去,发送给全部的连接 io.emit('sendMsged', data) }); // 接受登录事件 socket.on('login', data => { io.emit('logined', data) }) // 接受登出事件 socket.on('logout', data => { io.emit('logouted', data) }) // 监听客户端与服务端断开连接 socket.on('disconnecting', () => { console.log('客户端断开了连接') }) }); httpServer.listen(3011, function () { console.log('http://localhost:3011') });
1.2、客户端代码 chat_client
由于我这里还使用安装了vue-router4、element-plus、less-loader moment等库,当然您可以根据自己需要决定是否安装
第一步:初始化vue3+vite项目
npm create vite@latest 后 根据提示输入项目名称,选择vue版本进行后续操作
第二步:npm install socket.io-client以及其他需要使用到的库
第三步:添加环境配置文件,修改vite.config.js配置以及编写代码
a、vite.config.js:
import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' //引入gzip静态资源压缩 import viteCompression from 'vite-plugin-compression' const path = require('path'); const { resolve } = require('path'); export default ({ mode,command }) => { console.log('当前环境=========' + mode) const plugins = [ vue(), ]; // 如果是非开发环境,配置按需导入element-plus组件 if (mode !== 'development') { plugins.push( AutoImport({ resolvers: [ElementPlusResolver()], }) ); plugins.push( Components({ resolvers: [ElementPlusResolver()], }) ); } return defineConfig({ base: './', plugins, hmr: { overlay: false }, // 配置前端服务地址和端口(可注释掉,默认是localhost:3000) server: { host: '0.0.0.0', port: 9000, // 是否开启 https https: false, // 本地跨域代理 proxy: { '/***': { target:'http://****', changeOrigin: true, }, } }, // 起个别名,在引用资源时,可以用‘@/资源路径’直接访问 resolve: { alias: { "@": resolve(__dirname, "src"), }, }, css: { preprocessorOptions: { less: { modifyVars: { hack: `true; @import (reference) "${path.resolve("src/assets/css/base.less")}";`, }, javascriptEnabled: true, }, }, }, // 打包配置 build: { minify: 'terser', terserOptions: { compress: { drop_console: true, // 清除console drop_debugger: true, // 清除debugger }, }, chunkSizeWarningLimit: 1500, // 大文件报警阈值设置 rollupOptions: { output: { chunkFileNames: 'static/js/[name]-[hash].js', entryFileNames: 'static/js/[name]-[hash].js', assetFileNames: 'static/[ext]/[name]-[hash].[ext]', // 超大静态资源拆分 manualChunks(id) { if (id.includes('node_modules')) { let str = id.toString().split('node_modules/')[1].split('/')[0].toString(); return str.substring(str.lastIndexOf('@') + 1); } } } } } }) }
b、表情图片放置在chat_client/public/imgs/face目录下:
c、在src/common目录下新建constant.js文件:
/** * 常量 */ // 是否是开发环境 export const isDev = import.meta.env.VITE_APP_ENV == "development" ? true : false; // 当前所属环境 export const env = import.meta.env.VITE_APP_ENV; // api地址 export const baseUrl = import.meta.env.VITE_APP_BASEURL; export const wsUrl = import.meta.env.VITE_WS_URL; // 表情 export const faceImgs = [ { img: 'weixiao.png', name: '[微笑]' }, { img: 'yukuai.png', name: '[愉快]' }, { img: 'aoman.png', name: '[傲慢]' }, { img: 'baiyan.png', name: '[白眼]' }, { img: 'ciya.png', name: '[呲牙]' }, { img: 'daku.png', name: '[大哭]' }, { img: 'deyi.png', name: '[得意]' }, { img: 'fadai.png', name: '[发呆]' }, { img: 'fanu.png', name: '[发怒]' }, { img: 'ganga.png', name: '[尴尬]' }, { img: 'haixiu.png', name: '[害羞]' }, { img: 'jie.png', name: '[饥饿]' }, { img: 'jingkong.png', name: '[惊恐]' }, { img: 'jingya.png', name: '[惊讶]' }, { img: 'lenghan.png', name: '[冷汗]' }, { img: 'liulei.png', name: '[流泪]' }, { img: 'nanguo.png', name: '[难过]' }, { img: 'piezui.png', name: '[撇嘴]' }, { img: 'se.png', name: '[色]' }, { img: 'shui.png', name: '[睡]' }, { img: 'tiaopi.png', name: '[调皮]' }, { img: 'touxiao.png', name: '[偷笑]' }, { img: 'zhuakuang.png', name: '[抓狂]' }, ]
e、在src/views目录下新建chat文件夹,并在chat文件夹下新建Chat.vue文件
<template> <div class="container"> <div class="header" :style="{ 'border-bottom': isMobile ? '1px solid #eee' : '0 none', padding: isMobile ? '0 12px' : '0px' }"> <el-button type="primary" @click="onConnect" v-if="userName == ''">进入会话</el-button> <span v-else style="flex:1;">您的姓名:{{ userName }}</span> <el-button @click="onDisConnect" v-show="userName !== ''">退出会话</el-button> </div> <div class="content" id="content" @click="() => { isShowFace = false }" :style="{ border: isMobile ? 'none' : '1px solid #eee' }"> <ul> <li v-for="(item, i) in msgList" :key="i"> <p v-if="item.type == 0" class="item content-login">{{ item.date }} {{ item.userId == userId ? '您' : item.userName }}{{ item.msg }}</p> <p v-if="item.type == -1" class="item content-logout">{{ item.date }} {{ item.userId == userId ? '您' : item.userName }}{{ item.msg }}</p> <div v-if="item.type == 1" class="item" :class="{ 'text-right': userId == item.userId }"> <!-- 自己发送的消息 --> <div v-if="item.userId == userId" class="clearfix"> <div class="content-name" style="text-align:right;"> <span style="margin-right:10px;">{{ item.date }}</span> <span>{{ item.userName }}</span> </div> <div class="content-msg right" style="text-align: right;"> <p style="display:inline-block;text-align:left;border-top-right-radius: 0px;background: #73e273;" v-html="item.msg"></p> </div> </div> <!-- 别人发送的消息 --> <div v-else> <div class="content-name"> <span>{{ item.userName }}</span> <span style="margin-left:10px;">{{ item.date }}</span> </div> <div class="content-msg" style="text-align: left;"> <p style="display:inline-block;text-align:left;border-top-left-radius: 0px;" v-html="item.msg"></p> </div> </div> </div> </li> </ul> </div> <div class="footer" :style="{ padding: isMobile ? '10px 12px' : '10px 0px' }"> <el-input class="content-input" type="textarea" :autosize="{ minRows: 1, maxRows: 5 }" resize="none" v-model="msg" ref="inputRef" placeholder="请输入发送内容" @focus="onInputFocus" @keyup.enter="onSend" /> <div class="face-icon"> <img src="@/assets/img/face.png" alt="表情" @click="onToggleFace"> </div> <div class="send-dv"> <el-button type="primary" class="send-btn" @click="onSend" :disabled="userName == ''">发 送</el-button> </div> </div> <div class="face-dv" v-show="isShowFace" :class="{ 'face-dv-pc': !isMobile }"> <div class="arrow" v-show="!isMobile"></div> <div class="face"> <div class="face-item" v-for="item in faceImgs" :key="item.img" @click="onInputFace(item.img, item.name)"> <img :src="`./imgs/face/${item.img}`" /> </div> </div> </div> </div> </template> <script> export default { name: 'chat' } </script> <script setup> import { onMounted, ref, reactive, nextTick } from "vue" import moment from 'moment' import { ElMessage, ElMessageBox } from 'element-plus' import { io } from 'socket.io-client'; import { wsUrl, faceImgs } from '@/common/constant'; import { isPC, guid } from '@/utils/utils'; const isMobile = ref(!isPC()); const isShowFace = ref(false); const inputRef = ref(null); const msg = ref('') const userId = ref(''); // 用户id const userName = ref(''); // 用户姓名 let socket = null; const abc = ref('<span>asdf</span>') const msgList = reactive( [ // { id: 0, type: 1, userName: '张安', date: '2012-12-12 12:12:12', msg: '哈哈哈1' }, // { id: 1, type: 0, userName: '张安1', date: '2012-12-12 12:12:12', msg: '张安进入会话' }, // { id: 2, type: 1, userName: '张安2', date: '2012-12-12 12:12:12', msg: '哈哈哈3' }, ] ) // 表情框显示隐藏切换 const onToggleFace = () => { if (userName.value == '') { ElMessage.warning('请先点击进入会话'); return; } if (isShowFace.value) { isShowFace.value = false; } else { isShowFace.value = true; } } // 输入框获取焦点事件 const onInputFocus = (e) => { isShowFace.value = false; if (userName.value == '') { ElMessage.warning('请先点击进入会话'); } } // 点击表情 const onInputFace = (img, name) => { console.log(img, name) msg.value += name; isShowFace.value = false; } // 点击发送消息 const onSend = () => { if (msg.value.trim() == '') { ElMessage.warning('不可以发送空白消息') return } let str = msg.value; faceImgs.forEach(item => { if (str.indexOf(item.name) > -1) { str = str.replaceAll(item.name, `<img src="./imgs/face/${item.img}" style="width:20px;vertical-align:top;" />`); } }) socket.emit('sendMsg', { type: 1, userId: userId.value, userName: userName.value, date: moment(new Date()).format('HH:mm:ss'), msg: str }) msg.value = ''; isShowFace.value = false; } const onConnect = () => { console.log('onConnect') ElMessageBox.prompt('请输入您的姓名', '提示', { confirmButtonText: '确 认', cancelButtonText: '取 消', inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/, inputErrorMessage: '你的姓名', }).then(({ value }) => { userId.value = guid(); userName.value = value; socket = io.connect(wsUrl); setTimeout(() => { socket.emit('login', { id: Math.random() * 100000000, type: 0, date: moment(new Date()).format('HH:mm:ss'), userId: userId.value, userName: userName.value }) }, 200) socket.on('connect', () => { console.log('客户端建立连接'); // true }); // 监听登录事件 socket.on('logined', data => { msgList.push({ id: Math.random() * 100000000, type: data.type, userId: data.userId, userName: data.userName, date: data.date, msg: ' 进入会话' }) }) // 监听登录出事件 socket.on('logouted', data => { msgList.push({ id: Math.random() * 100000000, type: data.type, userId: data.userId, userName: data.userName, date: data.date, msg: '离开会话' }) }) // 监听发送消息事件 socket.on('sendMsged', data => { msgList.push({ id: Math.random() * 100000000, type: data.type, userId: data.userId, userName: data.userName, date: data.date, msg: data.msg }) nextTick(() => { let contentNode = document.getElementById('content') contentNode.scrollTop = contentNode.scrollHeight }) }) }).catch(() => { }) } const onDisConnect = () => { console.log('onDisConnect') socket.emit('logout', { id: Math.random() * 100000000, type: -1, date: moment(new Date()).format('HH:mm:ss'), userId: userId.value, userName: userName.value }) setTimeout(() => { socket.disconnect() userId.value = ''; userName.value = ''; }, 200) } </script> <style lang="less" scoped> .container { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; max-width: 522px; margin: 0px auto; .header { height: 50px; display: flex; align-items: center; border-bottom: 1px solid #eee; } .content { flex: 1; padding: 12px; overflow-y: auto; .item { margin-bottom: 20px; &.content-login { font-size: 12px; color: #666; text-align: center; } &.content-logout { font-size: 12px; color: #666; text-align: center; } } .content-name { font-size: 12px; color: #666; margin-bottom: 5px; } .content-msg { width: 90%; word-break: break-word; font-size: 16px; p { padding: 8px 10px; background: #eee; border-radius: 8px; color: #232323; } } } .footer { display: flex; align-items: end; .content-input { flex: 1 } .face-icon { position: relative; width: 56px; height: 100%; img { position: absolute; width: 26px; height: 26px; left: 17px; bottom: 4px; } } .send-dv { position: relative; width: 60px; height: 100%; .send-btn { position: absolute; width: 100%; height: 34px; left: 0; bottom: 0; } } } .face-dv { height: 150px; background: #eee; &.face-dv-pc { position: absolute; width: 100%; left: 0px; bottom: 53px; } .arrow { position: absolute; bottom: -20px; right: 75px; width: 0; height: 0; border: 10px solid; border-color: #eee transparent transparent transparent; } .face { width: 100%; height: 100%; padding: 4px; overflow: auto; .face-item { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; margin: 6px; border-radius: 4px; &:hover { background: #ced8d5; } img { width: 27px; height: 27px; } } } } } ::-webkit-scrollbar { /*滚动条整体样式*/ width: 4px; /*高宽分别对应横竖滚动条的尺寸*/ height: 4px; } ::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ border-radius: 3px; background: #1c1e2038; } :deep(.el-textarea__inner) { min-height: 34px !important; } :deep(.el-textarea__inner) { font-size: 16px; // 隐藏滚动条 scrollbar-width: none; /* firefox */ -ms-overflow-style: none; /* IE 10+ */ &::-webkit-scrollbar { display: none; /* Chrome Safari */ } }</style>
注意:上面Chat.vue文件里引入了utils/utils文件里的isPC和guid方法,这两个方法分别是用来判断当前是否是pc端和生成uuid的。
2、效果图
3、部署代码到服务器
最后分别把客户端代码和服务端代码部署到服务器上就可以玩耍了
需要购买阿里云产品和服务的,点击此链接可以领取优惠券红包,优惠购买哦,领取后一个月内有效: https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=fp9ccf07