乐趣区

关于html5:无米之炊-小程序内实现一个具有功能at功能的输入框

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

退出移动版