关于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…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理