Vue 3 + TS 实战:手写 v-no-emoji 自定义指令,彻底禁止输入框表情符号!

🚫 Vue 3 + TS 实战:手写 v-emoji 自定义指令,彻底禁止输入框表情符号!

导读 :在开发用户昵称、评论或表单系统时,我们经常遇到一个痛点:"如何禁止用户在输入框中输入 Emoji 表情?"

虽然可以通过后端校验拦截,但糟糕的用户体验(提交后才报错)会让用户抓狂。最好的方案是在前端输入时直接拦截

本文将带你使用 Vue 3 + TypeScript ,手写一个高性能的自定义指令 v-emoji(或更准确地说是 v-no-emoji),从正则原理指令生命周期边界情况处理,全方位实现"表情防火墙"。代码可直接复制生产环境使用!


项目源码:gitee源码地址

一、为什么需要禁止 Emoji?

在很多业务场景中,Emoji 是"不受欢迎"的:

  1. 数据库兼容性 :老旧的 MySQL (utf8) 不支持 4 字节的 Emoji,存入会报错(需 utf8mb4)。
  2. UI 布局崩坏:某些特殊 Emoji 宽度异常,导致移动端布局错位。
  3. 搜索与过滤:Emoji 会导致搜索引擎分词失败,或引发敏感词过滤误判。
  4. 业务规范:如实名认证姓名、企业发票抬头等严肃场景,严禁出现表情。

解决方案 :利用 Vue 的 自定义指令 (Custom Directives) ,在 input 事件触发时实时清洗数据,让用户"根本输不进去"。


二、核心原理:如何识别 Emoji?

Emoji 的 Unicode 编码范围比较复杂,主要集中在以下区间:

  • \u{1F600} - \u{1F64F} (Emoticons)
  • \u{1F300} - \u{1F5FF} (Misc Symbols and Pictographs)
  • \u{1F680} - \u{1F6FF} (Transport and Map)
  • \u{1F1E0} - \u{1F1FF} (Flags)
  • 以及肤色修饰符、零宽连接符等。

最稳健的正则表达式

typescript 复制代码
const emojiRegex = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/gu;

💡 注意 :必须加上 u (unicode) 和 g (global) 标志,才能正确匹配代理对(Surrogate Pairs)。


三、实战代码:实现 v-no-emoji 指令

我们将创建一个名为 noEmoji.ts 的文件。为了语义清晰,我们将其命名为 v-no-emoji(禁止表情),当然你也可以 alias 为 v-emoji 表示"开启表情过滤模式"。

3.1 定义指令 (directives/noEmoji.ts)

typescript 复制代码
import type { ObjectDirective } from 'vue';

/**
 * 匹配 Emoji 的正则表达式
 * \p{Extended_Pictographic}: 匹配所有扩展图形字符(包括大部分 Emoji)
 * \u{1F3FB}-\u{1F3FF}: 肤色修饰符
 * \u{1F9B0}-\u{1F9B3}: 头发/肢体修饰符
 * u 标志: 启用 Unicode 模式
 * g 标志: 全局匹配
 */
const EMOJI_REGEX = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}\u{1F9B0}-\u{1F9B3}]/gu;

interface NoEmojiElement extends HTMLInputElement {
  _prevValue?: string; // 用于记录上一次合法的值,以便回滚
}

export const noEmoji: ObjectDirective<NoEmojiElement> = {
  mounted(el, binding) {
    // 如果传入参数为 false,则不启用过滤(动态控制)
    if (binding.value === false) return;

    // 记录初始值(防止初始化时就有表情)
    cleanInput(el);

    el.addEventListener('input', handleInput);
  },
  updated(el, binding) {
    // 支持动态开启/关闭
    if (binding.value === false) {
      el.removeEventListener('input', handleInput);
    } else {
      el.addEventListener('input', handleInput);
      cleanInput(el); // 更新时再次检查
    }
  },
  unmounted(el) {
    el.removeEventListener('input', handleInput);
  }
};

/**
 * 处理输入事件
 */
function handleInput(this: NoEmojiElement, event: Event) {
  const target = event.target as NoEmojiElement;
  const originalValue = target.value;
  
  // 如果包含 Emoji
  if (EMOJI_REGEX.test(originalValue)) {
    // 1. 移除所有 Emoji
    const cleanedValue = originalValue.replace(EMOJI_REGEX, '');
    
    // 2. 更新 DOM 值
    target.value = cleanedValue;
    
    // 3. 重要:手动触发 input 事件,确保 Vue 的 v-model 能同步到最新值
    // 因为直接修改 target.value 不会自动触发 Vue 的响应式更新
    target.dispatchEvent(new Event('input', { bubbles: true }));
    
    // 4. (可选) 恢复光标位置,防止输入时光标跳到末尾
    // 简单策略:如果用户是在中间插入表情,移除后光标可能会乱,这里做一个简单的补偿
    // 复杂场景建议使用 selectionStart/selectionEnd 精细计算
    const diff = originalValue.length - cleanedValue.length;
    if (diff > 0 && target.selectionStart !== null) {
       target.setSelectionRange(
         Math.max(0, target.selectionStart - diff), 
         Math.max(0, target.selectionEnd - diff)
       );
    }
  }
}

/**
 * 初始化或强制清洗
 */
function cleanInput(el: NoEmojiElement) {
  if (EMOJI_REGEX.test(el.value)) {
    el.value = el.value.replace(EMOJI_REGEX, '');
    el.dispatchEvent(new Event('input', { bubbles: true }));
  }
}

3.2 全局注册 (main.ts)

为了让它在整个项目中可用,我们在入口文件注册它。

typescript 复制代码
import { createApp } from 'vue';
import App from './App.vue';
import { noEmoji } from './directives/noEmoji';

const app = createApp(App);

// 注册为 v-no-emoji
app.directive('no-emoji', noEmoji);

// 如果你非要叫 v-emoji (语义上是开启表情过滤),也可以这样:
// app.directive('emoji', noEmoji); 

app.mount('#app');

四、在组件中使用

现在,你可以在任何 <input><textarea> 上使用该指令。

示例:用户昵称设置

vue 复制代码
<template>
  <div class="form-item">
    <label>昵称 (禁止输入表情):</label>
    
    <!-- 基础用法 -->
    <input 
      v-model="nickname" 
      v-no-emoji 
      type="text" 
      placeholder="请输入您的昵称"
      class="input-box"
    />

    <!-- 动态控制:根据开关决定是否过滤 -->
    <div style="margin-top: 20px;">
      <label>
        <input type="checkbox" v-model="enableFilter" /> 
        开启表情过滤
      </label>
      <input 
        v-model="comment" 
        v-no-emoji="enableFilter" 
        type="text" 
        placeholder="测试动态开关"
        class="input-box"
      />
    </div>

    <p class="preview">当前值: {{ nickname }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const nickname = ref('HelloVue3');
const comment = ref('');
const enableFilter = ref(true);
</script>

<style scoped>
.input-box {
  border: 1px solid #ccc;
  padding: 8px;
  border-radius: 4px;
  width: 300px;
  font-size: 16px;
}
.preview {
  margin-top: 10px;
  color: #666;
  font-size: 14px;
}
</style>

五、关键技术点解析 (避坑指南)

1. 为什么要手动 dispatchEvent

input 事件回调中,如果我们直接修改 el.value,浏览器不会 再次触发 input 事件。而 Vue 的 v-model 是依赖 input 事件来更新数据的。

  • 后果:界面上看表情被删了,但 Vue 的数据模型里还留着表情。
  • 解决 :修改值后,手动 target.dispatchEvent(new Event('input')),通知 Vue 更新状态。

2. 光标位置错乱问题

当用户在字符串中间插入一个 Emoji(例如 ABC[表情]DEF),直接替换会导致字符串变短,光标通常会跳到末尾。

  • 优化 :代码中通过计算 diff (删除了多少个字符),反向调整 selectionStartselectionEnd,尽量保持光标在用户预期的位置。

3. 正则的兼容性

使用了 \p{...} 语法,这需要环境支持 ES2018+

  • 现代浏览器:Chrome 64+, Firefox 78+, Safari 11.1+ 均完美支持。
  • 老旧浏览器 :如果需要兼容 IE 或极老版本,需要引入 regexpu-core 进行转译,或者使用冗长的十六进制范围写法。但在 2026 年的今天,直接使用 Unicode 属性转义是最佳实践。

4. 指令参数 vs 绑定值

代码中利用了 binding.value

  • <input v-no-emoji>:默认 true,开启过滤。
  • <input v-no-emoji="false">:关闭过滤。
    这为业务提供了极大的灵活性(例如:普通用户禁止,VIP 用户允许)。

六、进阶:封装成 NPM 包思路

如果你想在多个项目中复用,可以将其封装:

typescript 复制代码
// types/index.d.ts
import type { App } from 'vue';
declare const EmojiPlugin: {
  install(app: App, options?: { directiveName?: string }): void;
};
export default EmojiPlugin;

// plugin/index.ts
import { noEmoji } from './noEmoji';
import type { App } from 'vue';

export default {
  install(app: App, options: { directiveName?: string } = {}) {
    const name = options.directiveName || 'no-emoji';
    app.directive(name, noEmoji);
  }
};

七、总结

通过 Vue 3 的自定义指令,我们实现了一个无侵入、高性能、类型安全的 Emoji 过滤器。

  • 用户体验好:输入时即时拦截,无需提交报错。
  • 代码解耦:逻辑封装在指令中,组件代码清爽。
  • TypeScript 友好:完整的类型提示。
  • 灵活可控:支持动态开关。

在涉及用户生成内容 (UGC) 的系统中,这样的防御性编程是必不可少的。赶紧把这个指令加入你的工具库吧!

💬 互动话题:你在项目中遇到过哪些奇葩的 Emoji 引发的 Bug?是数据库报错还是 UI 炸裂?欢迎在评论区分享你的"血泪史"!

👍 觉得有用请点赞❤️收藏⭐,关注我,获取更多 Vue 3 + TS 高级实战技巧!

相关推荐
文心快码BaiduComate2 小时前
有奖征集|解锁Comate超能力:一文玩转Comate Skills
前端·后端
小码哥_常2 小时前
Android 集合探秘:ArrayMap 与 SparseArray 的奇妙之旅
前端
林九生2 小时前
【Flutter】Flutter 拍照/相册选择后无法显示对话框问题解决方案
前端·javascript·flutter
程序员小寒2 小时前
JavaScript设计模式(四):发布-订阅模式实现与应用
开发语言·前端·javascript·设计模式
Highcharts.js2 小时前
Highcharts Gantt 实战:从框架集成到高级功能应用-打造现代化、交互式项目进度管理图表
前端·javascript·vue.js·信息可视化·免费
程序猿的程2 小时前
把股票数据能力接进 AI:stock-sdk-mcp 的实践整理
前端·javascript·node.js
终端鹿2 小时前
setup 语法糖从 0 到 1 实战教程
前端·javascript·vue.js
颜酱2 小时前
回溯算法实战练习(2)
javascript·后端·算法
周淳APP2 小时前
【React Fiber架构+React18知识点+浏览器原生帧流程和React阶段流程相串】
前端·javascript·react.js·架构