什么是at性能
所谓的at性能,就是指的在聊天框中输出人的姓名等信息时,容许用户在输出"@"字符之后,能够调起一个选人控件,不便用户疾速输出人名。
例如:微博输入框,QQ空间的说说输入框。咱们能够在一个输入框内输出 "@" 字符,而后会调起一个选人浮层或全屏选人控件(在桌面端通常是个浮层,在挪动端通常是一个全屏控件)。
at性能的需要差别
在接到 at 性能的需要时,咱们须要首先确定一个问题:即咱们at进去的人名,是否有重名的景象。即:在输入框中同时呈现 "@abc" 和 "@abc" 两个人名时,这俩人是否必然代表同一个人。
像新浪微博的场景下,其at进去的微博账户,必然是惟一的,因而其技术实现计划便可简化为:只需将用户从选人控件中抉择的人渲染到输入框即可
。
而像QQ空间等需要场景下,咱们at选出来的一个用户昵称,实际上是能够重名的,这时,咱们的技术计划必须思考到:如何将一个输入框中的人名要跟他对应的账户信息一一映射起来
。只有这样,当咱们把用户输出的音讯保留给后盾时,能力清晰的还原出两个"@abc"别离是哪个人。
如下是qq空间输入框,我能够输出2个同名的人,他别离能够给我两个不同的好友发送at音讯:
本文,我所探讨的是QQ空间这种可重名的场景。因而,咱们的技术计划须要思考如何将at人名与账户信息记录映射起来。
小程序: 难为无米之炊
在 web 端,通常咱们应用 div 配合 contentediable,另外再配合 Range和Selection的光标管制 api 来实现相似聊天框里的 at 性能。
其中:
- contenteditable api容许咱们将html标签插入到编辑器当中,这样咱们便能够将账户信息“塞到”标签里,从而在提交后盾时,从标签里把账户信息还原进去
- range api提供了管制光标选取和设置选取内容的能力,这容许咱们在用户选完控件人名后,咱们将at字符删除,并把新抉择的人名标签渲染到输入框里。
可是 小程序的输入框input和textarea并没有 web 那么多弱小的 API (比方 Range和Selection),也无奈在小程序内应用 contenteditable实现富文本。小程序仅有一个 bindinput 事件:
其事件返回3个参数:
- value: 以后输入框中最新的值。
- cursor: 以后光标的地位
- keyCode:以后输出事件的键盘按键
思路
咱们要在一个如此 奢侈
的输入框,利用仅有的 value\cursor\keyCode
3个参数实现 "at检测"、"人名渲染"、"删除检测"、"重名反对(即账户信息还原)"。 其最大的难点次要还是如何记录账户信息从而反对重名。
能想到的计划有3种:
- 每当输出at字符并抉择了人名后,咱们在另外一个数据结构中记录该账户信息。例如维持一个
persons: []
数组。 然而咱们要在用户对输入框内容进行增删改查
的同时对咱们的persons构造进行同步增删改查的更新,其批改难度会很简单。 - 咱们想,是否在at人名填入到输入框的时候,把账户信息塞到输入框里。就像contenteditable一样。因而,咱们能够尝试应用一些不可见的字符,用这些字符来表白某个账户信息的标识。 但这种计划,想想就简单,例如咱们删除一个人名时是否能同步把不可见字符也删掉,是否会有光标问题都不好说。
- 采纳了一种虚构层的思路。当用户输出任何字符时,咱们都对用户输出进行拦挡,拦挡到输出后,首先依照需要更新咱们外部
虚构层
的数据结构,在虚构层中咱们按肯定的构造保留好用户账户信息数据,而后将其渲染成文本再填到输入框中。
最终,我抉择了采纳第三种计划实现,计划如图:
调用办法
虚构层外部具体的计算逻辑封装到了 RichMessage 类中。小程序组件中,首先给input输入框绑定 input 事件:
<input placeholder="请输出" bindinput="eventInput" />
组件脚本中:
{ data: { inputContent: '', }, attached() { this.myCommentRichMessage = new RichMessage(); }, methods: { eventInput(e) { const res = this.myCommentRichMessage.doInput(event.detail); // 输出完之后,从新渲染input内容 res.then(str => { if (typeof str === 'string') { this.setData({ inputContent: str }); } }); } }}
当要把数据提交给后盾时,能够调用 toProto 办法,将音讯转成具体的数据结构:
const pbdata = myCommentRichMessage.transformToProto()
实现
RichMessage类
负责依据输出的光标和value,计算出是在什么地位新增或删除了字符。并负责保护虚构层的音讯盒子---msgbox。
- 当用户新增字符,则批改或新增音讯盒子中具体位置的音讯数据结构。
- 当用户删除字符,则删除音讯盒子中对应地位的字符数据
const MessageBox = require('./MessageBox');class RichMessage { constructor(options) { options = options || {}; this._msgBox = new MessageBox(); } doInput(inputInfo) { const { keyCode } = inputInfo; // 做下判断,避免鼠标或手机键盘移开时触发的input事件(keyCode是undefined) if (isNaN(keyCode)) return Promise.resolve(inputInfo); if (keyCode == 8) { return this.removeOneCharactor(inputInfo); } else { return this.typeOneCharactor(inputInfo); } }}module.exports = RichMessage;
音讯盒子实现
音讯盒子负责具体实现两种类型音讯的治理:纯文本音讯和at音讯。其必须实现以下3个api:
- addCharactor办法。在pos地位新增一个字符,并重构以后虚构层数据结构
- deleteCharactor办法。在pos地位删除一个字符,并重构以后虚构层数据结构
- print办法。将整个虚构层各个音讯全副渲染,失去一份残缺的纯文本
外部实现会有些简单,更多代码请查看github。例如:
- 增加字符时会波及到当
确定pos地位是新增还是批改现有音讯
、pos地位插入at音讯,是否要将某个文本音讯切分成两半
等状况的解决。 - 删除字符时,若是删除的at音讯则要将整个at音讯体删除
- 每次音讯改变后,要像整顿'内存'碎片一样,对音讯进行正当的合并解决(例如两个相邻的文本音讯要合并)
const TextMessage = require('./TextMessage');const AtMessage = require('./AtMessage');class MessageBox { constructor() { this._msgs = []; } // 向pos地位减少一般文本字符 addCharactor(pos, char) { // 筹备要add的音讯 const getNewMsg = this._getNewMsg(char); return getNewMsg.then(newMsg => { // 找到要搁置的地位 let countPos = 0; let findedMsg = null; let findedMsgIndex = -1; for (let i = 0, len = this._msgs.length; i < len; i++) { const msg = this._msgs[i]; const msgRenderLen = msg.render().length; if ((pos >= countPos) && (pos <= (countPos + msgRenderLen - 1))) { // 要操作的地位正好处于该音讯构造中 findedMsg = msg; findedMsgIndex = i; break; } countPos += msgRenderLen; } if (findedMsg) { // 找到了音讯msg,则把新msg塞到该msg构造里 this._mergeMsg(findedMsgIndex, newMsg, pos - countPos); } else { // 没找到音讯块,那就是放到box开端新增 this._msgs.push(newMsg); } // 音讯碎片整顿--即把同类型音讯合并(例如两个挨着的textmessage则用一个示意即可) this._defragmentation(); return this.print(); }); } // 删除start到end间的字符(蕴含end本身) deleteCharactor(start, end) { const findedMsgIndex = []; const findedMsgPos = []; let countPosStart = 0; for (let i = 0, len = this._msgs.length; i < len; i++) { const msg = this._msgs[i]; const msgRenderLen = msg.render().length; const countPosEnd = (countPosStart + msgRenderLen) - 1; if (end >= countPosStart && start <= countPosEnd) { findedMsgIndex.push(i); // 找出此msg里的交加坐标 const msgPosStart = Math.max(countPosStart, start); const msgPosEnd = Math.min(countPosEnd, end); findedMsgPos.push({ startPos: msgPosStart - countPosStart, endPos: msgPosEnd - countPosStart }); } countPosStart += msgRenderLen; } // 对找到的msg顺次进行删除的操作 (若是at信息,则整个都删掉;若是一般字符,则只删对应坐标的字符;若删除后整个msg变空了,则在碎片整顿时移除) if (findedMsgIndex && findedMsgIndex.length > 0) { findedMsgIndex.forEach((findedIndex, index) => { const msg = this._msgs[findedIndex]; if (msg.type === 'text') { const deletePos = findedMsgPos[index]; msg.removeChars(deletePos.startPos, deletePos.endPos); } if (msg.type === 'at') { this._msgs.splice(findedIndex, 1); } }); } this._defragmentation(); } // 输入以后所有 msg 构造转为可视字符串后的残缺字符串 print() { let str = ''; str = this._msgs.reduce((last, cur) => { return last += cur.render(); }, ''); return str; }}module.exports = MessageBox;
残缺代码
残缺代码看下github吧:
https://github.com/cuiyongjia...