什么是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种:

  1. 每当输出at字符并抉择了人名后,咱们在另外一个数据结构中记录该账户信息。例如维持一个 persons: [] 数组。 然而咱们要在用户对输入框内容进行 增删改查 的同时对咱们的persons构造进行同步增删改查的更新,其批改难度会很简单。
  2. 咱们想,是否在at人名填入到输入框的时候,把账户信息塞到输入框里。就像contenteditable一样。因而,咱们能够尝试应用一些不可见的字符,用这些字符来表白某个账户信息的标识。 但这种计划,想想就简单,例如咱们删除一个人名时是否能同步把不可见字符也删掉,是否会有光标问题都不好说。
  3. 采纳了一种虚构层的思路。当用户输出任何字符时,咱们都对用户输出进行拦挡,拦挡到输出后,首先依照需要更新咱们外部虚构层的数据结构,在虚构层中咱们按肯定的构造保留好用户账户信息数据,而后将其渲染成文本再填到输入框中。

最终,我抉择了采纳第三种计划实现,计划如图:

调用办法

虚构层外部具体的计算逻辑封装到了 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...