用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去看,大家有什么問題歡迎留言提問,以后會不定時寫一些插件,到時候也歡迎大家來捧場,以上(寫完要死了(ง ° ͜ °)ง)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM