用javascript寫一個emoji表情插件

好久沒有寫文章了,說實話本人如今受困於五月病已經快變成一條死鹹魚了(T_T),本次就當寫一個簡單的js插件教程了。本項目的代碼相對比較簡單,至於裏面有些變量命名的問題就請大家不要吐槽了Σ(゚д゚lll)(好的,我認可我英語就小學水平好吧。除了hello和goodbye其餘的都不會了____orz)。 廢話就講到這裏,下面開始正文。css

demo: 我是demo
git : 我是項目git
下載地址: 點我下載linux

1. 事前準備

事實上在寫一個插件前咱們都須要事先想好你要實現哪些功能,怎麼去實現,這些大方向的東西是須要事先考慮的,至於具體細節和優化選項咱們能夠在寫代碼的過程當中再進行修改。git

就以咱們寫的這個emoji插件爲例,網上已經有一些相關的插件了,但你總感受有些部分的需求不能被知足(如:能夠自行添加新的表情包而不用去改源代碼等等),這時咱們就能夠列出你想實現的功能項了:github

  1. 須要知足基本的表情插件的需求,包括圖片和對應code的相互轉換web

  2. 但願能夠經過參數來調整每行以及每列表情圖片的顯示個數,而且能夠針對不一樣表情包單獨調整api

  3. 但願用戶能夠在不瞭解源代碼的狀況下也能自行主動添加新的表情包數組

  4. 模板界面簡單,能夠進行自適應,而且兼容移動端閉包

  5. 儘量只提供簡單的api接口和方法,避免內部涉及其餘不是很相關的功能(如綁定某個特定的元素或者在內部進行數據傳輸等等),保持插件的靈活性等等app

以上就是咱們暫時能想到的功能和需求,下面就開始寫一個完整的插件了(固然原生js插件某種程度來說使用起來相對比較自由,由於不須要依賴某些特定庫,並且也不須要按照某些庫類的格式標準進行插件的編寫,但少了一些封裝好的方法也會使得插件寫起來更費力,至於怎麼取捨就須要看我的需求來定了)dom

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

相關文章
相關標籤/搜索