☰
Ant Design Vue实现打字机字符回退效果
2026/10/28 19:33:16 网站建设 项目流程

1. 项目概述:Ant Design Vue 实现字符回退效果

最近在开发一个需要动态展示文本输入过程的交互界面时,遇到了一个有趣的需求:如何实现类似打字机效果的字符回退功能。这个效果在密码输入提示、代码演示、教育类应用中特别有用。作为长期使用 Ant Design Vue 的开发者,我决定基于这个优秀的 UI 框架来实现这个功能。

Ant Design Vue 作为 Ant Design 的 Vue 实现版本,提供了丰富的组件和设计规范,但在动画效果方面需要开发者自己实现一些创意功能。字符回退效果就是这样一个典型的自定义交互需求,它需要在文本逐字显示后,再逐字删除,形成一种"输入-回退"的循环效果。

2. 核心需求解析与技术选型

2.1 效果拆解与需求分析

真正的字符回退效果不仅仅是简单的显示和隐藏,它需要实现以下几个关键点:

  1. 逐字显示:文本从左到右逐个字符显示,模拟打字输入
  2. 停顿保持:完整显示后保持一段时间,让用户看清内容
  3. 逐字删除:从最后一个字符开始向前逐个删除,模拟回退
  4. 循环播放:完成一次显示-回退后,自动开始下一次循环
  5. 可配置速度:能够控制打字和回退的速度

2.2 技术实现方案对比

在 Vue 生态中,实现这种动画效果有几种常见方案:

  1. CSS 动画:通过 @keyframes 和 transition 实现,但控制粒度较粗
  2. Web Animation API:功能强大但兼容性需要考虑
  3. JavaScript 定时器:setInterval/setTimeout 实现,控制精确但代码复杂
  4. 第三方动画库:如 anime.js、GSAP 等,功能丰富但增加体积

考虑到项目已经使用了 Ant Design Vue,为了保持技术栈一致性和减少依赖,我决定采用原生 JavaScript 定时器方案,结合 Vue 的响应式特性来实现。

3. 核心实现步骤详解

3.1 基础组件结构与数据设计

首先创建一个基础的 Vue 单文件组件,我们命名为TypewriterEffect.vue:

<template> <div class="typewriter-container"> <a-typography> <a-typography-text :style="{ fontFamily: 'monospace' }"> {{ displayedText }} </a-typography-text> </a-typography> </div> </template> <script> export default { name: 'TypewriterEffect', props: { texts: { type: Array, required: true, default: () => ['示例文本1', '示例文本2'] }, typingSpeed: { type: Number, default: 150 }, deletingSpeed: { type: Number, default: 100 }, pauseDuration: { type: Number, default: 2000 } }, data() { return { displayedText: '', currentTextIndex: 0, isDeleting: false, currentCharIndex: 0, timer: null } }, // 其他代码将在下面补充 } </script> <style scoped> .typewriter-container { padding: 16px; background-color: #f5f5f5; border-radius: 4px; } </style>

3.2 核心动画逻辑实现

在methods中添加核心的动画控制方法:

methods: { typeWriter() { const currentText = this.texts[this.currentTextIndex] if (this.isDeleting) { // 回退逻辑 this.displayedText = currentText.substring(0, this.currentCharIndex - 1) this.currentCharIndex-- if (this.currentCharIndex === 0) { this.isDeleting = false this.currentTextIndex = (this.currentTextIndex + 1) % this.texts.length setTimeout(this.typeWriter, this.pauseDuration) return } } else { // 打字逻辑 this.displayedText = currentText.substring(0, this.currentCharIndex + 1) this.currentCharIndex++ if (this.currentCharIndex === currentText.length) { this.isDeleting = true setTimeout(this.typeWriter, this.pauseDuration) return } } const speed = this.isDeleting ? this.deletingSpeed : this.typingSpeed this.timer = setTimeout(this.typeWriter, speed) } }

3.3 生命周期控制

添加组件的生命周期钩子,确保动画的正确启动和清理:

mounted() { this.typeWriter() }, beforeUnmount() { clearTimeout(this.timer) }

4. 功能增强与优化

4.1 添加光标动画效果

为了让效果更加逼真,我们可以添加一个闪烁的光标。修改模板和样式:

<template> <div class="typewriter-container"> <a-typography> <a-typography-text :style="{ fontFamily: 'monospace' }"> {{ displayedText }}<span class="cursor">|</span> </a-typography-text> </a-typography> </div> </template> <style scoped> .cursor { animation: blink 1s infinite; color: #1890ff; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } </style>

4.2 添加循环控制参数

有时候我们可能不需要无限循环,可以添加一个循环次数的控制:

props: { loop: { type: [Boolean, Number], default: true } }, data() { return { // 原有数据... loopCount: 0, maxLoops: typeof this.loop === 'boolean' ? (this.loop ? Infinity : 1) : this.loop } }

然后在typeWriter方法中增加循环控制逻辑:

if (this.currentCharIndex === 0 && !this.isDeleting) { this.loopCount++ if (this.loopCount >= this.maxLoops) { this.$emit('animation-complete') return } }

5. 实际应用与集成示例

5.1 在页面中使用组件

现在我们可以像使用普通 Ant Design Vue 组件一样使用我们的打字机组件:

<template> <a-card title="字符回退效果演示"> <typewriter-effect :texts="['Hello World!', '你好,世界!', 'こんにちは世界']" :typing-speed="100" :deleting-speed="50" :pause-duration="1500" @animation-complete="handleComplete" /> </a-card> </template> <script> import TypewriterEffect from './components/TypewriterEffect.vue' export default { components: { TypewriterEffect }, methods: { handleComplete() { this.$message.success('动画播放完成!') } } } </script>

5.2 结合 Ant Design Vue 其他组件

我们可以将这个效果与其他 Ant Design Vue 组件结合,创建更丰富的交互体验。例如,结合表单控件让用户可以实时调整参数:

<template> <a-space direction="vertical" style="width: 100%"> <a-card title="参数控制"> <a-form layout="inline"> <a-form-item label="打字速度"> <a-slider v-model:value="typingSpeed" :min="50" :max="500" :step="50" /> </a-form-item> <a-form-item label="回退速度"> <a-slider v-model:value="deletingSpeed" :min="30" :max="300" :step="30" /> </a-form-item> <a-form-item label="停顿时间"> <a-slider v-model:value="pauseDuration" :min="500" :max="3000" :step="100" /> </a-form-item> </a-form> </a-card> <a-card title="效果预览"> <typewriter-effect :texts="texts" :typing-speed="typingSpeed" :deleting-speed="deletingSpeed" :pause-duration="pauseDuration" /> </a-card> </a-space> </template> <script> import { ref } from 'vue' import TypewriterEffect from './components/TypewriterEffect.vue' export default { components: { TypewriterEffect }, setup() { const texts = ref([ 'Ant Design Vue 真不错', '这个字符回退效果很酷', '可以用于多种场景展示' ]) const typingSpeed = ref(150) const deletingSpeed = ref(100) const pauseDuration = ref(2000) return { texts, typingSpeed, deletingSpeed, pauseDuration } } } </script>

6. 性能优化与注意事项

6.1 性能优化技巧

  1. 使用 requestAnimationFrame 替代 setTimeout: 对于更流畅的动画效果,可以考虑使用 requestAnimationFrame:
methods: { typeWriter(timestamp) { if (!this.lastTimestamp) { this.lastTimestamp = timestamp } const delta = timestamp - this.lastTimestamp const speed = this.isDeleting ? this.deletingSpeed : this.typingSpeed if (delta >= speed) { this.lastTimestamp = timestamp // 原有的打字/回退逻辑 } this.animationId = requestAnimationFrame(this.typeWriter) } }
  1. 组件销毁时清理资源: 确保在组件销毁时取消所有动画帧请求:
beforeUnmount() { if (this.animationId) { cancelAnimationFrame(this.animationId) } clearTimeout(this.timer) }

6.2 常见问题与解决方案

  1. 文本跳动问题: 当文本长度变化较大时,可能会出现容器宽度变化导致的跳动。解决方法:
.typewriter-container { min-height: 60px; /* 根据实际情况调整 */ display: inline-block; }
  1. 多行文本支持: 如果需要支持多行文本,可以修改组件接收二维数组:
props: { textGroups: { type: Array, default: () => [['第一行', '第二行'], ['另一组第一行', '另一组第二行']] } }

然后在模板中使用 v-for 渲染多行。

  1. 字体等宽问题: 为了确保字符宽度一致,使用等宽字体并设置合适的字体大小:
.typewriter-container { font-family: 'Courier New', monospace; font-size: 18px; line-height: 1.5; }

7. 扩展应用场景

7.1 代码演示场景

这个组件非常适合用于代码演示,可以创建一个专门针对代码展示的版本:

<template> <pre class="code-container"><code>{{ displayedCode }}</code><span class="cursor">|</span></pre> </template> <script> export default { props: { codeSnippets: { type: Array, required: true }, // 其他参数... }, // 重用核心逻辑... } </script> <style scoped> .code-container { background-color: #f6f8fa; padding: 16px; border-radius: 4px; overflow-x: auto; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; line-height: 1.5; color: #24292e; } </style>

7.2 教育类应用

在教育类应用中,可以用这个效果来逐步展示解题过程或概念解释:

<template> <a-card :title="currentTitle"> <typewriter-effect :texts="steps" :typing-speed="200" :deleting-speed="150" :pause-duration="2500" :loop="false" /> <a-button type="primary" @click="resetAnimation" style="margin-top: 16px" > 重新播放 </a-button> </a-card> </template> <script> import { ref } from 'vue' import TypewriterEffect from './TypewriterEffect.vue' export default { components: { TypewriterEffect }, setup() { const steps = ref([ '第一步:理解问题描述', '第二步:分析已知条件', '第三步:确定解题思路', '第四步:逐步推导过程', '第五步:验证结果正确性' ]) const currentTitle = ref('数学解题步骤演示') const resetAnimation = () => { // 通过key强制重新渲染组件 componentKey.value++ } const componentKey = ref(0) return { steps, currentTitle, resetAnimation, componentKey } } } </script>

7.3 结合路由过渡效果

可以在页面路由切换时使用这个效果作为过渡:

// router.js import { createRouter, createWebHistory } from 'vue-router' import TypewriterEffect from './components/TypewriterEffect.vue' const router = createRouter({ history: createWebHistory(), routes: [ // 路由配置... ] }) router.beforeEach((to, from, next) => { // 在路由切换前显示打字机效果 const app = document.getElementById('app') const effect = createApp(TypewriterEffect, { texts: ['加载中...'], loop: false }) const instance = effect.mount(document.createElement('div')) app.appendChild(instance.$el) next() // 路由切换完成后移除效果 setTimeout(() => { app.removeChild(instance.$el) instance.unmount() }, 1000) })

8. 完整组件代码与使用建议

8.1 完整组件代码

以下是经过优化的完整 TypewriterEffect 组件代码:

<template> <div class="typewriter-container" :style="containerStyle"> <a-typography> <a-typography-text :style="textStyle"> {{ displayedText }}<span class="cursor" :style="cursorStyle">|</span> </a-typography-text> </a-typography> </div> </template> <script> import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue' export default { name: 'TypewriterEffect', props: { texts: { type: Array, required: true, default: () => ['示例文本1', '示例文本2'], validator: value => value.length > 0 }, typingSpeed: { type: Number, default: 150, validator: value => value > 0 }, deletingSpeed: { type: Number, default: 100, validator: value => value > 0 }, pauseDuration: { type: Number, default: 2000, validator: value => value >= 0 }, loop: { type: [Boolean, Number], default: true }, startDelay: { type: Number, default: 0 }, textStyle: { type: Object, default: () => ({}) }, containerStyle: { type: Object, default: () => ({}) }, cursorStyle: { type: Object, default: () => ({}) } }, emits: ['animation-complete', 'typing-started', 'deleting-started'], setup(props, { emit }) { const displayedText = ref('') const currentTextIndex = ref(0) const isDeleting = ref(false) const currentCharIndex = ref(0) const loopCount = ref(0) const animationId = ref(null) const timer = ref(null) const lastTimestamp = ref(0) const maxLoops = computed(() => { return typeof props.loop === 'boolean' ? (props.loop ? Infinity : 1) : props.loop }) const typeWriter = (timestamp = 0) => { const currentText = props.texts[currentTextIndex.value] if (!lastTimestamp.value) { lastTimestamp.value = timestamp } const delta = timestamp - lastTimestamp.value const speed = isDeleting.value ? props.deletingSpeed : props.typingSpeed if (delta >= speed) { lastTimestamp.value = timestamp if (isDeleting.value) { // 回退逻辑 displayedText.value = currentText.substring(0, currentCharIndex.value - 1) currentCharIndex.value-- if (currentCharIndex.value === 0) { isDeleting.value = false loopCount.value++ if (loopCount.value >= maxLoops.value) { emit('animation-complete') return } currentTextIndex.value = (currentTextIndex.value + 1) % props.texts.length timer.value = setTimeout(typeWriter, props.pauseDuration) return } } else { // 打字逻辑 if (currentCharIndex.value === 0) { emit('typing-started', currentText) } displayedText.value = currentText.substring(0, currentCharIndex.value + 1) currentCharIndex.value++ if (currentCharIndex.value === currentText.length) { isDeleting.value = true emit('deleting-started', currentText) timer.value = setTimeout(typeWriter, props.pauseDuration) return } } } animationId.value = requestAnimationFrame(typeWriter) } const startAnimation = () => { if (props.startDelay > 0) { timer.value = setTimeout(() => { animationId.value = requestAnimationFrame(typeWriter) }, props.startDelay) } else { animationId.value = requestAnimationFrame(typeWriter) } } const resetAnimation = () => { displayedText.value = '' currentTextIndex.value = 0 isDeleting.value = false currentCharIndex.value = 0 loopCount.value = 0 lastTimestamp.value = 0 if (timer.value) { clearTimeout(timer.value) timer.value = null } if (animationId.value) { cancelAnimationFrame(animationId.value) animationId.value = null } startAnimation() } watch(() => props.texts, () => { resetAnimation() }) onMounted(() => { startAnimation() }) onBeforeUnmount(() => { if (timer.value) { clearTimeout(timer.value) } if (animationId.value) { cancelAnimationFrame(animationId.value) } }) return { displayedText, resetAnimation } } } </script> <style scoped> .typewriter-container { display: inline-block; min-height: 60px; padding: 8px 16px; } .cursor { animation: blink 1s infinite; color: #1890ff; font-weight: bold; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } </style>

8.2 使用建议与最佳实践

  1. 性能敏感场景: 在需要同时展示多个打字机效果的页面中,考虑使用 CSS 方案替代 JavaScript 方案,或者对组件进行节流控制。

  2. 长文本处理: 对于特别长的文本,考虑分页或分段展示,避免用户等待时间过长。

  3. 无障碍访问: 为屏幕阅读器用户添加适当的 ARIA 属性:

<div class="typewriter-container" :aria-live="isDeleting ? 'off' : 'polite'" :aria-label="`正在展示: ${displayedText}`" > <!-- 原有内容 --> </div>
  1. 主题适配: 如果项目使用了 Ant Design Vue 的暗色主题,可以添加相应的样式适配:
.typewriter-container[data-theme='dark'] { background-color: #1f1f1f; color: #e6e6e6; } .typewriter-container[data-theme='dark'] .cursor { color: #69b1ff; }
  1. 移动端优化: 在移动设备上,可能需要调整速度和字体大小:
const isMobile = ref(window.innerWidth < 768) const typingSpeed = computed(() => isMobile.value ? props.typingSpeed * 1.5 : props.typingSpeed )

9. 与其他 Ant Design Vue 组件的深度集成

9.1 结合 Notification 组件

可以创建一个全局的通知打字效果:

// 在全局工具中注册 app.config.globalProperties.$typewriterNotify = (messages, options = {}) => { const duration = options.duration || 4500 const key = `typewriterNotify_${Date.now()}` const notification = antd.notification.open({ key, message: options.title || '通知', description: '', duration, onClose: () => { if (instance) { instance.unmount() document.body.removeChild(container) } } }) const container = document.createElement('div') document.body.appendChild(container) const instance = createApp(TypewriterEffect, { texts: messages, typingSpeed: options.typingSpeed || 100, deletingSpeed: options.deletingSpeed || 80, pauseDuration: options.pauseDuration || 1000, loop: options.loop || false, onAnimationComplete: () => { if (!options.loop) { setTimeout(() => { antd.notification.close(key) }, 1000) } } }) instance.mount(container) return { close: () => antd.notification.close(key) } }

9.2 结合 Modal 组件

创建一个带有打字机效果的模态框:

function openTypewriterModal(contentTexts, options = {}) { const modal = antd.Modal.info({ title: options.title || '提示', content: '', okText: options.okText || '知道了', width: options.width || 600, maskClosable: options.maskClosable || false }) const container = document.createElement('div') const modalContent = document.querySelector('.ant-modal-body') modalContent.appendChild(container) const instance = createApp(TypewriterEffect, { texts: contentTexts, typingSpeed: options.typingSpeed || 120, deletingSpeed: options.deletingSpeed || 90, pauseDuration: options.pauseDuration || 1500, loop: options.loop || false, containerStyle: { padding: '16px', fontSize: '16px', lineHeight: '1.6' } }) instance.mount(container) return { destroy: () => { modal.destroy() instance.unmount() } } }

10. 测试与调试技巧

10.1 单元测试要点

为打字机组件编写单元测试时,需要关注以下几个关键点:

import { mount } from '@vue/test-utils' import TypewriterEffect from '@/components/TypewriterEffect.vue' describe('TypewriterEffect', () => { it('正确初始化并显示空文本', () => { const wrapper = mount(TypewriterEffect, { props: { texts: ['测试文本'] } }) expect(wrapper.text()).toBe('') }) it('在指定延迟后开始动画', async () => { const wrapper = mount(TypewriterEffect, { props: { texts: ['测试文本'], startDelay: 500 } }) expect(wrapper.text()).toBe('') await new Promise(resolve => setTimeout(resolve, 600)) expect(wrapper.text()).not.toBe('') }) it('完成一个循环后触发动画完成事件', async () => { const wrapper = mount(TypewriterEffect, { props: { texts: ['短文本'], typingSpeed: 10, deletingSpeed: 10, pauseDuration: 10, loop: false } }) await new Promise(resolve => setTimeout(resolve, 500)) expect(wrapper.emitted('animation-complete')).toBeTruthy() }) })

10.2 E2E 测试策略

对于端到端测试,可以使用 Cypress 来验证动画效果:

describe('TypewriterEffect E2E 测试', () => { it('验证打字机效果是否正确工作', () => { cy.visit('/') cy.get('.typewriter-container').should('contain', '') // 等待第一个字符出现 cy.get('.typewriter-container', { timeout: 2000 }).should('not.contain', '') // 验证完整文本最终显示 cy.get('.typewriter-container', { timeout: 5000 }).should('contain', '示例文本1') }) })

10.3 调试技巧

  1. 速度调节调试法: 在开发过程中,可以临时将速度参数调大,方便观察动画过程:
const debugSpeed = 1000 // 1秒/字符
  1. 阶段标记法: 在关键阶段添加 console.log 或触发自定义事件:
if (currentCharIndex.value === 0 && !isDeleting.value) { console.log('开始新一轮打字:', currentText) emit('new-cycle-started', currentText) }
  1. 可视化调试工具: 使用 Vue Devtools 观察组件状态变化,特别是displayedText和currentCharIndex的变化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询