nodejs+express搭建多人聊天室步驟
前言
本文主要是筆者在學(xué)習(xí)node的時候,作為練手的一個小項目,花了幾天空余時間,邊碼邊寫教程的一個過程。適用于對node理論知識看的多,實戰(zhàn)少的同學(xué),那么現(xiàn)在就讓我們開始吧!
準(zhǔn)備工作
新建一個文件夾 chatroom
在終端輸入以下命令,按照步驟npm(沒裝過的去官網(wǎng)安裝下node和npm)會自動給你生成一個package.json文件
安裝express和socket.io
package.json文件如下:
//package.json
{
"name": "chatroom",
"version": "1.0.0",
"description": "A simple chatroom",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ddvdd008/chatroom.git"
},
"keywords": [
"chatroom",
"nodejs",
"express"
],
"author": "ddvdd",
"license": "ISC",
"bugs": {
"url": "https://github.com/ddvdd008/chatroom/issues"
},
"homepage": "https://github.com/ddvdd008/chatroom#readme"
}
安裝express和socket.io
npm install express --save npm install socket.io --save
package.json自動新增依賴
"dependencies": {
"express": "^4.16.2",
"socket.io": "^2.0.4"
}
因為我們使用express框架寫后端服務(wù),用socket.io(Socket.io實際上是WebSocket的父集,Socket.io封裝了WebSocket和輪詢等方法,他會根據(jù)情況選擇方法來進(jìn)行通訊。)來對客戶端和服務(wù)端建立一個持久鏈接,便于通訊。
到這里準(zhǔn)備工作進(jìn)行的差不多了,下面我們開始一步步實現(xiàn)。
搭建web服務(wù)器
express創(chuàng)建服務(wù)
學(xué)過node同學(xué)應(yīng)該不陌生,利用http.createServer就能簡單的創(chuàng)建一個服務(wù)器,這次我們利用express來創(chuàng)建服務(wù)。在項目根目錄創(chuàng)建一個app.js。
/**
* Created by ddvdd on 2018-02-07.
*/
const express = require('express');
const app = express(); // 創(chuàng)建express實例,賦值給app。
const fs = require('fs'); // 這個是node的文件讀取模塊,用于讀取文件
const path = require('path'); // 這是node的路徑處理模塊,可以格式化路徑
app.listen(3000,()=>{
console.log("server running at 127.0.0.1:3000"); // 代表監(jiān)聽3000端口,然后執(zhí)行回調(diào)函數(shù)在控制臺輸出。
});
/**
* app.get(): express中的一個中間件,用于匹配get請求,說的簡單點就是node處理請求的路由,對于不同url請求,讓對應(yīng)的不同app.get()去處理
* '/': 它匹配get請求的根路由 '/'也就是 127.0.0.1:3000/就匹配到它了
* req帶表瀏覽器的請求對象,res代表服務(wù)器的返回對象
*/
app.get('/',(req,res)=>{
res.redirect('/chat.html'); // express的重定向函數(shù)。如果瀏覽器請求了根路由'/',瀏覽器就給他重定向到 '127.0.0.1:3000/chat.html'路由中
});
/**
* 這里匹配到的是/chat.html就是上面重定向到的路徑。
*/
app.get('/chat.html',(req,res)=>{
fs.readFile(path.join(__dirname,'./public/chat.html'),function(err,data){ //讀取文件,readFile里傳入的是文件路徑和回調(diào)函數(shù),這里用path.join()格式化了路徑。
if(err){
console.error("讀取chat.html發(fā)生錯誤",err); //錯誤處理
res.send('4 0 4'); //如果發(fā)生錯誤,向瀏覽器返回404
} else {
res.end(data); //這里的data就是回調(diào)函數(shù)的參數(shù),在readFile內(nèi)部已經(jīng)將讀取的數(shù)據(jù)傳遞給了回調(diào)函數(shù)的data變量。
} //我們將data傳到瀏覽器,就是把html文件傳給瀏覽器
})
});
你們看了以后會說,這express框架看來也沒那么簡便啊,一個最簡單的發(fā)送單頁面的方法跟node自帶http.createServer沒太大區(qū)別餓,也挺麻煩的。從目前來看確實如此,我這不是為了讓你們?nèi)菀桌斫饴铩?express提供了一個非常強(qiáng)大的中間件,幫我們托管靜態(tài)資源文件,下面我們就來實現(xiàn):
app.use('/',express.static(path.join(__dirname,'./public'))); //一句話就搞定。
代替原來的:
app.get('/chat.html',(req,res)=>{
fs.readFile(path.join(__dirname,'./public/chat.html'),function(err,data){
if(err){
console.error("讀取chat.html發(fā)生錯誤",err);
res.send('4 0 4');
} else {
res.end(data);
}
})
});
__dirname表示當(dāng)前文件所在的絕對路徑,所以我們使用path.join將app.js的絕對路徑和public加起來就得到了public的絕對路徑。用path.join是為了避免出現(xiàn) ././public 這種奇怪的路徑,express.static就幫我們托管了public文件夾中的靜態(tài)資源。只要有 127.0.0.1:3000/XXX/AAA 的路徑都會去public文件夾下找XXX文件夾下的AAA文件然后發(fā)送給瀏覽器。
現(xiàn)在再來看這段代碼是不是簡介了很多,具體了解app.use()干了什么的同學(xué)可以去這里
socket.io建立客戶端和服務(wù)端的鏈接
創(chuàng)建完上面的服務(wù)后,我們需要把socket.io引用進(jìn)來,讓客戶端和服務(wù)端建立長久鏈接。我們把a(bǔ)pp.js進(jìn)行如下改造:
/**
* Created by ddvdd on 2018-02-07.
*/
const express = require('express');
const app = express(); // 創(chuàng)建express實例,賦值給app。
const server = require('http').Server(app);
const io = require('socket.io')(server); //將socket的監(jiān)聽加到app設(shè)置的模塊里。這兩句理解不了的可以去socket.io官網(wǎng)去看
const path = require('path'); // 這是node的路徑處理模塊,可以格式化路徑
server.listen(3000,()=>{
console.log("server running at 127.0.0.1:3000"); // 代表監(jiān)聽3000端口,然后執(zhí)行回調(diào)函數(shù)在控制臺輸出。
});
...
...
app.use('/',express.static(path.join(__dirname,'./public'))); //一句話就搞定。
/*socket*/
io.on('connection',(socket)=>{ //監(jiān)聽客戶端的連接事件
});
o.on表示監(jiān)聽某個事件,該事件一發(fā)生,就觸發(fā)回調(diào)函數(shù)。'connection‘就是一個事件名,它已經(jīng)定義好了,只要用戶連接上就會觸發(fā)?,F(xiàn)在app.js基本已經(jīng)完成,我們在根目錄執(zhí)行:
node app.js
>
現(xiàn)在訪問http://127.0.0.1:3000/static/chat.html:

哎?啥也沒有。。。那不廢話!我們都沒url請求對應(yīng)的靜態(tài)資源!
添加靜態(tài)html
我們在項目根目錄創(chuàng)建public文件夾,public文件夾里面新建chat.html文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>聊天室</title> </head> <body> 這是我們的聊天室 </body> </html>
現(xiàn)在我們刷新下頁面,你看頁面出現(xiàn)了:
>
到這里其實一個最簡單的瀏覽器和web服務(wù)器協(xié)作的項目就已經(jīng)完成,后面我們要不斷完善頁面,給服務(wù)器后端加業(yè)務(wù)功能來實現(xiàn)多人聊天室。
基本功能實現(xiàn)
登陸功能,我們需要一個用戶名,(不需要密碼),該用戶名必須客戶端服務(wù)器都有存儲。每次傳輸信息基本都需要包括用戶名,否則不知道是誰發(fā)的。
群聊功能,我們需要分辨信息來己方和對方
登陸功能實現(xiàn)
login頁面重構(gòu)
最基本的登陸界面由一個用戶名輸入框和登錄按鈕組成:
//chat.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室</title>
<style>
*{
margin:0;
padding:0;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
}
.container{
position: absolute;
top:0;
left:0;
right:0;
bottom:0;
background-color: grey;
padding: 50px;
}
.container .title{
width:300px;
margin: 0 auto;
font-size: 30px;
font-family: 'Franklin Gothic Medium';
font-weight: bold;
text-align: center;
margin-bottom:50px;
}
.container .login-wrap{
width:400px;
padding: 20px;
border: 1px solid #000;
margin: 0 auto;
text-align: center;
}
.login-wrap .user-ipt{
width:360px;
text-align: center;
vertical-align: middle;
}
.login-wrap .login-button{
width:60px;
height:24px;
line-height:20px;
font-size: 14px;
padding: 2px 0;
border-radius: 5px;
margin-top:10px;
}
</style>
</head>
<body>
<div class="container">
<div class="title">歡迎來到ddvdd聊天室</div>
<div class="login-wrap">
<div class="user-ipt">
<span class="user-name">用戶名:</span>
<input id="name" class="name-ipt" type="text" />
</div>
<button id="loginbutton" class="login-button">登陸</button>
</div>
</div>
</body>
</html>
簡單的加點樣式,靜態(tài)頁面就完成了,我們刷新下頁面:

login頁面交互
昨天下午寫到一半。。。部門突然要去團(tuán)建聚會,只能匆匆提交代碼,草草了事。今天一大早來到公司繼續(xù)給大家碼
廢話不多說進(jìn)入正題,登陸這塊交互,當(dāng)用戶訪問服務(wù)器并且成功登陸算一個在線登陸人數(shù),每登陸一個用戶,服務(wù)器都會把用戶信息存入一個數(shù)組中,保存在服務(wù)器,這里要注意一點,服務(wù)器會對用戶登陸的用戶名進(jìn)行校驗,校驗結(jié)果會返回給客戶端,客戶端通過校驗結(jié)果,改變當(dāng)前頁面是否進(jìn)入聊天頁面。
上面的服務(wù)器和客戶端交互都是通過socket.io來實現(xiàn)通訊的,前端的業(yè)務(wù)交互我們這里就采用jquery來實現(xiàn),在public文件夾下新建js文件夾,下載jquery-3.2.1.min.js、新建main.js。然后對chat.html引入需要的sdk:
<script src="js/jquery-3.2.1.min.js"></script> <script src="js/main.js"></script> //socket.io官網(wǎng)要求這么引入 <script src="/socket.io/socket.io.js"></script>
引入完sdk,我們對main的js添加登錄功能:
//main.js
/**
* Created by ddvdd on 2018-02-08.
*/
$(function(){
const url = 'http://127.0.0.1:3000';
let _username = '';
let _$inputname = $('#name');
let _$loginButton = $('#loginbutton');
let socket = io.connect(url);
//設(shè)置用戶名,當(dāng)用戶登錄的時候觸發(fā)
let setUsername = () => {
_username = _$inputname.val().trim(); //得到輸入框中用戶輸入的用戶名
//判斷用戶名是否存在
if(_username) {
socket.emit('login',{username: _username}); //如果用戶名存在,就代表可以登錄了,我們就觸發(fā)登錄事件,就相當(dāng)于告訴服務(wù)器我們要登錄了
}
else{
alert('請輸入用戶名!');
}
};
/*前端事件*/
_$loginButton.on('click',function (event) { //監(jiān)聽按鈕的點擊事件,如果點擊,就說明用戶要登錄,就執(zhí)行setUsername函數(shù)
setUsername();
});
/*socket.io部分邏輯*/
socket.on('loginResult',(data)=>{
/**
* 如果服務(wù)器返回的用戶名和剛剛發(fā)送的相同的話,就登錄
* 否則說明有地方出問題了,拒絕登錄
*/
if(data.code === 0) {
// 登陸成功,切換至聊天室頁面
}
else if(data.code ===1){
alert('用戶已登錄!');
}
else{
alert('登錄失??!');
}
})
});
//app.js
/**
* Created by ddvdd on 2018-02-07.
*/
const express = require('express');
const app = express(); // 創(chuàng)建express實例,賦值給app。
const server = require('http').Server(app);
const io = require('socket.io')(server); //將socket的監(jiān)聽加到app設(shè)置的模塊里。這兩句理解不了的可以去socket.io官網(wǎng)去看
const path = require('path'); // 這是node的路徑處理模塊,可以格式化路徑
const users = []; //用來保存所有的用戶信息
let usersNum = 0; //統(tǒng)計在線登錄人數(shù)
server.listen(3000,()=>{
console.log("server running at 127.0.0.1:3000"); // 代表監(jiān)聽3000端口,然后執(zhí)行回調(diào)函數(shù)在控制臺輸出。
});
/**
* app.get(): express中的一個中間件,用于匹配get請求,說的簡單點就是node處理請求的路由,對于不同url請求,讓對應(yīng)的不同app.get()去處理
* '/': 它匹配get請求的根路由 '/'也就是 127.0.0.1:3000/就匹配到它了
* req帶表瀏覽器的請求對象,res代表服務(wù)器的返回對象
*/
app.get('/',(req,res)=>{
res.redirect('/static/chat.html'); // express的重定向函數(shù)。如果瀏覽器請求了根路由'/',瀏覽器就給他重定向到 '127.0.0.1:3000/chat.html'路由中
});
/**
* __dirname表示當(dāng)前文件所在的絕對路徑,所以我們使用path.join將app.js的絕對路徑和public加起來就得到了public的絕對路徑。
* 用path.join是為了避免出現(xiàn) ././public 這種奇怪的路徑
* express.static就幫我們托管了public文件夾中的靜態(tài)資源。
* 只要有 127.0.0.1:3000/XXX/AAA 的路徑都會去public文件夾下找XXX文件夾下的AAA文件然后發(fā)送給瀏覽器。
*/
app.use('/static',express.static(path.join(__dirname,'./public'))); //一句話就搞定。
/*socket*/
io.on('connection',(socket)=>{ //監(jiān)聽客戶端的連接事件
socket.on('login',(data)=>{
if(checkUserName(data)){
socket.emit('loginResult',{code:1}); //code=1 用戶已登錄
}
else{
//將該用戶的信息存進(jìn)數(shù)組中
users.push({
username: data.username,
message: []
});
socket.emit('loginResult',{code:0}); //code=0 用戶登錄成功
usersNum = users.length;
console.log(`用戶${data.username}登錄成功,進(jìn)入ddvdd聊天室,當(dāng)前在線登錄人數(shù):${usersNum}`);
}
});
//斷開連接后做的事情
socket.on('disconnect',()=>{ //注意,該事件不需要自定義觸發(fā)器,系統(tǒng)會自動調(diào)用
usersNum = users.length;
console.log(`當(dāng)前在線登錄人數(shù):${usersNum}`);
});
});
//校驗用戶是否已經(jīng)登錄
const checkUserName = (data) => {
let isExist = false;
users.map((user) => {
if(user.username === data.username){
isExist = true;
}
});
return isExist;
}
上面代碼大家需要了解以下幾點:
- socket.on 表示監(jiān)聽事件,后面接一個回調(diào)函數(shù)用來接收emit發(fā)出事件傳遞過來的對象。
- socket.emit 用來觸發(fā)事件,傳遞對象給on監(jiān)聽事件。
- 我們socket連接之后的監(jiān)聽觸發(fā)事件都要寫在io.on('connection')的回調(diào)里面,因為這些事件都是連接之后發(fā)生的,就算是斷開連接的事件 disconnect 也是在連接事件中發(fā)生的,沒有正在連接的狀態(tài),哪來的斷開連接呢?
- 理解雖然服務(wù)器端只有app.js一個文件,但是不同的客戶端連接后信息是不同的,所以我們必須要將一些公用的信息,比如說,儲存所有登錄用戶的數(shù)組,所有用戶發(fā)送的所有信息存儲在外部,一定不能存儲在connecion里
效果展示:



群聊功能實現(xiàn)
寫完簡單的登錄功能,現(xiàn)在我們來寫這項目最重要的功能群聊。首先我們先來處理下頁面,因為功能簡單,所以不單獨建立html來顯示聊天室,就直接寫在login頁面,通過class名稱的變化來切換登錄后,聊天室的顯示。
聊天室頁面重構(gòu)
下面我們對chat.html進(jìn)行整改:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>聊天室</title>
<script src="js/jquery-3.2.1.min.js"></script>
<script src="js/main.js"></script>
<script src="/socket.io/socket.io.js"></script>
<style>
*{
margin:0;
padding:0;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
}
.container{
position: absolute;
top:0;
left:0;
right:0;
bottom:0;
background-color: darkgrey;
padding: 50px;
overflow-y: scroll;
}
.container .title{
margin: 0 auto;
font-size: 30px;
font-family: 'Franklin Gothic Medium';
font-weight: bold;
text-align: center;
margin-bottom:20px;
}
.container .login-wrap{
width:400px;
padding: 20px;
border: 1px solid #000;
margin: 0 auto;
text-align: center;
}
.login-wrap .user-ipt{
width:360px;
text-align: center;
vertical-align: middle;
}
.login-wrap .login-button{
width:60px;
height:24px;
line-height:20px;
font-size: 14px;
padding: 2px 0;
border-radius: 5px;
margin-top:10px;
}
.chat-wrap .chat-content{
width:100%;
height:600px;
background-color: whitesmoke;
padding:10px;
}
.chat-wrap .send-wrap{
margin-top: 20px;
}
.message-ipt{
width: 200px;
height: 100px;
padding: 0 5px;
vertical-align: bottom;
}
.chat-content p{
display: block;
margin-bottom: 10px;
}
.chat-content p .msg{
display: inline-block;
padding: 8px 11px;
border-radius:6px;
}
.chat-content .self-message .msg{
background-color:#d0e7ff;
border: 1px solid #c9dfff;
}
.chat-content .other-message .msg{
background-color:white;
border: 1px solid #eee;
}
.chat-content .self-message{
text-align:right;
}
.chat-content .other-message{
text-align-last:left;
}
</style>
</head>
<body>
<div class="container">
<div id="loginbox" class="login-wrap">
<div class="title">登錄</div>
<div class="user-ipt">
<span class="user-name">用戶名:</span>
<input id="name" class="name-ipt" type="text" />
</div>
<button id="loginbutton" class="login-button">登錄</button>
</div>
<div id="chatbox" class="chat-wrap" style="display:none">
<div id="content" class="chat-content">
<!-- 聊天內(nèi)容 -->
</div>
<div class="send-wrap">
<textarea rows="3" cols="20" id="chatmessage" class="message-ipt" type="textarea" placeholder="請輸入要發(fā)送的信息內(nèi)容"></textarea>
</div>
</div>
</div>
</body>
</html>
新增chatbox容器來作為聊天室,里面有一個群聊的聊天框,和一個發(fā)送消息的文本框。通過上面loginResult回調(diào),對loginbox進(jìn)行隱藏,顯示chatbox:
//顯示聊天室界面
let showChatRoom = () => {
/**
* 1.隱藏登錄框,取消它綁定的事件
* 2.顯示聊天界面
*/
$('#loginbox').hide('slow');
_$loginButton.off('click');
/**
* 顯示聊天界面,并顯示一行文字,歡迎用戶
*/
$(`<div class="title">歡迎${_username}來到ddvdd聊天室</div>`).insertBefore($("#content"));
$("#chatbox").show('slow');
}
消息事件發(fā)送監(jiān)聽機(jī)制
聊天一定是客戶端觸發(fā)的,所以發(fā)送信息是客戶端觸發(fā),服務(wù)器監(jiān)聽。
服務(wù)器監(jiān)聽到發(fā)送信息的事件后會存儲信息,然后觸發(fā)發(fā)送信息成功事件廣播給所有客戶端,將信息傳給所有客戶端。
發(fā)送消息sendMessage事件
//main.js
//發(fā)送消息
let sendMessage = function () {
/**
* 得到輸入框的聊天信息,如果不為空,就觸發(fā)sendMessage
* 將信息和用戶名發(fā)送過去
*/
let _message = _$chattextarea.val();
if(_message) {
socket.emit('sendMessage',{username: _username, message: _message});
}
else{
alert('請輸入發(fā)送消息!');
}
};
...
/*聊天事件*/
_$chattextarea.on('keyup',function (event) {
if(event.keyCode === 13) {
sendMessage();
_$chattextarea.val('');
}
});
服務(wù)器端監(jiān)聽sendMessage事件
//app.js
/**
* 監(jiān)聽sendMessage,我們得到客戶端傳過來的data里的message,并存起來。
*/
socket.on('sendMessage',(data)=>{
for(let _user of users) {
if(_user.username === data.username) {
_user.message.push(data.message);
//信息存儲之后觸發(fā)receiveMessage將信息發(fā)給所有瀏覽器-廣播事件
io.emit('receiveMessage',data);
break;
}
}
});
我們是遍歷服務(wù)器端的用戶數(shù)組,找到該用戶,將發(fā)送的信息存起來,然后觸發(fā)receiveMessage事件廣播到所有瀏覽器,sendMessage是寫在connection里,login之外的,為什么這么做大家一定要理解,發(fā)送消息是連接時候做的事情,而不是登錄時做的事情。
注意的是,我使用的是io.emit,他是真正的廣播到所有瀏覽器,socket.broadcast.emit則不會廣播到自己的瀏覽器。
客戶端監(jiān)聽receiveMessage事件
//main.js
socket.on('receiveMessage',(data)=>{
/**
*
* 監(jiān)聽服務(wù)器廣播的消息
*/
showMessage(data);
})
//顯示消息
let showMessage = function (data) {
//先判斷這個消息是不是自己發(fā)出的,然后再以不同的樣式顯示
if(data.username === _username){
$("#content").append(`<p class='self-message'><span class='msg'>${data.message}</span><span class='name'> :${data.username}</span></p>`);
}else {
$("#content").append(`<p class='other-message'><span class='name'>${data.username}: </span><span class='msg'>${data.message}</span></p>`);
}
};
寫到這邊,我們的聊天室基本功能已經(jīng)完成了,來看看效果吧!打開三個瀏覽器,分別登錄老大、老二、老三,發(fā)一句“大噶好~,我是渣渣輝!”。



相關(guān)文章
使用Nodejs編寫一個腳本實現(xiàn)markdown轉(zhuǎn)pdf功能
Markdown?是一種輕量級的標(biāo)記語言,非常適合用來寫作和記錄,將?Markdown?轉(zhuǎn)換為?PDF?可以讓文檔在格式和樣式上更加統(tǒng)一,也方便在不同設(shè)備和平臺上查看和打印,在接下來的內(nèi)容中我們將講解如何使用?NodeJs?編寫一個?Markdown?轉(zhuǎn)?PDF?的腳本來實現(xiàn)我們這個想要的功能2024-05-05
使用node.js半年來總結(jié)的 10 條經(jīng)驗
從3月初來到帝都某創(chuàng)業(yè)公司的服務(wù)器團(tuán)隊實習(xí),到現(xiàn)在已接近半年的時間。PS: 已轉(zhuǎn)正,服務(wù)器端用的 Node。2014-08-08
Nodejs使用exceljs實現(xiàn)excel導(dǎo)入導(dǎo)出
在日常開發(fā)中,我們常需在后臺管理系統(tǒng)中實現(xiàn)數(shù)據(jù)的導(dǎo)入與導(dǎo)出功能,以便與?Excel?文件進(jìn)行交互,本文將使用使用exceljs實現(xiàn)excel導(dǎo)入導(dǎo)出功能,需要的可以參考下2024-03-03
Node.js中利用js-xlsx處理xlsx文件的實現(xiàn)
js-xlsx庫是目前Github上star數(shù)量最多的處理Excel的庫,本文介紹用 Node.js中的js-xls庫來處理Excel文件,具有一定的參考價值,感興趣的可以了解一下2023-10-10
node封裝一個控制臺進(jìn)度條插件???????詳情
這篇文章主要介紹了node封裝一個控制臺進(jìn)度條插件???????詳情,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-08-08

