用javascript写一个emoji表情插件

很久没有写文章了,说实话本人现在受困于五月病已经快变成一条死咸鱼了(T_T),本次就当写一个简单的js插件教程了。本项目的代码相对比较简单,至于里面有些变量命名的问题就请你们不要吐槽了Σ(゚д゚lll)(好的,我承认我英语就小学水平好吧。除了hello和goodbye其他的都不会了____orz)。 废话就讲到这里,下面开始正文。

demo: 我是demo
git : 我是项目git
下载地址: 点我下载

1. 事前准备

事实上在写一个插件前我们都需要事先想好你要实现哪些功能,怎么去实现,这些大方向的东西是需要事先考虑的,至于具体细节和优化选项我们可以在写代码的过程中再进行修改。

就以我们写的这个emoji插件为例,网上已经有一些相关的插件了,但你总感觉有些部分的需求不能被满足(如:可以自行添加新的表情包而不用去改源代码等等),这时我们就可以列出你想实现的功能项了:

  1. 需要满足基本的表情插件的需求,包括图片和对应code的相互转换

  2. 希望可以通过参数来调整每行以及每列表情图片的显示个数,并且可以针对不同表情包单独调整

  3. 希望用户可以在不了解源代码的情况下也能自行主动添加新的表情包

  4. 模板界面简单,可以进行自适应,并且兼容移动端

  5. 尽可能只提供简单的api接口和方法,避免内部涉及其他不是很相关的功能(如绑定某个特定的元素或者在内部进行数据传输等等),保持插件的灵活性等等

以上就是我们暂时能想到的功能和需求,下面就开始写一个完整的插件了(当然原生js插件某种程度来讲使用起来相对比较自由,因为不需要依赖某些特定库,而且也不需要按照某些库类的格式标准进行插件的编写,但少了一些封装好的方法也会使得插件写起来更费力,至于怎么取舍就需要看个人需求来定了)

2. 进行结构划分

当我们正式开始代码编写的时候,当然想自己写出来的代码不敢说很强势,但至少结构清晰,易于读懂,而且代码的性能也需要保证。这时我们就需要回到前面的需求了,由上面列出的5点可以看出,大部分的功能需求都是在我们程序内部去实现的,唯一需要考虑的是上面的第3点。

这时我们可能已经想到办法了,比如说将新的表情包填好相关的参数后由接口传入程序内部去作处理。当然这是一个合理的选择,但考虑到代码的复杂度和使用的简易度,我们最好还是建立一个对应config文件。因为首先这样我们可以提供一些默认的表情包,并且配置好相关的参数并注释,后面的使用者只需要按照相关的格式复制然后修改就行了。而且将一些非逻辑性的数据单独隔离开来有利于维持清晰的代码结构,增加代码的易读性。所以到这里已经可以基本上确定我们需要的文件了:

  1. 一个模板css文件; 2. 一个数据配置文件config.js; 3. 一个逻辑实现文件js;

3. 填写配置文件

这里先填写配置文件是为了有一个更明确的需求,以及防止在coding过程中忘记了某些需求(像我一样,老了,脑袋不好使゚゚(゚´Д`゚)゚),当然并不是所有插件都用配置文件比较好,新手请务必不要有这样的误区,下面是我写的配置代码:


    var path = "http://localhost/wantEmoji/",  //项目所在的根地址
    emojis = {
        "paopao" : {
            "name" : "泡泡", //名字
            "col" : 10, //每一行最大的表情个数(建议填选的时候值不要太大或太小)
            "path" : path+"emojiSources/paopao/", //相对于项目根地址的路径
            "enable" : true, //是否启用本表情包
            "sources" : ["1.jpg"] //中间的值也支持{title:"笑",url:"1.jpg"}的形式,且可单独设置
        }
    }

这部分代码考虑了几个点:
一是考虑到可能会在不同路径的文件中调用同一个配置文件,所以为了保证路径不出错,需要确定每个包的绝对路径值。
二是考虑到某些表情包现在可能并不想用,但代码删来删去可能会很麻烦,所以提供了一个是否启用的接口。
三是考虑到不同表情包的图片尺寸可能不同,为了让每张图片尽可能清晰我们允许调整每行显示的图片个数(在程序中每个单项的size都是自动计算的)
四是考虑到每张表情图片可能有的需要设置title来提示用户这个表情是什么意思,所以允许sources项数组中的值可以为string也可以为object
最后也是主要考虑的问题,我们希望每个表情对应的code值能够自动生成而不是人为的对每个图片去进行单独设置,所以需要保证每个code的值都是唯一的,而且是容易被解析的。
这里emojis变量不是数组而是对象就是基于这个原因。 (我们最终生成的code值为[wem:emojis的key值_图片名_图片类型:wem]这种形式,如[wem:paopao_1_jpg:wem],表示的是paopao表情包里面的1.jpg)

4. 插件开写

前面的准备工作都做好后,现在我们终于可以开始写真正的代码了。虽然前面的内容不怎么多,但对于一个插件乃至一个项目来说都是必不可少的一个步骤,特别是初学者,开始动手写自己的插件时多想想该怎么做总是没错的。

首先我们需要创建一个对象(当然你通过闭包来写也是可以的),明确好哪些数据和函数是可以共用的,哪些是不能共用的。就我个人的经验来讲,一般对于用来保存数据用的变量,最好都放在函数体内,而方法则都放在原型上。

var wantEmoji = function(options){
    options = options || {};
    var selector = options.wrapper || "body";  

    this.wrapper = document.querySelector(selector);    //包裹元素
    this.row = options.row || 4;                          //每页表情的行数
    this.callback = options.callback || function(){};     //当表情被点击时的回调,返回表情的code值

    this.emojis = window.emojis || emojis;        //加载表情包配置

    this.content = null;                   //.wEmoji-content
    this.navRow = null;                    //.wEmoji-row
    this.currentWrapper = null;         //.wEmoji-wrapper[data-choose="true"]

    this.activePage = 0;
    this.totalPage = 0;
    this.eachPartsNum = 4;                 //每一批显示的表情包数(导航栏的表情包的最大显示个数)

    this.wrapWidth = 0;
    this.count = this.getEMJPackageCount();
    
    if(options.autoInit) //当设置了autoInit之后会自动调用init函数,默认不会
    this.init();
};

上面的代码我都加了注释就不做细说了,下面是各个功能部分的实现(马上就可以看到我英语捉急的地方了(`・ω・´))。

首先是init(): 完成某些数据的获取以及确认进入哪种情况

init : function(){
        //当表情包的实际启用个数大于设定值时,启用.wEmoji-more
        if(this.count > this.eachPartsNum)
        this.wrapper.className += " wEmoji wEmoji-more";
        else
        this.wrapper.className += " wEmoji";

        this.wrapWidth = this.wrapper.clientWidth;

        this.initTemplete();
},

initTemplete(): 初始化模板,更新某些数据变量,并执行接下来的工作

initTemplete : function(){

        var wrapper = this.wrapper,
            tpl = '<div class="wEmoji-header">'+
                    '<div class="wEmoji-prev-btn">&lt;</div>'+
                    '<div class="wEmoji-nav">'+
                        '<div class="wEmoji-row"></div>'+
                    '</div>'+
                    '<div class="wEmoji-next-btn">&gt;</div>'+
                '</div>'+
                '<div class="wEmoji-container">'+
                    '<div class="wEmoji-content"></div>'+
                    '<div class="wEmoji-pages"></div>'+
                '</div>';

        wrapper.innerHTML = tpl;

        this.content = wrapper.querySelector(".wEmoji-content");
        this.navRow = wrapper.querySelector(".wEmoji-row");

        this.__initData();
        this.__bindEvent();
},

接下来是__initData():生成具体的表情图片和导航等,这里需要注意的是进行dom操作时不要让重排发生多次,使需要操作的dom元素脱离文档流是减少重排的方法之一。另外这里还将许多属性保存为临时变量是为了提高程序性能(至于代码优化需要自己去找资料看,这里就简单提一下)。

__initData : function(){
        var emojis = this.emojis,
            wrapper = this.wrapper,
            navRow = this.navRow,
            content = this.content,
            rowWidth = navRow.clientWidth,
            count = this.count;

        //减少重排
        wrapper.style.display = "none";

        content.innerHTML = "";
        navRow.style.width = count / this.eachPartsNum * 100 + "%";

        for( var key in emojis ){
            var emj = emojis[key];

            if(!emj.enable)
            continue;
            //将每个生成的表情包的容器放入content中
            content.appendChild(this.__initContent(key,emj)); 
            navRow.innerHTML += '<div class="wEmoji-list" data-eid="'+key+'" style="width:'+(1/count*100)+'%;">'+emj.name+'</div>';
        }

        this.__initStyle();

        this.wrapper.style.display = "block";
},

事件绑定:正常流程来走就行,注意某些地方需要用事件委托来提升性能,而这里没用addEventListener是为了防止多次初始化init的时候导致事件重复绑定,on+“event”事实上已经够用了。

__bindEvent : function(){
        var _self = this,
            wrapper = this.wrapper,
            row = this.navRow,
            pageBox = wrapper.querySelector('.wEmoji-pages'),
            prev = wrapper.querySelector('.wEmoji-prev-btn'),
            next = wrapper.querySelector('.wEmoji-next-btn'),
            content = this.content,
            down = "ontouchstart" in document ? "touchstart" : "mousedown",
            up = "ontouchend" in document ? "touchend" : "mouseup",
            move = "ontouchmove" in document ? "touchmove" : "mousemove",
            drag = false,
            x = 0;

        pageBox.onclick = function(e){
            e = e || event;
            var target = e.target || e.srcElement,
                idx = target.getAttribute("data-pageIdx");
            if(target.tagName.toLowerCase() != "li" || !idx){
                return false;
            }
            _self.showPage(idx-1);
        };

        row.onclick = function(e){
            e = e || event;
            var target = e.target || e.srcElement,
                eid = target.getAttribute("data-eid");

            if( eid && _self.emojis[eid] ){
                _self.chooseEmoji(eid);
                _self.showPage(0);
            }
        };

        var parts = Math.ceil(this.count / this.eachPartsNum), //可以将表情包数分为N批(默认4个一批)
            partsIdx = 0,
            navWidth = wrapper.querySelector(".wEmoji-nav").clientWidth;

        prev.onclick = function(e){
            partsIdx = partsIdx - 1 < 0 ? 0 : partsIdx - 1;
            row.style.webkitTransform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
            row.style.transform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
        };

        next.onclick = function(e){
            partsIdx = partsIdx + 1 >= parts ? partsIdx : partsIdx + 1;
            row.style.webkitTransform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
            row.style.transform = "translateX("+(-partsIdx * navWidth)+"px) translateZ(0px)";
        };

        content.onclick = function(e){
            e = e || event;
            var target = e.target || e.srcElement,
                trueTarget = getTargetNode(target,".wEmoji-item"),
                emjCode;

            if(trueTarget)
            emjCode = trueTarget.getAttribute("data-emj");

            if(!emjCode)
            return false;

            _self.callback.call(_self,emjCode);
            console.log(emjCode);
        };

        content["on"+down] = function(e){
            e = e || event;
            drag = true;
            x = e.pageX || e.touches[0].pageX;
        };

        content["on"+move] = function(e){
            e = e || event;
            e.stopPropagation();
            e.preventDefault();
        };

        content["on"+up] = function(e){
            e = e || event;
            if(drag){
                drag = false;
                var endX = e.pageX || e.changedTouches[0].pageX,
                    dis = endX - x,
                    idx;

                if(dis > 50){
                    idx = Math.max(_self.activePage - 1,0);
                    _self.showPage(idx);
                } else if (dis < -50){
                    idx = Math.min(_self.activePage + 1,_self.totalPage - 1);
                    _self.showPage(idx);
                }
                x = 0;
            }
        };

},

下面是选择表情包的功能chooseEmoji():封装好后只需要调用接口即可,不管是初始化的时候还是事件触发的时候,将表情包改变时会发生操作全都放一起,因为大部分操作都是同时变化的,所以没必要继续细分了。

chooseEmoji : function(eid){
        var navRow = this.navRow,
            content = this.content,
            targetWrapper = content.querySelector(".wEmoji-wrapper[data-eid='"+eid+"']"),
            targetList = navRow.querySelector(".wEmoji-list[data-eid='"+eid+"']"),
            chooseWrapper = content.querySelector(".wEmoji-wrapper[data-choose='true']"),
            chooseList = navRow.querySelector(".wEmoji-list[data-choose='true']");

        if(chooseWrapper){
            chooseList.setAttribute("data-choose","false");
            chooseWrapper.setAttribute("data-choose","false");
        }
        targetWrapper.setAttribute("data-choose","true");
        targetList.setAttribute("data-choose","true");

        this.currentWrapper = targetWrapper;
        this.__createPageList();
},

下面是页面的切换showPage():完成初始化和事件触发时页面的切换

showPage : function(idx){
        this.activePage = idx;
        var wrapper = this.wrapper,
            currentWrapper = this.currentWrapper,
            pageTargetList = wrapper.querySelector(".wEmoji-page-list[data-pageIdx='"+(idx+1)+"']"),
            pageChoose = wrapper.querySelector(".wEmoji-page-list[data-choose='true']");

        if(pageChoose)
        pageChoose.setAttribute("data-choose","false");
        pageTargetList.setAttribute("data-choose","true");

        currentWrapper.style.webkitTransform = "translateX("+(-this.wrapWidth*idx)+"px) translateZ(0px)";
        currentWrapper.style.transform = "translateX("+(-this.wrapWidth*idx)+"px) translateZ(0px)";
}

最后一个是将code解释成img的功能函数explain(): 大家通过前面的介绍可以知道code的生成规则

explain : function(str){
        var reg = /\[wem:(\w+):wem\]/g,
            _self = this;

        return str.replace(reg,function(str,target){
            var tempArr = target.split("_"),
                eid = tempArr.shift(),
                type = tempArr.pop(),
                name = tempArr.join("_");
                path = _self.emojis[eid].path;
                url = name+"."+type;

            return '<img src="'+path+url+'" />';
        });
},

基本上主要代码就这么多了,还有一部分代码可以看源代码来了解,因为我基本上都有写注释所以应该不怎么难理解。

5. 结语

虽然我很想进一步把教程写完全,但基于本人身体已经被掏空的现实情况考虑,就不做打算了,效果的话可以点开上面的demo去看,大家有什么问题欢迎留言提问,以后会不定时写一些插件,到时候也欢迎大家来捧场,以上(写完要死了(ง ° ͜ °)ง)。

    原文作者:孤月
    原文地址: https://segmentfault.com/a/1190000009455711
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞