普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月20日首页

Vue的“小外挂”:玩转自定义指令

作者 晷龙烬
2025年12月19日 19:56

🔥 以龙息淬炼代码,在时光灰烬中重铸技术星河 !

欢迎来到 晷龙烬的博客小窝✨! 这里记录技术学习点滴,分享实用技巧,偶尔聊聊奇思妙想~

原创内容✍️,转载请注明出处~感谢支持❤️!请尊重原创📩! 欢迎在评论区交流🌟!

引言

你好呀!在Vue的世界里,我们每天都在用 v-modelv-if 这些内置指令,它们就像官方给我们的“瑞士军刀”,特别好用。但有时候,我们总想搞点“个性化”操作,比如一进页面就让某个输入框自动聚焦,或者给按钮加个“防抖”防止用户疯狂点击。

这时候,Vue的“自定义指令”就该闪亮登场了!它就像是你给Vue安装的“小外挂”,让你能自己定义一些特殊的DOM行为。今天,咱们就把它聊明白。

一、 自定义指令是啥?能吃吗?

简单说,自定义指令就是一套你自己写的、能用在HTML元素上的“操作说明书” ‍。

Vue的核心思想是“数据驱动视图”,我们通常通过改变数据来让视图自动更新。但有些时候,我们不得不直接去操作真实的DOM元素(比如让输入框聚焦、初始化一个第三方库),这些“副作用”操作,就是自定义指令的用武之地。

你可以把它理解成一个可复用的“DOM操作工具箱” ‍。哪里需要,就“v-你的指令名”一下,工具箱里的工具就会自动在那个元素上工作。

二、 怎么创建一个“小外挂”?

创建自定义指令主要有两种方式:全局注册和局部注册。咱们先看最常用的全局注册,一次定义,全项目通用。

1. 全局注册:给Vue加个“全家桶”工具 在你的主文件(比如 main.js)里,可以这样写:

 // 定义一个名为 `v-focus` 的指令
 Vue.directive('focus', {
   // 当被绑定的元素插入到 DOM 中时……
   inserted: function (el) {
     // 聚焦元素
     el.focus()
   }
 })

用起来超简单,在模板里直接:

 <input v-focus>

这样,只要这个 input 元素被插入页面,它就会自动获得焦点,用户可以直接打字,体验满分!

2. 局部注册:某个组件私有的“小工具” ‍ 如果这个指令只在某个特定组件里用,可以把它定义在该组件内部:

 export default {
   directives: {
     focus: {
       inserted: function (el) {
         el.focus()
       }
     }
   }
 }

用法和全局的一样。这种方式让指令的作用范围更清晰。

三、 指令的“生命周期钩子”

一个指令不是简单执行一下就完了,它也有自己的“人生阶段”,Vue提供了几个钩子函数让我们在关键时刻介入:

  • bind只调用一次,指令第一次绑定到元素时调用。可以在这里做一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档)。上面让输入框聚焦的例子,就用这个钩子。
  • update所在组件的 VNode 更新时调用,但可能发生在其子 VNode 更新之前。指令的值可能发生了变化,也可能没有。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind只调用一次,指令与元素解绑时调用。适合在这里做清理工作,比如移除事件监听器,防止内存泄漏。

这些钩子函数都会接收到几个参数,最常用的是 el(指令绑定的元素)和 binding(一个包含指令信息的对象)。

四、 钩子函数的参数详解

让我们深入看看 binding 这个对象,它包含了指令的所有关键信息:

  • name:指令名,不包括 v- 前缀。
  • value:指令的绑定值。例如 v-my-directive="1 + 1" 中,绑定值是 2
  • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。
  • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式是 "1 + 1"
  • arg:传给指令的参数。例如 v-my-directive:foo 中,参数是 "foo"
  • modifiers:一个包含修饰符的对象。例如 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }

理解这些参数,你就能写出更灵活、强大的指令。

五、 来点更深入实用的例子

光说不练假把式,咱们看几个深入、并且实用的场景。

例子1:按钮权限控制

假设我们有不同权限的用户,有些按钮只有管理员能点。

 Vue.directive('permission', {
   inserted: function (el, binding) {
     // binding.value 就是我们传给指令的值,比如用户角色
     const userRole = 'user' // 假设当前用户是普通用户
     const requiredRole = binding.value // 指令要求的管理员角色 'admin'
 
     if (userRole !== requiredRole) {
       // 如果不是管理员,就把按钮禁用或者隐藏
       el.style.display = 'none'
       // 或者 el.disabled = true
     }
   }
 })

模板里可以这样用:

 <button v-permission="'admin'">删除文章</button>

普通用户就看不到这个删除按钮啦,权限管理轻松实现。

例子2:按钮防重复点击

 // main.js(Vue2 全局注册)
 Vue.directive('throttle', {
   // 元素插入DOM时绑定事件
   inserted(el, binding) {
     // 1. 基础配置:默认节流间隔500ms,支持通过参数自定义(如 v-throttle:1000)
     const delay = binding.arg ? Number(binding.arg) : 500;
     // 2. 校验回调函数(避免非函数报错)
     const callback = typeof binding.value === 'function' ? binding.value : () => {};
     
     // 3. 核心变量:记录上一次执行时间
     let lastClickTime = 0;
 
     // 4. 节流点击事件处理函数
     const handleClick = () => {
       const now = Date.now();
       // 距离上次点击超过设定间隔,才执行回调
       if (now - lastClickTime >= delay) {
         callback(); // 执行业务逻辑
         lastClickTime = now; // 更新最后点击时间
       }
     };
 
     // 5. 绑定点击事件 + 缓存函数(方便后续销毁)
     el.addEventListener('click', handleClick);
     el._throttleClick = handleClick; // 把函数存到元素上
   },
 
   // 元素销毁时清理事件(避免内存泄漏)
   unbind(el) {
     el.removeEventListener('click', el._throttleClick);
     el._throttleClick = null; // 清空缓存
   }
 });

使用方式:

 <template>
   <!-- 1. 默认500ms节流 -->
   <button v-throttle="handleClick">默认节流点击</button>
 
   <!-- 2. 自定义1000ms节流(通过参数指定) -->
   <button v-throttle:1000="handleClick">1秒节流点击</button>
 </template>

例子3:图片懒加载 这是一个非常经典的自定义指令应用场景,可以大幅提升页面加载性能。

 Vue.directive('lazy', {
   inserted: function (el, binding) {
     const observer = new IntersectionObserver((entries) => {
       entries.forEach(entry => {
         if (entry.isIntersecting) {
           // 图片进入视口
           const img = entry.target
           img.src = binding.value // 将data-src的值赋给src
           observer.unobserve(img) // 停止观察
         }
       })
     }, {
       rootMargin: '0px',
       threshold: 0.1
     })
 
     observer.observe(el)
     // 存储observer,以便在unbind时清理
     el._lazyObserver = observer
   },
   unbind: function (el) {
     if (el._lazyObserver) {
       el._lazyObserver.disconnect()
     }
   }
 })

HTML中使用:

 <img v-lazy="'https://example.com/image.jpg'" alt="懒加载图片">

六、 自定义指令的适用场景总结

  1. DOM操作封装:聚焦、选择文本、内容复制等。
  2. 事件处理优化:防抖、节流、长按、双击等。
  3. UI功能增强:拖拽、滚动监听、无限滚动、懒加载。
  4. 权限与状态控制:按钮权限、元素可见性、功能开关。
  5. 第三方库集成:图表库初始化、富文本编辑器、地图组件。
  6. 样式与动画:动态样式绑定、动画触发、主题切换。

结语

自定义指令不是什么高深莫测的黑魔法,它就是一个帮你封装DOM操作、提升代码复用性和可维护性的好帮手。下次当你发现自己在多个地方重复写着相同的DOM操作代码时,不妨停下来想想:“是不是可以抽象成一个自定义指令?”

从简单的自动聚焦,到复杂的权限管理、性能优化,自定义指令都能让你的Vue项目代码更干净、更专业。希望这篇文章能帮你打开思路,在实际项目中用起来!


—— 完 ——

✨ 至此结束 ✨

💡 点赞关注,解锁更多技术干货!

我是 晷龙烬 期待与你的下次相遇~

【性能优化】给Vue应用“瘦身”:让你的网页快如闪电的烹饪秘籍

作者 JS_Likers
2025年12月19日 16:26

欢迎使用我的小程序👇👇👇👇

small.png


想象一下:你精心烹制的Vue应用端上桌,用户却因加载慢而转身离开...别担心!今天我来教你几招性能优化“烹饪技巧”,让你的应用“色香味”俱全!

🍳 前菜:为什么需要性能优化?

你的Vue应用就像一道菜,用户希望:

  • 快速上菜(首屏加载快)
  • 口感顺滑(交互流畅)
  • 回味无穷(使用体验好)

性能差的网站就像冷掉的披萨,再好吃也没人爱!

🔪 主菜:Vue性能优化七大秘籍

1. 代码打包:给食材“瘦身”

// vue.config.js - 就像主厨的调味秘诀
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  chainWebpack: config => {
    // 开启Gzip压缩 - 像真空压缩食材
    config.plugin('compression').use(require('compression-webpack-plugin'))
    
    // 拆分包 - 分开装盘更优雅
    config.optimization.splitChunks({
      chunks: 'all',
      maxSize: 244 * 1024, // 每个“餐盘”不超过244KB
    })
  }
})

小技巧:使用 vue-cli--report 参数生成打包分析报告,像X光一样看清你的“脂肪”分布!

2. 懒加载:像自助餐一样按需取用

<template>
  <div>
    <!-- 常规加载 - 一次全上桌 -->
    <!-- <HeavyComponent /> -->
    
    <!-- 懒加载 - 客人需要时才上菜 -->
    <button @click="showComponent = true">点这道菜</button>
    <component v-if="showComponent" :is="HeavyComponent" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      showComponent: false,
      HeavyComponent: () => import('./components/HeavyComponent.vue')
    }
  }
}
</script>

路由懒加载更是神器:

// 路由配置 - 每道菜单独包装
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue') // 客人进入餐厅才准备这道菜
  }
]

3. 虚拟滚动:长列表的“折叠椅”

想象一下10000个项目的列表——就像要同时展示10000道菜,不可能!虚拟滚动只渲染可视区域:

<template>
  <!-- 普通列表 - 所有菜都摆出来 -->
  <!-- <div v-for="item in 10000" :key="item.id">{{ item.name }}</div> -->
  
  <!-- 虚拟滚动 - 只摆客人能看到的几道 -->
  <VirtualList :items="largeList" :item-height="50">
    <template #default="{ item }">
      <ListItem :item="item" />
    </template>
  </VirtualList>
</template>

4. 计算属性 vs 方法:聪明的“预制菜”

<template>
  <div>
    <!-- 方法调用 - 每次点单都现做(性能差) -->
    <!-- <p>{{ calculateTotal() }}</p> -->
    
    <!-- 计算属性 - 提前准备好的预制菜(性能好) -->
    <p>{{ totalPrice }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { price: 100, quantity: 2 },
        { price: 200, quantity: 1 }
      ]
    }
  },
  computed: {
    // 依赖变化时才重新计算
    totalPrice() {
      return this.items.reduce((sum, item) => 
        sum + item.price * item.quantity, 0
      )
    }
  },
  methods: {
    // 每次渲染都会执行
    calculateTotal() {
      return this.items.reduce((sum, item) => 
        sum + item.price * item.quantity, 0
      )
    }
  }
}
</script>

5. Keep-Alive:给组件盖“保温盖”

<template>
  <!-- 常规组件 - 离开就倒掉 -->
  <!-- <TabContent /> -->
  
  <!-- Keep-Alive - 盖上保温盖,回来还是热的 -->
  <keep-alive>
    <component :is="currentTab" />
  </keep-alive>
</template>

6. 图片优化:给视觉“减负”

懒加载图片:

<template>
  <img 
    v-lazy="imageUrl" 
    alt="美味佳肴"
    loading="lazy"  <!-- 原生懒加载 -->
  />
</template>

<script>
import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload, {
  preLoad: 1.3, // 提前1.3屏加载
  attempt: 3    // 尝试3次加载
})
</script>

现代图片格式:

  • WebP:比JPEG小25-35%
  • AVIF:下一代格式,压缩率更高

7. 监控与分析:安装“厨房摄像头”

// 性能监控
export default {
  mounted() {
    // 测量组件加载时间
    const start = performance.now()
    
    this.$nextTick(() => {
      const end = performance.now()
      console.log(`组件渲染耗时: ${end - start}ms`)
      
      // 发送到监控平台
      this.sendMetrics('component_render_time', end - start)
    })
  }
}

🍰 甜点:快速优化清单

立即能做的:

  1. 开启Gzip压缩(服务器配置)
  2. 使用路由懒加载
  3. 压缩图片(Tinypng.com)
  4. 移除未使用的代码

进阶技巧:

  1. 使用CDN分发静态资源
  2. 服务端渲染(SSR)改善首屏
  3. PWA提升离线体验
  4. Web Workers处理繁重计算

📊 成果展示:优化前后对比

指标 优化前 优化后 提升
首屏加载 4.2s 1.8s ⬇️ 57%
打包体积 2.1MB 890KB ⬇️ 58%
Lighthouse评分 62 92 ⬆️ 30分

🎯 结语:优化是持续的过程

性能优化就像保持身材——不是一次性节食,而是养成健康习惯。每周花15分钟检查你的Vue应用:

  1. 运行 npm run build -- --report
  2. 查看Lighthouse报告
  3. 优化最慢的3个组件

记住:每毫秒都很重要——100毫秒的延迟就能让转化率下降7%!

今日主厨推荐:从路由懒加载开始,这是性价比最高的优化方式!


💡 小测验:你的Vue应用现在“体重”多少?运行 npm run build 看看打包后的体积,在评论区分享你的“减肥”成果吧!

优化愉快,让你的Vue应用飞起来!🚀

昨天 — 2025年12月19日首页
昨天以前首页

微信内容emoji表情包编辑器 + vue3 + ts + WrchatEmogi Editor

2025年12月18日 18:14

wechat-emoji-editor组件效果

功能 效果
添加表情 image.png
预览 image.png
目录 image.png

代码-组件: (需要源码留言)

// 
import WechatEmojiEditor from '@/components/wechat-emoji-editor/index.vue'
const welcomeMsg = ''

<WechatEmojiEditor
  :rows="4"
  type="textarea"
  :maxlength="1200"
  show-word-limit
  v-model="welcomeMsg"
></WechatEmojiEditor>

图片打包脚本 (需要源码留言)

package.json

    {
              "name": "build",
              "version": "1.0.0",
              "description": "build",
              "main": "index.js",
              "scripts": {
                "test": "echo "Error: no test specified" && exit 1",
                "buildImg": "sudo node sharpbuild2.js",
                "build": "node sharpbuild3.js && vite build" 
              },
              "devDependencies": {
                "sharp": "^0.34.3"
              }
            }

sharpbuild脚本

```
        const imgs = [
        {
            cn: "[微笑]",
            hk: "[微笑]",
            us: "[Smile]",
            code: "/::)",
            web_code: "/微笑",
            src: "/src/assets/emojis/Smile.png",
          },
          {
            cn: "[撇嘴]",
            hk: "[撇嘴]",
            us: "[Grimace]",
            code: "/::~",
            web_code: "/撇嘴",
            src: "/src/assets/emojis/Grimace.png",
          },
          // ...
        ]
        console.log('imgs', imgs)
        const list = imgs.map((item, index) => {
          return {
            input: `${__dirname}${item.src}`,
            top: Math.floor(index / 10) * 128, // 垂直偏移量
            left: (index % 10) * 128, // 水平偏移量
          };
        });
        const sharp = require("sharp");
        const fs = require("fs");
        // 指定输入文件路径
        const url = `${__dirname}/src/assets/wechat.png`;
        sharp({
          create: {
            width: 10 * 128,
            height: 11 * 128,
            channels: 3,
            background: { r: 255, g: 255, b: 255, alpha: 0 },
          },

        })

          .composite(list)
          .toFile(url)
          .then((info) => {
            console.log("Image composite successfully:", info);
          })
          .catch((error) => {
            console.error("Error processing image:", error);
          });
   ```
❌
❌