📋 背景
最近接到一个需求:在系统中添加表情输入功能。由于需要与腾讯某平台保持数据一致,表情包的数量和取值都要完全相同。
翻阅文档后发现并没有提供现成组件,只能自己实现。
先观察了实现方式:打开控制台面板,点击表情后会瞬间请求大量图片。
输入逻辑上,用户选择 😊 后值自动变成 [微笑],用户直接输入 [微笑] 也能映射成对应表情图片。
🚩 最终目标
- ✅ 支持点击表情面板插入表情
- ✅ 支持输入
[微笑]自动转换为表情图片 - ✅ 完成双向绑定,取值时图片转回
[微笑]文本 - ✅ 字符长度计算:中文 1 个字符,英文 0.5 个,表情 1 个
- ✅ 光标定位准确,体验流畅
- ✅ 输入法友好,不在拼音输入阶段转换
🧩 实现步骤
1、获取表情包数据
最初尝试在网上找表情包资源,但数量总是对不上。近百个表情包如果手动逐个校对太过折磨,于是尝试从页面爬取数据。 在浏览器控制台执行以下脚本:
// 获取所有表情元素
const emojiItems = document.querySelectorAll('.emoji-list li');
// 提取关键信息:图片地址、文本代码、文件名
const emojiArray = Array.from(emojiItems).map(li => {
const img = li.querySelector('img');
return img ? {
src: img.src,
alt: img.alt,
dataImage: img.getAttribute('data-image')
} : null;
}). filter(Boolean);
console.log(emojiArray);
执行后直接在控制台复制数组,保存为 JSON 文件。
src 只做下载使用,alt 需要用来做映射,dataImage 用于拼接读取表情图片路径。
[ { "alt": "[微笑]", "src": "https://xxx.qq.com/xxx/emojis/smiley_0.png" "dataImage": "smiley_0" }]
2、批量下载图片
使用 Node. js 脚本批量下载表情图片:
const fs = require('fs');
const path = require('path');
const outputDir = path. resolve(__dirname, 'emojis');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
async function downloadImage(item) {
const fileName = `${item.dataImage || item.alt || 'emoji'}. png`;
const filePath = path.join(outputDir, fileName);
try {
const res = await fetch(item.src);
if (!res.ok) throw new Error(`Failed to fetch ${item.src}`);
const buffer = await res.arrayBuffer();
fs.writeFileSync(filePath, Buffer.from(buffer));
console.log(`Downloaded: ${fileName}`);
} catch (err) {
console.error(`Error downloading ${item.src}:`, err.message);
}
}
async function downloadAll() {
for (const item of emojiArray) {
await downloadImage(item);
}
console.log('All downloads completed!');
}
downloadAll();
表情下载完,后面就好办了——交给 AI 🤖。
3、组件实现
本质就是一个 contenteditable 的 div,组件实现完全让 AI 完成,但需要考虑以下特殊情况:
1️⃣ 光标位置管理
| 元素类型 | 是否支持光标 | 说明 |
|---|---|---|
<div> 普通 | ❌ | 不可编辑 |
<div contenteditable> | ✅ | 可编辑,有光标 |
| 文本节点 | ✅ | 光标可以定位在字符之间 |
<img> 单独存在 | ⚠️ | 光标难以稳定定位到其后 |
<img> + 文本节点 | ✅ | 光标可以定位在文本节点中 |
<img> + \u200B | ✅ | 零宽字符提供文本节点锚点 |
- 在关键事件(input、mouseup、keyup)中保存光标位置,处理 DOM 变更后恢复到正确位置
- 长度达到限制时裁剪字符,统一将光标设置到末尾(与 element-plus 行为一致)
- 点击空白区域自动定位到末尾(通过计算内容宽度与点击位置判断)
- 表情图片后如果没有文本节点,插入零宽字符
\u200B作为光标定位锚点
2️⃣ 文本与图片双向转换
文本 → 图片
- 使用 200ms 防抖避免频繁触发
- 通过正则
/\[([^\]]+)\]/g匹配文本格式 - 转换条件:
match.index < cursorOffset(光标在[之后即可转换,无需等待额外空格) - 仅转换光标前的最后一个匹配项,光标后的内容保持不变
- 转换后立即更新显示长度,保持同步
图片 → 文本
- 遍历 DOM 树提取内容:文本节点提取
textContent,图片节点提取data-alt属性 - 过滤零宽字符
\u200B
3️⃣ 字符长度计算
精确计算混合内容长度:中文 1 个字符,英文/数字 0.5 个,表情 1 个
- 精确长度(不取整)用于限制检查,显示长度(向上取整)用于界面展示
- 零宽字符完全过滤,不计入长度
- 输入、粘贴、组合输入结束时都要检查长度
- 超限时恢复到上次有效内容,避免内容丢失
4️⃣ 输入法兼容
处理中文输入法组合事件:compositionstart → compositionupdate → compositionend
- 标记组合状态,在组合过程中不触发表情转换和长度检查
- 组合结束后才处理长度限制和表情转换
- 组合输入超限时裁剪到最大长度
5️⃣ 删除表情
确保一次删除完成,不需要多次按键
- Backspace:光标在零宽字符后时,同时删除零宽字符和前面的表情图片
- Delete:删除表情图片时,一并清理后面的零宽字符
- 删除后立即更新显示长度和保存有效内容
6️⃣ 零宽字符使用
- 仅在表情图片后没有其他节点时插入,作为光标定位锚点
- 删除表情时一并清理关联的零宽字符,避免累积
- 所有长度计算逻辑都要过滤零宽字符
7️⃣ 状态同步
- 关键操作后同步:更新显示长度、保存有效内容、保存光标位置、防抖触发父组件更新
- 使用处理中标志位避免循环触发
- 外部值变化时判断是否与当前值相同,避免不必要的更新
最终效果:
💡总结
借助 AI 辅助开发,核心在于:告知 AI 需要处理的特殊情况,以及不断测试和调优。
只要明确需求规范和边界情况,就能高效实现功能。怎么有种从开发转测试的感觉。