566
社区成员
本项目基于socket.io+Vue+Express设计和实现网页聊天室。
Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为Web和移动应用程序提供一组强大的功能。通过使用您所选择的各种 HTTP 实用工具和中间件,快速方便地创建强大的 API。通过使用HTTP、fs、Socket.io、session等中间件,能够快速搭建一个web应用程序。
Vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
使用Vue的核心插件VueRouter可方便地实现单页应用,提供组件间路由功能。Vue Router 是 Vue.js (opens new window)官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:嵌套的路由/视图表、模块化的基于组件的路由配置、路由参数、查询、通配符等。
Websocket介绍
为了实现客户端和服务器的对等通信,解决轮询方法带来的开销问题,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
优点
Socket.io介绍
Socket.io是一个基于Node.js的websocket实现,在客户端和服务端都可用。在服务端和客户端之间提供高效率的通信。高可用性的服务,如果WebSocket连接无法实现,它将退回到HTTP长轮询,如果连接丢失,客户端将自动尝试重新连接。易用性,能够方便地实现向多个服务器发送多个事件信息。
Axios是一个基于Promise的HTTP库,可以用在浏览器和node.js中。本项目使用Axios在客户端用于异步获取静态信息。实时性要求比较高的聊天信息使用socket.io进行收发。
实现的功能:
1. 用户输入登陆的昵称和头像,进入聊天页面
2. 用户在页面选择不同的聊天室房间,输入并且收发信息
用户的用例图如下:
登陆聊天室的流程:
发送聊天信息的流程:
ER图设计如下:
通过以上的ER图导出如下的关系模式:
1. 用户(用户ID,昵称,头像号)
2. 聊天室(ID,聊天室名字,用户ID)
3. 历史消息(消息ID,发送者,消息内容,聊天室ID)
实际的业务过程中,因为用户可以看到所有的聊天室,并在不同的聊天室之间切换,聊天室不记录用户加入的记录,所以Rooms关系模式不存储clientID。
Clients{
string clientID,
string clientName,
int avatarNum
}
Rooms{
String roomId,
String roomName,
}
HistoryMsgs{
String msgId,
String clientId,
String msg,
String roomId
}
· 采用VueCLI脚手架搭建前端。将页面分成Chatroom.vue和Login.vue两个组件,使用Vue-router在页面间跳转。Chatmsgs.vue和GroupInfo.vue是用于显示信息使用的组件。
· 前端还使用View-UI样式库和Socket.io-client作为组件库和websocket通信组件。
Login.vue
<template>
<div id="login">
<Form ref="formInline" :model="formInline" :rules="ruleInline" inline>
<FormItem prop="clientName">
<Input
type="text"
v-model="formInline.clientName"
placeholder="clientName"
>
<Icon type="ios-person-outline" slot="prepend"></Icon>
</Input>
</FormItem>
<br />
<FormItem>
<Button type="info" @click="handleSubmit('formInline')">Signin</Button>
</FormItem>
<div id="avatar">
<Avatar :style="{ background: formInline.color }">{{
formInline.avatarNum
}}</Avatar>
<Button size="large" @click="avatarChange">选择头像</Button>
</div>
</Form>
</div>
</template>
<script>
const ColorList = ["#f56a00", "#7265e6", "#ffbf00", "#00a2ae"];
import axios from "axios";
export default {
name: "Login",
data() {
return {
clientID: "",
formInline: {
clientName: "",
avatarNum: UserList[0],
},
color: ColorList[0],
ruleInline: {
user: [
{
required: true,
message: "Please fill in the user name",
trigger: "blur",
},
],
},
};
},
methods: {
avatarChange() {
const index = UserList.indexOf(this.formInline.avatarNum);
this.formInline.avatarNum =
index < UserList.length - 1 ? UserList[index + 1] : UserList[0];
this.color =
index < ColorList.length - 1 ? ColorList[index + 1] : ColorList[0];
},
handleSubmit(name) {
this.$refs[name].validate((valid) => {
if (valid) {
let __this = this;
axios({
url: "localhost:3000/api/Login",
method: "post",
data: {
...__this.$data.formInline,
},
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
.then((res) => {
if (res.data.code === -1) {
alarm("输入新的昵称!");
return;
} else if (res.data.code === 1) {
__this.$data.clientID = res.data.clientID;
__this.$route.push("/Chatroom");
}
})
.catch((err) => {
console.log(err);
});
} else {
this.$Message.error("昵称不能为空!");
}
});
},
},
};
</script>
Chatroom.vue
<template>
<div>
<div class="layout">
<Layout>
<Header>聊天室</Header>
<Layout>
<Sider hide-trigger :style="{ background: '#FFF' }">
<Row id="qunliaoliebiao">群聊列表</Row>
<GroupInfo
v-for="(name, key) in groupNames"
v-bind:groupName="name"
v-bind:key="key"
@click="chooseGroup"
>
</GroupInfo>
</Sider>
<Layout :style="{ padding: '0 24px 24px' }">
<Content
:style="{
padding: '0px',
minHeight: '600px',
background: '#fff',
margin: '24px 0',
}"
>
<div id="msgWindow">
<ChatMsgs
v-for="(msg, index) in historyMsgs"
v-bind:hmsg="msg.m"
v-bind:from="msg.from"
v-bind:key="index"
></ChatMsgs>
</div>
<form id="msgInputWnd" ref="msgInputWnd" action="">
<input id="input" autocomplete="off" v-model="inputMsg" />
<button @click.prevent="sendMsg">发送消息</button>
</form>
</Content>
</Layout>
</Layout>
</Layout>
</div>
</div>
</template>
路由的编写,./router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Login.vue')
},
{
path: '/Login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/Chatroom',
name: 'Chatroom',
component: () => import('../views/Chatroom.vue')
},
]
const router = new VueRouter({
mode: 'history',
//base: process.env.BASE_URL,
routes
})
export default router
const ex = require('express');
const path = require('path');
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 3000;
const bodyParser = require('body-parser');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
var jsonParser = bodyParser.json();
var urlencodedParser = bodyParser.urlencoded({ extended: false });
app.use(ex.static(path.join(__dirname, '../dist')));
app.get('/api/initInfo', (req, res) => {
console.log("initInfo");
res.json(Rooms);
});
app.post('/api/Login', (req, res) => {
let client;
client.clientName = req.data.clientName;
client.avatarNum = req.data.avatarNum;
for (c in Clients) {
if (c.clientName === client.clientName) {
client.clientID = "";
client.status = -1;
res.json(client);
return;
}
}
client.clientID = genClientID();
client.status = 1;
Clients.push(client);
res.json(client);
});
io.on('connection', function (socket) {
console.log('a user connected');
socket.on('login', function (obj) {
socket.name = obj.userid;
if (!onlineUsers.hasOwnProperty(obj.userid)) {
onlineUsers[obj.userid] = obj;
onlineCount++;
}
socket.on('disconnect', function () {
if (onlineUsers.hasOwnProperty(socket.name)) {
var obj = { userid: socket.name, username: onlineUsers[socket.name] };
delete onlineUsers[socket.name];
onlineCount--;
io.emit('logout', { user: obj });
}
});
socket.on('changeInfo', function (obj) {
io.emit('changeInfo', obj);
});
socket.on('sendMsg', function (obj) {
io.emit('message', obj);
});
});
在package.json中记录相应模块的信息,方便再次构建项目的时候快速安装。
"dependencies": {
"async-validator": "^1.11.5",
"axios": "^0.25.0",
"body-parser": "^1.19.1",
"core-js": "^2.6.12",
"cors": "^2.8.5",
"element-ui": "^2.15.6",
"express": "^4.17.2",
"fs": "^0.0.1-security",
"http": "^0.0.1-security",
"path": "^0.12.7",
"sass": "^1.45.1",
"sass-loader": "^7.3.1",
"socket-io": "^1.0.0",
"socket.io": "^4.4.1",
"socket.io-client": "^4.4.0",
"symbol": "^0.3.1",
"uuid": "^8.3.2",
"view-design": "^4.7.0",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vue-socket.io": "^3.0.10",
"vuex": "^3.6.2"
}
使用vue build将Vue-CLI的项目打包到dist,在app.js中包含相应的目录即可。
app.use(ex.static(path.join(__dirname, '../dist')));
登陆页面
聊天室页面
作者:NP266