兔年到,一起来看看兔子家族都在聊些什么吧

1,567 阅读7分钟

我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

做了一个兔子家族的聊天室,欢迎大家来聊天,本打算用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%);
}

这样就实现了一个非常简单的动画了;

对于vuetransition不熟悉可以看看我的这一篇文章:用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%,因为这样效果会更好一点;

可以看到我这里为了区分右侧和左侧的消息,我给每个消息都添加了一个rightclass,这样就可以区分了;

发送消息

消息区域也是非常的简单,就是一个输入框和一个按钮;

<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来实现的,这里我使用了localStoragestorage事件,当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))
    
    // ...
}

这样我们就实现了消息的同步,这里只是简单的讲解了这个小聊天室的实现,如果感兴趣的话,可以直接看码上掘金的源码,这次就到这里了。