前端老哥必看:input光标处插入字符串的骚操作(附完整代码)

前端老哥必看:input光标处插入字符串的骚操作(附完整代码)
在这里插入图片描述


前端老哥必看:input光标处插入字符串的骚操作(附完整代码)

前端老哥必看:input光标处插入字符串的骚操作(附完整代码)

开篇先吐槽这破需求

说实话,我到现在还记得那天下午产品经理晃悠到我工位旁边,一脸轻松地说:“哥,咱们这个输入框啊,得支持在光标位置插入文本,就那种@人的功能,你懂的吧?”

我当时嘴里叼着半根辣条,差点没噎死。

啥?光标位置插入?我脑子里第一反应是:这玩意儿不是挺简单的吗,不就一个input.value += 'xxx'的事?然后产品经理补了一句:“要在当前光标位置插入哦,不能跑到最后面去。”

我当场就愣住了。

辣条也不嚼了,脑子里开始疯狂检索我这些年写过的表单代码。好像…确实没搞过这需求?平时都是直接赋值,谁管你光标在哪啊。但嘴上不能说不会啊,咱是前端,前端不能说不行,只能说好,我研究研究。

等我真坐下来开始撸代码的时候,才发现事情没那么简单。你以为input就是个框,往里怼值就完事了?天真了兄弟。这破玩意儿涉及到选区、焦点、浏览器兼容性、移动端键盘、输入法状态…坑多到能让你怀疑人生。

特别是当你发现不同浏览器对"光标位置"这四个字有各自独特的理解时,那种绝望感,就像你写好的CSS在IE里打开一样酸爽。

但需求已经接了,产品经理的期待眼神还在我脑海里挥之不去。没办法,撸起袖子干吧。这篇文章就是我踩了三天坑之后的心血总结,希望能让你少走点弯路。咱不整那些虚的,直接上干货。

先说结论:selectionStart和selectionEnd是你最好的朋友

在深入代码之前,我得先把这俩API给你讲明白,因为后面所有的操作都围着它俩转。

selectionStartselectionEnd是HTMLInputElement和HTMLTextAreaElement的原生属性。这俩货告诉你用户当前在输入框里选了啥,或者说光标在哪杵着。

// 先拿个input元素过来const input = document.getElementById('myInput');// 看看当前选区的起始位置 console.log(input.selectionStart);// 比如输出 5// 看看选区的结束位置  console.log(input.selectionEnd);// 比如输出 8

如果selectionStartselectionEnd的值一样,比如都是5,那就说明用户没在选文字,光标就孤零零地停在第5个字符后面。如果不一样,比如5和8,那就是从第5位选到了第8位,选中了3个字符。

这俩属性现代浏览器都支持,包括textarea和input[type=“text”]、input[type=“password”]这些。但注意啊,input[type=“number”]这货有点特殊,有些浏览器不让用这API,所以如果你要做数字输入框的光标插入,建议还是用type="text"然后自己校验数字,省得给自己找麻烦。

// 先判断一下支不支持,养成好习惯functionisSelectionSupported(element){returntypeof element.selectionStart ==='number'&&typeof element.selectionEnd ==='number';}// 用起来就放心多了const input = document.querySelector('input');if(isSelectionSupported(input)){// 放心大胆地干 console.log('光标在:', input.selectionStart);}else{// 老古董浏览器,直接往末尾追加吧,爱咋咋地 console.warn('这浏览器太老了,selection API都不支持');}

还有个selectionDirection,告诉你选区是从左往右选的还是从右往左选的,不过一般用不上,知道有这玩意就行。

核心代码其实就那几行

好了,原理讲完了,上正菜。在光标位置插入文本的核心逻辑就四步:

  1. 拿到当前光标位置(selectionStart)
  2. 把原字符串切成两半:光标前和光标后
  3. 中间插入新内容
  4. 把光标挪到插入内容后面,让用户能继续打字

看代码:

/** * 在光标位置插入文本 * @param {HTMLInputElement|HTMLTextAreaElement} input - 输入框元素 * @param {string} text - 要插入的文本 * @param {boolean} moveCursor - 是否移动光标到插入文本之后,默认true */functioninsertTextAtCursor(input, text, moveCursor =true){// 安全第一,先检查支持性if(!isSelectionSupported(input)){// 不支持的话直接追加到末尾,总比报错强 input.value += text;return;}// 记录当前光标位置const start = input.selectionStart;const end = input.selectionEnd;// 拿到当前值const originalValue = input.value;// 切成三段:前面 + 插入内容 + 后面const before = originalValue.substring(0, start);const after = originalValue.substring(end);// 注意这里用end,如果有选中文本要替换掉// 组装新值const newValue = before + text + after;// 赋值 input.value = newValue;// 关键步骤:重新设置光标位置if(moveCursor){const newCursorPos = start + text.length; input.selectionStart = newCursorPos; input.selectionEnd = newCursorPos;}// 触发input事件,让Vue、React这些框架知道值变了 input.dispatchEvent(newEvent('input',{bubbles:true}));}

就这么几行,但里面的门道不少。我逐行给你解释:

为什么要用substring(end)而不是substring(start)?

因为用户可能选中了文本。比如输入框里是"hello world",用户选中了"world"(start=6, end=11),这时候插入"Kimi",应该变成"hello Kimi",而不是"hello Kimiworld"。所以后半段要从end开始切,把选中的内容替换掉。

为什么要手动设置selectionStart和End?

因为直接给input.value赋值后,浏览器会把光标扔到末尾去。你想想,用户本来在中间打字,插了个@用户名,结果光标跑最后去了,还得手动点回来,这体验能忍?所以必须手动把光标挪到插入内容后面。

为什么要dispatchEvent?

因为现代框架(Vue、React、Angular)都监听input事件来做双向绑定。你直接改value,框架不知道值变了,数据不同步,后面会出各种诡异bug。手动触发个input事件,告诉框架:“嘿,我改了,你也更新一下”。

来,看个完整的例子:

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>光标插入演示</title><style>.container{max-width: 600px;margin: 50px auto;padding: 20px;}textarea{width: 100%;height: 150px;padding: 10px;font-size: 14px;}.btn-group{margin-top: 10px;}button{margin-right: 10px;padding: 8px 16px;cursor: pointer;}.info{margin-top: 10px;color: #666;font-size: 12px;}</style></head><body><divclass="container"><h3>在光标处插入文本演示</h3><textareaid="editor"placeholder="在这打字,然后点下面的按钮..."></textarea><divclass="btn-group"><buttononclick="insertEmoji('😀')">插入😀</button><buttononclick="insertEmoji('🎉')">插入🎉</button><buttononclick="insertEmoji('[图片]')">插入[图片]</button><buttononclick="insertAtUser()">@某人</button></div><divclass="info"> 当前光标位置: <spanid="cursorPos">0</span> | 选区: <spanid="selection">无</span></div></div><script>const editor = document.getElementById('editor');const cursorPosSpan = document.getElementById('cursorPos');const selectionSpan = document.getElementById('selection');// 实时显示光标位置,方便观察 editor.addEventListener('keyup', updateCursorInfo); editor.addEventListener('click', updateCursorInfo); editor.addEventListener('selectionchange', updateCursorInfo);// 部分浏览器支持functionupdateCursorInfo(){ cursorPosSpan.textContent =`${editor.selectionStart} - ${editor.selectionEnd}`;if(editor.selectionStart !== editor.selectionEnd){const selectedText = editor.value.substring(editor.selectionStart, editor.selectionEnd); selectionSpan.textContent =`选中"${selectedText}"`;}else{ selectionSpan.textContent ='无';}}functioninsertEmoji(emoji){insertTextAtCursor(editor, emoji);updateCursorInfo(); editor.focus();// 保持焦点}functioninsertAtUser(){// 模拟@功能,实际项目中可能是弹个选择框const userName ='张三';const insertText =`@${userName}`;// 后面加个空格,方便继续打字insertTextAtCursor(editor, insertText);updateCursorInfo(); editor.focus();}// 就是上面讲的那套逻辑functioninsertTextAtCursor(input, text){if(typeof input.selectionStart !=='number'){ input.value += text;return;}const start = input.selectionStart;const end = input.selectionEnd;const value = input.value; input.value = value.substring(0, start)+ text + value.substring(end);const newPos = start + text.length; input.selectionStart = newPos; input.selectionEnd = newPos;// 通知框架 input.dispatchEvent(newEvent('input',{bubbles:true}));}</script></body></html>

你把这段代码保存成html文件打开试试,在textarea里随便打点字,然后把光标放中间,点插入按钮,看看光标是不是乖乖待在插入内容后面。这就是我们要的效果。

不同浏览器给你整的幺蛾子

写前端最烦啥?兼容性问题。这功能在Chrome里跑得好好的,到Safari里可能就抽风,到IE里直接躺平。

Chrome/Edge/Firefox:这三个算是比较老实的,基本按W3C标准来,selection API支持得挺好。但也要注意版本,特别老的Chrome(比如60以下)可能会有点小毛病,不过现在应该没人用那么老的版本了。

Safari:苹果的浏览器总是有点自己的想法。桌面版Safari问题不大,但iOS Safari就精彩了。特别是当虚拟键盘弹出来的时候,光标位置可能会飘。有时候你明明在input.selectionStart读到了位置是5,但视觉上光标好像在第3位,这就是iOS的锅。

IE家族:IE11及以下版本直接不支持selectionStart/End。对,你没看错,直接undefined。所以如果你公司还要支持IE(比如某些国企、银行项目),得准备降级方案。

移动端输入法:这是个大坑。安卓机的第三方输入法(搜狗、百度、讯飞这些)在输入过程中会进入"合成状态"(composition),这时候selectionStart可能返回0或者不准。还有那种联想输入、滑动输入,都会导致光标位置混乱。

来,给你个兼容性更好的版本:

/** * 兼容性更好的光标插入函数 * 处理了IE、移动端、合成状态等情况 */functioninsertTextRobust(input, text){// 先聚焦,确保有光标 input.focus();// 现代浏览器if(typeof input.selectionStart ==='number'){const start = input.selectionStart;const end = input.selectionEnd;const value = input.value; input.value = value.substring(0, start)+ text + value.substring(end);// 设置新光标位置const newPos = start + text.length;// 用setTimeout确保在移动端键盘弹出后也能正确设置setTimeout(()=>{ input.selectionStart = input.selectionEnd = newPos;},0); input.dispatchEvent(newEvent('input',{bubbles:true}));return;}// IE6-10的降级方案,用TextRangeif(document.selection && document.selection.createRange){const range = document.selection.createRange(); range.text = text;// 直接替换选区文本// 移动光标到插入内容后面 range.collapse(false);// false表示移到末尾 range.select(); input.dispatchEvent(newEvent('input',{bubbles:true}));return;}// 终极降级:直接追加 input.value += text;}// 针对移动端的特殊处理functionisComposing(input){// 检查是否处于输入法合成状态return input.isComposing ===true;}// 监听合成事件,避免在拼音输入过程中插入 editor.addEventListener('compositionstart',()=>{ editor.isComposing =true;}); editor.addEventListener('compositionend',()=>{ editor.isComposing =false;});// 使用时判断一下functionsafeInsert(input, text){if(isComposing(input)){// 等合成结束再插入,或者提示用户 console.warn('正在输入中,请稍后再试');return;}insertTextRobust(input, text);}

看到没,就一个简单的插入功能,为了兼容各种奇葩环境,代码量直接翻倍。这就是前端开发的日常,表面上光鲜亮丽,背地里各种if-else判断浏览器。

实际项目里这些场景逃不掉

光讲API没意思,给你看看实际项目中哪些地方会用到这套技术。你会发现,原来那些看起来高大上的功能,底层都是这么个原理。

场景一:聊天框@人

这是最常见的需求了。微信、钉钉、飞书,哪个没有@功能?实现思路就是:

  1. 用户输入@或者点击@按钮
  2. 弹出成员选择列表
  3. 选择后,在光标位置插入"@用户名 "
  4. 用户名要高亮显示(这个得用contenteditable,普通input做不到高亮)
// 简化版@功能classAtFunctionality{constructor(inputElement){this.input = inputElement;this.atList =['张三','李四','王五','赵六'];}showAtList(){// 显示选择框,实际项目中可能是个弹层 console.log('可选成员:',this.atList);}selectUser(userName){const insertText =`@${userName}`;// 插入文本const start =this.input.selectionStart;const end =this.input.selectionEnd;const value =this.input.value;this.input.value = value.substring(0, start)+ insertText + value.substring(end);// 光标后移const newPos = start + insertText.length;this.input.setSelectionRange(newPos, newPos);// 触发事件this.input.dispatchEvent(newEvent('input',{bubbles:true}));this.input.focus();// 实际项目中这里还要记录@的用户ID,方便后续发消息时解析this.recordAtUser(userName, start);}recordAtUser(name, position){// 记录@信息,发消息时要用 console.log(`在位置${position}@了${name}`);}}// 使用const chatInput = document.getElementById('chatInput');const atFunc =newAtFunctionality(chatInput);// 监听@键 chatInput.addEventListener('keyup',(e)=>{if(e.key ==='@'){ atFunc.showAtList();}});

场景二:代码编辑器的自动补全

VS Code、CodePen这些编辑器,你打个<div,然后按Tab,自动给你补全成<div></div>,光标还在中间。这背后的逻辑也是操作selection。

functionautoComplete(input, before, after, placeholder =''){const start = input.selectionStart;const end = input.selectionEnd;const value = input.value;// 插入前后缀const newValue = value.substring(0, start)+ before + placeholder + after + value.substring(end); input.value = newValue;// 光标放到placeholder位置,方便用户直接输入const cursorStart = start + before.length;const cursorEnd = cursorStart + placeholder.length; input.setSelectionRange(cursorStart, cursorEnd); input.focus(); input.dispatchEvent(newEvent('input',{bubbles:true}));}// 使用示例:HTML标签补全 document.getElementById('codeEditor').addEventListener('keydown',(e)=>{if(e.key ==='Tab'){ e.preventDefault();// 阻止默认的失焦行为const input = e.target;const before = input.selectionStart;const currentWord =getCurrentWord(input);// 假设有个函数能拿到当前单词if(currentWord ==='div'){autoComplete(input,'<div>','</div>','内容');}elseif(currentWord ==='span'){autoComplete(input,'<span>','</span>','');}}});

场景三:表单自动格式化

比如手机号输入,自动加空格分隔:138 1234 5678。或者银行卡号,每四位一个空格。这种需求也是光标插入的变种,只不过插入的是空格或者横杠。

functionformatPhoneNumber(input){const value = input.value.replace(/\s/g,'');// 去掉所有空格let formatted ='';// 按规则插入空格if(value.length >3){ formatted += value.substring(0,3)+' ';if(value.length >7){ formatted += value.substring(3,7)+' '+ value.substring(7);}else{ formatted += value.substring(3);}}else{ formatted = value;}// 记录光标相对位置const oldPos = input.selectionStart;const beforeLength = input.value.length; input.value = formatted;// 计算新光标位置(考虑新增的空格)const addedSpaces = formatted.length - beforeLength;const newPos = oldPos + addedSpaces; input.setSelectionRange(newPos, newPos);}

场景四:富文本编辑器

Draft.js、Quill、TinyMCE这些富文本编辑器,底层也是操作selection和range,只不过它们操作的是DOM节点而不是纯文本。原理是一样的:获取选区、插入内容、恢复选区。

场景五:评论区插入表情

这个和@人很像,只不过插入的是表情符号或者图片占位符。

functioninsertEmoji(input, emojiCode){// emojiCode可能是":smile:"这种,需要转换成实际字符😀const emojiMap ={':smile:':'😀',':cry:':'😢',':heart:':'❤️'};const emoji = emojiMap[emojiCode]|| emojiCode;// 插入insertTextAtCursor(input, emoji);// 可以加个动画效果,让用户知道插入了showInsertAnimation(input);}functionshowInsertAnimation(input){// 视觉反馈,比如闪一下 input.style.backgroundColor ='#fff3cd';setTimeout(()=>{ input.style.backgroundColor ='';},200);}

踩坑之后的血泪排查经验

写了这么多年代码,踩的坑比吃的盐还多。这里分享几个我血淋淋的教训,希望你别重蹈覆辙。

坑一:异步更新导致光标位置不对

有次我在React里用setState更新值,然后立刻设置光标位置,结果发现光标总是不对。后来才反应过来,setState是异步的,值还没更新到DOM里,我就去设置光标了,当然有问题。

// 错误示范consthandleInsert=(text)=>{const newValue = value.slice(0, cursor)+ text + value.slice(cursor);setValue(newValue);// 异步的!// 这时候DOM还没更新,设置光标无效 inputRef.current.setSelectionRange(newPos, newPos);};// 正确做法:用useEffect监听值变化后再设置光标useEffect(()=>{if(inputRef.current && pendingCursorPos !==null){ inputRef.current.setSelectionRange(pendingCursorPos, pendingCursorPos);setPendingCursorPos(null);}},[value]);// 或者用setTimeout hack一下(不推荐,但应急可用)setTimeout(()=>{ input.setSelectionRange(newPos, newPos);},0);

坑二:value改了但光标没动

有时候你直接input.value = 'xxx',发现光标跑到最前面去了。这是因为赋值操作会重置选区。记得赋值后手动设置selectionStart和End。

坑三:移动端focus之后键盘弹起,光标跑到开头

iOS Safari特别容易出现这问题。用户点击输入框,键盘弹起来,这时候你插入文本,光标可能在0位置而不是用户之前的位置。

解决方案是:在插入前记录光标位置,插入后用setTimeout延迟设置光标。

functioninsertOnMobile(input, text){// 先记录const savedStart = input.selectionStart;const savedEnd = input.selectionEnd;// 插入const value = input.value; input.value = value.substring(0, savedStart)+ text + value.substring(savedEnd);// 延迟设置,等键盘稳定requestAnimationFrame(()=>{const newPos = savedStart + text.length; input.setSelectionRange(newPos, newPos);});}

坑四:用setRangeText可以简化代码

其实现代浏览器提供了setRangeText方法,可以一行代码搞定插入:

input.setRangeText(text, start, end,'end');// 第四个参数'end'表示插入后光标在插入文本之后// 还可以选'start'(在之前)、'select'(选中插入的文本)、'preserve'(保持原样)

这个方法的好处是原子操作,不会触发多余的事件,而且自动处理光标。但兼容性比手动操作稍差,IE全系列不支持,部分移动端浏览器也不支持。所以如果你想用,得准备好降级方案。

functionmodernInsert(input, text){if(typeof input.setRangeText ==='function'){// 现代浏览器,一行搞定 input.setRangeText(text, input.selectionStart, input.selectionEnd,'end'); input.dispatchEvent(newEvent('input',{bubbles:true}));}else{// 降级到手动操作insertTextAtCursor(input, text);}}

坑五:CSS样式影响光标视觉位置

有次我遇到个诡异问题:代码逻辑没错,光标位置设置也对,但用户就是觉得光标位置不对。查了半天,发现是字体搞的鬼。输入框用了等宽字体,但计算位置时按普通字体算的,导致视觉偏差。

还有letter-spacingtext-indent这些样式,都会影响光标的实际显示位置。如果你发现光标位置对不上,先检查CSS,别死磕JS代码。

坑六:控制台打印selectionStart看看是不是你期望的值

调试技巧:遇到光标问题,先在控制台打印input.selectionStartinput.selectionEnd,看看值是不是你预期的。有时候你以为光标在第5位,实际在第0位,这时候插入当然不对。

// 调试代码,随时看看光标在哪setInterval(()=>{const input = document.activeElement;if(input && input.tagName ==='TEXTAREA'){ console.log('Cursor at:', input.selectionStart,'Selection:', input.value.substring(input.selectionStart, input.selectionEnd));}},2000);

让代码更优雅的几条野路子

基础功能实现了,但代码写得太乱以后维护是灾难。这里分享几个让代码更优雅的思路。

封装成工具函数,别到处复制粘贴

把插入逻辑封装成一个独立的工具函数,项目里哪里需要哪里调。别在每个组件里都写一遍那几行切割字符串的代码,万一要改逻辑,得改十几个地方,漏一个就bug。

// utils/cursor.jsexportconst CursorUtils ={/** * 在光标处插入文本 */insertText(element, text, options ={}){const{ moveCursor =true, replaceSelection =true,// 是否替换选中的文本 triggerInput =true// 是否触发input事件}= options;if(!this.isSupported(element)){ element.value += text;return;}const start = element.selectionStart;const end = element.selectionEnd;const value = element.value;const before = value.substring(0, start);const after = replaceSelection ? value.substring(end): value.substring(start); element.value = before + text + after;if(moveCursor){const newPos = start + text.length;this.setCursorPosition(element, newPos);}if(triggerInput){this.triggerInputEvent(element);}return{newValue: element.value,cursorPosition: start + text.length };},/** * 设置光标位置 */setCursorPosition(element, position){if(typeof element.setSelectionRange ==='function'){ element.setSelectionRange(position, position);}elseif(element.createTextRange){// IEconst range = element.createTextRange(); range.move('character', position); range.select();}},/** * 获取当前选区文本 */getSelectedText(element){if(!this.isSupported(element))return'';return element.value.substring(element.selectionStart, element.selectionEnd);},/** * 替换选区文本 */replaceSelection(element, text){returnthis.insertText(element, text,{replaceSelection:true});},/** * 检查是否支持Selection API */isSupported(element){returntypeof element.selectionStart ==='number';},/** * 触发input事件 */triggerInputEvent(element){const event =newEvent('input',{bubbles:true}); element.dispatchEvent(event);}};// 使用import{ CursorUtils }from'./utils/cursor';// 在组件里直接调用 CursorUtils.insertText(myInput,'@张三 ');

React里怎么处理

React的受控组件有点特殊,因为值存在state里,直接操作DOM会被React的渲染覆盖。

import React,{ useRef, useState, useEffect }from'react';functionSmartInput(){const[value, setValue]=useState('');const[cursorPosition, setCursorPosition]=useState(null);const inputRef =useRef(null);// 监听cursorPosition变化,设置真实光标useEffect(()=>{if(cursorPosition !==null&& inputRef.current){ inputRef.current.setSelectionRange(cursorPosition, cursorPosition);setCursorPosition(null);// 重置}},[cursorPosition, value]);consthandleInsert=(insertText)=>{const input = inputRef.current;const start = input.selectionStart;const end = input.selectionEnd;// 计算新值const newValue = value.substring(0, start)+ insertText + value.substring(end);// 计算新光标位置const newPos = start + insertText.length;// 更新statesetValue(newValue);setCursorPosition(newPos);// 记录,等useEffect里设置};return(<input ref={inputRef} value={value} onChange={(e)=>setValue(e.target.value)}/>);}

Vue里怎么处理

Vue的v-model也会干扰,建议用ref直接操作DOM,绕过v-model的语法糖。

<template> <textarea ref="myInput" v-model="content"></textarea> <button @click="insertAtCursor">插入</button> </template> <script> export default { data() { return { content: '' }; }, methods: { insertAtCursor() { const input = this.$refs.myInput; const text = '[插入的内容]'; // 直接操作DOM,Vue会检测到变化并同步到data const start = input.selectionStart; const end = input.selectionEnd; this.content = this.content.substring(0, start) + text + this.content.substring(end); // 等DOM更新后设置光标 this.$nextTick(() => { const newPos = start + text.length; input.setSelectionRange(newPos, newPos); input.focus(); }); } } }; </script> 

批量插入的性能优化

如果你要一次性插入很多内容(比如粘贴一大段文本,里面包含多个需要自动替换的标记),别每次都操作DOM,攒一波再更新。

functionbatchInsert(input, insertions){// insertions是个数组,[{position: 5, text: 'xxx'}, {position: 10, text: 'yyy'}]// 注意要按位置倒序排序,这样插入不会影响后面的位置const sorted = insertions.sort((a, b)=> b.position - a.position);let value = input.value; sorted.forEach(({position, text})=>{ value = value.substring(0, position)+ text + value.substring(position);}); input.value = value;// 最后设置一次光标const lastInsertion = sorted[sorted.length -1];const finalPos = lastInsertion.position + lastInsertion.text.length; input.setSelectionRange(finalPos, finalPos); input.dispatchEvent(newEvent('input',{bubbles:true}));}

加点动画让用户感知

插入内容后,可以加个微小的动画,让用户明确知道发生了什么。特别是插入的是表情、图片这种非文本内容时。

functioninsertWithAnimation(input, text){// 先插入insertTextAtCursor(input, text);// 高亮一下刚插入的内容(需要包装成span,普通input做不到,textarea也做不到,得用contenteditable)// 这里简单做个输入框闪烁 input.animate([{backgroundColor:'transparent'},{backgroundColor:'#fff3cd'},{backgroundColor:'transparent'}],{duration:300,iterations:1});}

最后说点掏心窝子的话

写到这儿,估计你也看出来了,就一个"在光标处插入文本"的小功能,真要做好了,里面门道多着呢。这大概就是前端开发的日常吧,看似简单的东西,深挖下去全是坑。

我刚开始接到这个需求的时候,以为半小时搞定,结果折腾了三天。第一天写基础功能,第二天处理浏览器兼容性,第三天解决移动端的各种诡异问题。特别是iOS Safari,那光标位置飘忽不定的劲儿,差点让我怀疑人生。

所以啊,几个建议送给你:

多换几个浏览器测试。别只在你心爱的Chrome里测完就完事了,至少得看看Safari、Firefox、Edge。如果用户群体里有用IE的(虽然2024年了应该很少了),那还得准备降级方案。

移动端一定要真机测试。模拟器不靠谱,特别是涉及键盘、光标的交互,必须在真机上点一点。安卓机多测几个品牌,华为、小米、OPPO、vivo,它们的系统定制程度不同,表现也可能不一样。

代码写好注释。你现在觉得逻辑很清晰,三个月后回来看,绝对一脸懵逼。特别是那些兼容性处理的代码,为什么要加setTimeout,为什么要判断isComposing,写清楚,不然以后自己都想抽自己。

MDN文档比你想象的有用。遇到不确定的API行为,别瞎猜,去MDN查。selection API的文档写得很详细,还有各种浏览器兼容性表格,能省你很多试错时间。

找个成熟库也不丢人。如果你做的功能很复杂,比如完整的@功能、富文本编辑,别硬自己写,找个成熟的库(比如Tribute.js做@功能,Quill做富文本)。早点下班不香吗?老板又不会因为你用了库而扣你工资,他只看功能能不能用。

好了,就写到这儿吧。希望这篇文章能帮你少踩几个坑。如果看完你还是搞不定…那要不考虑转后端?开玩笑的,前端虐我千百遍,我待前端如初恋。加油,兄弟!

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!

专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!
在这里插入图片描述

Read more

75元!复刻Moji 2.0 小智 AI 桌面机器人,基于乐鑫ESP32开发板,内置DeepSeek、Qwen大模型

文末联系小编,获取项目源码 Moji 2.0 是一个栖息在你桌面上的“有灵魂的伴侣”,采用乐鑫 ESP32-C5开发板,配置 1.5寸 360x360 高清屏,FPC 插接方式,支持 5G Wi-Fi 6 极速连接,内置小智 AI 2.0 系统,主要充当智能电子宠物的角色,在你工作学习枯燥时,通过圆形屏幕上的动态表情包卖萌解压,提供情绪陪伴;同时它也是功能强大的AI 语音助手,支持像真人一样流畅的连续对话,随时为你查询天气、解答疑惑或闲聊解闷,非常适合作为极客桌搭或嵌入式学习的开源平台。 🛠️ 装配进化 告别手焊屏幕的噩梦。全新设计的 FPC 插座连接,排线一插即锁,将复刻门槛降至最低。 🚀 性能进化 主控升级为 ESP32-C5。支持 5GHz Wi-Fi 6,

小龙虾配置飞书机器人(适合本地部署)

小龙虾配置飞书机器人(适合本地部署)

🚀 OpenClaw 手把手教学:配置飞书机器人 📖 目录 1. 前置准备 2. 创建飞书应用 3. 配置机器人能力 4. 获取必要凭证 5. 配置 OpenClaw 6. 测试机器人 前置准备 在开始之前,请确保你具备以下条件: ✅ 必需条件 * 飞书管理员权限 * 需要创建企业自建应用的权限 * 或联系管理员协助创建 OpenClaw 已安装 # 检查是否已安装 openclaw --version 📋 准备清单 * OpenClaw 已安装并运行 * 有飞书企业管理员权限 * 基本的命令行操作能力 创建飞书应用 步骤 1:进入飞书开放平台 1. 打开浏览器,访问 飞书开放平台 2. 使用��书账号登录 点击右上角 “开发者后台” 步骤 2:创建企业自建应用

XILINX PCIE IP核详解、FPGA实现及仿真全流程(Virtex-7 FPGA Gen3 Integrated Block for PCI Express v4.3)

XILINX PCIE IP核详解、FPGA实现及仿真全流程(Virtex-7 FPGA Gen3 Integrated Block for PCI Express v4.3)

一、XILINX几种IP核区别         传统系列芯片 IP核名称核心特点用户接口开发难度适用场景7 Series Integrated Block for PCI Express最基础的PCIe硬核,提供物理层和数据链路层AXI4-Stream TLP包最高,需处理TLP包需深度定制PCIe通信,对资源敏感的项目AXI Memory Mapped To PCI Express桥接IP,将PCIe接口转换为AXI接口AXI4内存映射中等,类似操作总线FPGA需主动读写主机内存,平衡效率与灵活性DMA/Bridge Subsystem for PCI Express (XDMA)集成DMA引擎,提供"一站式"解决方案AXI4 (另有AXI-Lite等辅助接口)最低,官方提供驱动高速数据批量传输(如采集卡),追求开发效率         注意:         1.硬件平台限制:不同系列的Xilinx FPGA(如7系列、UltraScale、Versal)支持的PCIe代数和通道数可能不同。在选择IP核前,请务必确认您的FPGA型号是否支持所需的PCIe配置(

智能客服对话机器人设计全流程:从架构设计到生产环境部署

最近在做一个智能客服项目,从零开始搭建一个能实际处理用户问题的对话机器人,踩了不少坑,也积累了一些经验。今天就来聊聊从架构设计到最终部署上线的全流程,希望能给有类似需求的开发者一些参考。 1. 背景与痛点:为什么需要智能客服? 传统的客服系统,无论是电话热线还是在线聊天,主要依赖人工坐席。这种方式有几个明显的痛点: * 人力成本高:7x24小时服务需要三班倒,人力成本巨大。 * 响应速度慢:高峰期排队严重,用户体验差。 * 服务质量不稳定:不同客服的业务熟练度和服务态度参差不齐。 * 知识难以沉淀:优秀的客服经验很难系统化地传承和复用。 而早期的“智能”客服,很多是基于关键词匹配的规则引擎。比如用户说“我要退款”,系统就回复一个预设的退款流程链接。这种方案的局限性非常大: * 理解能力弱:无法处理同义词、口语化表达和上下文关联。用户说“钱怎么退”和“我要退款”,在规则引擎里可能就是两条完全不同的规则。 * 维护成本高:业务规则一变,就需要人工添加大量新规则,容易产生规则冲突。 * 毫无灵活性:对话僵硬,无法进行多轮交互,用户体验像在和“人工智障”聊天。 正是这