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