

编写界面
1. HTML 部分
上边有一个标题栏,显示与 XXX 聊天。 中间是聊天信息面板,包含着双方发送的消息,每条消息由发送者头像和消息内容组成,我发送的在右侧,对方发送的在左侧。 下方是底部信息,有表情选择按钮、编辑消息文本框和发送按钮。
<main><div class="chat"><div class="titleBar">与 XXX 聊天</div><div class="panel"><div class="message mine"><img src="./me.png" alt="" /><p><span>你好</span></p></div><div class="message yours"><img class="avatar" src="./you.png" alt="" /><p><span>Hi</span></p></div><!-- 省略其它消息 --></div><footer><button class="chooseSticker"><img src="./emoji.svg" alt="" /><div class="stickers"></div></button><inputclass="messageInput"type="text"name=""id=""placeholder="请输入聊天信息"/><button class="send">发送</button></footer></div></main>
<main />元素是一个整体的容器,用于把聊天窗口居中对齐<div class="chat">是聊天应用的容器,用于布局标题栏、聊天面板和底部发送框。<div class="titleBar">用于显示标题栏。<div class="panel">是消息面板,用于布局其中的消息。<div class="message">为消息容器,使用不同的 class 来区分发送方,mine代表我发送的,yours代表对方发送的。每条消息里边使用<img class="avatar" >来展示头像,使用<p>元素来显示文本,<p>元素里边的<span>元素将会作为 lottie 的容器来播放表情动画。<footer>用于布局底部操作按钮和消息发送框。其中:
<button class="chooseSticker">是表情选择按钮,使用一个笑脸 svg 图片表示,里边的<div class="stickers">是表情选择框弹出层,里边的表情将在 JS 中动态加载,目的是为了实现动画预览。<input class="messageInput" />是聊天消息输入框,没什么特别的。<button class="send">是发送按钮
2. CSS 部分
<link rel="stylesheet" href="style.css" />2.1 全局样式
:root {--primary-color: hsl(200, 100%, 48%);--inverse-color: hsl(310, 90%, 60%);--shadow-large: 0 0px 24px hsl(0, 0%, 0%, 0.2);--shadow-medium: 0 0 12px hsl(0, 0%, 0%, 0.1);}
--primary-color: hsl(200, 100%, 48%),主色调,例如我发送的消息的蓝色背景。--inverse-color: hsl(310, 90%, 60%),反色调,或强调色调,与主色调形成鲜明对比,例如发送按钮的背景色。--shadow-large: 0 0px 24px hsl(0, 0%, 0%, 0.2),大阴影,例如标题栏、底部栏的阴影。--shadow-medium: 0 0 12px hsl(0, 0%, 0%, 0.1),小阴影,例如输入框和表情选择弹出层。
* {box-sizing: border-box;padding: 0;margin: 0;font-family: Helvetica, "PingFang SC", "Microsoft Yahei", sans-serif;}
border-box ,这样内边距、边框都算在宽高之内,设置内间距和外间距为 0,最后设置默认字体。main {display: grid;place-items: center;width: 100vw;height: 100vh;background-color: hsl(0, 0%, 10%);}
.chat {width: 375px;height: 700px;background: hsl(0, 0%, 100%);border-radius: 8px;display: grid;grid-template-rows: max-content 1fr max-content;}
grid-template-rows 把聊天应用分成了 3 行,第一行的标题栏和最后一行的标底部操作栏的高度分别为内容的最大高度,中间的聊天面板则是浮动高度。2.4 标题栏
.titleBar {padding: 24px 0;text-align: center;box-shadow: var(--shadow-large);}
界面优化提示:内间距用来增加留白,在视觉上引起放松,阴影则为了和下边的聊天面板区分开
2.5 聊天面板
.panel {display: flex;flex-direction: column;padding: 24px 12px;overflow: auto;}
界面优化提示:这里的 padding 同样是为了留出足够多的空白,来与其它元素隔开一段距离,以避免拥挤感。
2.6 消息
.message {display: flex;max-width: 80%;font-size: 14px;margin: 8px 0;position: relative;}
position 设置为 relative 是为了定位后边的全屏特效动画。.message img {width: 40px;height: 40px;border-radius: 12px;margin-right: 12px;}
界面优化提示:这里不得不再提一下间距的重要性,一定不要把各个元素安排的太过紧凑,否则十分影响视觉效果,最直接的影响就是引起视觉上的拥挤感,造成视觉疲劳。
.message p {padding: 8px 12px;border-radius: 12px;box-shadow: var(--shadow-large);display: flex;align-items: center;}
.message.mine {flex-flow: row-reverse;align-self: flex-end;}
.message.mine img {margin-right: 0;margin-left: 12px;}
.message.mine p {background-color: var(--primary-color);color: white;}
footer {display: grid;grid-template-columns: 48px 1fr 75px;justify-items: center;padding: 12px;box-shadow: var(--shadow-large);}
.chooseSticker {justify-self: start;position: relative;}.chooseSticker img {width: 36px;height: 36px;}
.stickers {display: grid;grid-template-columns: repeat(auto-fill, 24px);column-gap: 18px;border-radius: 8px;background-color: white;box-shadow: var(--shadow-medium);padding: 6px 12px;font-size: 24px;position: absolute;top: calc(-100% - 18px);width: 300px;opacity: 0;}
弹出层使用 grid 布局,repeat(auto-fill, 24px) 指的是在宽度允许的条件下,在一行中尽可能放置最多的列元素,每列的宽度固定为 24px。然后设置列间距为 18px。 设置圆角、背景色、阴影、内间距和字体大小。 定位设置为绝对定位,把它向上移动包含元素高度(也就是 .chooseSticker 的高度)的 100% 并减去 18px,调整到合适的位置。宽度设置为 300px,透明度设置为 0 把它隐藏。
justify-self: end 把自己进行靠右对齐。这里把代码一次性贴出来:.messageInput {box-shadow: var(--shadow-medium);padding: 0px 12px;border-radius: 8px;width: 100%;}.send {height: 100%;width: 90%;border-radius: 8px;justify-self: end;color: white;background-color: var(--inverse-color);}
.show 样式,用于在点击发送表情按钮时,给表情弹出层添加该样式以显示出来:.show {opacity: 1;}
<body><!-- 其它代码省略 --><script src="index.js"></script></body>
const panelEle = document.querySelector(".panel");const chooseStickerBtn = document.querySelector(".chooseSticker");const stickersEle = document.querySelector(".stickers");const msgInputEle = document.querySelector(".messageInput");const sendBtn = document.querySelector(".send");
panelEle是消息面板元素,用于追加新消息。chooseStickerBtn是选择表情按钮,点击它会弹出表情选择框。stickersEle就是弹出的表情选择框。msgInputEle是消息输入框。sendBtn为发送按钮。
<script src="lottie.min.js"></script>南瓜表情:https://lottiefiles.com/43215-pumpkins-sticker-4 炸弹表情:https://lottiefiles.com/3145-bomb 爆炸动画:https://lottiefiles.com/9990-explosion

发送普通消息
sendBtn.addEventListener("click", () => {const msg = msgInputEle.value;if (msg) {appendMsg(msg);}});
判断用户是否输入了消息。 如果输入了就追加到消息列表中。
appendMsg() 函数的代码:function appendMsg(msg, type) {// 创建消息元素const msgEle = panelEle.appendChild(document.createElement("div"));msgEle.classList.add("message", "mine"); // 设置为“我“发送的样式msgEle.innerHTML = `<img class="avatar" src="./me.png" alt="" /><p><span>${msg}</span></p>`;// 滚动到最新消息panelEle.scrollTop = panelEle.scrollHeight;msgInputEle.value = "";}
按照消息的 HTML 结构创建一个新的消息元素 msgEle,并追加到消息列表中。 把消息的样式设置为我发送的。 内部的元素分别为头像和文本消息,使用模板字符串的形式赋值给 msgEle 的 innerHTML 属性中,并在 <p>中使用 msg 变量的值。最后把滚动条滚动到最新的消息处,并清空输入框中的消息。

发送动画表情
const stickers = {bomb: {path: "./3145-bomb.json",},pumpkin: {path: "./43215-pumpkins-sticker-4.json",},};
bomb 、 pumkin 这样的 key 来找到对应的动画路径。接着初始化弹出层中的表情以供用户选择:// 初始化表情面板,也可以在表情选择窗弹出时再初始化Object.keys(stickers).forEach((key) => {const lottieEle = stickersEle.appendChild(document.createElement("span"));// 对每个表情创建 lottie 播放器const player = lottie.loadAnimation({container: lottieEle,renderer: "svg",loop: true,autoplay: false,path: stickers[key].path,});// 当选择表情时,发送消息,并设置类型为 sticker 表情消息lottieEle.addEventListener("click", () => {appendMsg(key, "sticker");});// 当鼠标划过时,播放动画预览lottieEle.addEventListener("mouseover", () => {player.play();});// 当鼠标划过时,停止动画预览lottieEle.addEventListener("mouseleave", () => {player.stop();});});
遍历存储表情信息的对象。 创建一个 lottie 的容器,使用 span 元素,因为 lottie 动画的播放器需要挂载到一个具体的 html 元素中。 调用 lottie 的 loadAnimation() 加载动画,它需要传递这样几个参数:
container: 播放器要挂载到的容器。 renderer:可以选择是使用 svg 还是 canvas 渲染动画。 loop: 是否循环播放,由于此处是在表情选择弹出层中预览动画,所以支持循环播放。 autoplay:是否自动播放,这里设置为了否,后边让它在鼠标划过时再播放动画。 path:动画 json 文件路径,直接从对象中获取。
loadAnimation() 会返回 lottie 的实例,把它保存在 player 中。
当 lottieEle 也就是表情被点击时,发送表情消息,给 appendMsg() 的 msg 参数设置为表情的 key,type 参数设置为 "sticker"。 当鼠标划过表情时,开始播放动画。 当鼠标划出表情时,停止动画。
chooseStickerBtn.addEventListener("click", () => {stickersEle.classList.toggle("show");});
<p> 元素里添加任何信息:function appendMsg(msg, type) {// ...msgEle.innerHTML = `<img class="avatar" src="./me.png" alt="" /><p><span>${type === "sticker" ? "" : msg}</span></p>`;}
// 处理表情消息,播放相关动画if (type === "sticker") {playSticker(msg, msgEle);}
function playSticker(key, msgEle) {// 表情消息,创建 lottie 动画const lottieEle = msgEle.querySelector("span");lottieEle.style.width = "40px";lottieEle.style.height = "40px";lottie.loadAnimation({container: lottieEle,renderer: "svg",loop: false,autoplay: true,path: stickers[key].path,});}
获取消息中的 span 元素,它将作为 lottie 的动画容器。 设置表情动画的宽高为 40px。 使用 lottie 加载动画,并设置循环播放为 false,自动播放为 true,来让表情发送时自动播放动画,且只播放一次。

function appendMsg(msg, type) {if (type === "sticker") {playSticker(msg, msgEle);if (msg === "bomb") {// 播放爆炸动画setTimeout(() => {playExplosion(msgEle);}, 800);// 晃动消息列表shakeMessages();}}}
function playExplosion(anchor) {const explosionAnimeEle = anchor.appendChild(document.createElement("div"));explosionAnimeEle.style.position = "absolute";explosionAnimeEle.style.width = "200px";explosionAnimeEle.style.height = "100px";explosionAnimeEle.style.right = 0;explosionAnimeEle.style.bottom = 0;const explosionPlayer = lottie.loadAnimation({container: explosionAnimeEle,renderer: "svg",loop: false,autoplay: true,path: "./9990-explosion.json",});explosionPlayer.setSpeed(0.3);// 播放完成后,销毁爆炸相关的动画和元素explosionPlayer.addEventListener("complete", () => {explosionPlayer.destroy();explosionAnimeEle.remove();});}
添加全屏动画元素,设置为绝对定位,宽度 200px,高度 100px,放在最新消息元素的右下角。 加载 lottie 动画,不循环、自动播放。 由于原动画速度过快,这里调用 lottie 实例的 setSpeed() 方法,把速度设置为 0.3 倍速。 之后给 lottie 实例设置事件监听:"complete",它会在动画执行完成时触发,里边销毁了 lottie 实例和全屏动画元素。
function shakeMessages() {[...panelEle.children].reverse().slice(0, 5).forEach((messageEle) => {const avatarEle = messageEle.querySelector("img");const msgContentEle = messageEle.querySelector("p");avatarEle.classList.remove("shake");msgContentEle.classList.remove("shake");setTimeout(() => {avatarEle.classList.add("shake");msgContentEle.classList.add("shake");}, 700);});}
使用 reverse() 和 slice() 对最新的 5 条消息进行晃动,也可以把 5 改大一点,对更多消息进行晃动。 然后在循环中,分别给头像和消息添加 shake class 执行晃动动画,这个 class 的内容稍后再介绍。 要注意的是,在添加 shake class执行动画前,需要先删除 shake,因为有的消息可能在之前已经晃动过了,例如当连续发了多个炸弹表情时。后边在添加 shake class 时,使用 setTimeout() 延迟了 700 毫秒,目的是在全屏动画执行到一定程度时再晃动消息。
.shake {animation: shake 0.8s ease-in-out;}@keyframes shake {from {transform: translate3d(0, 0px, 0px);}10% {transform: translate3d(6px, -6px, 0px);}20% {transform: translate3d(-5px, 5px, 0px);}30% {transform: translate3d(4px, -4px, 0px);}35% {transform: translate3d(-3px, 3px, 0px);}39% {transform: translate3d(2px, -2px, 0px);}41% {transform: translate3d(-1px, 1px, 0px);}42% {transform: translate3d(0px, 0px, 0px) rotate(20deg);}52% {transform: rotate(-15deg);}60% {transform: rotate(8deg);}65% {transform: rotate(-3deg);}67% {transform: rotate(1deg);}70% {transform: rotate(0deg);}to {transform: translate3d(0px, 0px, 0px) rotate(0);}}
.shake 中使用了 shake keyframes 定义的动画,执行时间为 0.8s,动画执行函数为 ease-in-out。Keyframes 里的代码比较多,但是都是很简单的,就是模拟了爆炸时的效果,移动 x 轴和 y 轴的偏移,每次的偏移幅度越来越小,并且越来越快,可以看到百分比的间隔越来越小。在动画进行到 42% 的时候,加了一些旋转动画,这样就有了落地时的震动效果。由于使用 rotate() 旋转时的轴心在元素中间,我们可以把消息气泡的轴心修改一下来实现更真实的效果:.message p {transform-origin: left bottom;}.message.mine p {transform-origin: right bottom;}

总结
使用 lottie 库加载并播放动画。 确定全屏动画的位置和播放时机。 消息晃动动画的 CSS 实现。
作者:峰华,简介:前端工程师,以直观、专业的方式分享编程知识。Bilibili UP@峰华前端工程师
示例地址:https://codechina.csdn.net/mirrors/zxuqian/html-css-examples 代码地址:https://codechina.csdn.net/mirrors/zxuqian/html-css-examples/-/tree/master/31-05-wechat-emoji-effect lottie: https://cdnjs.com/libraries/bodymovin ,下载 lottie.min.js 南瓜表情:https://lottiefiles.com/43215-pumpkins-sticker-4 炸弹表情:https://lottiefiles.com/3145-bomb 爆炸动画:https://lottiefiles.com/9990-explosion Lottie 官网:https://airbnb.io/lottie

手机扫码打开
