近来刚开始学node.js,基础,深入,express,koa,react,keystone,mongress,等等各种各样的关于Node.js的东西,每个都学的迷迷糊糊的,原因在于没有练习,感觉学的每一个知识点就像一块拼图,最终却没有拼到一起,于是就想多写几个小项目练练手,于是,就有了本文。。。
----------------------------------我是分割线-----------------------------------------------
恩,就按我写代码的顺序来记录一下我的小成果。
github地址:https://github.com/maichonglyd/mySmailChat.git 该项目是在本地运行
先看文件结构,其实也没几个文件:
mySmailChat
|---web
| |---script
| | |---main.js
| |---index.css
| |---index.html
|---server.js
写代码总是有个先后顺序的,做项目也是,那先做哪一块后做哪一块就会对整个项目有那么些影响,我在想,一个网页聊天室,首先应该有的是网页,不论是聊天还是后台没有网页上的显示就不能准确的知道代码执行的结果,于是我就先做了网页的页面。
index.html:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>我的聊天室</title> <link rel="stylesheet" type="text/css" href="./index.css"> </head> <body> <div class="container"> <div class="titles"> <h1 >my smail chat</h1> </div> <div class="chatValue" id="chatValue"> </div> <div class="sends"> <input type="text" class="writeBox" placeHolder="请输入文本" id="chatMsg"></input> <input type="button" class="sendButton" id="chatSendBtn" value="发送"></input> </div> </div> <div id="loginPage"> <p id="info">正在连接....</p> <div id="nickWrapper"> <input type="text" placeHolder="请输入昵称" id="nicknameInput" /> <input type="button" value="确定" id="loginBtn" /> </div> </div> <script src="/socket.io/socket.io.js"></script> <script src="script/main.js"></script> </body> </html>
整个页面显示如下:
在页面上我用了npm仓库中的socket.io,使用socket来连接服务端,进行即时通信,另外使用了main.js来装载我们前端页面上的逻辑处理代码,呃,css样式还用说么?好吧!!下面是css样式的代码:
index.css
body,html{ background-color: #000; vertical-align: center; } div.container{ width: 800px; height: 900px; margin-left: auto; margin-right: auto; background-color: #222; } div.titles{ text-align: center; background-color: #333; color: #888; padding: 10px; } div.chatValue{ width: 790px; margin:5px; height: 690px; overflow:auto; } div.sends{ width: 100%; height: 100px; background-color: #333; } input.writeBox{ height: 90px; width: 710px; margin-left: 3px; margin-top: 3px; margin-bottom: 3px; font-size: 20px; background-color: #333; border: 1px solid #222; color: #888; } input.sendButton{ width: 78px; height: 94px; margin-top: 3px; margin-bottom: 3px; margin-right: 3px; float: right; border: 1px solid #222; background-color: #444; color: #888; font-size: 24px; } #loginPage{ position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(50,50,50,0.6); text-align: center; color: #888; display: block; padding-top: 400px; } .systemMsg{ width: 100%; text-align: center; color: #f00; }
这里面的内容就不用多说了,到目前为止,前端页面已经做好了,下面就开始做服务端,先让这个页面能显示出来:
server.js:
const express = require('express'); const app = express(); const http = require('http').Server(app); const io = require('socket.io')(http); let userList = []; http.listen(3000); app.use("/",express.static(__dirname+"/web"));
本项目依赖的包有express, http, socket.io.
express是node.js中管理路由响应请求的模块,根据请求的URL返回相应的HTML页面。这里我们使用一个事先写好的静态页面返回给客户端,只需使用express指定要返回的页面的路径即可。如果不用这个包,我们需要将HTML代码与后台JavaScript代码写在一起进行请求的响应,不太方便。
http 是node.js的核心模块,不了解的同学就自己查一下吧,这里就不多说了。
socket.io 是用来做socket连接的模块,我们需要把这几个包相互绑定一下,于是就有了
const http = require('http').Server(app); const io = require('socket.io')(http);
这两行代码,然后我们监听了3000端口,指定了要返回给前端的静态页面路径。
app.use("/",express.static(__dirname+"/web"));
在这里我们打开浏览器 输入 http://localhost:3000 就能看见我们写的界面了。当然输入昵称那个图应该不会太正,那个图的效果是需要连接上服务器进行一些js操作才会看起来更好看嘀,好吧,那我就需要先把服务器架起来,让前端页面可以连接上:
server.js:
io.on("connection",userConnction); function userConnction(socket){ console.log("a user connecting!!"); let user = new User("",socket); userList.push(user); } class User{ constructor(name,socket){ this.nickName = name; this.socket = socket; } setName(name){ this.nickName = name; } setSocket(){ this.socket = socket; } getName(){ return this.nickName; } getSocket(){ return this.socket; } }
io 监听连接成功事件,触发回调函数userConnection,服务端会输出一句话说明有用户连接上了。然后把这个用户先保存到数组里,既然是后台监听,那只有前端去主动连接了,好吧,那就前端主动连接:
main.js:
var nickName = ""; window.onload = function(){ init(); } function init(){ var info = document.getElementById("info"); var nickWrapper = document.getElementById("nickWrapper"); var nicknameInput = document.getElementById("nicknameInput"); var loginBtn = document.getElementById("loginBtn"); var chatMsg = document.getElementById("chatMsg"); var chatSendBtn = document.getElementById("chatSendBtn"); var loginPage = document.getElementById("loginPage"); info.textContent = "正在连接...." nickWrapper.style.display = "none"; socket = io.connect("http://localhost:3000"); socket.on("connect", function (){ info.textContent = "请输入昵称!"; nickWrapper.style.display = "block"; nicknameInput.focus(); }); }
希望上面那一片doucument.getElementById 没有让你看晕。我是把页面里用到的元素都先提前获取到了,下面就直接写获取后的变量名,不再写那么长了。
这里我们应该在网页刚加载完的时候就要先连接服务器,
socket = io.connect("http://localhost:3000");
连上之后把输入昵称那个图变成遮罩层盖住所有的操作区域,让用户输入名字,把焦点指向输入名字的文本框。
socket.on("connect", function (){......};
输入完名字之后我们需要把名字传到后台,应该在点击事件中完成:
main.js:
loginBtn.addEventListener("click",function (){ var _nickName = nicknameInput.value; if(_nickName.trim().length!=0){ socket.emit("login",_nickName); nickName = _nickName; }else{ nicknameInput.focus(); nicknameInput.value = ""; } },false);
点击事件中我们先获取文本框的值,去掉两端的空格之后如果长度不等于0那就说明输入的有内容,然后就向后台派发事件 login,再把名字传过去。同时我们声明一个全局变量nickName,记录下自己的名字。
socket.emit("login",_nickName); nickName = _nickName;
既然我们向后台派发了事件,那后台应该要有接收的地方:
server.js:
socket.on("login",function (data){ if(checkName(data)){ socket.emit("login","ok"); addName(data,socket); io.sockets.emit("system_login",data,userList.length); }else{ socket.emit("login","have"); } }); function checkName(nickName){ for(let i = 0,length=userList.length;i<length;i++){ if(nickName == userList[i].nickName){ return false; } } return true; } function addName(name,socket){ for(let i = 0,length=userList.length;i<length;i++){ if(userList[i].socket === socket){ userList[i].nickName = name; console.log("nickName:",name); } } }
后台监听前台派发的login事件,通过checkName函数来检查是否有重复名字,如果有就向前台派发login事件,带上参数"have",告诉前台名字已有,没有重复也派发lonin事件,带上参数"ok"告诉前台名字可用。然后用addName把名字记录下来。
socket.emit("login","ok"); addName(data,socket);
然后告诉所有的用户有新用户登录进来了.
io.sockets.emit("system_login",data,userList.length);
这里有个容易迷糊的地方:
login事件,是这样的,socket派发的事件只需要前后台对应就行,前台派发login,后台要有相应的接受,后台派发login,前台要有相应的接收,但前台派发login,前台就算有Login的接收也是没用的。
在后端通知前端名字可用派发了login事件,那前台要有相应的接收:
main.js:
socket.on("login",function (data){ if(data == "ok"){ loginPage.style.display = "none"; document.title = "我的聊天室 | " + nickName; chatMsg.focus(); }else if(data == "have"){ info.textContent = "昵称被占用,请重新输入!"; nicknameInput.value = ""; nicknameInput.focus(); } });
接收到以后,通过附带的参数来确定名字是否可用,可用就隐藏遮罩层,把自己的名字显示到网页的标题上,把焦点给发送信息的文本框,不可用就提示昵称被占用,清空输入名字的文本框,并给予焦点。
刚才后台还派发了一个事件,是告诉所有用户有新用户登录进来了,我们需要接收这个事件:
main.js:
socket.on("system_login",function (data,count){ if(data == nickName){ insertMsg("system_login","我","已加入聊天室,目前有" + count + "位用户在线"); }else{ insertMsg("system_login",data,"已加入聊天室,目前有" + count + "位用户在线"); } }); function insertMsg(type,name,msg){ var color = "#f00"; var space = ":"; switch(type){ case "system_login": case "system_logout": color="#f00"; space = ""; break; case "userChat": if(name == nickName){ color = "#080"; name = "我"; }else{ color = "#088"; } space = ":<br/> " break; } var chatValue = document.getElementById("chatValue"); var newNode = document.createElement("div"); newNode.innerHTML = "<p width='790px' style='word-wrap:break-word'><font color='"+color+"'>"+name +space+msg+"</font></p>"; chatValue.appendChild(newNode); chatValue.scrollTop = chatValue.scrollHeight; }
这里我做了一个处理,如果新登录用户是自己的名字,那就显示 “我xxxx”;
这里我用insertMsg函数处理要显示在聊天框里的内容,其实就是通过判断不同的消息来源显示不同的字号,颜色及分割符,因为我们的聊天框是一个DIV,我们要往里面添加新的元素来显示每个人说的话,包括系统,
var chatValue = document.getElementById("chatValue"); var newNode = document.createElement("div"); newNode.innerHTML = "<p width='790px' style='word-wrap:break-word'><font color='"+color+"'>"+name +space+msg+"</font></p>"; chatValue.appendChild(newNode);
聊天记录是不断从下面添加,越是下面的记录,越是最新的,所以我们应该把滚动条设置到最底部:
chatValue.scrollTop = chatValue.scrollHeight;
既然有登录那就有退出,退出也会有一个事件,我们应该监听这个事件,在监听到有用户退出的时候后台应该告诉所有用户有人走了:
server.js:
socket.on("disconnect",userDisconnect); function userDisconnect(){ let i = 0; while(i<userList.length){ if(userList[i].socket.disconnected){ if(userList[i].nickName.trim().length!=0){ io.sockets.emit("system_logout",userList[i].nickName,userList.length-1); } userList.splice(i,1); i--; } i++; } }
这里我用循环来判断是否有用户已断开连接:
if(userList[i].socket.disconnected)
当有用户断开的话就发个通知告诉所有用户这个人离开了,还剩下多少人:
io.sockets.emit("system_logout",userList[i].nickName,userList.length-1);
好吧,后台又发通知了,前台也要接收:
main.js:
socket.on("system_logout",function (data,count){ insertMsg("system_logout",data,"已离开聊天室,目前有" + count + "位用户在线"); });
到目前为止,准备工做已经做的差不多了,还有最重要的一件事没做,那就是聊天,聊天内容需要我们点击按钮发送到服务器:
main.js:
chatSendBtn.addEventListener("click",function(){ var _msg = chatMsg.value; if(_msg.trim().length !=0){ socket.emit("userChat",_msg); } chatMsg.value = ""; chatMsg.focus(); });
发送的内容长度不算空格也不等于0的话,说明发送的是有真实内容的,那就派发给后台:
socket.emit("userChat",_msg);
同时要清空说话框并把焦点给它。
chatMsg.value = ""; chatMsg.focus();
后台,后台,快出来,我有话要对大家说:
server.js:
socket.on("userChat",function(data){ for(let i = 0,length=userList.length;i<length;i++){ if(socket === userList[i].getSocket()){ let msgObj = { nickName : userList[i].getName(), msg : data }; io.sockets.emit("userChat",msgObj); } } });
后台脑子里记了很多人,需要知道你是谁,再把你要说的话和你的名字告诉大家,于是就需要检测socket对应的名字是谁然后连同发送的话一起推送给大家:
io.sockets.emit("userChat",msgObj);
呃,忘了解释一下:io.sockets.emit 是给所有连接上的客户端派发事件,某个socket.emit是给当前这个socket派发事件。
好吧,又是前台要接收:
main.js:
socket.on("userChat",function (data){ insertMsg("userChat",data.nickName,data.msg); });
到了这里,项目基本上完成了。看看效果吧,