一个念头的起点
很多人做项目是从任务开始,而我这次,是从一个“哇,这也太可爱了吧”的瞬间起步的。事情的起点很简单:我在某个博客页里无意点击了几下,页面上跳出几个漂浮的爱心,每次点击都绽放出一点小小的喜悦感。我当时第一反应不是“这个好酷”,而是“这个我能不能也做一个”。点子就这样发芽了。我开始着手做一个可配置的 Vue 组件,能根据点击触发各种动画,比如爱心、emoji、波纹等等,而且必须够轻、够灵活,能插件式集成到任意项目里。
我当时并不想做成一个庞大复杂的动画库,而是希望它就像一粒糖果,能在网页上轻轻融化,给交互多一分乐趣。于是我开了个新项目,把它命名为 vue-click-effect,打开编辑器,准备开始这段说不上宏大、但肯定有趣的开发过程。
项目的基本设计思路
一开始我就给自己定了一个基调:要么优雅,要么不做。点击特效这个东西说简单也简单,说难也确实有点意思,难在“刚刚好”。太多元素会拖慢页面性能,太少细节就显得粗糙。所以我定了几个核心点:
- 每个点击特效都可以独立设置颜色、尺寸、持续时间
- 支持多种特效类型(文字、emoji、SVG 或简单波纹)
- 支持局部绑定和全局绑定两种模式
- 使用组合式 API 封装通用逻辑
- 动画使用原生 CSS 执行,减少 JS 干预,性能优先
我知道,如果能把这些目标做好,这个项目就已经超出“练手”范畴了,它可以被真实使用,甚至可以发布成 npm 包。
下面是一张我对项目逻辑结构所画的草图图示:

项目结构与技术选择
我用的是 Vue 3 + Vite 的组合。这套搭配的优势是开发快、热更新丝滑、组合式 API 写起来很顺手。我直接用 TypeScript 起手,确保组件可维护性和 IDE 自动提示都拉满。整个目录结构我整理得很精简:
src/
├── main.ts                // 入口
├── components/
│   └── ClickEffect.vue    // 点击特效组件
├── directives/
│   └── vClickEffect.ts    // 自定义指令封装
├── composables/
│   └── useClickEffect.ts  // 核心逻辑组合函数
├── styles/
│   └── animations.css     // 所有动画效果
├── utils/
│   └── createDom.ts       // DOM 操作相关
└── demo/
    └── App.vue            // 演示页面为了保持整体精致感,我提前把几个通用的特效样式预定义成 CSS 动画,保持逻辑部分干净。
最早的组件原型
最早我其实只是打算做个爱心动画,组件接收几个 props,像是 color、size、duration 等。点击事件触发后就在当前位置插入一个爱心,然后再通过 CSS 动画让它漂浮离开并消失。
这一版的核心逻辑写在 ClickEffect.vue 里,点击时调用如下函数:
const createHeart = (event: MouseEvent) => {
  const heart = document.createElement('div')
  heart.className = 'effect-heart'
  heart.textContent = '❤️'
  Object.assign(heart.style, {
    position: 'fixed',
    left: `${event.clientX - 10}px`,
    top: `${event.clientY - 10}px`,
    fontSize: '20px',
    animation: 'heart-float 1.2s ease-out forwards',
    pointerEvents: 'none',
    zIndex: 9999
  })
  document.body.appendChild(heart)
  heart.addEventListener('animationend', () => {
    heart.remove()
  })
}动画使用的 CSS 如下:
@keyframes heart-float {
  0% {
    transform: scale(0.5) translateY(0);
    opacity: 1;
  }
  50% {
    transform: scale(1.2) translateY(-30px);
    opacity: 1;
  }
  100% {
    transform: scale(0.8) translateY(-80px);
    opacity: 0;
  }
}就是这样一段简单的代码,跑起来的效果却让人“哇”了一下。爱心从点击点跳出来、轻轻飘上去、渐渐消失。那种“我写的代码活了”的感觉又回来了。
开始封装逻辑:useClickEffect
组件是用来展示的,但我希望核心逻辑是解耦的、可复用的,于是我把大部分逻辑封装进了一个组合式函数 useClickEffect 中。它负责:
- 动态合成点击坐标
- 生成不同类型的 DOM 元素
- 应用对应动画
- 控制最大并发元素数量
- 监听动画结束后移除元素
函数结构大致如下:
export function useClickEffect(defaultOptions: ClickEffectOptions) {
  const activeList = new Set<HTMLElement>()
  const createEffect = (event: MouseEvent, options?: Partial<ClickEffectOptions>) => {
    const opts = { ...defaultOptions, ...options }
    const el = document.createElement('div')
    // 动态内容、样式、动画 class 设定
    ...
    activeList.add(el)
    document.body.appendChild(el)
    el.addEventListener('animationend', () => {
      el.remove()
      activeList.delete(el)
    })
  }
  return { createEffect }
}我把 createEffect 返回给组件使用,同时也提供给自定义指令调用。这样一来,整个逻辑就清晰了:组件负责提供 UI 入口,组合函数负责底层实现,指令负责简洁绑定。
实现自定义指令:v-click-effect
为了方便地在 DOM 上绑定点击特效,我加了个自定义指令 v-click-effect。使用方式非常简洁:
<div v-click-effect="{ type: 'emoji', emojiList: ['😻','✨','🌈'] }">
  点我一下,有惊喜
</div>实现过程也不复杂:
import { useClickEffect } from '../composables/useClickEffect'
export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const { createEffect } = useClickEffect(binding.value)
    const handler = (e: MouseEvent) => createEffect(e)
    el.__clickHandler__ = handler
    el.addEventListener('click', handler)
  },
  unmounted(el: HTMLElement) {
    el.removeEventListener('click', el.__clickHandler__)
  }
}注意我给元素挂了一个私有字段 __clickHandler__,这样在卸载阶段能正确解绑,避免内存泄露。
功能扩展与自定义能力提升
当基础能力实现之后,我开始思考:这个组件库是否能支持更多场景、更复杂的交互?于是我把焦点放在「自定义能力」的扩展上。我希望用户不仅能选择预设特效,还可以根据自己的需求自定义动画内容、触发方式、触发区域乃至动画逻辑本身。
动画注册机制的构建
首先,我引入了一种“动画注册机制”,允许用户向库中注册自定义动画。这样一来,组件不再限制于内置的三种动画,而是变得开放而灵活。
type CustomEffectCreator = (event: MouseEvent, options: ClickEffectOptions) => void
const customEffectMap = new Map<string, CustomEffectCreator>()
export const registerClickEffect = (name: string, creator: CustomEffectCreator) => {
  customEffectMap.set(name, creator)
}
const handleClick = (e: MouseEvent) => {
  const { type } = props
  const effectFn = customEffectMap.get(type) || builtinEffectMap[type]
  effectFn?.(e, props)
}这个机制的好处是显而易见的:
- 用户可以自己定义比如「星星爆炸」或者「粒子扩散」的动画逻辑;
- 动画逻辑与组件解耦,保持了良好的可扩展性;
- 未来即便是后台配置页面也可以动态加载动画类型,适应不同风格的需求。
示例:注册一个“星星飞溅”特效
我还为此写了一个自定义动画的例子,叫做“星星飞溅”:
registerClickEffect('star', (e, options) => {
  const star = document.createElement('div')
  star.textContent = '✨'
  Object.assign(star.style, {
    position: 'fixed',
    left: `${e.clientX}px`,
    top: `${e.clientY}px`,
    fontSize: `${options.size}px`,
    animation: `star-splash ${options.duration}ms ease-out forwards`,
    pointerEvents: 'none',
    zIndex: 9999
  })
  document.body.appendChild(star)
  star.addEventListener('animationend', () => star.remove(), { once: true })
})然后定义 CSS 动画如下:
@keyframes star-splash {
  0% { transform: scale(0.5) rotate(0deg); opacity: 1; }
  50% { transform: scale(1.5) rotate(180deg); }
  100% { transform: scale(0.3) rotate(360deg) translateY(-100px); opacity: 0; }
}高级用法场景示意
这一扩展也带来了更多实际的应用场景,下面是一个典型的功能流程图,展示了用户注册自定义动画并触发的过程:

实战打包与发布流程
在项目的后期,我开始将这个组件库包装成 npm 包,方便复用和分发。这一阶段涉及到了构建配置、依赖管理、打包格式等许多工程问题。
打包构建配置
为了让组件库能被别的项目引用,我配置了多格式打包:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
export default defineConfig({
  plugins: [vue(), dts()],
  build: {
    lib: {
      entry: 'src/index.ts',
      name: 'VueClickEffect',
      formats: ['es', 'umd'],
      fileName: (format) => `vue-click-effect.${format}.js`
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' }
      }
    }
  }
})这段配置做了几个关键处理:
- 将库构建为 ES Module和UMD两种格式;
- 将 vue设为外部依赖,不打包进最终文件;
- 使用 vite-plugin-dts自动生成.d.ts类型声明文件。
最终打包结果如下:
dist/
├── vue-click-effect.es.js
├── vue-click-effect.umd.js
└── index.d.tsnpm 发布流程
在本地测试通过后,我使用 pnpm publish 发布到 npm:
pnpm login
pnpm publish --access public配合精心撰写的 README.md 文档和完整的类型定义,这个小库便可以被全世界使用了。
实际项目中的集成与反馈
我将这个组件库应用在了几个实际项目中,比如一个动画型博客系统的首页背景点击特效、一个在线互动表单的点击鼓励机制、还有一个教育类小游戏页面的点击反馈。
在这些项目中,它展示出了以下几个优点:
- 轻量快速:即便在移动端也能快速响应;
- 定制灵活:项目有不同品牌主色和风格,动画都能快速适配;
- 复用性强:组合式函数和组件可按需引入,开发体验良好;
- 响应式良好:自动适配大小,兼容性强。
这些反馈也让我意识到,尽管这个库起源于一次灵感之举,但它已经拥有了真实的价值。
最后
这段开发点击特效库的经历,对我来说不仅是一次技术实践,更是一段思维方式的转变旅程。它让我意识到:
- “小交互也有大意义”,哪怕是一个简单的爱心动画,也能提升用户的情绪感受;
- “创造而非复制”,比起照搬别人的实现,自己动手造轮子能带来更深刻的成长;
- “抽象是复用的前提”,写工具时,要多考虑他人使用场景;
- “完善比完美重要”,不一定要一开始做出十全十美的东西,但要保持迭代和打磨。
如今,这个库已被我用于多个实际项目中,也发布到了 npm 上接受更多开发者的使用与反馈。我想,这就是代码世界中最美好的事吧——将一个灵光一现的创意,打磨成真正可用的工具,并以开放的姿态与世界分享。
如果你也曾因为一个点击爱心动画而会心一笑,那我想我们在某种程度上,是同路人。
(完)
 
 
                     
            
        













 
                    

 
                 
                    