at功能是什么?输入“@”字符后,可以调用人物选择控件,方便用户快速输入人物姓名。比如:微博输入框,QQ空间聊输入框。我们可以在输入框中输入“@”字符,然后会调出一个选区浮层或者全屏选区控件(一般桌面端是浮层,移动端是全屏控件).本文介绍了在一个小程序中实现@函数的过程。Web版的实现在另一篇文章中介绍了基于contenteditable技术实现@selection功能。at函数的需求差异在接到at函数的需求时,我们首先要判断一个问题:我们at出来的人的名字是否重名。即:当“@abc”和“@abc”这两个名字同时出现在输入框中时,这两个人是否一定代表同一个人。以新浪微博为例,其at出来的微博账号必须是唯一的,所以其技术实现可以简化为:只需要将用户选择的人物从人物选择控件渲染到输入框即可。在QQ空间等需求场景中,我们at选择的用户昵称其实可以重名。这时候我们的技术方案就必须考虑:如何将一个输入框里的人名和他的Account信息一一映射起来。只有这样,当我们将用户输入的消息保存到后台时,才能清楚的还原出这两个“@abc”是谁。下面是QQ空间输入框,我可以输入2个同名的人,他们可以给我两个不同的好友发送at消息:本文讨论的是同名可以重复的场景QQ空间。因此,我们的技术方案需要考虑如何映射at名称和账户信息记录。小程序:无米之炊在web端,我们通常使用div配合contentediable,再配合Range和Selection的光标控制api,实现类似聊天框的at功能。其中:contenteditableapi允许我们在编辑器中插入html标签,这样我们就可以将账户信息“塞”进标签中,这样后台提交时,账户信息就从标签中恢复出来。rangeapi提供了控件光标选择和设置选择内容的能力,它允许我们在用户选择控件名称后删除at字符,并将新选择的名称标签渲染到输入框中。但是小程序的输入框input和textarea没有web那么多强大的API(比如Range和Selection),不可能在小程序中使用contenteditable来实现富文本。小程序只有一个bindinput事件:该事件返回3个参数:value:当前输入框的最新值。cursor:当前光标的位置keyCode:当前输入事件的键盘按键思路我们需要用到只有value\cursor\keyCode这三个参数来实现“检测时”、“人名渲染”和“删除”“在这么简单的输入框检测”,“重名支持(即账户信息还原)”。最大的难点主要是如何记录账户信息来支持重名。可以想到的方案有3种:每当输入at字符,选择人名,我们将账户信息记录在另一个数据结构中,比如维护一个persons:[]数组,但是我们需要在增删改查的同时更新我们的persons结构体输入框的内容,修改难度会很复杂,我们想知道是否可以在输入框填写at人姓名时,将账户信息插入输入框。就像contenteditable。因此,我们可以尝试使用一些不可见的字符来表示对某个账户信息的标识。但是这种解法想想就复杂。比如我们删除一个人的名字的时候,能不能同时删除不可见的字符,会不会出现游标的问题就不好说了。采用了虚拟层的思想。当用户输入任何字符时,我们拦截用户输入。拦截输入后,我们首先根据需求更新我们内部虚拟层的数据结构。在虚拟层中,我们将用户账户信息数据按照一定的结构进行保存,然后将其渲染为文本填充到输入框中。最终我选择实现方案三,如图:调用方法虚拟层内部的具体计算逻辑封装在RichMessage类中。在小程序组件中,首先给input输入框绑定input事件:在组件脚本中:{这个.myCommentRichMessage=newRichMessage();},方法:{eventInput(e){constres=this.myCommentRichMessage.doInput(event.detail);//输入后,重新渲染输入内容res.then(str=>{if(typeofstr==='string'){this.setData({inputContent:str});}});}}}在向后台提交数据时,可以调用toProto方法将消息转换成具体的数据结构:constpbdata=myCommentRichMessage.transformToProto()实现RichMessage类负责计算根据哪里添加或删除字符在输入光标和值上。并负责维护虚拟层的消息框---msgbox。当用户添加新字符时,修改或添加消息框中特定位置的消息数据结构。当用户删除一个字符时,删除消息框对应位置的字符数据constMessageBox=require('./MessageBox');classRichMessage{constructor(options){options=options||{};this._msgBox=newMessageBox();}doInput(inputInfo){const{keyCode}=inputInfo;//判断防止鼠标或移动键盘移开时触发输入事件(keyCode未定义)if(isNaN(keyCode))returnPromise.resolve(inputInfo);如果(keyCode==8){返回this.removeOneCharactor(inputInfo);}else{returnthis.typeOneCharactor(inputInfo);}}}module.exports=RichMessage;消息框负责实现两种类型的消息管理:纯文本消息和at消息。它必须实现以下3个API:addCharactor方法。在pos位置添加一个新字符,重构当前虚拟层数据结构的deleteCharactor方法。删除pos位置的一个字符,重构当前虚拟层数据结构的打印方法。渲染整个虚拟层的所有消息以获得完整的纯文本内部实现会有些复杂。更多代码请查看github。例如:添加字符时,会涉及到判断pos位置是添加还是修改已有的消息,pos位置是否插入到at消息中,某条短信是否要一分为二等。删除字符时,如果删除at消息,则必须删除整个at消息体。每次消息变化后,要像整理‘内存’碎片一样合理合并消息(比如相邻的两条短信需要合并)constTextMessage=require('./TextMessage');constAtMessage=require('./AtMessage');classMessageBox{constructor(){this._msgs=[];}//添加普通文本字符到pos位置addCharactor(pos,char){//准备要添加的消息constgetNewMsg=this._getNewMsg(char);returngetNewMsg.then(newMsg=>{//找到要放置的位置letcountPos=0;letfoundMsg=null;letfoundMsgIndex=-1;for(leti=0,len=this._msgs.length;i=countPos)&&(pos<=(countPos+msgRenderLen-1))){//要操作的位置正好在消息结构中foundMsg=msg;foundMsgIndex=i;break;}countPos+=msgRenderLen;}if(findedMsg){//如果找到消息msg,则将新的msg插入到msg结构中this._mergeMsg(foundMsgIndex,newMsg,pos-countPos);}else{//如果没有找到消息块,那么在消息块的末尾添加this._msgs.push(newMsg)即可;}//消息碎片整理——即合并相同类型的消息(例如,两个相邻的文本消息可以用一个表示)this._defragmentation();返回this.print();});}//删除从开始到结束的字符(包括结束本身)deleteCharactor(start,end){constfoundMsgIndex=[];constfoundMsgPos=[];让countPosStart=0;for(leti=0,len=this._msgs.length;i=countPosStart&&start<=countPosEnd){foundMsgIndex.push(i);//找到这个msg中的交点坐标constmsgPosStart=Math.max(countPosStart,start);constmsgPosEnd=Math.min(countPosEnd,end);foundMsgPos.push({startPos:msgPosStart-countPosStart,endPos:msgPosEnd-countPosStart});}countPosStart+=msgRenderLen;}//依次删除找到的msg(如果是atinformation,则全部删除;如果是普通字符,只删除对应坐标的字符;如果删除后整条msg变空,则在碎片整理时删除)if(foundMsgIndex&&foundMsgIndex.length>0){foundMsgIndex.forEach((foundIndex,index)=>{constmsg=this._msgs[foundedIndex];if(msg.type==='text'){constdeletePos=foundMsgPos[索引];msg.removeChars(deletePos.startPos,deletePos.endPos);}if(msg.type==='at'){this._msgs.splice(foundIndex,1);}});}这个._碎片整理();}//将当前所有msg结构转换为可见字符串后输出完整的字符串print(){letstr='';str=this._msgs.reduce((last,cur)=>{returnlast+=cur.render();},'');返回海峡;}}module.exports=MessageBox;完整代码完整代码查看github:https://github.com/cuiyongjia...