我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛
做了一个兔子家族的聊天室,欢迎大家来聊天,本打算用shareWorker来实现跨页面多个聊天室的通信,可惜码上掘金不能创建文件,只能用localStorage来实现了;
先来体验一下效果,稍后讲解实现:
这里两个消息是可以同步的,不是多放了呀!!!
布局
这里的布局非常简单,都是通过flex来实现的,先看代码:
<div class="chat-container">
<!-- 头部群聊名称显示 -->
<div class="chat-header">
兔年聊到🐰
</div>
<!-- 聊天内容显示 -->
<div class="chat-wrap">
<!-- 暂时忽略 -->
</div>
<!-- 聊天输入框 -->
<div class="input-box">
<input class="input"/>
<button class="send-btn">发送</button>
</div>
</div>
布局非常简单,就只有一个容器,里面有三个部分,头部,聊天内容,聊天输入框;
.chat-container {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100vh;
width: 100%;
}
.chat-header {
height: 38px;
width: 100%;
text-align: center;
line-height: 38px;
font-size: 18px;
font-weight: 700;
border-bottom: 1px solid #ccc;
flex-shrink: 0;
}
.chat-wrap {
padding: 20px;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.input-box {
display: flex;
justify-content: space-between;
align-items: center;
height: 50px;
padding: 20px;
border-top: 1px solid #ccc;
flex-shrink: 0;
}
这里外层容器使用flex布局,flex-direction设置为column,这样就可以让头部,聊天内容,聊天输入框三个部分分别在容器的上中下三个位置;
聊天内容部分使用flex: 1,这样就可以让聊天内容部分占满剩余的空间,头部和输入框部分使用flex-shrink: 0,这样就可以让头部和输入框部分不会被压缩;
聊天内容
这里的重点样式就是聊天内容的样式,这里聊天内容分为两个部分,头像和聊天气泡;
<div class="message-box">
<div class="avatar">
<svg>
<use xlink:href="#id"></use>
</svg>
</div>
<div class="message">
message
<svg class="decorate-icon">
<use xlink:href="#id"></use>
</svg>
</div>
</div>
先来看看这一块的样式的实现:
/* 消息容器 */
.message-box {
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin-bottom: 20px;
}
/* 头像 */
.message-box .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid #ccc;
overflow: hidden;
margin-right: 10px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
.message-box .avatar svg {
font-size: 32px;
width: 32px;
height: 32px;
}
/* 消息内容部分 */
.message-box .message {
position: relative;
padding: 10px;
padding-right: 20px;
border-radius: 4px;
background-color: #fff;
word-break: break-word;
max-width: 300px;
}
/* 消息内容的小三角 */
.message-box .message:after {
content: '';
position: absolute;
top: 20px;
left: -10px;
margin-top: -5px;
width: 0;
height: 0;
border: 5px solid transparent;
border-right: 5px solid #fff;
}
/* 消息内容的装饰图标 */
.decorate-icon {
position: absolute;
right: 0;
bottom: 0;
width: 42px;
height: 42px;
transform: translate(50%, 4px);
transition: all 1s;
}
这里的样式实现也非常简单,就是使用flex布局,头像和消息内容部分分别在容器的左边和右边;
消息内容部分使用定位来实现小三角的效果,这里的小三角是使用border来实现的,这里的border是使用transparent来实现的,这样就可以让小三角的颜色和背景颜色一致;
装饰
这里为了突出“兔”这个主题,我这里图像和装饰的图标都是使用兔子的图标,所有的图标都是使用svg来实现的;
我们可以通过<use>标签来使用svg中的图标,这里的xlink:href属性就是用来指定图标的,这里的id就是图标的id;
<!-- 存在的图标 -->
<svg id="svg1">
<!-- 省略 -->
</svg>
<!-- 使用 -->
<svg>
<use xlink:href="#svg1"></use>
</svg>
可以看到上面的示例,通过<use>标签来使用svg,这里的xlink:href属性就是用来指定图标的,属性值就是图标的id,记得需要加上#;
然后我们还需要将svg图标放到页面中,我们通过head标签天然不显示的特性来实现,这样就可以将svg图标放到页面中,但是不会显示出来;
const svg = [
'<svg id="svg1">...</svg',
'<svg id="svg2">...</svg',
'<svg id="svg3">...</svg',
];
svg.forEach(item => {
const svg = new DOMParser().parseFromString(item, 'image/svg+xml')
document.head.appendChild(svg.rootElement)
})
因为是字符串的形式,所以我们需要使用DOMParser来解析字符串,然后将解析后的svg图标放到head标签中;
解析出来的svg图标是单独的一个document,所以我们需要使用rootElement来获取svg图标,否则会报错;
经过这样的处理,我们就可以在页面中使用svg图标了;
动画
样式的问题是解决了,但是页面干巴巴的一点意思都没有,那就随便加点动画吧;
我这里使用的vue,那就直接使用transition来实现动画吧;
<transition-group name="roll" mode="in-out">
<div
class="message-box"
v-for="(message, index) in messages"
:key="index"
>
<div class="avatar">
<svg>
<use :xlink:href="message.avatar"></use>
</svg>
</div>
<div class="message">
{{ message.message }}
<svg class="decorate-icon">
<use :xlink:href="message.avatar"></use>
</svg>
</div>
</div>
</transition-group>
因为这里是一个列表,所以需要使用transition-group来实现动画,这里的name属性就是动画的名称,这里的mode属性就是动画的模式,这里的in-out就是先进入,后退出;
然后我们需要在css中实现动画;
.roll-enter-active {
transition: all 1s;
}
.roll-enter-to {
transform: translateX(0);
}
.roll-enter-from {
transform: translateX(-100%);
}
这样就实现了一个非常简单的动画了;
对于
vue的transition不熟悉可以看看我的这一篇文章:用Transition组件犯迷糊?看我这篇给你安排的明明白白的;
注意:贴出来的动画代码是
Vue3版本的,码上掘金的代码版本是Vue2版本的,在名称上有点区别;
右侧的消息
上面实现的是左侧的消息,那右侧的消息就是一个镜像的过程,我们只需要修改一点点样式就好了;
.message-box.right {
flex-direction: row-reverse;
}
.message-box.right .avatar {
margin-left: 10px;
margin-right: 0;
}
.message-box.right .message {
background-color: #00b7ff;
color: #fff;
padding: 10px;
padding-left: 20px;
}
.message-box.right .message:after {
left: auto;
right: -4px;
border-right: none;
border-left: 5px solid #00b7ff;
}
.message-box.right .decorate-icon {
left: 0;
transform: translate(-50%, 4px);
}
/* 动画 */
.roll-enter-to.right {
transform: translateX(0%);
}
.roll-enter-from.right {
transform: translateX(50%);
}
右侧先直接用flex-direction: row-reverse来实现镜像,然后其他的细节位置需要重新修改,例如小三角和装饰的图标;
这里的动画也是一个镜像的过程,只需要修改translateX的值就好了,这里使用的是50%,因为这样效果会更好一点;
可以看到我这里为了区分右侧和左侧的消息,我给每个消息都添加了一个right的class,这样就可以区分了;
发送消息
消息区域也是非常的简单,就是一个输入框和一个按钮;
<div class="input-box" @keyup.enter="handleSendMsg">
<input class="input" v-model="message"/>
<button class="send-btn" @click="handleSendMsg">发送</button>
</div>
这里主要还是样式,样式就随随便便写一下吧;
.input-box {
display: flex;
justify-content: space-between;
align-items: center;
height: 50px;
padding: 20px;
border-top: 1px solid #ccc;
flex-shrink: 0;
}
.input-box .input {
flex: 1;
height: 100%;
padding: 0 10px;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
}
.input-box .send-btn {
width: 80px;
height: 100%;
margin-left: 10px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #fff;
outline: none;
cursor: pointer;
}
可以看到我上面已经加上了事件监听,为了用户体验,还加上了回车发送的功能;
发送的功能也是非常简单,就是把输入框的值添加到消息列表中,然后清空输入框;
function handleSendMsg() {
if (!this.message) return;
// 页面所有的头像ID
const avatars = [
'#rabbit_01',
'#rabbit_02',
'#rabbit_03',
'#rabbit_04',
'#rabbit_05',
'#rabbit_06',
];
// 随机获取一个头像
const avatar = avatars[Math.floor(Math.random() * avatars.length)];
// 组装消息内容
const message = {
avatar,
message: this.message,
direction: 'right',
};
// 添加到消息列表
this.messages.push(message)
// 清空输入框
this.message = ''
// 滚动到底部
this.$nextTick(() => {
const chatWrap = document.querySelector('.chat-wrap')
chatWrap.scrollTop = chatWrap.scrollHeight
})
}
这一块也是非常简单的,我这里每次发送消息都会随机获取一个头像,然后把消息添加到消息列表中,然后清空输入框;
最后还有一个滚动到底部的功能,这里我使用了$nextTick,因为这个方法是在下一次DOM更新循环结束之后执行的,所以这里可以保证DOM已经更新完毕;
消息同步
在我最开始展示的示例中, 我引入两个代码片段,它们发送消息时是可以互相同步的,这一块是怎么实现的呢?
我最开始是想使用SharedWorker来实现的,但是SharedWorker是需要单独弄一个文件的,掘金的码上掘金虽然也可以引入外部的文件,但是我可没有线上的环境;
所以退而求其次,我这里使用了localStorage来实现的,这里我使用了localStorage的storage事件,当storage事件触发时,就会执行回调函数,这里我就可以获取到其他页面发送的消息;
function handleReceiveMsg() {
window.addEventListener('storage', (e) => {
if (e.key === 'message') {
const message = JSON.parse(e.newValue)
this.messages.push({
avatar: message.avatar,
message: message.message,
direction: 'left',
})
this.$nextTick(() => {
const chatWrap = document.querySelector('.chat-wrap')
chatWrap.scrollTop = chatWrap.scrollHeight
})
}
})
}
这里我监听了storage事件,当storage事件触发时,我就会获取到其他页面发送的消息,然后把消息添加到消息列表中,最后滚动到底部;
这里注意的是,收到消息需要将消息的方向设置为left,因为这里是接收到的消息;
然后在发送消息的时候,我就会把消息存储到localStorage中,这样其他页面就可以收到消息了;
function handleSendMsg() {
// ...
const message = {
avatar,
message: this.message,
direction: 'right',
};
this.messages.push(message)
this.message = ''
// 存储消息到 localStorage
localStorage.setItem('message', JSON.stringify(message))
// ...
}
这样我们就实现了消息的同步,这里只是简单的讲解了这个小聊天室的实现,如果感兴趣的话,可以直接看码上掘金的源码,这次就到这里了。