Nodejs + Socket.io + Nginx 搭建聊天
一照弥、技術棧
- 服務端
- Nodejs (express框架)
- Mongodb
- Nginx
- 前端
- vue-element-admin(基于vue element-ui 的一套前端VUE框架)
- Socket
- Socket.io
- 瀏覽器端消息通知
- Notify.js
二灶泵、開發(fā)事項
1、前端代碼
// index.vue
<template>
<div class="socket-io">
<el-row class="chat-block">
<el-col v-if="!Object.keys(activeUser).length"></el-col>
<div v-else class="chat-window">
<el-col class="socket-user-info">
<div class="chat-user-info">
<span class="chat-user-name">{{activeUser.name || '沒名字的人'}}</span>
</div>
</el-col>
<el-col class="socket-window">
<ul>
<li
v-for="(msg, index) in activeUserMsg"
:key="index"
:class="msg.sendUserId === loginUserInfo._id ? 'mine' : 'others'"
>
<img :src="msg.sendUserId === loginUserInfo._id ? loginUserInfo.avatar : activeUser.avatar"/>
<div>
<span v-if="msg.sendUserId !== loginUserInfo._id">{{msg.sendUserName}}</span>
<p>{{msg.content}}</p>
</div>
</li>
</ul>
</el-col>
<el-col class="socket-btn">
<textarea placeholder="說點什么..." id="msgInput"></textarea>
<div class="send-btn-content">
<span>按鈕欄</span>
<button
class="send-btn"
@click="sendMsg"
>發(fā)送</button>
</div>
</el-col>
</div>
</el-row>
<el-row class="user-list">
<el-col
v-for="(user, key) in userList"
:key="key"
class="user-info"
:class="{active : user._id === activeUser._id}"
>
<div
@click="selectUser(user)"
style="width: 100%;"
class="user-info-row"
>
<div class="user-info-row__avatar">
<img
:src="user.avatar"
:alt="user.name"
class="user-avatar"
>
<span
v-if="user.unReadCount"
class="unread-count"
>{{user.unReadCount}}</span>
</div>
<span class="user-name">{{user.name || '沒名字的人'}}</span>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { getUserList } from '@/api/user';
import SOCKETIO from './socket-io.js';
export default {
data() {
return {
/**
* userList: {
* userId: {
* _id: XXX,
* avatar: xxxx,
* ....
* }
* }
*/
userList: {},
activeUser: {},
activeUserMsg: [],
/**
* usersMsgs: {
* receiveUserId: {
* msgs: []
* }
* }
*/
usersMsgs: {},
// 主要用于處理 用戶未讀消息數(shù)
newMsg: {},
currentPage: 1,
limit: 10,
};
},
created() {
// 獲取 用戶列表
getUserList().then(users => {
this.userList = users.list;
this.currentPage = users.currentPage;
});
},
mounted () {
// 初始化 socket.IO豹爹,并將一些需要操作 vue 組件的方法傳進去赘艳。
SOCKETIO.init(this.$store.state.user._id, {
receiveMsgFromUserThroughServer: this.receiveMsgFromUserThroughServer
});
},
methods: {
/**
* 接收到其他用戶發(fā)來的信息
* @param {Object} msg
*/
receiveMsgFromUserThroughServer (msg) {
// 如果當前 活躍的窗口正好是信息發(fā)送者,則將信息填入 this.activeUserMsg吼拥,否則不做任何操作
if (msg.sendUserId === this.activeUser._id) {
if (this.activeUserMsg && this.activeUserMsg.length) {
this.activeUserMsg.push(msg);
} else {
this.activeUserMsg = [msg];
}
}
// 不論什么情況都將信息填入 消息列表中
this.setUsersMsg(msg.sendUserId, msg);
this.newMsg = msg;
},
selectUser(user) {
this.activeUser = user;
this.activeUserMsg = this.usersMsgs[user._id] ? this.usersMsgs[user._id]['msgs'] : [];
const oldThisUserList = JSON.parse(JSON.stringify(this.userList));
oldThisUserList[user._id]['unReadCount'] = 0;
this.userList = oldThisUserList;
},
sendMsg () {
const msgInput = document.getElementById('msgInput');
const msg = msgInput.value;
const msgObj = {
sendUserId: this.$store.state.user._id,
sendUserName: this.$store.state.user.name,
receiveUserId: this.activeUser._id,
content: msg
};
SOCKETIO.sendMsg(msgObj);
this.addSendedMsg(msgObj, this);
msgInput.value = '';
},
// 將發(fā)送出的消息 添加到消息列表中
addSendedMsg (msgObj) {
if (this.activeUserMsg && this.activeUserMsg.length) {
this.activeUserMsg.push(msgObj);
} else {
this.activeUserMsg = [msgObj];
}
this.setUsersMsg(msgObj.receiveUserId, msgObj);
},
// 設置 消息列表
setUsersMsg (receiveUserId, msgObj) {
if (this.usersMsgs[receiveUserId] && this.usersMsgs[receiveUserId]['msgs'].length) {
this.usersMsgs[receiveUserId] && this.usersMsgs[receiveUserId]['msgs'].push(msgObj);
} else {
this.usersMsgs[receiveUserId] = { msgs: [msgObj] };
}
}
},
watch: {
/**
* 用于設置用戶未讀消息數(shù)
*/
newMsg(newVal) {
const oldThisUserList = JSON.parse(JSON.stringify(this.userList));
if (oldThisUserList[newVal.sendUserId] && oldThisUserList[newVal.sendUserId]['unReadCount'] !== undefined) {
oldThisUserList[newVal.sendUserId]['unReadCount'] >= 99 ? '99+' : oldThisUserList[newVal.sendUserId]['unReadCount']++;
} else {
oldThisUserList[newVal.sendUserId]['unReadCount'] = 1;
}
this.userList = oldThisUserList;
}
},
computed: {
loginUserInfo () {
return this.$store.state.user;
}
}
};
</script>
<style scoped lang="scss">
.active {
background-color: rgba(57,116,204,.15);
}
.socket-io {
position: absolute;
top: 85px;
left: 180px;
right: 0;
bottom: 0;
display: flex;
background-color: rgb(243, 243, 243);
}
.chat-block {
width: 1500px;
height: 100%;
display: flex;
flex-direction: column;
}
.chat-window {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.socket-user-info {
height: 60px;
}
.socket-window {
background-color: #f2f2f2;
height: 80%;
overflow-y: scroll;
ul {
li {
list-style: none;
display: flex;
margin-bottom: 20px;
span {
font-size: 14px;
margin-bottom: 5px;
display: inline-block;
}
p {
position: relative;
background-color: white;
padding: 5px 4px;
margin: 0;
border-radius: 4px;
min-height: 40px;
max-width: 500px;
line-height: 40px;
}
}
img {
width: 40px;
height: 40px;
margin-right: 12px;
}
.others {
p::before {
content: '';
position: absolute;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid white;
position: absolute;
left: -6px;
}
}
.mine {
flex-direction: row-reverse;
img {
margin-left: 12px;
}
p {
background-color: #83bff7 !important;
}
p::before {
content: '';
position: absolute;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid #83bff7;
position: absolute;
right: -6px;
}
}
}
}
.socket-btn {
height: 150px;
border-top: 2px solid #f3f3f3;
position: relative;
background-color: white;
textarea {
border: none;
resize: none;
width: 100%;
height: 100px;
padding: 8px 10px;
outline: none;
}
}
.chat-user-info {
height: 100%;
position: relative;
box-shadow: 0 2px 4px #888888;
}
.user-list {
background-color: white;
width: 400px;
height: 100%;
overflow-y: scroll;
border-left: 2px solid #f3f3f3;
}
.user-info {
max-height: 60px;
.user-info-row {
display: flex;
align-items: center;
cursor: pointer;
padding: 10px 0;
padding-left: 10px;
border-bottom: 1px solid #E4E4E4;
&__avatar {
position: relative;
.unread-count {
position: absolute;
background-color: red;
border-radius: 50%;
display: inline-block;
width: 20px;
height: 20px;
color: white;
top: -6px;
right: -10px;
font-size: 10px;
text-align: center;
line-height: 20px;
}
}
}
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
}
.chat-user-name {
margin-left: 15px;
transform: translateY(-50%);
top: 50%;
position: absolute;
}
.user-name {
margin-left: 15px;
}
.send-btn-content {
position: absolute;
right: 10px;
bottom: 5px;
}
.send-btn {
cursor: pointer;
margin: 0 20px 0 0;
width: 54px;
height: 30px;
text-align: center;
font-size: 12px;
color: #fff;
background-color: #3974cc;
border-color: #3974cc;
padding: 1px 7px;
border-radius: 4px;
}
</style>
// socket-io.js
import io from 'socket.io-client';
// 瀏覽器 消息提醒 組件
import Notify from '@wcjiang/notify';
// let a_interval;
let chatSocket;
const iNotify = new Notify({
effect: 'flash',
audio: {
// 可以使用數(shù)組傳多種格式的聲音文件
file: '/static/media/message_remind.mp3'
}
});
const BASE_API = {
development: 'http://localhost:8008',
production: `${location.origin}`
};
export default {
methods(ms) {
return ms;
},
/**
* 初始化SocketIO
* @param {String} loginUserId 當前登錄的用戶的ID
*/
init(loginUserId, methods) {
const that = this;
this.methods = methods;
// 與聊天服務器進行連接
chatSocket = io.connect(`${BASE_API[process.env.NODE_ENV || 'production']}/chat`);
// 接收到其他用戶 從 服務器發(fā)來的信息
const socketType = `chat:server-sendMsg-to-user:${loginUserId}`;
chatSocket.on(socketType, this.receiveMsgFromUserThroughServer.bind(this));
chatSocket.on('hello-client', function(data) {
console.log(data);
});
chatSocket.on('server-response', (msg) => {
that.playNewMsg(iNotify, msg);
});
},
sendMsg(msg) {
chatSocket.emit('chat:user-sendMsg', msg);
},
/**
* 接收到其他用戶發(fā)來的信息
* @param {Object} msg
*/
receiveMsgFromUserThroughServer(msg) {
// 回調(diào) vue 中傳過來的方法,用于操作 消息列表
this.methods.receiveMsgFromUserThroughServer(msg);
this.playNewMsg(iNotify, msg);
},
// 播放消息提醒
playNewMsg(iNotify, msg) {
iNotify.player();
iNotify.notify({
title: msg.sendUserName,
body: msg.content,
onclick: function() {
window.focus();
},
onshow: function() {
console.log('on show');
}
});
}
};
2线衫、服務端代碼
let server = null;
let io = null;
let _socket = null;
let chat = null;
const socketIO = {
/**
*
* @param {ExpressServer} app express()
*/
init(app) {
server = require('http').Server(app);
io = require('socket.io')(server);
server.listen(8008);
io.on('connection', this.onConnect.bind(this));
/**
* 創(chuàng)建 聊天專用 namespace
*/
chat = io.of('chat');
chat.on('connection', this.onChatConnect.bind(this))
},
/**
* 根鏈接
* @param {Socket} socket
*/
onConnect (socket) {
_socket = socket;
// 通知客戶端凿可,已與服務端建立鏈接
socket.emit('hello-client', { 'server-msg': '與服務器連接成功' });
// 客戶端 向服務端打招呼
socket.on('hello-server', function (data) {
console.log(data);
socket.emit('server-response', `服務端接收到消息:${data.my}`);
});
},
/**
* 聊天 namespace
* @param {Socket} socket
*/
onChatConnect (socket) {
const that = this;
socket.emit('hello-client', { 'server-msg': '聊天鏈接--與服務器連接成功' });
// 接收到用戶發(fā)來的消息
socket.on('chat:user-sendMsg', function (msg) {
that.receiveUserSendMsg(socket, msg);
});
},
/**
* 接收到用戶發(fā)來的信息
* @param {Socket} socket
* @param {Object} msg
*/
receiveUserSendMsg (socket, msg) {
const socketType = `chat:server-sendMsg-to-user:${msg.receiveUserId}`;
console.log('將信息發(fā)送給目標用戶:', socketType);
/**
* 必須通過 broadcast 進行廣播,才能將消息發(fā)送出去
*/
socket.broadcast.emit(socketType, msg);
}
}
module.exports = socketIO;
三授账、部署事項
1枯跑、消息通知
由于消息通知基于 瀏覽器的 Notifications API,但是它基于Https
白热,所以項目需要部署為https
全肮。
https
的域名申請自行解決。
2棘捣、Nginx部署
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /var/www/art_key/你的私鑰.pem;
ssl_certificate_key /var/www/art_key/你的公鑰.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 前端靜態(tài)資源
location / {
root /var/www/example/dist;
index index.html;
}
# 后端接口的 location
location /api/ {
proxy_pass http://127.0.0.1:4040;
}
# socket 服務的 location
location /socket.io/ {
proxy_pass http://127.0.0.1:8008;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
四、其他
1、Websocket
使用ws
或 wss
的統(tǒng)一資源標志符乍恐,類似于 HTTP
或 HTTPS
评疗,其中 wss
表示在 TLS
之上的 Websocket
,相當于 HTTPS
茵烈。
2百匆、socket
必須 使用broadcast
進行廣播,才能將消息發(fā)送出去
五呜投、未完善的工作
- 1加匈、目前的消息沒有進行服務端存儲
- 2、登錄后獲取用戶列表時應該把所有未讀消息一并查出來
六仑荐、BUG
接收消息時會彈出兩遍雕拼,一直沒查出為啥,一臉懵逼中粘招。啥寇。。希望有大神看到 幫忙排查一下??