为了实现AI对话的打字效果,我封装一个vue3自定义指令
前言随着 DeepSeek 的火爆,公司前段时间也接了一个有关 AI 的项目。其中有一个人机交付对话框页面。逻辑也挺简单,通过 websocket 建立长连接。后端调用 AI 接口给前端推消息。
需求产品希望对话的效果能像市面的 AI 产品一样呈现出一个个出的那种「打字效果」!
技术实现1.CSS + 动画// css
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
.typing-effect {
overflow: hidden;
white-space: nowrap;
animation: typing 6s steps(50, end);
}
//html
div class="typing-effect"
日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。
/div
效果效果虽然出来了,但是存在一个比较大的弊端:「无法多行呈现」只要文本较多,超过了一行的情况就出现文字展示不全的情况。
2.js + 定时器function typeWriter(elementId, message, speed) {
const element = document.getElementById(elementId);
let i = 0; // 当前字符索引
const interval = setInterval(() = {
if (i message.length) {
element.textContent += message.charAt(i); // 逐字符添加文本
i++;
} else {
clearInterval(interval); // 完成打字后停止间隔调用
}
}, speed); // 控制打字速度,例如50毫秒/字符
}
// 使用函数
typeWriter('typewriter', '日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。', 100); // 100毫秒/字符速度
效果与上面是一样的,解决了多行展示。但是熟悉 JS 定时器的小伙伴们都知道,定时器做动画有时候会出现卡顿。所以还得优化一下(把定时器替换成动画帧)。
3. 优化后function typeWriter(elementId, message, speed) {
const element = document.getElementById(elementId);
let i = 0;
let startTime = null;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
if (currentTime - startTime = i * speed) {
if (i message.length) {
element.textContent += message.charAt(i);
i++;
requestAnimationFrame(animate);
}
} else {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// 使用示例:
typeWriter('typewriter', '日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。', 100);
经过一系列优化,效果也达到了,但是用 JS 来做的话页面的 dom 频繁的在改变。如果对话比较多对性能感觉不那么友好。有什么办法能既不频繁的对 dom 进行修改又能多行展示呢?
经过一系列思考,还是回到第一步吧! 用 css 的动画~
但是第一步的弊端已经说过了,就是如果文字是多行就失效了。那此时我们就想到了一个办法,把文字截取变为 2 行呢?或者 3 行?然后每一行的动画执行完再执行下一行呢?
于是乎要写 2 个方法:根据 div 的宽度计算一行能显示多少个文字
function calculateCharactersPerLine(divElement, fontSize, text) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${fontSize}px sans-serif`; // 设置字体大小和样式
const metrics = context.measureText(text);
const textWidth = metrics.width;
const divWidth = divElement.offsetWidth; // 获取div的宽度
// 计算一行可以容纳多少个字符
const charactersPerLine = Math.floor(divWidth / textWidth) * text.length;
if (charactersPerLine == 0) {
return calculateCharactersPerLine(divElement, fontSize, text.slice(0, -1))
}
return charactersPerLine;
}
calculateCharactersPerLine('id',16,'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。')
根据 calculateCharactersPerLine 方法的返回值对整个文本进行切片
function splitIntoChunks(str, chunkSize) {
const regexPattern = new RegExp(`.{1,${chunkSize}}`, 'g');
return str.match(regexPattern);
}
整体思路就是假如 div 宽度是 100px, 那么可能整个文本会显示 3 行,那么切片出来就是 3 段文字,然后把这 3 段文字创建一个 div 包裹,每个 div 身上再定义动画就行了
「直接用 vue3 指令封装一下吧~ 完整代码如下」:
export function typewriter(app) {
app.directive('typewriter', (el, binding) = {
console.log(binding)
if (binding.oldValue) return;
renderText(el,binding)
}
function renderText(el, binding) {
const arg = binding.arg || 1
const style = document.createElement('style');
style.textContent = `
@keyframes width {
0% {
width: 0;
}
100% {
width: 100%;
}
}
document.head.appendChild(style);
const textLen = calculateCharactersPerLine(el, 16, binding.value);
const divList = splitIntoChunks(binding.value, textLen)
divList.forEach((row, index) = {
const oDiv = document.createElement('div');
oDiv.innerText = row
oDiv.style.cssText = `
position: relative;
overflow: hidden;
width: 0;
white-space: nowrap;
animation: width ${arg}s steps(50) forwards;
animation-delay: ${index*arg}s
el.appendChild(oDiv)
})
}
//计算一行的文字字数
function calculateCharactersPerLine(divElement, fontSize, text) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${fontSize}px sans-serif`; // 设置字体大小和样式
const metrics = context.measureText(text);
const textWidth = metrics.width;
const divWidth = divElement.offsetWidth; // 获取div的宽度
// 计算一行可以容纳多少个字符
const charactersPerLine = Math.floor(divWidth / textWidth) * text.length;
if (charactersPerLine == 0) {
return calculateCharactersPerLine(divElement, fontSize, text.slice(0, -1))
}
return charactersPerLine;
}
//切片
function splitIntoChunks(str, chunkSize) {
const regexPattern = new RegExp(`.{1,${chunkSize}}`, 'g');
return str.match(regexPattern);
}
template
div v-typewriter:[2]="`回环(文)诗、剥皮诗、离合诗、宝塔诗、字谜诗、辘轳诗、八音歌诗、藏头诗、打油诗、诙谐诗、集句诗、联句诗、百年诗、嵌字句首诗、绝弦体诗、神智体诗等40多种。这些杂体诗各有特点,虽然均有游戏色彩,但有些则具有一定的思想性和艺术性,所以深受人们的喜爱`"/div
/template
效果总结:好多人会误以为把简单的问题在复杂化。其实写代码就是各种思路的碰撞,如果单纯从实现功能的角度出发,用什么方法都行!怎么样兄弟们,如果你们项目中也有这种需求。你会怎么做?欢迎留言讨论~
AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding
点击"阅读原文"了解详情~
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线