原文链接:https://www.sinye.xyz/front-end/729.html
我纠结了很长时间要不要写这篇文章。因为,如果自己开发着玩,需要学的东西太多了,如果真正开发项目,肯定不是一个人能完成的,所以我写的这点皮毛根本不够用。
既然是我自己业余时间写这篇文章,那我就按照我开发这个’即时通讯’项目的流程简单描述一下。
1、页面先行
作为前端开发,没有页面怎么行,功能都是在静态页面的基础上添加的。
html:
<div class="wrapper">
<div class="container">
<div :class="'mask '+(user_list_bool?'active':'')" @click="user_list_bool=false"></div>
<div :class="'left '+(user_list_bool?'active':'')">
<div class="top">
<div class="">在线人数:<span id="numbers">{{userLength}}</span> 人
</div>
</div>
<ul class="people">
<li class="person flex_c_b" data-chat="person1" v-for="(item,index) in user_list" :key="index">
<div class="flex_c_s">
<img :src="item.headerimg" alt=""/>
<div>
<span class="name">{{item.username}}</span>
<span class="preview">在线</span>
</div>
</div>
<span class="time">{{item.login_time}}</span>
</li>
</ul>
</div>
<div class="right">
<div class="top flex_c_b">
<span>群名: <span class="name">又甘又刻</span></span>
<div class="chakang" @click="user_list_bool=true"><div class="numbers">{{userLength}}</div>在线</div>
</div>
<!-- <van-pull-refresh v-model="isLoading" @refresh="onRefresh"> -->
<div class="mesbox" ref="mesbox">
<div class="chat active-chat" data-chat="person1">
<van-loading type="spinner" size="18px" v-show="isLoading"/>
<div v-for="(item,index) in mes" :key="index" class="mg_t20">
<div v-if="item.type=='sys'">
<div class="conversation-start">
<span>{{item.msg}}</span>
</div>
</div>
<div v-else>
<div class="message">
<img :class="item.youMe=='me'?'me-header':''" :src="item.head" alt=""/>
<div :class="'bubble '+item.youMe" v-html="item.msg"></div>
</div>
</div>
</div>
</div>
</div>
<div class="write">
<a href="javascript:;" class="write-link attach"></a>
<input type="text" id="input-value" @keyup.enter="confirm()" v-model="inputValue"/>
<a href="javascript:;" class="write-link smiley"></a>
<a href="javascript:;" class="write-link send" @click="send()"></a>
</div>
</div>
</div>
</div>
css:
*, *:before, *:after {box-sizing: border-box;}html{height: 100%;}:root {--white: #fff;--black: #000;--bg: #f8f8f8;--grey: #999;--dark: #1a1a1a;--light: #e6e6e6;--wrapper: 100vw;--blue: #00b0ff;}body {background-color: var(--bg);-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-rendering: optimizeLegibility;font-family: 'Source Sans Pro', sans-serif;font-weight: 400;font-size: .16rem;background-image: url("../img/image.jpg");background-size: cover;background-repeat: none;height: 100%;}.wrapper {position: relative;width: var(--wrapper);height: 100%;}.container {position: relative;width: 100%;height: 100%;background-color: var(--white);}.container .left {position: absolute;left: -100%;width: 60%;max-width: 250px;height: 100%;border: 1px solid var(--light);background-color: var(--white);z-index: 3;-webkit-transition: ease .4s;-moz-transition: ease .4s;-o-transition: ease .4s;transition: ease .4s;}.container .left.active{left: 0%;}.mask{position: absolute;width: 100vw;height: 100%;left: 0;top: 0;z-index: 2;background-color: rgba(0, 0, 0, 0.3);display: none;}.mask.active{display: block;}.container .left .top {position: relative;width: 100%;height: 47px;padding: .29rem;font-size: 14px;}.container .left .top #numbers{font-size: 16px;}.container .left .top:after {position: absolute;bottom: 0;left: 50%;display: block;width: 90%;height: 1px;content: '';background-color: var(--light);-webkit-transform: translate(-50%, 0);transform: translate(-50%, 0);}.container .left input {float: left;width: 1.88rem;height: .42rem;padding: 0 .15rem;border: 1px solid var(--light);background-color: #eceff1;border-radius: .21rem;font-family: 'Source Sans Pro', sans-serif;font-weight: 400;}.container .left input:focus {outline: none;}.container .left a.search {display: block;float: left;width: .42rem;height: .42rem;margin-left: .1rem;border: 1px solid var(--light);background-color: var(--blue);background-image: url("../img//name-type.png");background-repeat: no-repeat;background-position: top .12rem left .14rem;border-radius: 50%;}.container .left .people {margin-left: -1px;border-right: 1px solid var(--light);border-left: 1px solid var(--light);width: calc(100% + .02rem);}.container .left .people .person {position: relative;width: 100%;padding: 10px 14px;cursor: pointer;background-color: var(--white);}.container .left .people .person:after {position: absolute;bottom: 0;left: 50%;display: block;width: 86%;height: 1px;content: '';background-color: var(--light);-webkit-transform: translate(-50%, 0);transform: translate(-50%, 0);}.container .left .people .person img {width: 40px;height: 40px;border-radius: 50%;border: 1px solid #eee;margin-right: 5px;}.container .left .people .person .name {font-size: 14px;line-height: .22rem;color: var(--dark);font-family: 'Source Sans Pro', sans-serif;font-weight: 600;}.container .left .people .person .time {font-size: 12px;color: var(--grey);background-color: var(--white);}.container .left .people .person .preview {font-size: 12px;color: green;display: block;margin-top: 3px;text-align: start;}.container .left .people .person.active, .container .left .people .person:hover {margin-top: -1px;margin-left: -1px;padding-top: .13rem;border: 0;background-color: var(--blue);width: calc(100% + .02rem);padding-left: calc(10% + 1px);}.container .left .people .person.active span, .container .left .people .person:hover span {color: var(--white);background: transparent;}.container .left .people .person.active:after, .container .left .people .person:hover:after {display: none;}.container .right {position: relative;float: left;width: 100%;height: 100%;}.container .right .top {width: 100%;height: 47px;padding: .15rem .29rem;background-color: #eceff1;font-size: 14px;}.container .right .top .chakang{display: flex;align-items: center;color: var(--blue);cursor: pointer;}.container .right .top span {color: var(--grey);}.container .right .top span .name {color: var(--dark);font-family: 'Source Sans Pro', sans-serif;font-weight: 600;}.container .right .mesbox{padding: 0 15px;height: calc(100% - 89px);overflow: auto;}.container .right .chat {position: relative;display: none;flex-direction: column;}.container .right .chat.active-chat {display: flex;text-align: left;}.container .right .chat .mg_t20:last-child{margin-bottom: 15px;}.container .right .write {position: absolute;bottom: 0;height: 42px;padding-left: 8px;border: 1px solid var(--light);background-color: #e8ebee;width: 100%;display: flex;align-items: center;padding: 0 10px;}.container .right .write input {font-size: 16px;width: 100%;height: 30px;margin: 5px 10px 5px 5px;padding: 0 10px;color: var(--dark);border: 0;outline: none;background-color: #f6f7f8;font-family: 'Source Sans Pro', sans-serif;font-weight: 400;border-radius: 4px;}.container .right .write .write-link.attach:before {display: inline-block;width: 20px;height: 42px;content: '';background-image: url("../img/attachment.png");background-repeat: no-repeat;background-position: center;}.container .right .write .write-link.smiley:before {display: flex;width: 20px;height: 42px;content: '';background-image: url("../img/smiley.png");background-repeat: no-repeat;background-position: center;}.container .right .write .write-link.send:before {display: flex;float: left;width: 20px;height: 42px;margin-left: 11px;content: '';background-image: url("../img/send.png");background-repeat: no-repeat;background-position: center;}.container .right .bubble {font-size: 16px;position: relative;display: inline-block;clear: both;padding: 10px 14px;vertical-align: top;border-radius: 5px;word-break: break-all }.container .right .bubble:before {position: absolute;top: 14px;display: block;width: 12px;height: 12px;content: '\00a0';-webkit-transform: rotate(29deg) skew(-35deg);transform: rotate(29deg) skew(-35deg);}.container .right .bubble.you {float: left;color: var(--dark);background-color: #eceff1;align-self: flex-start;-webkit-animation-name: slideFromLeft;animation-name: slideFromLeft;}.container .right .bubble.you:before {left: -4px;background-color: #eceff1;}.container .right .bubble.me {float: right;color: var(--white);background-color: var(--blue);align-self: flex-end;-webkit-animation-name: slideFromRight;animation-name: slideFromRight;}.container .right .bubble.me:before {right: -4px;background-color: var(--blue);}.container .right .conversation-start {position: relative;width: 100%;text-align: center;}.container .right .conversation-start span {font-size: 12px;display: inline-block;color: var(--grey);}.container .right .conversation-start span:before, .container .right .conversation-start span:after {position: absolute;top: 7px;display: inline-block;width: 20%;height: 1px;content: '';background-color: var(--light);}.container .right .conversation-start span:before {left: 0;}.container .right .conversation-start span:after {right: 0;}@keyframes slideFromLeft {0% {margin-left: -2rem;opacity: 0;}100% {margin-left: 0;opacity: 1;}}@-webkit-keyframes slideFromLeft {0% {margin-left: -2rem;opacity: 0;}100% {margin-left: 0;opacity: 1;}}@keyframes slideFromRight {0% {margin-right: -2rem;opacity: 0;}100% {margin-right: 0;opacity: 1;}}@-webkit-keyframes slideFromRight {0% {margin-right: -2rem;opacity: 0;}100% {margin-right: 0;opacity: 1;}}.message img {float: left;width: 40px;height: 40px;margin-right: 12px;border-radius: 50%;border: 1px solid #eee;}.you {margin-left: 60px;margin-top: -39px;}.me-header {float: right !important;margin-right: 0 !important;}.me {margin-right: 60px;margin-top: -39px;}.active-chat::-webkit-scrollbar, .left::-webkit-scrollbar {width: 2px;}
css里加载了一些图片,自己找几张图片替换了
2、请慢用js脱发剂
我先简单的说一下我的逻辑:由于没有登入这一步操作,就不能确认是谁发的消息,所以我就用ip当作用户名(虽然ip会变,当短时间不变,对短时间测试没有影响)。然后前端需要操作的数据就是区分信息是在左侧还是右侧显示以及历史信息回显,就是用v-if判断ip显示不同html就行了。
websocket关键代码
connect() {
// 创建一个 websocket 连接 ws://ip:端口号
this.ws = new WebSocket("ws://websockets.sinye.xyz/websocket");
// 连接状态 1已建立连接
// console.log(this.ws.readyState)
// 连接建立时触发
this.ws.onopen = ()=>{
this.onopen();
};
// 客户端接收服务端数据时触发
this.ws.onmessage = (event)=>{
this.onmessage(event)
};
// 连接关闭时触发
this.ws.onclose = ()=>{
this.onclose();
};
// 通信发生错误时触发
this.ws.onerror = ()=>{
this.onerror();
};
},
// 通信建立成功
onopen(){
var data = "系统消息:建立连接成功";
console.log(data);
},
// 接收客户端的数据,发送数据
onmessage(e){
var data = JSON.parse(e.data);
// console.log(data)
switch (data.type) {
case 'handShake':
//首次登录,发送登陆数据
var user_info = {'type': 'login', 'msg': this.uname, 'headerimg': this.headerimg};
this.sendMsg(user_info);
break;
case 'login':
this.userList(data.user_list);
this.systemMessage('系统消息: ' + data.msg + ' 已上线');
break;
case 'logout':
this.userList(data.user_list);
if (data.msg.length > 0) {
this.systemMessage('系统消息: ' + data.msg + ' 已下线');
}
break;
case 'user':
this.messageList1(data);
break;
case 'system':
this.systemMessage();
break;
}
},
// 关闭连接时触发
onclose(){
console.log("连接关闭,定时重连");
this.connect();
},
// websocket 错误事件
onerror(){
var data = "系统消息 : 出错了,请退出重试.";
console.log(data);
},
// 输入框输入后按回车,向服务器发送信息
confirm() {
this.send();
},
// 发送的数据先保存到数据库
send() {
let msg = this.inputValue.replace(/^[\s+]$/g, ' ')
var data = {type: "user", msg: msg, id: this.userid}
if(msg!=''){
this.$axios.post(
'/api/InfoSave/save', data
).then((response)=>{
if(response.data !== ''){
// 保存成功后再通过websocket群发
this.sendMsg(data);
this.inputValue = "";
}
})
}
},
// 群发数据
sendMsg(msg) {
var data = JSON.stringify(msg);
this.ws.send(data);
},
发送和接收就已经完成了,接下来就是回显历史信息
历史信息回显
// 追加数据 上下线的系统消息
systemMessage(msg) {
this.mes.push({type:'sys',msg:msg})
this.active_chat.scrollTop = this.active_chat.scrollHeight;
},
// 追加从服务端返回的数据 左侧在线人数列表
userList(user) {
this.user_list = []
for (var i = 0; i < user.length; i++) {
this.user_list.push({
headerimg: user[i].headerimg,
username: user[i].username,
login_time: user[i].login_time,
})
}
this.userLength = user.length
},
// 右侧聊天记录列表
messageList1(data) {
// var html
// 判读是不是自己发送的消息,对应的样式不同
if (data.ip == this.uname) {
// 如果当前用户名和feom的用户名相同,就说明时自己发送的消息
this.mes.push({type:'info', youMe:'me', msg:data.msg, head: data.headerimg})
} else {
// 别人发送的信息列表
this.mes.push({type:'info', youMe:'you', msg:data.msg, head: data.headerimg})
}
this.$nextTick(() =>{
this.active_chat.scrollTop = this.active_chat.scrollHeight;
})
},
// 右侧聊天记录列表
messageList2(data,bool) {
// var html
// 判读是不是自己发送的消息,对应的样式不同
if (data.ip == this.uname) {
this.mes.splice(0,0,{type:'info', youMe:'me', msg:data.msg, head: data.headerimg})
} else {
this.mes.splice(0,0,{type:'info', youMe:'you', msg:data.msg, head: data.headerimg})
}
if(bool){
this.$nextTick(() =>{
this.active_chat.scrollTop = this.active_chat.scrollHeight;
})
}
else{
this.$nextTick(() =>{
this.active_chat.scrollTop = this.active_chat.scrollHeight - this.oldH
})
}
},
// 获取历史信息ajax
onRefresh(bool) {
this.isLoading = true;
this.page++;
this.$axios.post(
'/api/InfoSave/get', {page: this.page}
).then((response)=>{
if(response.data){
for (let i = 0; i < response.data.length; i++) {
const data = response.data[i];
this.messageList2(data,bool)
}
this.isLoading = false;
}else{
this.page--
this.isLoading = false;
}
})
},
// 监听滚动
handleScroll () {
if (this.active_chat.scrollTop <= 0) {
this.oldH = JSON.parse(JSON.stringify(this.active_chat.scrollHeight))
this.onRefresh(false)
}
},
// 获取ip并连接服务器
login(){
this.$axios.get(
'/api/login'
).then((response)=>{
var data = response.data
this.uname = data.ip
this.headerimg = data.headerimg
this.userid = data.id
if(this.uname!=''){
this.connect()
this.onRefresh(true)
}else{
console.log('连接失败!')
}
})
},
初始化还是必须的
mounted(){
this.login()
this.$nextTick(() =>{
this.active_chat = document.querySelector('.mesbox')
this.$refs.mesbox.addEventListener('scroll', this.handleScroll)
})
},
data() {
return {
userid: '',//用户id
ws: {},
uname: '',//用户名
headerimg: '',//用户头像
mes:[],//消息
inputValue: '',//输入的信息
active_chat: '',//信息dome
user_list: [],//在线用户列表
user_list_bool: false,//在线用户列表弹窗是否显示
userLength: '',//在线人数
page: 0,
isLoading: false,
oldH: '',//原来信息高度
}
},