安裝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自動(dòng)新增依賴
"dependencies": { "express": "^4.16.2", "socket.io": "^2.0.4" }
因?yàn)槲覀兪褂胑xpress框架寫后端服務(wù),用socket.io(Socket.io實(shí)際上是WebSocket的父集,Socket.io封裝了WebSocket和輪詢等方法,他會(huì)根據(jù)情況選擇方法來(lái)進(jìn)行通訊。)來(lái)對(duì)客戶端和服務(wù)端建立一個(gè)持久鏈接,便于通訊。
到這里準(zhǔn)備工作進(jìn)行的差不多了,下面我們開(kāi)始一步步實(shí)現(xiàn)。
搭建web服務(wù)器
express創(chuàng)建服務(wù)
學(xué)過(guò)node同學(xué)應(yīng)該不陌生,利用http.createServer就能簡(jiǎn)單的創(chuàng)建一個(gè)服務(wù)器,這次我們利用express來(lái)創(chuàng)建服務(wù)。在項(xiàng)目根目錄創(chuàng)建一個(gè)app.js。
const express = require('express'); const app = express(); // 創(chuàng)建express實(shí)例,賦值給app。 const fs = require('fs'); // 這個(gè)是node的文件讀取模塊,用于讀取文件 const path = require('path'); // 這是node的路徑處理模塊,可以格式化路徑 app.listen(3000,()=>{ console.log("server running at 127.0.0.1:3000"); // 代表監(jiān)聽(tīng)3000端口,然后執(zhí)行回調(diào)函數(shù)在控制臺(tái)輸出。 }); app.get('/',(req,res)=>{ res.redirect('/chat.html'); // express的重定向函數(shù)。如果瀏覽器請(qǐng)求了根路由'/',瀏覽器就給他重定向到 '127.0.0.1:3000/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ā)生錯(cuò)誤",err); //錯(cuò)誤處理 res.send('4 0 4'); //如果發(fā)生錯(cuò)誤,向?yàn)g覽器返回404 } else { res.end(data); //這里的data就是回調(diào)函數(shù)的參數(shù),在readFile內(nèi)部已經(jīng)將讀取的數(shù)據(jù)傳遞給了回調(diào)函數(shù)的data變量。 } //我們將data傳到瀏覽器,就是把html文件傳給瀏覽器 }) });
你們看了以后會(huì)說(shuō),這express框架看來(lái)也沒(méi)那么簡(jiǎn)便啊,一個(gè)最簡(jiǎn)單的發(fā)送單頁(yè)面的方法跟node自帶http.createServer沒(méi)太大區(qū)別餓,也挺麻煩的。從目前來(lái)看確實(shí)如此,我這不是為了讓你們?nèi)菀桌斫饴铩?express提供了一個(gè)非常強(qiáng)大的中間件,幫我們托管靜態(tài)資源文件,下面我們就來(lái)實(shí)現(xiàn):
app.use('/',express.static(path.join(__dirname,'./public'))); //一句話就搞定。
代替原來(lái)的:
app.get('/chat.html',(req,res)=>{ fs.readFile(path.join(__dirname,'./public/chat.html'),function(err,data){ if(err){ console.error("讀取chat.html發(fā)生錯(cuò)誤",err); res.send('4 0 4'); } else { res.end(data); } }) });
__dirname表示當(dāng)前文件所在的絕對(duì)路徑,所以我們使用path.join將app.js的絕對(duì)路徑和public加起來(lái)就得到了public的絕對(duì)路徑。用path.join是為了避免出現(xiàn) ././public 這種奇怪的路徑,express.static就幫我們托管了public文件夾中的靜態(tài)資源。只要有 127.0.0.1:3000/XXX/AAA 的路徑都會(huì)去public文件夾下找XXX文件夾下的AAA文件然后發(fā)送給瀏覽器。
現(xiàn)在再來(lái)看這段代碼是不是簡(jiǎn)介了很多,具體了解app.use()干了什么的同學(xué)可以去這里
socket.io建立客戶端和服務(wù)端的鏈接
創(chuàng)建完上面的服務(wù)后,我們需要把socket.io引用進(jìn)來(lái),讓客戶端和服務(wù)端建立長(zhǎng)久鏈接。我們把a(bǔ)pp.js進(jìn)行如下改造:
const express = require('express'); const app = express(); // 創(chuàng)建express實(shí)例,賦值給app。 const server = require('http').Server(app); const io = require('socket.io')(server); //將socket的監(jiān)聽(tīng)加到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)聽(tīng)3000端口,然后執(zhí)行回調(diào)函數(shù)在控制臺(tái)輸出。 }); ... ... app.use('/',express.static(path.join(__dirname,'./public'))); //一句話就搞定。 io.on('connection',(socket)=>{ //監(jiān)聽(tīng)客戶端的連接事件 });
o.on表示監(jiān)聽(tīng)某個(gè)事件,該事件一發(fā)生,就觸發(fā)回調(diào)函數(shù)。'connection‘就是一個(gè)事件名,它已經(jīng)定義好了,只要用戶連接上就會(huì)觸發(fā)。現(xiàn)在app.js基本已經(jīng)完成,我們?cè)诟夸泩?zhí)行:
node app.js
>
現(xiàn)在訪問(wèn)http://127.0.0.1:3000/static/chat.html:
哎?啥也沒(méi)有。。。那不廢話!我們都沒(méi)url請(qǐng)求對(duì)應(yīng)的靜態(tài)資源!
添加靜態(tài)html
我們?cè)陧?xiàng)目根目錄創(chuàng)建public文件夾,public文件夾里面新建chat.html文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>聊天室</title> </head> <body> 這是我們的聊天室 </body> </html>
現(xiàn)在我們刷新下頁(yè)面,你看頁(yè)面出現(xiàn)了:
>
到這里其實(shí)一個(gè)最簡(jiǎn)單的瀏覽器和web服務(wù)器協(xié)作的項(xiàng)目就已經(jīng)完成,后面我們要不斷完善頁(yè)面,給服務(wù)器后端加業(yè)務(wù)功能來(lái)實(shí)現(xiàn)多人聊天室。
基本功能實(shí)現(xiàn)
登陸功能,我們需要一個(gè)用戶名,(不需要密碼),該用戶名必須客戶端服務(wù)器都有存儲(chǔ)。每次傳輸信息基本都需要包括用戶名,否則不知道是誰(shuí)發(fā)的。
群聊功能,我們需要分辨信息來(lái)己方和對(duì)方
登陸功能實(shí)現(xiàn)
login頁(yè)面重構(gòu)
最基本的登陸界面由一個(gè)用戶名輸入框和登錄按鈕組成:
//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> <p class="container"> <p class="title">歡迎來(lái)到ddvdd聊天室</p> <p class="login-wrap"> <p class="user-ipt"> <span class="user-name">用戶名:</span> <input id="name" class="name-ipt" type="text" /> </p> <button id="loginbutton" class="login-button">登陸</button> </p> </p> </body> </html>
簡(jiǎn)單的加點(diǎn)樣式,靜態(tài)頁(yè)面就完成了,我們刷新下頁(yè)面:
login頁(yè)面交互
昨天下午寫到一半。。。部門突然要去團(tuán)建聚會(huì),只能匆匆提交代碼,草草了事。今天一大早來(lái)到公司繼續(xù)給大家碼
廢話不多說(shuō)進(jìn)入正題,登陸這塊交互,當(dāng)用戶訪問(wèn)服務(wù)器并且成功登陸算一個(gè)在線登陸人數(shù),每登陸一個(gè)用戶,服務(wù)器都會(huì)把用戶信息存入一個(gè)數(shù)組中,保存在服務(wù)器,這里要注意一點(diǎn),服務(wù)器會(huì)對(duì)用戶登陸的用戶名進(jìn)行校驗(yàn),校驗(yàn)結(jié)果會(huì)返回給客戶端,客戶端通過(guò)校驗(yàn)結(jié)果,改變當(dāng)前頁(yè)面是否進(jìn)入聊天頁(yè)面。
上面的服務(wù)器和客戶端交互都是通過(guò)socket.io來(lái)實(shí)現(xiàn)通訊的,前端的業(yè)務(wù)交互我們這里就采用jquery來(lái)實(shí)現(xiàn),在public文件夾下新建js文件夾,下載jquery-3.2.1.min.js、新建main.js。然后對(duì)chat.html引入需要的sdk:
<script src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif" src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif" src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif">引入完sdk,我們對(duì)main的js添加登錄功能:
//main.js $(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)用戶登錄的時(shí)候觸發(fā) let setUsername = () => { _username = _$inputname.val().trim(); //得到輸入框中用戶輸入的用戶名 //判斷用戶名是否存在 if(_username) { socket.emit('login',{username: _username}); //如果用戶名存在,就代表可以登錄了,我們就觸發(fā)登錄事件,就相當(dāng)于告訴服務(wù)器我們要登錄了 } else{ alert('請(qǐng)輸入用戶名!'); } }; _$loginButton.on('click',function (event) { //監(jiān)聽(tīng)按鈕的點(diǎn)擊事件,如果點(diǎn)擊,就說(shuō)明用戶要登錄,就執(zhí)行setUsername函數(shù) setUsername(); }); socket.on('loginResult',(data)=>{ if(data.code === 0) { // 登陸成功,切換至聊天室頁(yè)面 } else if(data.code ===1){ alert('用戶已登錄!'); } else{ alert('登錄失敗!'); } }) }); //app.js const express = require('express'); const app = express(); // 創(chuàng)建express實(shí)例,賦值給app。 const server = require('http').Server(app); const io = require('socket.io')(server); //將socket的監(jiān)聽(tīng)加到app設(shè)置的模塊里。這兩句理解不了的可以去socket.io官網(wǎng)去看 const path = require('path'); // 這是node的路徑處理模塊,可以格式化路徑 const users = []; //用來(lái)保存所有的用戶信息 let usersNum = 0; //統(tǒng)計(jì)在線登錄人數(shù) server.listen(3000,()=>{ console.log("server running at 127.0.0.1:3000"); // 代表監(jiān)聽(tīng)3000端口,然后執(zhí)行回調(diào)函數(shù)在控制臺(tái)輸出。 }); app.get('/',(req,res)=>{ res.redirect('/static/chat.html'); // express的重定向函數(shù)。如果瀏覽器請(qǐng)求了根路由'/',瀏覽器就給他重定向到 '127.0.0.1:3000/chat.html'路由中 }); app.use('/static',express.static(path.join(__dirname,'./public'))); //一句話就搞定。 io.on('connection',(socket)=>{ //監(jiān)聽(tīng)客戶端的連接事件 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}`); } }); //斷開(kāi)連接后做的事情 socket.on('disconnect',()=>{ //注意,該事件不需要自定義觸發(fā)器,系統(tǒng)會(huì)自動(dòng)調(diào)用 usersNum = users.length; console.log(`當(dāng)前在線登錄人數(shù):${usersNum}`); }); }); //校驗(yàn)用戶是否已經(jīng)登錄 const checkUserName = (data) => { let isExist = false; users.map((user) => { if(user.username === data.username){ isExist = true; } }); return isExist; }上面代碼大家需要了解以下幾點(diǎn):
socket.on 表示監(jiān)聽(tīng)事件,后面接一個(gè)回調(diào)函數(shù)用來(lái)接收emit發(fā)出事件傳遞過(guò)來(lái)的對(duì)象。
socket.emit 用來(lái)觸發(fā)事件,傳遞對(duì)象給on監(jiān)聽(tīng)事件。
我們socket連接之后的監(jiān)聽(tīng)觸發(fā)事件都要寫在io.on('connection')的回調(diào)里面,因?yàn)檫@些事件都是連接之后發(fā)生的,就算是斷開(kāi)連接的事件 disconnect 也是在連接事件中發(fā)生的,沒(méi)有正在連接的狀態(tài),哪來(lái)的斷開(kāi)連接呢?
理解雖然服務(wù)器端只有app.js一個(gè)文件,但是不同的客戶端連接后信息是不同的,所以我們必須要將一些公用的信息,比如說(shuō),儲(chǔ)存所有登錄用戶的數(shù)組,所有用戶發(fā)送的所有信息存儲(chǔ)在外部,一定不能存儲(chǔ)在connecion里
效果展示:
群聊功能實(shí)現(xiàn)
寫完簡(jiǎn)單的登錄功能,現(xiàn)在我們來(lái)寫這項(xiàng)目最重要的功能群聊。首先我們先來(lái)處理下頁(yè)面,因?yàn)楣δ芎?jiǎn)單,所以不單獨(dú)建立html來(lái)顯示聊天室,就直接寫在login頁(yè)面,通過(guò)class名稱的變化來(lái)切換登錄后,聊天室的顯示。
聊天室頁(yè)面重構(gòu)
下面我們對(duì)chat.html進(jìn)行整改:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>聊天室</title> <script src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif" <script src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif" <script src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif" <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> <p class="container"> <p id="loginbox" class="login-wrap"> <p class="title">登錄</p> <p class="user-ipt"> <span class="user-name">用戶名:</span> <input id="name" class="name-ipt" type="text" /> </p> <button id="loginbutton" class="login-button">登錄</button> </p> <p id="chatbox" class="chat-wrap" style="display:none"> <p id="content" class="chat-content"> <!-- 聊天內(nèi)容 --> </p> <p class="send-wrap"> <textarea rows="3" cols="20" id="chatmessage" class="message-ipt" type="textarea" placeholder="請(qǐng)輸入要發(fā)送的信息內(nèi)容"></textarea> </p> </p> </p> </body> </html>新增chatbox容器來(lái)作為聊天室,里面有一個(gè)群聊的聊天框,和一個(gè)發(fā)送消息的文本框。通過(guò)上面loginResult回調(diào),對(duì)loginbox進(jìn)行隱藏,顯示chatbox:
//顯示聊天室界面 let showChatRoom = () => { $('#loginbox').hide('slow'); _$loginButton.off('click'); $(`<p class="title">歡迎${_username}來(lái)到ddvdd聊天室</p>`).insertBefore($("#content")); $("#chatbox").show('slow'); }消息事件發(fā)送監(jiān)聽(tīng)機(jī)制
聊天一定是客戶端觸發(fā)的,所以發(fā)送信息是客戶端觸發(fā),服務(wù)器監(jiān)聽(tīng)。
服務(wù)器監(jiān)聽(tīng)到發(fā)送信息的事件后會(huì)存儲(chǔ)信息,然后觸發(fā)發(fā)送信息成功事件廣播給所有客戶端,將信息傳給所有客戶端。
發(fā)送消息sendMessage事件
//main.js //發(fā)送消息 let sendMessage = function () { let _message = _$chattextarea.val(); if(_message) { socket.emit('sendMessage',{username: _username, message: _message}); } else{ alert('請(qǐng)輸入發(fā)送消息!'); } }; ... _$chattextarea.on('keyup',function (event) { if(event.keyCode === 13) { sendMessage(); _$chattextarea.val(''); } });服務(wù)器端監(jiān)聽(tīng)sendMessage事件
//app.js socket.on('sendMessage',(data)=>{ for(let _user of users) { if(_user.username === data.username) { _user.message.push(data.message); //信息存儲(chǔ)之后觸發(fā)receiveMessage將信息發(fā)給所有瀏覽器-廣播事件 io.emit('receiveMessage',data); break; } } });我們是遍歷服務(wù)器端的用戶數(shù)組,找到該用戶,將發(fā)送的信息存起來(lái),然后觸發(fā)receiveMessage事件廣播到所有瀏覽器,sendMessage是寫在connection里,login之外的,為什么這么做大家一定要理解,發(fā)送消息是連接時(shí)候做的事情,而不是登錄時(shí)做的事情。
注意的是,我使用的是io.emit,他是真正的廣播到所有瀏覽器,socket.broadcast.emit則不會(huì)廣播到自己的瀏覽器。
客戶端監(jiān)聽(tīng)receiveMessage事件
//main.js socket.on('receiveMessage',(data)=>{ showMessage(data); }) //顯示消息 let showMessage = function (data) { //先判斷這個(gè)消息是不是自己發(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)完成了,來(lái)看看效果吧!打開(kāi)三個(gè)瀏覽器,分別登錄老大、老二、老三,發(fā)一句“大噶好~,我是渣渣輝!”。
相關(guān)推薦:
vue組件父子間通信實(shí)現(xiàn)聊天室實(shí)例詳解
實(shí)例詳解vue組件父子間通信之聊天室
用Node.js編寫多人實(shí)時(shí)在線聊天室
以上就是nodejs和express搭建多人聊天室的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!