什么是 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…