Vue3 实现超丝滑打字机效果组件 - 进阶

🔥 Vue3 实现超丝滑打字机效果组件(可复用、高定制)

在前端开发中,打字机效果能极大提升页面的交互趣味性和视觉体验,比如 AI 聊天回复、个性化介绍页等场景都非常适用。本文将分享一个基于 Vue3 + Composition API 开发的高性能、高定制化打字机组件,支持打字/删除循环、光标闪烁、自定义样式等核心功能,且代码结构清晰、易于扩展。

🎯 组件特性

  • ✅ 自定义打字速度、删除速度、循环延迟
  • ✅ 支持光标显示/隐藏、闪烁效果开关
  • ✅ 打字完成后自动删除(可选)+ 循环播放
  • ✅ 完全自定义样式(字体、颜色、大小等)
  • ✅ 暴露控制方法(开始/暂停),支持手动干预
  • ✅ 无第三方依赖,纯原生 Vue3 实现
  • ✅ 性能优化:组件卸载自动清理定时器,避免内存泄漏

📝 完整组件代码

js 复制代码
<template>
  <div class="typewriter-container" :style="fontsConStyle">
    <!-- 打字文本 - 逐字符渲染 -->
    <span class="typewriter-text">
      <span 
        v-for="(char, index) in displayedText" 
        :key="index" 
        class="character"
        :data-index="index"
      >
        {{ char }}
      </span>
    </span>
    
    <!-- 光标 - 精准控制显示/闪烁 -->
    <span 
      v-if="showCursor && isCursorVisible"
      class="cursor"
      :class="{ 'blink': showBlinkCursor }"
      aria-hidden="true"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, computed, watch, watchEffect } from "vue";

// 组件 Props 定义(带完整校验)
const props = defineProps({
  // 要显示的文本内容
  text: {
    type: String,
    required: true,
    default: ""
  },
  // 打字速度(ms/字符)
  speed: {
    type: Number,
    default: 80,
    validator: (value) => value > 0
  },
  // 是否显示光标
  showCursor: {
    type: Boolean,
    default: true
  },
  // 光标是否闪烁
  blinkCursor: {
    type: Boolean,
    default: true
  },
  // 是否自动开始打字
  autoStart: {
    type: Boolean,
    default: true
  },
  // 是否循环播放
  loop: {
    type: Boolean,
    default: false
  },
  // 循环延迟(打字完成后等待时间)
  loopDelay: {
    type: Number,
    default: 1000,
    validator: (value) => value >= 0
  },
  // 容器样式(自定义字体、颜色等)
  fontsConStyle: {
    type: Object,
    default: () => ({
      fontSize: "2rem",
      fontFamily: "'Courier New', monospace",
      color: "#333",
      lineHeight: "1.5"
    })
  },
  // 是否开启删除效果
  deleteEffect: {
    type: Boolean,
    default: false
  },
  // 删除速度(ms/字符)
  deleteSpeed: {
    type: Number,
    default: 30,
    validator: (value) => value > 0
  },
  // 字符入场动画开关
  charAnimation: {
    type: Boolean,
    default: true
  }
});

// 响应式状态
const displayedText = ref("");    // 当前显示的文本
const currentIndex = ref(0);      // 当前字符索引
const isPlaying = ref(false);     // 是否正在播放
const isDeleting = ref(false);    // 是否正在删除
const isCursorVisible = ref(true);// 光标是否显示

// 定时器标识(用于清理)
let intervalId = null;
let timeoutId = null;
let cursorTimer = null;

// 计算属性:控制光标闪烁状态
const showBlinkCursor = computed(() => {
  return props.blinkCursor && 
         !isDeleting.value &&
         (displayedText.value.length === props.text.length || displayedText.value.length === 0);
});

// 工具函数:清除所有定时器
const clearAllTimers = () => {
  if (intervalId) clearInterval(intervalId);
  if (timeoutId) clearTimeout(timeoutId);
  if (cursorTimer) clearInterval(cursorTimer);
  intervalId = null;
  timeoutId = null;
  cursorTimer = null;
};

// 核心方法:开始打字
const startTyping = () => {
  // 重置状态
  clearAllTimers();
  isPlaying.value = true;
  isDeleting.value = false;
  currentIndex.value = 0;
  displayedText.value = "";
  isCursorVisible.value = true;
  
  // 打字逻辑
  intervalId = setInterval(() => {
    if (currentIndex.value < props.text.length) {
      displayedText.value = props.text.substring(0, currentIndex.value + 1);
      currentIndex.value++;
    } else {
      // 打字完成
      clearInterval(intervalId);
      isPlaying.value = false;
      
      // 循环逻辑
      if (props.loop) {
        if (props.deleteEffect) {
          timeoutId = setTimeout(startDeleting, props.loopDelay);
        } else {
          timeoutId = setTimeout(startTyping, props.loopDelay);
        }
      }
    }
  }, props.speed);
};

// 核心方法:开始删除
const startDeleting = () => {
  clearAllTimers();
  isDeleting.value = true;
  
  intervalId = setInterval(() => {
    if (currentIndex.value > 0) {
      currentIndex.value--;
      displayedText.value = props.text.substring(0, currentIndex.value);
    } else {
      // 删除完成
      clearInterval(intervalId);
      isDeleting.value = false;
      
      // 循环打字
      if (props.loop) {
        timeoutId = setTimeout(startTyping, props.loopDelay);
      } else {
        isCursorVisible.value = false; // 非循环模式下删除完成隐藏光标
      }
    }
  }, props.deleteSpeed);
};

// 监听文本变化:自动重启打字(适配动态文本场景)
watch(() => props.text, (newText) => {
  if (newText && props.autoStart) {
    startTyping();
  }
}, { immediate: true });

// 初始化光标闪烁(非闪烁模式下固定显示)
watchEffect(() => {
  if (props.showCursor && !cursorTimer) {
    cursorTimer = setInterval(() => {
      if (!showBlinkCursor.value) {
        isCursorVisible.value = true;
      } else {
        isCursorVisible.value = !isCursorVisible.value;
      }
    }, 500);
  }
});

// 组件挂载:自动开始打字
onMounted(() => {
  if (props.autoStart && props.text) {
    startTyping();
  }
});

// 组件卸载:清理所有定时器(避免内存泄漏)
onBeforeUnmount(() => {
  clearAllTimers();
});

// 暴露组件方法(供父组件调用)
defineExpose({
  start: startTyping,        // 手动开始
  pause: clearAllTimers,     // 暂停
  restart: () => {           // 重启
    clearAllTimers();
    startTyping();
  },
  isPlaying,                 // 当前播放状态
  isDeleting                 // 当前删除状态
});
</script>

<style scoped>
/* 容器样式 - 适配行内/块级显示 */
.typewriter-container {
  display: inline-flex;
  align-items: center;
  position: relative;
  font-size: inherit;
  line-height: inherit;
  font-family: inherit;
  white-space: pre-wrap; /* 支持换行符 */
  word-break: break-all; /* 防止长文本溢出 */
}

/* 文本容器 */
.typewriter-text {
  display: inline;
  font-size: inherit;
  line-height: inherit;
  font-family: inherit;
  color: inherit;
}

/* 字符样式 - 入场动画 */
.character {
  display: inline-block;
  animation: typeIn 0.1s ease-out forwards;
  opacity: 0;
}

/* 字符入场动画 */
@keyframes typeIn {
  0% {
    transform: translateY(5px);
    opacity: 0;
  }
  100% {
    transform: translateY(0);
    opacity: 1;
  }
}

/* 光标样式 - 垂直居中优化 */
.cursor {
  display: inline-block;
  width: 2px;
  height: 1.2em; /* 匹配字体高度 */
  background-color: currentColor;
  margin-left: 2px;
  vertical-align: middle;
  position: relative;
  top: 0;
  opacity: 1;
}

/* 光标闪烁动画 */
.cursor.blink {
  animation: blink 1s infinite step-end;
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

/* 禁用字符动画的样式 */
:deep(.character) {
  animation: none !important;
  opacity: 1 !important;
}
</style>

🚀 核心优化点说明

1. 功能增强

  • 新增 charAnimation 属性:可开关字符入场动画,适配不同场景
  • 支持动态文本:监听 text 属性变化,文本更新自动重启打字
  • 优化光标逻辑:非闪烁模式下光标固定显示,避免闪烁干扰
  • 新增 restart 方法:支持手动重启打字效果
  • 文本换行支持:添加 white-space: pre-wrap,兼容带换行符的文本

2. 性能优化

  • 定时器统一管理:所有定时器集中清理,避免内存泄漏
  • 减少不必要渲染:通过 watchEffect 精准控制光标定时器创建/销毁
  • 样式优化:使用 currentColor 继承文本颜色,光标颜色与文本一致
  • 边界处理:添加 word-break: break-all,防止长文本溢出

3. 代码健壮性

  • 完善 Prop 校验:所有数值类型添加范围校验,避免非法值
  • 状态重置:每次开始打字前重置所有状态,避免多轮执行冲突
  • 注释完善:关键逻辑添加注释,提升代码可读性

📖 使用示例

基础使用

js 复制代码
<template>
  <Typewriter 
    text="Hello Vue3! 这是一个超丝滑的打字机效果组件✨"
    speed="50"
  />
</template>

<script setup>
import Typewriter from './components/Typewriter.vue';
</script>

高级使用(循环+删除效果)

js 复制代码
<template>
  <div>
    <Typewriter 
      ref="typewriterRef"
      text="Vue3 打字机组件 | 支持循环删除 | 自定义样式"
      :speed="60"
      :deleteSpeed="40"
      :loop="true"
      :deleteEffect="true"
      :loopDelay="1500"
      :fontsConStyle="{
        fontSize: '1.5rem',
        color: '#409eff',
        fontFamily: '微软雅黑'
      }"
    />
    <button @click="handleRestart">重启打字</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Typewriter from './components/Typewriter.vue';

const typewriterRef = ref();

// 手动重启打字
const handleRestart = () => {
  typewriterRef.value.restart();
};
</script>

🎨 样式自定义说明

属性 说明 默认值
fontSize 字体大小 2rem
fontFamily 字体 'Courier New', monospace
color 文本颜色 #333
lineHeight 行高 1.5

你可以通过 fontsConStyle 属性完全自定义组件样式,例如:

js 复制代码
fontsConStyle: {
  fontSize: "18px",
  color: "#e6a23c",
  fontWeight: "bold",
  background: "#f5f7fa",
  padding: "10px 15px",
  borderRadius: "8px"
}

🛠️ 扩展方向

  1. 自定义字符动画:通过 Prop 传入动画类名,支持不同的字符入场效果
  2. 分段打字:支持数组形式的文本,分段打字+间隔
  3. 速度渐变:实现打字速度由快到慢/由慢到快的效果
  4. 暂停/继续:扩展暂停后继续打字的功能(记录当前索引)
  5. 结合 AI 流式响应:对接 AI 接口的流式返回,实时更新打字文本

📌 总结

这个打字机组件基于 Vue3 Composition API 开发,具备高复用性、高定制性的特点,核心优化点如下:

  1. 完善的定时器管理,避免内存泄漏
  2. 精准的状态控制,支持打字/删除/循环全流程
  3. 灵活的样式自定义,适配不同业务场景
  4. 暴露控制方法,支持父组件手动干预

组件可直接集成到 Vue3 项目中,适用于 AI 聊天、个人主页、产品介绍等需要打字机效果的场景,开箱即用!

相关推荐
阿坤带你走近大数据2 小时前
JavaScript脚本语言的简单介绍
开发语言·javascript·ecmascript
神仙姐姐QAQ2 小时前
vue3更改.el-dialog__header样式不生效
前端·javascript·vue.js
AI_56782 小时前
Vue.js 深度开发指南:从数据绑定到状态管理的最佳实践
前端·javascript·vue.js
前端-文龙刚2 小时前
浅记Vue3中 ref 和 reactive 是两种主要的响应式数据声明方式,它们有以下主要区别
前端·javascript·vue.js
小先生8122 小时前
关于vue-element-plus-admin的mini分支踩坑集锦
前端·vue.js·前端框架·c#
hhcccchh2 小时前
学习vue第十天 V-Model学习指南:双向绑定的魔法师
前端·vue.js·学习
无羡仙10 小时前
从零构建 Vue 弹窗组件
前端·vue.js
源心锁11 小时前
👋 手搓 gzip 实现的文件分块压缩上传
前端·javascript
phltxy12 小时前
从零入门JavaScript:基础语法全解析
开发语言·javascript