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

 

posted @ 2023-02-22 11:12  web喵神  阅读(597)  评论(0编辑  收藏  举报