
本文用最接地气的思路拆解每一步:比如用数组渲染表情面板、通过DOM API获取光标位置插入表情节点、用自定义标签存表情数据;最后直接给出HTML+CSS+JS完整可运行代码,哪怕是刚接触前端的新手,跟着步骤走也能立刻把“表情插入功能”加到自己的编辑器里。不用绕复杂框架,只讲最本质的实现逻辑,帮你少走弯路。
你有没有试过自己做个小编辑器?比如给个人博客加个评论框,或者帮公司做个内部留言系统,想着加个表情功能活跃气氛,结果打开代码编辑器就懵了——表情面板怎么和输入框联动?点了表情怎么准确插到光标位置?存到数据库里的表情再读出来怎么原样显示?我去年帮朋友的美食博客做评论区时就踩过这些坑,一开始找了一堆框架文档,越看越复杂,后来拆成三步逻辑,居然半天就搞定了,今天把这个“笨办法”分享给你,不用学复杂框架,新手也能跟着做。
先想清楚:表情功能的核心逻辑到底是什么?
其实不管是用React还是Vue,或者原生JS,表情功能的底层逻辑就三件事——你把这三件事想通了,代码写起来就像搭积木一样简单:
第一,把表情展示成用户能点的按钮(表情面板);第二,用户点表情时,把表情准确放到光标所在的位置;第三,用户输入的内容(包括表情)存到数据库时,得“记清楚”哪个是表情,读出来的时候才能原样显示。
我之前之所以绕弯路,就是一开始没把这三件事分开,想着找个“一键解决”的第三方组件,结果组件里的逻辑套娃一样,出问题都不知道在哪改。后来朋友催得急,我干脆把组件扔了,自己用原生JS写——没想到反而更稳,兼容性也没毛病,因为核心逻辑就用了MDN推荐的标准API。
一步一步来:从0到1实现表情插入功能
表情面板不用搞得多复杂,用户要的是“快”,不是“全”。我去年帮朋友做的时候,他一开始想放20个表情,我说“评论区而已,放10个常用的就行,多了用户反而找不到想用的”,最后选了微信最常用的10个表情,用图片存到服务器静态文件夹里(32×32的小图,加载快)。
你得有表情数据——用数组存就行,比如图片表情的数组:
const emojis = [
{ src: '/static/emojis/smile.png', alt: '微笑' },
{ src: '/static/emojis/laugh.png', alt: '大笑' },
{ src: '/static/emojis/like.png', alt: '喜欢' },
{ src: '/static/emojis/funny.png', alt: '调皮' },
{ src: '/static/emojis/cry.png', alt: '哭' },
{ src: '/static/emojis/angry.png', alt: '生气' },
{ src: '/static/emojis/think.png', alt: '思考' },
{ src: '/static/emojis/thumbs-up.png', alt: '点赞' },
{ src: '/static/emojis/thumbs-down.png', alt: '踩' },
{ src: '/static/emojis/pray.png', alt: '祈祷' }
];
然后用JS循环渲染成按钮——不用写一堆HTML,循环数组更灵活:
const emojiPanel = document.getElementById('emoji-panel'); // 先在HTML里写个空div,id叫emoji-panel
emojis.forEach(emoji => {
const btn = document.createElement('button');
btn.className = 'emoji-btn'; // 加个类名,方便写样式
// 创建图片节点
const img = document.createElement('img');
img.src = emoji.src;
img.alt = emoji.alt;
img.className = 'emoji-img'; // 控制图片大小
btn.appendChild(img);
// 把按钮加到面板里
emojiPanel.appendChild(btn);
});
样式也简单,用flex把按钮排成一行,加个边框和圆角:
.emoji-panel {
display: flex;
gap: 8px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
}
.emoji-btn {
border: none;
background: none;
cursor: pointer;
padding: 0;
}
.emoji-img {
width: 24px;
height: 24px;
vertical-align: middle;
}
这样一个简单的表情面板就做好了——我当时用这个方法,5分钟就搭好了面板,比找组件快多了。
这一步是最容易踩坑的,但其实用原生JS的Selection
和Range
对象就能解决。我之前试过两个“笨办法”:
第一个是用input.value += emoji
——结果光标跑到 用户得重新点回原来的位置,体验特别差;
第二个是用innerHTML
拼接——比如editor.innerHTML += '
,结果把原有内容的标签结构搞乱了,比如用户输入的'
好吃
变成了纯文本,朋友看到后说“这哪行,评论里要能加粗的”。
后来查MDN文档才知道,正确的做法是操作光标所在的范围(Range)——简单说就是:用document.getSelection()
获取当前光标位置,然后在这个位置插入表情节点,这样既不会覆盖原有内容,光标还能留在表情后面,用户接着输入也顺畅。
具体代码是这样的(我把它封装成了函数,方便复用):
function insertEmoji(emojiNode) {
// 获取当前选中文本或光标位置
const selection = document.getSelection();
if (selection.rangeCount === 0) return; // 没有光标,直接返回
// 获取第一个光标范围(通常用户只有一个光标)
const range = selection.getRangeAt(0);
// 删除选中文本(如果有的话,比如用户选中了一段文字,点表情就替换成表情)
range.deleteContents();
// 插入表情节点
range.insertNode(emojiNode);
// 把光标移到表情后面,提升体验
range.collapse(false); // false表示光标在节点后面
selection.removeAllRanges(); // 清空原有范围
selection.addRange(range); // 重新设置光标位置
}
然后给表情按钮加点击事件——点击按钮时,创建表情节点,调用insertEmoji
函数:
// 给每个表情按钮加点击事件
document.querySelectorAll('.emoji-btn').forEach(btn => {
btn.addEventListener('click', () => {
// 获取表情的src和alt(从按钮里的img节点拿)
const img = btn.querySelector('img');
const emojiSrc = img.src;
const emojiAlt = img.alt;
// 创建表情节点(用img标签,加data属性存路径,方便后续序列化)
const emojiNode = document.createElement('img');
emojiNode.src = emojiSrc;
emojiNode.alt = emojiAlt;
emojiNode.className = 'emoji-img';
emojiNode.dataset.emojiSrc = emojiSrc; // 存data属性,方便后续存数据库
// 插入表情
insertEmoji(emojiNode);
});
});
我去年测试的时候,用Chrome的开发者工具看光标位置——点击表情后,光标果然在表情后面,用户接着输入完全没问题,朋友看到后说“这才对嘛,之前用组件的时候光标总乱跑”。
你有没有遇到过这种情况?插入的表情存到数据库里,再读出来变成了乱码或者只显示文字?我朋友的博客一开始就遇到了——因为他直接存emoji字符,结果数据库用的是latin1
编码,emoji变成了“?”。后来我改成用自定义标签+data属性,问题就解决了。
其实原理很简单:插入表情时,生成一个带data
属性的节点(比如img
或span
),这样存到数据库里的是HTML字符串,读出来的时候,浏览器会自动解析成原来的节点,用CSS样式保持显示效果。
比如插入的表情节点是这样的:

存到数据库里的是这个HTML字符串,读出来的时候,浏览器会自动加载src
属性的图片,根本不用额外处理。如果后来服务器换了域名,比如从old.com
改成new.com
,只需要用JS替换data-emoji-src
里的旧域名就行:
// 读出来后更新图片路径
const emojiImgs = document.querySelectorAll('.emoji-img');
emojiImgs.forEach(img => {
const oldSrc = img.dataset.emojiSrc;
const newSrc = oldSrc.replace('old.com', 'new.com');
img.src = newSrc;
});
这样就不用修改数据库里的内容,只需要改一行JS代码,特别灵活。
常见表情存储方式对比
我把去年试过的几种存储方式整理成了表格,你可以根据自己的场景选:
存储方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
直接存emoji字符 | 简单,无需额外处理 | 部分旧浏览器/数据库编码不支持 | 现代浏览器+UTF-8数据库 |
存图片路径(带data属性) | 兼容性好,可自定义样式 | 需要维护图片资源 | 需要自定义表情或旧浏览器 |
自定义标签+data属性 | 易扩展,可灵活修改 | 需要额外解析标签 | 复杂场景(如表情带交互) |
最后一步:测试——确保表情“插得进、存得住、读得出”
写完代码后,你可以用Chrome的开发者工具做两个简单测试:
data
属性的节点(比如img
标签有data-emoji-src
); console.log
里,看是不是包含这些节点的HTML字符串; 我去年测试的时候,朋友在旁边看着,我点了个“微笑”表情,编辑器里显示出来,复制内容到控制台,果然有data-emoji-src
属性,朋友说“对,就是这样”,然后我们把内容存到数据库里,再读出来——完全没问题,表情原样显示,加粗的文字也没乱。
我把完整的代码打包成了一个demo,包括HTML结构、CSS样式和JS逻辑,你可以直接下载下来(链接:https://github.com/yourname/emoji-editor-demo,加nofollow),改改表情数组就能用到自己的项目里。如果遇到问题,比如光标位置不对,或者表情显示不出来,欢迎在评论区留言,我帮你看看——毕竟这些坑我都踩过,能省你不少时间~
对了,如果你用的是emoji字符而不是图片,代码更简单,把img
节点换成span
就行,比如:
const emojiNode = document.createElement('span');
emojiNode.className = 'emoji';
emojiNode.textContent = '😀';
emojiNode.dataset.emoji = '😀';
这样存到数据库里的是😀
,读出来的时候用CSS样式调整大小就行,比如.emoji { font-size: 1.2em; vertical-align: middle; }
。
表情功能真的没那么复杂,拆成三步逻辑,用原生JS就能搞定——你试试就知道,比找组件快多了~
我之前帮朋友选表情类型的时候,他纠结了快半小时——到底用图片还是系统自带的emoji字符?其实真不用想太复杂,就看你要啥场景。要是你想搞点有特色的,比如奶茶店评论区要用带品牌logo的笑脸,或者公司内部系统得兼容老浏览器、旧数据库(有些数据库默认编码不是UTF-8,emoji很容易变问号),那肯定选图片啊。你自己把表情图片存到服务器静态文件夹里,想设计成圆的方的、带渐变的都没问题,哪怕用户用三年前的Chrome打开,图片也能正常显示,兼容性稳得很,完全不用怕乱码。
但要是你图省事,比如个人博客的评论区就想用常见的😀😆这些,那直接用系统emoji字符更简单——不用维护一堆图片文件,代码里写个span标签,textContent放emoji就行,比图片少了好多步骤。不过有个小细节得注意:数据库一定要改成UTF-8编码。我之前有次帮朋友做博客的时候没改,结果用户发的emoji全变成问号,朋友还以为我代码写错了,查了半天才发现是数据库编码的问题,后来改成UTF-8就好了。其实两种方式没好坏,就看你更在意啥——要特色和兼容选图片,要简单省事选系统emoji,怎么方便怎么来。
还有次帮一个美食博主做评论区,他一开始想用系统emoji,结果发了条“这家蛋糕😋”,数据库没改编码,显示成“这家蛋糕?”,粉丝以为他在吐槽蛋糕不好吃,评论区都炸了,后来改成图片表情才解决。你看,选对方式真的能避免好多麻烦。要是你怕麻烦又想稳,选图片肯定没错;要是你嫌维护图片麻烦,那就记住先改数据库编码,再用系统emoji。
用原生JS实现表情插入,会不会兼容性不好?
文章里用的是MDN推荐的标准API(如Selection、Range),大部分现代浏览器(Chrome、Firefox、Edge)都支持。如果需要兼容IE11等老浏览器,可以加polyfill(比如selection-range-polyfill),但一般个人博客或内部系统不需要考虑太老的浏览器,原生实现足够稳定。
表情用图片还是emoji字符更好?
看场景选择:如果需要自定义表情(比如品牌专属或特殊风格),图片更灵活,兼容性也更好(不会因数据库编码问题变成乱码);如果用系统自带emoji字符(比如😀),代码更简单,但要确保数据库用UTF-8编码(避免emoji显示为“?”)。文章里的表格也对比了两种方式的优缺点,可以根据需求优先选。
为什么插入表情后要把光标移到表情后面?
这是为了提升用户体验——用户点表情的目的通常是“在当前位置加表情,然后继续输入”。如果光标跑到内容 用户得重新点击回到表情后面,操作很麻烦。用Range对象的collapse(false)把光标移到表情节点后,用户可以直接继续输入,流畅性会好很多。
存自定义标签的HTML字符串,会不会增加数据库存储量?
几乎不会有影响。一个带data属性的img标签(比如)只有几十字节,相比用户输入的文字(比如一条评论几百字),存储量可以忽略。而且自定义标签的扩展性好,后续要加表情交互(比如hover显示文字),直接改标签属性就行,不用改数据库结构。
想加GIF动态表情,需要改哪些代码?
和图片表情的实现逻辑一样——只需把表情数组里的src路径换成GIF文件(比如/static/emojis/funny.gif),样式不用调整(因为都是img标签)。注意选小尺寸的GIF(比如24×24或32×32),避免加载太慢;如果GIF太大,可以用TinyPNG等工具压缩,保证加载速度。