先言
这我以前写的,这里就是单纯分享下代码,不算正经文章。效果如下,前端用一个单html文件。然后后端用node.js和socket.io,也是只用一个单js文件就好。这里可以看下代码的实现逻辑就好,因为来连数据库才能运行的。有需要的话告诉我,我也可以把存取数据库代码逻辑和接口逻辑删了,改成时时存时时失效的,这样就能直接打开html文件直接随意运行了。
代码
前端:
html:
<!DOCTYPE html>
<html>
<head>
<title>Socket.IO chat</title>
<link rel="stylesheet" href="./chat.css" />
</head>
<body>
<img class="bg" src="../bg.webp" alt="" />
<div class="chat">
<div class="left">
<div class="title">
<h3>聊天室</h3>
</div>
<div class="content" id="content">
<div class="history" id="history">
<button>查看最近的聊天</button>
</div>
<ul id="messages">
<!-- <li class="data">
<div class="img">
<img
src="https://bbs-img.huaweicloud.com/user/img/head/1617968414164_1201_6320.jpg"
alt=""
/>
</div>
<p><span>小杰(老师):</span> 【摘要】</p>
</li>
<li class="data2">
<p>
<span>我:</span> 【摘要】
首先,强类型不允许随意的隐式类型转换,而弱类型是允许的。JavaScript就是经典的弱类型语言。而Typescript可以说是JavaScript的超集,在js的基础上新增了许多语法特性,使得类型不再可以随意转换,能大大减少开发阶段的错误。
</p>
<div class="img">
<img
src="https://bbs-img.huaweicloud.com/user/img/head/1617968414164_1201_6320.jpg"
alt=""
/>
</div>
</li> -->
</ul>
</div>
<div class="shuru">
<textarea
id="m"
type="text"
autocomplete="off"
autofocus=""
></textarea>
<button id="btn">
发送
</button>
<div class="emoji" id="emoji">
😊
<div id="ej" style="display: none;">
<span>😂</span>
<span>😐</span>
<span>😤</span>
<span>😴</span>
<span>😫</span>
<span>😧</span>
<span>🤪</span>
<span>😛</span>
<span>😭</span>
<span>😡</span>
<span>👹</span>
<span>🤢</span>
<span>❤️</span>
<span>💔</span>
<span>👄</span>
<span>🧝</span>
<span>💇♀️</span>
<span>💘</span>
<span>👻</span>
<span>☠️</span>
<span>😱</span>
<span>🌬️</span>
<span>👌</span>
<span>🤝</span>
</div>
</div>
</div>
</div>
<div class="right">
<div class="title">
<h3>当前在线成员<span id="people">(0)</span></h3>
</div>
<div class="list" id="list">
<!-- <div class="item">
<img
src="https://bbs-img.huaweicloud.com/user/img/head/1617968414164_1201_6320.jpg"
alt=""
/>
<p>小杰(学生)33333330000</p>
</div> -->
</div>
</div>
</div>
</body>
<!-- <script src="https://unpkg.com/axios/dist/axios.min.js"></script> -->
<script>
// 封装的ajax请求
function ajax(url, params) {
return new Promise((resolve, reject) => {
//要发个get请求知道是你来了,后端解析你的信息
let xhr = new XMLHttpRequest();
// 设置属性
xhr.open("post", url);
// 如果想要使用post提交数据,必须添加此行
xhr.setRequestHeader(
"Content-type",
"application/x-www-form-urlencoded"
);
// 将数据通过send方法传递
xhr.send(params);
// 发送并接受返回值
xhr.onreadystatechange = function() {
// 这步为判断服务器是否正确响应
if (xhr.readyState == 4 && xhr.status == 200) {
resolve(xhr.responseText);
}
};
});
}
</script>
<script src="./static/dist/socket.io.js"></script>
<script>
//要发个请求知道是你来了,后端解析你的信息
/* axios
.post("http://localhost:8849/", {
username: "787",
})
.then(function(response) {
console.log(response);
}); */
let net_name = window.sessionStorage.getItem("net_name");
let icon = window.sessionStorage.getItem("icon");
let role = window.sessionStorage.getItem("role");
let uid = window.sessionStorage.getItem("uid");
if (role == "1") {
role = "学生";
} else if (role == "2") {
role = "老师";
} else {
role = "管理员";
}
/* let uid = Number(
Math.random()
.toString()
.substr(3, 10) + Date.now()
).toString(36); */
ajax(
"http://localhost:8849/",
`net_name=${net_name}&icon=${icon}&role=${role}&uid=${uid}`
).then(function(response) {
console.log(response);
});
// 做个判断
var socket = io("http://localhost:8849/");
var btn = document.getElementById("btn");
var ul = document.getElementById("messages");
let cxt = document.getElementById("m");
let people = document.getElementById("people");
let content = document.getElementById("content");
let list = document.getElementById("list");
let history = document.getElementById("history");
let emoji = document.getElementById("emoji");
let ej = document.getElementById("ej");
//选表情
emoji.onclick = function(e) {
if (ej.style.display == "block") {
ej.style.display = "none";
} else {
ej.style.display = "block";
}
};
ej.onclick = function(e) {
// console.log(e.target.innerText);
if (e.target.nodeName.toLowerCase() == "span") {
cxt.value += e.target.innerText;
}
};
history.onclick = function() {
//查看历史记录
history.style.display = "none";
ajax("http://localhost:8849/query", "").then(function(response) {
let data = JSON.parse(response).data;
//先看下有几个小li了
lis = document.getElementById("messages").getElementsByTagName("li")
.length;
console.log(lis);
for (let i = lis; i < data.length - 1; i++) {
let newli = document.createElement("li");
if (data[i].uid == uid) {
newli.setAttribute("class", "data2");
newli.innerHTML = `
<p>
<span>${data[i].net_name}(${data[i].role}):</span> ${data[i].content}
</p>
<div class="img">
<img
src="http://localhost:8848/${data[i].icon}"
alt=""
/>
</div>
`;
} else {
newli.setAttribute("class", "data");
newli.innerHTML = `
<div class="img">
<img
src="http://localhost:8848/${data[i].icon}"
alt=""
/>
</div>
<p>
<span>${data[i].net_name}(${data[i].role}):</span> ${data[i].content}
</p>`;
}
ul.insertBefore(newli, ul.childNodes[0]);
}
//滚动到底部
content.scrollTo({
top: content.scrollHeight,
behavior: "smooth",
});
});
};
// 点击send按钮,把消息发送给服务器
btn.onclick = function() {
if (cxt.value == "") return;
// 把登录的用户名和输入框内容全部发送给服务器,让服务器做一次广播,才能同步用户信息。
//把自己名字传给服务器,其实也不用,我在那局部变量保存了
socket.emit("message", { net_name, role, uid, icon, inpval: cxt.value });
//数据库添加数据
ajax(
"http://localhost:8849/add",
`net_name=${net_name}&icon=${icon}&role=${role}&content=${cxt.value}&uid=${uid}`
).then(function(response) {
console.log(response);
});
return false;
};
//监听服务器的广播消息,同步用户信息,msg就是点击发送按钮发送的用户信息
socket.on("message", function(msg) {
//在这可以通过名字判断是自己就放右边,别人就放左边
// 每个客户端将用户的消息渲染
var newli = document.createElement("li");
// newli.innerHTML = msg.net_name + "(" + msg.role + ")" + ":" + msg.inpval;
if (msg.uid == uid) {
newli.setAttribute("class", "data2");
newli.innerHTML = `
<p>
<span>${msg.net_name}(${msg.role}):</span> ${msg.inpval}
</p>
<div class="img">
<img
src="http://localhost:8848/${msg.icon}"
alt=""
/>
</div>`;
} else {
newli.setAttribute("class", "data");
newli.innerHTML = `
<div class="img">
<img
src="http://localhost:8848/${msg.icon}"
alt=""
/>
</div>
<p>
<span>${msg.net_name}(${msg.role}):</span> ${msg.inpval}
</p>`;
}
ul.appendChild(newli);
//滚动到底部
content.scrollTo({
top: content.scrollHeight,
behavior: "smooth",
});
cxt.value = "";
});
// 服务器端监听服务端建立连接发来的信息,用于渲染温馨提示信息,msg是服务器返回广播的用户对象数据
socket.on("loginin", function(msg) {
// 生成用户进入房间提示信息标签
let tip = document.createElement("p");
console.log(msg.userList);
tip.innerHTML = msg.des;
// 设置样式
tip.className = "tips";
ul.appendChild(tip);
// people是显示当前聊天室人数
people.innerHTML = "( " + msg.count + "人 )";
//更新在线用户列表
list.innerHTML = " ";
for (let i = 0; i < msg.userList.length; i++) {
list.innerHTML += `
<div class="item">
<img
src="http://localhost:8848/${msg.userList[i].icon}"
alt=""
/>
<p>${msg.userList[i].net_name}(${msg.userList[i].role})</p>
</div>
`;
}
});
//服务器端监听服务端建立连接发来的信息,msg是服务器返回广播的用户对象数据
//关掉浏览器会执行下面这个函数
socket.on("loginout", function(msg) {
// 生成用户退出提示信息
let tip = document.createElement("p");
console.log(msg.userList);
tip.innerHTML = msg.des;
tip.className = "tips";
ul.appendChild(tip);
people.innerHTML = "( " + msg.count + "人 )";
//更新在线用户列表
list.innerHTML = " ";
for (let i = 0; i < msg.userList.length; i++) {
list.innerHTML += `
<div class="item">
<img
src="http://localhost:8848/${msg.userList[i].icon}"
alt=""
/>
<p>${msg.userList[i].net_name}(${msg.userList[i].role})</p>
</div>
`;
}
});
</script>
</html>
css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 600px;
display: flex;
justify-content: center;
}
.bg {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
object-fit: cover;
z-index: -1000;
}
.history {
width: 100%;
text-align: center;
}
.history button {
border: none;
height: 30px;
width: 120px;
cursor: pointer;
background-color: rgb(88, 180, 230);
border-radius: 2px;
color: white;
}
.history button:hover {
background-color: rgb(125, 195, 236);
}
.history button:active {
background-color: rgb(88, 180, 230);
}
.chat {
width: 1100px;
height: 596px;
margin: 20px auto;
}
.left {
width: 800px;
height: 100%;
float: left;
background-color: rgba(223, 241, 251, 0.8);
border-radius: 10px;
box-shadow: 0 0 5px rgb(153, 153, 153);
}
.left .emoji {
position: absolute;
width: 30px;
height: 30px;
background-color: rgb(88, 180, 230);
right: 90px;
line-height: 30px;
text-align: center;
border-radius: 2px;
cursor: pointer;
}
.left .emoji div {
width: 200px;
height: 100px;
background-color: #fff;
position: absolute;
top: -110px;
left: -50%;
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
border-radius: 3px;
align-items: center;
}
.left .emoji div::after {
content: "";
position: absolute;
bottom: -9px;
left: 25px;
width: 10px;
height: 10px;
background-color: rgb(255, 255, 255);
-webkit-clip-path: polygon(53% 100%, 0 0, 100% 0);
clip-path: polygon(53% 100%, 0 0, 100% 0);
}
.left .emoji div span {
line-height: 25px;
text-align: center;
font-size: 15px;
width: 25px;
height: 25px;
}
.right {
width: 290px;
float: right;
height: 100%;
border-radius: 10px;
background-color: rgba(223, 241, 251, 0.8);
box-shadow: 0 0 5px rgb(153, 153, 153);
}
.right .list {
padding: 10px;
width: 100%;
height: 520px;
display: flex;
flex-wrap: wrap;
align-content: start;
overflow: auto;
}
.right .list .item {
margin-top: 6px;
width: 100%;
height: 50px;
background-color: red;
}
.right .list .item img {
width: 50px;
height: 50px;
object-fit: cover;
float: left;
}
.right .list .item p {
line-height: 50px;
background-color: #fff;
text-indent: 1em;
font-family: "fangsong";
}
.title {
border-radius: 10px 10px 0 0;
line-height: 60px;
color: black;
border-bottom: 1px solid #999;
text-align: center;
background-color: rgb(128, 199, 237);
}
.content {
padding: 10px;
width: 100%;
overflow: auto;
height: 400px;
}
#messages li {
list-style: none;
padding: 5px;
}
.shuru {
border-top: solid 2px rgb(143, 143, 143);
height: 130px;
position: relative;
}
#m {
width: 100%;
height: 83px;
outline: none;
color: #666;
overflow: auto;
font-size: 16px;
line-height: 25px;
padding: 5px 10px;
border: none;
background-color: transparent;
text-indent: 0;
}
#btn {
border: none;
height: 30px;
width: 80px;
cursor: pointer;
position: absolute;
right: 6px;
bottom: 10px;
background-color: rgb(88, 180, 230);
border-radius: 2px;
}
#btn:hover {
background-color: rgb(125, 195, 236);
}
#btn:active {
background-color: rgb(88, 180, 230);
}
.tips {
width: 50%;
margin: 4px auto;
padding: 2px 5px;
text-align: center;
font-size: 8px;
border-radius: 10px;
background-color: #cfcfcf;
color: #fff;
}
.title #people {
font-size: 8px;
color: rgb(250, 27, 27);
}
.data {
width: 100%;
display: flex;
justify-content: flex-start;
min-height: 60px;
}
.data .img {
width: 60px;
}
.data img {
flex: 1;
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
box-shadow: 0 0 3px rgb(153, 153, 153);
}
.data p {
font-size: 14px;
line-height: 20px;
margin-left: 10px;
background-color: rgb(18, 183, 245);
color: white;
border-radius: 5px;
padding: 10px;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
}
.data p span {
width: 120px;
overflow: hidden;
color: rgb(255, 123, 0);
background-color: rgb(223, 241, 251);
border-radius: 2px;
padding: 0 5px;
font-size: 14px;
margin-bottom: 5px;
}
.data2 p span {
width: 120px;
overflow: hidden;
color: rgb(255, 123, 0);
background-color: rgb(223, 241, 251);
border-radius: 2px;
padding: 0 5px;
font-size: 14px;
margin-bottom: 5px;
}
.data p::after {
position: absolute;
content: "";
top: 10px;
left: -9px;
width: 10px;
height: 10px;
background-color: rgb(18, 183, 245);
-webkit-clip-path: polygon(100% 100%, 100% 0, 0 51%);
clip-path: polygon(100% 100%, 100% 0, 0 51%);
}
/* */
.data2 {
width: 100%;
display: flex;
justify-content: flex-end;
min-height: 60px;
}
.data2 .img {
width: 60px;
}
.data2 img {
flex: 1;
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
box-shadow: 0 0 3px rgb(153, 153, 153);
}
.data2 p {
line-height: 20px;
margin-right: 10px;
font-size: 14px;
background-color: rgb(18, 183, 245);
color: white;
border-radius: 5px;
padding: 10px;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
}
.data2 p::after {
position: absolute;
content: "";
top: 10px;
right: -9px;
width: 10px;
height: 10px;
background-color: rgb(18, 183, 245);
-webkit-clip-path: polygon(100% 50%, 0 0, 0 100%);
clip-path: polygon(100% 50%, 0 0, 0 100%);
}
后端js文件:
/*
* @Author: yournet_name
* @Date: 2022-03-10 21:39:37
* @LastEditTime: 2022-03-13 11:50:33
* @LastEditors: Please set LastEditors
* @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
* @FilePath: \onlinechat-main\app.js
*/
const express = require('express')
const app = express()
var http = require('http').Server(app)
var io = require('socket.io')(http, { cors: true })
// 使得能接收post请求参数
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended:false }))
//使得express接收json格式数据
app.use(express.json());
// 解决跨域
const cors = require('cors')
app.use(cors({credentials:true,origin:true}))
// knex插件,连接数据库
const knex = require('knex')({
client: 'mysql',
connection: {
hhost : 'localhost', //
user : '',
password : '',
database : ''
}
})
//时间格式转换
Date.prototype.Format = function (fmt) { // author: meizz
var o = {
"M+": this.getMonth() + 1, // 月份
"d+": this.getDate(), // 日
"h+": this.getHours(), // 小时
"m+": this.getMinutes(), // 分
"s+": this.getSeconds(), // 秒
"q+": Math.floor((this.getMonth() + 3) / 3), // 季度
"S": this.getMilliseconds() // 毫秒
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
/* / 使用
var date = new Date();
date.format("yyyy-MM-dd"); */
/* app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
}); */
//meoji表情转义
function utf16toEntities(str) {
var patt=/[\ud800-\udbff][\udc00-\udfff]/g; // 检测utf16字符正则
str = str.replace(patt, function(char){
var H, L, code;
if (char.length===2) {
H = char.charCodeAt(0); // 取出高位
L = char.charCodeAt(1); // 取出低位
code = (H - 0xD800) * 0x400 + 0x10000 + L - 0xDC00; // 转换算法
return "&#" + code + ";";
} else {
return char;
}
});
return str;
}
var userList = [] //当前在线用户数组
var user = {
net_name:"",
role:"",
uid:"",
icon:""
} //新进入用户 , 要公告推送消息
var count = 0
//通用接口,通过接口获取是谁进来了 ,要公告推送消息
app.post('/',(req,res)=>{
let {net_name,role,uid,icon} = req.body;
user.net_name = net_name;
user.role = role;
user.uid = uid;
user.icon = icon;
userList.push({
net_name,
role,
uid,
icon
})
// 保存用户的名称
// 返回状态码,通过状态码执行客户端页面跳转
res.send({state:net_name})
// res.sendFile(__dirname + '/index.html')
})
//新增聊天数据 用户点击发送应该也触发这个接口 到达50条后应该删除以前的
app.post('/add',(req,res)=>{
// req.file得到文件信息,req.body的到文件文本信息
let {net_name,icon,role,content,uid} = req.body
content = utf16toEntities(content)
let create_time = new Date();
create_time.Format("yyyy-MM-dd-hh:mm");
knex('chat').insert({net_name,icon,role,create_time,content,uid}).then(value=>{
// res.send({msg:'新增成功'})
}).catch(value=>{
res.send({code:'400',msg:'发生错误'})
console.log(value);
})
})
//查询历史聊天记录 直接只取20条把
app.post('/query',(req,res)=>{
knex.schema.raw(`select * from chat order by id desc limit 20`).then(value=>{
if(res.length === 0){
res.send({code:200,msg:'无结果'})
}else{
res.send({code:200,msg:'查询成功',data:value[0]})
}
}).catch(value=>{
console.log(value);
})
})
//入口函数,连接进程 每一个用户(连接状态)都有一个下面这个函数,独立的
io.on('connection', function (socket) {
/* let userSelf = {
net_name:"",
role:"",
uid:""};
userSelf.net_name = user.net_name;
userSelf.role = user.role;
userSelf.uid = user.uid; */
let net_name =user.net_name; //局部变量,保存当前连接的是谁
let icon =user.icon; //局部变量,保存当前连接的是谁
let uid =user.uid; //局部变量,保存当前连接的是谁
let role =user.role; //局部变量,保存当前连接的是谁
// console.log('有人连接');
// 每建立连接一次,在线人数加一
count++
//这里是发送消息
// on用来监听客户端message事件,回调函数处理。
socket.on('message', function (msg) {
// 如果在这里通过url解析的username来改变下面33行即将渲染的name,会出现异步问题。name还没有赋值就被传到客户端
// 所以通过ajax请求,先让后端拿到username,然后再做提示信息的渲染
// console.log(msg.net_name+'('+msg.role+')'+':'+ msg.inpval);
// 将客户端发送来的消息中转给所有客户端
io.emit('message', msg)
});
// loginin是自定义事件,第二个参数返回数据对象用于渲染,用于登陆后向客户端发送用户登录信息
io.emit('loginin',{count,des:`温馨提示:${net_name}(${role})进入聊天室 ${new Date().Format("yyyy-MM-dd-hh:mm")}`,userList:userList})
//io.emit('loginin',{count,des:{a:'哈哈哈'}})
//登陆后向客户端发送用户退出信息
//当断开连接disconnect,前端会执行下面这个loginout函数
socket.on('disconnect', function (value) {
//console.log(value);
// loginout是自定义事件,第二个参数返回数据对象用于渲染
count--
//在线用户数组减去一个
for(let i=0;i<=userList.length-1;i++){
if(userList[i].uid == uid){
userList.splice(i,1)
}
}
io.emit('loginout',{count,des:`温馨提示:${net_name}(${role})退出聊天室 ${new Date().Format("yyyy-MM-dd-hh:mm")}`,userList:userList})
// 连接每断开一次,在线人数减一
})
});
http.listen(8849, function () {
console.log('websocket连接成功!')
})
结语
我的哔哩哔哩空间
Gitee仓库地址:全部css、js特效源码
其它文章:
~关注我看更多简单创意特效:
文字烟雾效果 html+css+js
环绕倒影加载特效 html+css
气泡浮动背景特效 html+css
简约时钟特效 html+css+js
赛博朋克风格按钮 html+css
仿网易云官网轮播图 html+css+js
水波加载动画 html+css
导航栏滚动渐变效果 html+css+js
书本翻页 html+css
3D立体相册 html+css
霓虹灯绘画板效果 html+css+js
记一些css属性总结(一)
Sass总结笔记
…等等
进我主页看更多~