🚫 Vue 3 + TS 实战:手写 v-emoji 自定义指令,彻底禁止输入框表情符号!
导读 :在开发用户昵称、评论或表单系统时,我们经常遇到一个痛点:"如何禁止用户在输入框中输入 Emoji 表情?"
虽然可以通过后端校验拦截,但糟糕的用户体验(提交后才报错)会让用户抓狂。最好的方案是在前端输入时直接拦截。
本文将带你使用 Vue 3 + TypeScript ,手写一个高性能的自定义指令
v-emoji(或更准确地说是v-no-emoji),从正则原理 、指令生命周期 到边界情况处理,全方位实现"表情防火墙"。代码可直接复制生产环境使用!
项目源码:gitee源码地址

一、为什么需要禁止 Emoji?
在很多业务场景中,Emoji 是"不受欢迎"的:
- 数据库兼容性 :老旧的 MySQL (
utf8) 不支持 4 字节的 Emoji,存入会报错(需utf8mb4)。 - UI 布局崩坏:某些特殊 Emoji 宽度异常,导致移动端布局错位。
- 搜索与过滤:Emoji 会导致搜索引擎分词失败,或引发敏感词过滤误判。
- 业务规范:如实名认证姓名、企业发票抬头等严肃场景,严禁出现表情。
解决方案 :利用 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(删除了多少个字符),反向调整selectionStart和selectionEnd,尽量保持光标在用户预期的位置。
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 高级实战技巧!