摘要:使用構(gòu)造函數(shù)那么有沒(méi)有一種辦法,可以不寫(xiě)函數(shù)名,直接聲明一個(gè)函數(shù)并自動(dòng)調(diào)用它呢答案肯定的,那就是使用自執(zhí)行函數(shù)。
日常工作中經(jīng)常會(huì)發(fā)現(xiàn)有大量業(yè)務(wù)邏輯是重復(fù)的,而用別人的插件也不能完美解決一些定制化的需求,所以我決定把一些常用的組件抽離、封裝出來(lái),形成一套自己的插件庫(kù)。同時(shí),我將用這個(gè)教程系列記錄下每一個(gè)插件的開(kāi)發(fā)過(guò)程,手把手教你如何一步一步去造出一套實(shí)用性、可復(fù)用性高的輪子。
So, Let"s begin!
目前項(xiàng)目使用 ES5及UMD 規(guī)范封裝,所以在前端暫時(shí)只支持 JavaScript模塊化要開(kāi)發(fā)一個(gè)JavaScript的插件,首先要從JavaScript的模塊化講起。
什么是模塊化?簡(jiǎn)單的說(shuō)就是讓JavaScript能夠以一個(gè)整體的方式去組織和維護(hù)代碼,當(dāng)多人開(kāi)發(fā)時(shí)可以互相引用對(duì)方的代碼塊又不造成沖突。
ECMAScript6標(biāo)準(zhǔn)之前常見(jiàn)的模塊化規(guī)范有:CommonJS、AMD、UMD等,因?yàn)槲覀兊拇a暫時(shí)是采用ES5語(yǔ)法進(jìn)行開(kāi)發(fā),所以我們選用UMD的規(guī)范來(lái)組織代碼。
關(guān)于模塊化的發(fā)展過(guò)程可以參考:JavaScript模塊化編程簡(jiǎn)史(2009-2016)
JavaScript模塊演化簡(jiǎn)史
在這種模塊規(guī)范的標(biāo)準(zhǔn)之上,我們還需要一種機(jī)制來(lái)加載不同的模塊,例如實(shí)現(xiàn)了AMD規(guī)范的require.js,其用法可以參考阮一峰寫(xiě)的這篇教程:
Javascript模塊化編程(三):require.js的用法
因?yàn)槲覀冮_(kāi)發(fā)的輪子暫時(shí)不涉及到多模塊加載,所以模塊的加載暫時(shí)不予過(guò)多討論,讀者可自己進(jìn)行拓展學(xué)習(xí)。
回到我們的主題上,在正式開(kāi)發(fā)之前,還需要補(bǔ)充一點(diǎn)其他方面的知識(shí)。
自執(zhí)行函數(shù)定義一個(gè)函數(shù),ES5一般有三種方式:
函數(shù)聲明
function foo () {}這樣聲明的函數(shù)和變量一樣,會(huì)被自動(dòng)提升,所以我們可以把函數(shù)聲明放在調(diào)用它的語(yǔ)句后面:
foo(); function foo () {}函數(shù)表達(dá)式
var foo = function () {}右邊其實(shí)是一個(gè)匿名函數(shù),只不過(guò)賦值給了一個(gè)變量,我們可以通過(guò)這個(gè)變量名來(lái)調(diào)用它,但是和第一種方式不同的是,通過(guò)表達(dá)式聲明的函數(shù)不會(huì)被提升。
使用Function構(gòu)造函數(shù)
var foo = new Function ()那么有沒(méi)有一種辦法,可以不寫(xiě)函數(shù)名,直接聲明一個(gè)函數(shù)并自動(dòng)調(diào)用它呢?
答案肯定的,那就是使用自執(zhí)行函數(shù)。(實(shí)際上我的另一篇文章打磚塊——js面向?qū)ο蟪踝R(shí)中就曾提到過(guò))自執(zhí)行函數(shù)Immediately-Invoked Function Expression,顧名思義,就是自動(dòng)執(zhí)行的函數(shù),有的地方也稱為立即調(diào)用的函數(shù)表達(dá)式。
它的基本形式如下:(function () { console.log("hello") }()); (function () { console.log("hello") })();兩種寫(xiě)法是等效的,只不過(guò)前者讓代碼看起來(lái)更像是一個(gè)整體。可以看到,這兩種寫(xiě)法的作用其實(shí)就是在()內(nèi)定義函數(shù),然后又使用()來(lái)執(zhí)行該函數(shù),因此它就是自執(zhí)行的。
IIFE的一些好處如下:
避免污染全局變量
減少命名沖突
惰性加載
最重要的一點(diǎn),它可以創(chuàng)建一個(gè)獨(dú)立的作用域,而在ES6之前JavaScript是沒(méi)有塊級(jí)作用域的。
利用這一點(diǎn),我們可以很輕松的保證多個(gè)模塊之間的變量不被覆蓋了:// libA.js (function(){ var num = 1; })(); // libB.js (function(){ var num = 2; })();上面這兩個(gè)文件模塊中的作用域都是獨(dú)立的,互不影響。(如果模塊之間想要互相引用,就需要用到模塊的加載器了,例如上面提到的require.js等庫(kù))、
在此基礎(chǔ)上,我們就可以看看一個(gè)實(shí)現(xiàn)了UMD規(guī)范的IIFE模板是什么樣子了:
// if the module has no dependencies, the above pattern can be simplified to (function (root, factory) { if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. define([], factory); } else if (typeof module === "object" && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) root.returnExports = factory(); } }(typeof self !== "undefined" ? self : this, function () { // Just return a value to define the module export. // This example returns an object, but the module // can return a function as the exported value. return {}; }));可以看到,UMD規(guī)范同時(shí)兼容了瀏覽器、Node環(huán)境及AMD規(guī)范,這樣我們的代碼使用UMD包裝后就可以在不同的環(huán)境中運(yùn)行了。
插件模板開(kāi)發(fā)插件最重要的一點(diǎn),就是插件的兼容性,一個(gè)插件至少要能同時(shí)在幾種不同的環(huán)境中運(yùn)行。其次,它還需要滿足以下幾種功能及條件:
插件自身的作用域與用戶當(dāng)前的作用域相互獨(dú)立,也就是插件內(nèi)部的私有變量不能影響使用者的環(huán)境變量;
插件需具備默認(rèn)設(shè)置參數(shù);
插件除了具備已實(shí)現(xiàn)的基本功能外,需提供部分API,使用者可以通過(guò)該API修改插件功能的默認(rèn)參數(shù),從而實(shí)現(xiàn)用戶自定義插件效果;
插件支持鏈?zhǔn)秸{(diào)用;
插件需提供監(jiān)聽(tīng)入口,及針對(duì)指定元素進(jìn)行監(jiān)聽(tīng),使得該元素與插件響應(yīng)達(dá)到插件效果。
第一點(diǎn)我們利用UMD包裝的方式已經(jīng)實(shí)現(xiàn)了,現(xiàn)在來(lái)看看第二和第三點(diǎn)。
通常情況下,一個(gè)插件內(nèi)部會(huì)有默認(rèn)參數(shù),并且會(huì)提供一些參數(shù)讓用戶實(shí)現(xiàn)部分功能的自定義。那么怎么實(shí)現(xiàn)呢,這其實(shí)就是一個(gè)對(duì)象合并的問(wèn)題,例如:
function extend(o, n, override) { for (var p in n) { if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override)) o[p] = n[p]; } } // 默認(rèn)參數(shù) var options = { pageNumber: 1, pageShow: 2 }; // 用戶設(shè)置 var userOptions = { pageShow: 3, pageCount: 10 } extend(options, userOptions, true); // 合并后 options = { pageNumber: 1, pageShow: 3, pageCount: 10 }如上,采用一個(gè)類似的extend函數(shù)就可以實(shí)現(xiàn)對(duì)象的合并了,這樣我們插件也就實(shí)現(xiàn)了設(shè)置參數(shù)的功能。
這里的extend函數(shù)為淺拷貝,因?yàn)椴寮挠脩魠?shù)一般是不會(huì)修改的,如果想實(shí)現(xiàn)深拷貝可參考jQuery中extend的實(shí)現(xiàn)方法。第四點(diǎn)我們插件暫時(shí)不需要這樣的功能,可以暫時(shí)不支持它。第五點(diǎn)在代碼中我們會(huì)通過(guò)回調(diào)函數(shù)去逐步實(shí)現(xiàn)它。
綜上,我們就可以實(shí)現(xiàn)出一個(gè)基礎(chǔ)的插件模板了:
;// JavaScript弱語(yǔ)法的特點(diǎn),如果前面剛好有個(gè)函數(shù)沒(méi)有以";"結(jié)尾,那么可能會(huì)有語(yǔ)法錯(cuò)誤 (function(root, factory) { if (typeof define === "function" && define.amd) { define([], factory); } else if (typeof module === "object" && module.exports) { module.exports = factory(); } else { root.Plugin = factory(); } }(typeof self !== "undefined" ? self : this, function() { "use strict"; // tool function extend(o, n, override) { for (var p in n) { if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override)) o[p] = n[p]; } } // polyfill var EventUtil = { addEvent: function(element, type, handler) { // 添加綁定 if (element.addEventListener) { // 使用DOM2級(jí)方法添加事件 element.addEventListener(type, handler, false); } else if (element.attachEvent) { // 使用IE方法添加事件 element.attachEvent("on" + type, handler); } else { // 使用DOM0級(jí)方法添加事件 element["on" + type] = handler; } }, // 移除事件 removeEvent: function(element, type, handler) { if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else if (element.datachEvent) { element.detachEvent("on" + type, handler); } else { element["on" + type] = null; } }, getEvent: function(event) { // 返回事件對(duì)象引用 return event ? event : window.event; }, // 獲取mouseover和mouseout相關(guān)元素 getRelatedTarget: function(event) { if (event.relatedTarget) { return event.relatedTarget; } else if (event.toElement) { // 兼容IE8- return event.toElement; } else if (event.formElement) { return event.formElement; } else { return null; } }, getTarget: function(event) { //返回事件源目標(biāo) return event.target || event.srcElement; }, preventDefault: function(event) { //取消默認(rèn)事件 if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, stopPropagation: function(event) { if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } }, // 獲取mousedown或mouseup按下或釋放的按鈕是鼠標(biāo)中的哪一個(gè) getButton: function(event) { if (document.implementation.hasFeature("MouseEvents", "2.0")) { return event.button; } else { //將IE模型下的button屬性映射為DOM模型下的button屬性 switch (event.button) { case 0: case 1: case 3: case 5: case 7: //按下的是鼠標(biāo)主按鈕(一般是左鍵) return 0; case 2: case 6: //按下的是中間的鼠標(biāo)按鈕 return 2; case 4: //鼠標(biāo)次按鈕(一般是右鍵) return 1; } } }, //獲取表示鼠標(biāo)滾輪滾動(dòng)方向的數(shù)值 getWheelDelta: function(event) { if (event.wheelDelta) { return event.wheelDelta; } else { return -event.detail * 40; } }, // 以跨瀏覽器取得相同的字符編碼,需在keypress事件中使用 getCharCode: function(event) { if (typeof event.charCode == "number") { return event.charCode; } else { return event.keyCode; } } }; // plugin construct function function Plugin(selector, userOptions) { // Plugin() or new Plugin() if (!(this instanceof Plugin)) return new Plugin(selector, userOptions); this.init(selector, userOptions) } Plugin.prototype = { constructor: Plugin, // default option options: {}, init: function(selector, userOptions) { extend(this.options, userOptions, true); } }; return Plugin; }));這里還使用到了一個(gè)EventUtil對(duì)象,它主要是針對(duì)事件注冊(cè)的一些兼容性做了一些polyfill封裝,具體原理可以參閱:
EventUtil——跨瀏覽器的事件對(duì)象
跨瀏覽器的事件對(duì)象-------EventUtil 中的方法及用法
到此,一個(gè)插件的基本模板就大致成型了。下一節(jié),我們終于可以正式開(kāi)始分頁(yè)插件的開(kāi)發(fā)了!
思路分析有人說(shuō)計(jì)算機(jī)的本質(zhì)就是對(duì)現(xiàn)實(shí)世界的抽象,而編程則是對(duì)這個(gè)抽象世界規(guī)則的制定。正如上面這句話所說(shuō),在實(shí)際編碼之前我們一般需要對(duì)要實(shí)現(xiàn)的需求效果進(jìn)行一個(gè)思路的分析,最后再進(jìn)一步把這個(gè)思路過(guò)程抽象為有邏輯的代碼。
我們先看一下要實(shí)現(xiàn)的分頁(yè)效果是什么樣的,我把它分成兩種情況,顯示和不顯示省略號(hào)的,首先來(lái)看第一種:// 總共30頁(yè) // 第一種情況:不顯示省略號(hào),當(dāng)前頁(yè)碼前后最多顯示2個(gè)頁(yè)碼 當(dāng)前頁(yè)碼為 1,那么顯示 1 2 3 4 5 當(dāng)前頁(yè)碼為 2,那么顯示 1 2 3 4 5 當(dāng)前頁(yè)碼為 3,那么顯示 1 2 3 4 5 當(dāng)前頁(yè)碼為 4,那么顯示 2 3 4 5 6 ... 當(dāng)前頁(yè)碼為 15,那么顯示 13 14 15 16 17 ... 當(dāng)前頁(yè)碼為 27,那么顯示 25 26 27 28 29 當(dāng)前頁(yè)碼為 28,那么顯示 26 27 28 29 30 當(dāng)前頁(yè)碼為 29,那么顯示 26 27 28 29 30 當(dāng)前頁(yè)碼為 30,那么顯示 26 27 28 29 30雖然上面每一個(gè)數(shù)字在實(shí)際應(yīng)用中都是一個(gè)按鈕或超鏈接,但現(xiàn)在既然是分析,我們不妨就把它簡(jiǎn)化并忽略,于是這個(gè)問(wèn)題就變成了一個(gè)簡(jiǎn)單的字符串輸出題。
我們先定義一個(gè)函數(shù):function showPages (page, total, show) { }函數(shù)傳入的參數(shù)分別為:當(dāng)前頁(yè)碼、總頁(yè)碼數(shù)、當(dāng)前頁(yè)碼面前后最多顯示頁(yè)碼數(shù),然后我們需要循環(huán)調(diào)用這個(gè)函數(shù)打印分頁(yè):
var total = 30; for (var i = 1; i <= total; i++) { console.log(showPages(i, total)); }這樣從頁(yè)碼為1到最后一頁(yè)的結(jié)果就全輸出了,最后我們需要完成showPages()函數(shù):
function showPages (page, total, show) { var str = ""; if (page < show + 1) { for (var i = 1; i <= show * 2 + 1; i++) { str = str + " " + i; } } else if (page > total - show) { for (var i = total - show * 2; i <= total; i++) { str = str + " " + i; } } else { for (var i = page - show; i <= page + show; i++) { str = str + " " + i; } } return str.trim(); }思路是分段拼出頁(yè)碼,打印結(jié)果如下:
不顯示省略號(hào)的頁(yè)碼正常輸出了,然后我們來(lái)看顯示省略號(hào)的情況:
// 第二種情況:顯示省略號(hào),當(dāng)前頁(yè)碼前后最多顯示2個(gè)頁(yè)碼 當(dāng)前頁(yè)碼為 1,那么顯示 1 2 3 ... 30 當(dāng)前頁(yè)碼為 2,那么顯示 1 2 3 4 ... 30 當(dāng)前頁(yè)碼為 3,那么顯示 1 2 3 4 5 ... 30 當(dāng)前頁(yè)碼為 4,那么顯示 1 2 3 4 5 6 ... 30 當(dāng)前頁(yè)碼為 5,那么顯示 1 ... 3 4 5 6 7 ... 30 ... 當(dāng)前頁(yè)碼為 15,那么顯示 1 ... 13 14 15 16 17 ... 30 ... 當(dāng)前頁(yè)碼為 26,那么顯示 1 ... 24 25 26 27 28 ... 30 當(dāng)前頁(yè)碼為 27,那么顯示 1 ... 25 26 27 28 29 30 當(dāng)前頁(yè)碼為 28,那么顯示 1 ... 26 27 28 29 30 當(dāng)前頁(yè)碼為 29,那么顯示 1 ... 27 28 29 30 當(dāng)前頁(yè)碼為 30,那么顯示 1 ... 28 29 30同樣需要完成showPages()函數(shù):
function showPages(page, length, show) { var str = ""; var preIndex = page - (show + 1); var aftIndex = page + (show + 1); if (page < show + 3) { for (var i = 1; i <= show * 2 + 3; i++) { if ((i !== preIndex && i !== aftIndex) || (i === 1 || i === total)) { str = str + " " + i; } else { str = str + " ... " + total; break; } } } else if (page > total - (show + 2)) { for (var i = total; i >= total - (show * 2 + 2); i--) { if ((i !== preIndex && i !== aftIndex) || (i === 1 || i === total)) { str = i + " " + str; } else { str = "1 ... " + str; break; } } } else { for (var i = preIndex + 1; i <= aftIndex - 1; i++) { str = str + " " + i; } str = "1 ... " + str + " ... " + total; } return str.trim(); }同樣也是采用分段拼的思路,能成功打印出結(jié)果:
但是仔細(xì)看看上面的代碼會(huì)發(fā)現(xiàn)有大量重復(fù)冗余的邏輯了,能不能優(yōu)化呢?下面是一種更為取巧的思路:
function showPages (page, total, show) { var str = page + ""; for (var i = 1; i <= show; i++) { if (page - i > 1) { str = page - i + " " + str; } if (page + i < total) { str = str + " " + (page + i); } } if (page - (show + 1) > 1) { str = "... " + str; } if (page > 1) { str = 1 + " " + str; } if (page + show + 1 < total) { str = str + " ..."; } if (page < total) { str = str + " " + total; } return str; }打印結(jié)果是一樣的,但代碼卻大為精簡(jiǎn)了。
基本架構(gòu)一個(gè)好的插件,代碼一定是高復(fù)用、低耦合、易拓展的,因此我們需要采用面向?qū)ο蟮姆椒▉?lái)搭建這個(gè)插件的基本架構(gòu):
// 模仿jQuery $() function $(selector, context) { context = arguments.length > 1 ? context : document; return context ? context.querySelectorAll(selector) : null; } var Pagination = function(selector, pageOption) { // 默認(rèn)配置 this.options = { curr: 1, pageShow: 2, ellipsis: true, hash: false }; // 合并配置 extend(this.options, pageOption, true); // 分頁(yè)器元素 this.pageElement = $(selector)[0]; // 數(shù)據(jù)總數(shù) this.dataCount = this.options.count; // 當(dāng)前頁(yè)碼 this.pageNumber = this.options.curr; // 總頁(yè)數(shù) this.pageCount = Math.ceil(this.options.count / this.options.limit); // 渲染 this.renderPages(); // 執(zhí)行回調(diào)函數(shù) this.options.callback && this.options.callback({ curr: this.pageNumber, limit: this.options.limit, isFirst: true }); // 改變頁(yè)數(shù)并觸發(fā)事件 this.changePage(); }; Pagination.prototype = { constructor: Pagination, changePage: function() {} }; return Pagination;如上,一個(gè)采用原型模式的分頁(yè)器對(duì)象就搭建完成了,下面我們對(duì)上面的代碼進(jìn)行一一講解。
分頁(yè)配置本分頁(yè)器提供如下基本參數(shù):
// 分頁(yè)元素ID(必填) var selector = "#pagelist"; // 分頁(yè)配置 var pageOption = { // 每頁(yè)顯示數(shù)據(jù)條數(shù)(必填) limit: 5, // 數(shù)據(jù)總數(shù)(一般通過(guò)后端獲取,必填) count: 162, // 當(dāng)前頁(yè)碼(選填,默認(rèn)為1) curr: 1, // 是否顯示省略號(hào)(選填,默認(rèn)顯示) ellipsis: true, // 當(dāng)前頁(yè)前后兩邊可顯示的頁(yè)碼個(gè)數(shù)(選填,默認(rèn)為2) pageShow: 2, // 開(kāi)啟location.hash,并自定義hash值 (默認(rèn)關(guān)閉) // 如果開(kāi)啟,在觸發(fā)分頁(yè)時(shí),會(huì)自動(dòng)對(duì)url追加:#!hash值={curr} 利用這個(gè),可以在頁(yè)面載入時(shí)就定位到指定頁(yè) hash: false, // 頁(yè)面加載后默認(rèn)執(zhí)行一次,然后當(dāng)分頁(yè)被切換時(shí)再次觸發(fā) callback: function(obj) { // obj.curr:獲取當(dāng)前頁(yè)碼 // obj.limit:獲取每頁(yè)顯示數(shù)據(jù)條數(shù) // obj.isFirst:是否首次加載頁(yè)面,一般用于初始加載的判斷 // 首次不執(zhí)行 if (!obj.isFirst) { // do something } } };在構(gòu)造函數(shù)里調(diào)用extend()完成了用戶參數(shù)與插件默認(rèn)參數(shù)的合并。
回調(diào)事件通常情況下,在改變了插件狀態(tài)后(點(diǎn)擊事件等),插件需要作出一定的反應(yīng)。因此我們需要對(duì)用戶行為進(jìn)行一定的監(jiān)聽(tīng),這種監(jiān)聽(tīng)習(xí)慣上就叫作回調(diào)函數(shù)。
在上面代碼中我們可以看到有這么一段:// 執(zhí)行回調(diào)函數(shù) this.options.callback && this.options.callback({ curr: this.pageNumber, limit: this.options.limit, isFirst: true });這種寫(xiě)法是不是有點(diǎn)奇怪呢,其實(shí)它相當(dāng)于:
if(this.options.callback){ this.options.callback({ curr: this.pageNumber, limit: this.options.limit, isFirst: true }); }想必聰明的你已經(jīng)明白了吧,這里的callback并不是某個(gè)具體的東西,而是一個(gè)引用。不管callback指向誰(shuí),我們只需要判斷它有沒(méi)有存在,如果存在就執(zhí)行它。
事件綁定接下來(lái)需要對(duì)分頁(yè)器進(jìn)行點(diǎn)擊事件的綁定,也就是完成我們的changePage()方法:
changePage: function() { var self = this; var pageElement = self.pageElement; EventUtil.addEvent(pageElement, "click", function(ev) { var e = ev || window.event; var target = e.target || e.srcElement; if (target.nodeName.toLocaleLowerCase() == "a") { if (target.id === "prev") { self.prevPage(); } else if (target.id === "next") { self.nextPage(); } else if (target.id === "first") { self.firstPage(); } else if (target.id === "last") { self.lastPage(); } else if (target.id === "page") { self.goPage(parseInt(target.innerHTML)); } else { return; } self.renderPages(); self.options.callback && self.options.callback({ curr: self.pageNumber, limit: self.options.limit, isFirst: false }); self.pageHash(); } }); }整體的邏輯大家應(yīng)該都能輕松看懂,無(wú)非就是判斷當(dāng)前點(diǎn)擊的是什么,然后執(zhí)行對(duì)應(yīng)的邏輯操作,但具體的實(shí)現(xiàn)方式有的同學(xué)可能會(huì)有一點(diǎn)陌生。
Q:這個(gè)target是啥?這個(gè)srcElement又是啥?
A:這其實(shí)是JavaScript事件委托方面的知識(shí),大家可以參考如下文章進(jìn)行學(xué)習(xí),這里不再贅述。js中的事件委托或是事件代理詳解
插件對(duì)象、配置完成了,事件也綁定了,那接下來(lái)就應(yīng)該完成我們頁(yè)碼上顯示的DOM節(jié)點(diǎn)的渲染了。
渲染DOM渲染的過(guò)程其實(shí)就是對(duì)上面我們封裝的那幾個(gè)字符串打印函數(shù)的改進(jìn),把字符串改為具體的DOM節(jié)點(diǎn),然后添加進(jìn)頁(yè)面即可。
首先我們需要完成一個(gè)createHtml()函數(shù):createHtml: function(elemDatas) { var self = this; var fragment = document.createDocumentFragment(); var liEle = document.createElement("li"); var aEle = document.createElement("a"); elemDatas.forEach(function(elementData, index) { liEle = liEle.cloneNode(false); aEle = aEle.cloneNode(false); liEle.setAttribute("class", CLASS_NAME.ITEM); aEle.setAttribute("href", "javascript:;"); aEle.setAttribute("id", elementData.id); if (elementData.id !== "page") { aEle.setAttribute("class", CLASS_NAME.LINK); } else { aEle.setAttribute("class", elementData.className); } aEle.innerHTML = elementData.content; liEle.appendChild(aEle); fragment.appendChild(liEle); }); return fragment; }這個(gè)函數(shù)的作用很簡(jiǎn)單,就是生成一個(gè)節(jié)點(diǎn):
代碼中有涉及到兩個(gè)性能優(yōu)化的API,第一個(gè)API是document.createDocumentFragment(),它的作用是創(chuàng)建一個(gè)臨時(shí)占位符,然后存放那些需要插入的節(jié)點(diǎn),可以有效避免頁(yè)面進(jìn)行DOM操作時(shí)的重繪和回流,減小頁(yè)面的負(fù)擔(dān),提升頁(yè)面性能。相關(guān)知識(shí)點(diǎn),可參閱以下文章:
JS性能優(yōu)化之創(chuàng)建文檔碎片
前端性能優(yōu)化第三篇-documentFragment
第二個(gè)API是cloneNode(),如果需要?jiǎng)?chuàng)建很多元素,就可以利用這個(gè)API來(lái)減少屬性的設(shè)置次數(shù),不過(guò)必須先提前準(zhǔn)備一個(gè)樣板節(jié)點(diǎn),例如:
var frag = document.createDocumentFragment(); for (var i = 0; i < 1000; i++) { var el = document.createElement("p"); el.innerHTML = i; frag.appendChild(el); } document.body.appendChild(frag); //替換為: var frag = document.createDocumentFragment(); var pEl = document.getElementsByTagName("p")[0]; for (var i = 0; i < 1000; i++) { var el = pEl.cloneNode(false); el.innerHTML = i; frag.appendChild(el); } document.body.appendChild(frag);
完成這個(gè)函數(shù)后,再進(jìn)一步封裝成兩個(gè)插入節(jié)點(diǎn)的函數(shù):(這一步可省略)
addFragmentBefore: function(fragment, datas) { fragment.insertBefore(this.createHtml(datas), fragment.firstChild); } addFragmentAfter: function(fragment, datas) { fragment.appendChild(this.createHtml(datas)); }
前者在最前插入節(jié)點(diǎn),后者在最后插入節(jié)點(diǎn)。
一些常量和重復(fù)操作也可以進(jìn)一步抽?。?/p>
pageInfos: [{ id: "first", content: "首頁(yè)" }, { id: "prev", content: "前一頁(yè)" }, { id: "next", content: "后一頁(yè)" }, { id: "last", content: "尾頁(yè)" }, { id: "", content: "..." } ] getPageInfos: function(className, content) { return { id: "page", className: className, content: content }; }
利用上面封裝好的對(duì)象和方法,我們就可以對(duì)最開(kāi)始那兩個(gè)字符串函數(shù)進(jìn)行改造了:
renderNoEllipsis: function() { var fragment = document.createDocumentFragment(); if (this.pageNumber < this.options.pageShow + 1) { fragment.appendChild(this.renderDom(1, this.options.pageShow * 2 + 1)); } else if (this.pageNumber > this.pageCount - this.options.pageShow) { fragment.appendChild(this.renderDom(this.pageCount - this.options.pageShow * 2, this.pageCount)); } else { fragment.appendChild(this.renderDom(this.pageNumber - this.options.pageShow, this.pageNumber + this.options.pageShow)); } if (this.pageNumber > 1) { this.addFragmentBefore(fragment, [ this.pageInfos[0], this.pageInfos[1] ]); } if (this.pageNumber < this.pageCount) { this.addFragmentAfter(fragment, [this.pageInfos[2], this.pageInfos[3]]); } return fragment; } renderEllipsis: function() { var fragment = document.createDocumentFragment(); this.addFragmentAfter(fragment, [ this.getPageInfos(CLASS_NAME.LINK + " current", this.pageNumber) ]); for (var i = 1; i <= this.options.pageShow; i++) { if (this.pageNumber - i > 1) { this.addFragmentBefore(fragment, [ this.getPageInfos(CLASS_NAME.LINK, this.pageNumber - i) ]); } if (this.pageNumber + i < this.pageCount) { this.addFragmentAfter(fragment, [ this.getPageInfos(CLASS_NAME.LINK, this.pageNumber + i) ]); } } if (this.pageNumber - (this.options.pageShow + 1) > 1) { this.addFragmentBefore(fragment, [this.pageInfos[4]]); } if (this.pageNumber > 1) { this.addFragmentBefore(fragment, [ this.pageInfos[0], this.pageInfos[1], this.getPageInfos(CLASS_NAME.LINK, 1) ]); } if (this.pageNumber + this.options.pageShow + 1 < this.pageCount) { this.addFragmentAfter(fragment, [this.pageInfos[4]]); } if (this.pageNumber < this.pageCount) { this.addFragmentAfter(fragment, [ this.getPageInfos(CLASS_NAME.LINK, this.pageCount), this.pageInfos[2], this.pageInfos[3] ]); } return fragment; } renderDom: function(begin, end) { var fragment = document.createDocumentFragment(); var str = ""; for (var i = begin; i <= end; i++) { str = this.pageNumber === i ? CLASS_NAME.LINK + " current" : CLASS_NAME.LINK; this.addFragmentAfter(fragment, [this.getPageInfos(str, i)]); } return fragment; }
邏輯和最開(kāi)始的showPages()完全一樣,只是變成了DOM的操作而已。
至此,渲染部分的函數(shù)基本也封裝完成,最后還剩一些操作頁(yè)碼的函數(shù),比較簡(jiǎn)單,這里就不作講解了,可自行參考源碼。
使用場(chǎng)景相信大家也看出來(lái)了,此分頁(yè)器只負(fù)責(zé)分頁(yè)本身的邏輯,具體的數(shù)據(jù)請(qǐng)求與渲染需要另外去完成。
不過(guò),此分頁(yè)器不僅能應(yīng)用在一般的異步分頁(yè)上,還可直接對(duì)一段已知數(shù)據(jù)進(jìn)行分頁(yè)展現(xiàn),使用場(chǎng)景如下:
在callback里對(duì)總數(shù)據(jù)進(jìn)行處理,然后取出當(dāng)前頁(yè)需要展示的數(shù)據(jù)即可
后端分頁(yè)利用url上的頁(yè)碼參數(shù),可以在頁(yè)面載入時(shí)就定位到指定頁(yè)碼,并且可以同時(shí)請(qǐng)求后端指定頁(yè)碼下對(duì)應(yīng)的數(shù)據(jù) 在callback回調(diào)函數(shù)里取得當(dāng)前頁(yè)碼,可以使用window.location.href改變url,并將當(dāng)前頁(yè)碼作為url參數(shù),然后進(jìn)行頁(yè)面跳轉(zhuǎn),例如"./test.html?page="
插件調(diào)用插件的調(diào)用也非常方便,首先,我們?cè)陧?yè)面引入相關(guān)的CSS、JS文件:
樣式如果覺(jué)得不滿意可自行調(diào)整
然后將HTML結(jié)構(gòu)插入文檔中:
最后,將必填、選填的參數(shù)配置好即可完成本分頁(yè)插件的初始化:
// 分頁(yè)元素ID(必填) var selector = "#pagelist"; // 分頁(yè)配置 var pageOption = { // 每頁(yè)顯示數(shù)據(jù)條數(shù)(必填) limit: 5, // 數(shù)據(jù)總數(shù)(一般通過(guò)后端獲取,必填) count: 162, // 當(dāng)前頁(yè)碼(選填,默認(rèn)為1) curr: 1, // 是否顯示省略號(hào)(選填,默認(rèn)顯示) ellipsis: true, // 當(dāng)前頁(yè)前后兩邊可顯示的頁(yè)碼個(gè)數(shù)(選填,默認(rèn)為2) pageShow: 2, // 開(kāi)啟location.hash,并自定義hash值 (默認(rèn)關(guān)閉) // 如果開(kāi)啟,在觸發(fā)分頁(yè)時(shí),會(huì)自動(dòng)對(duì)url追加:#!hash值={curr} 利用這個(gè),可以在頁(yè)面載入時(shí)就定位到指定頁(yè) hash: false, // 頁(yè)面加載后默認(rèn)執(zhí)行一次,然后當(dāng)分頁(yè)被切換時(shí)再次觸發(fā) callback: function(obj) { // obj.curr:獲取當(dāng)前頁(yè)碼 // obj.limit:獲取每頁(yè)顯示數(shù)據(jù)條數(shù) // obj.isFirst:是否首次加載頁(yè)面,一般用于初始加載的判斷 // 首次不執(zhí)行 if (!obj.isFirst) { // do something } } }; // 初始化分頁(yè)器 new Pagination(selector, pageOption);
在兩種基礎(chǔ)模式之上,還可以開(kāi)啟Hash模式
那么,整個(gè)分頁(yè)器插件的封裝到這里就全部講解完畢了,怎么樣,是不是覺(jué)得還挺簡(jiǎn)單?偷偷告訴你,接下來(lái)我們會(huì)逐漸嘗試點(diǎn)更有難度的插件哦!敬請(qǐng)期待~~
平心而論,整體的代碼質(zhì)量雖然一般,但是邏輯和結(jié)構(gòu)我覺(jué)得還是寫(xiě)得算比較清晰的吧。代碼的不足之處肯定還有很多,也希望各位看官多多指教!更新(2018-7-29) ES6-環(huán)境配置
2015年,ECMAScript正式發(fā)布了它的新版本——ECMAScript6,對(duì)JavaScript語(yǔ)言本身來(lái)說(shuō),這是一次徹徹底底的升級(jí)。
經(jīng)過(guò)這次更新,不僅修復(fù)了許多ES5時(shí)代留下來(lái)的“坑”,更是在原有的語(yǔ)法和規(guī)則上增加了不少功能強(qiáng)大的新特性,盡管目前瀏覽器對(duì)新規(guī)范支持得并不完善,但經(jīng)過(guò)一些神奇的工具處理后就能讓瀏覽器“認(rèn)識(shí)”這些新東西,并兼容它們了。
so,我們還有什么理由不用強(qiáng)大的ES6呢?接下來(lái)就讓我們先來(lái)看看這些神奇的工具是怎么使用的吧。
Babel首先,我們需要一個(gè)工具來(lái)轉(zhuǎn)換ES6的代碼,它的芳名叫Babel。
Babel是一個(gè)編譯器,負(fù)責(zé)將源代碼轉(zhuǎn)換成指定語(yǔ)法的目標(biāo)代碼,并使它們很好的執(zhí)行在運(yùn)行環(huán)境中,所以我們可以利用它來(lái)編譯我們的ES6代碼。
要使用Babel相關(guān)的功能,必須先用npm安裝它們:(npm及node的使用方法請(qǐng)自行學(xué)習(xí))
npm i babel-cli babel-preset-env babel-core babel-loader babel-plugin-transform-runtime babel-polyfill babel-runtime -D
安裝完成后,我們就可以手動(dòng)使用命令編譯某個(gè)目錄下的js文件,并輸出它們了。
But,這就是完美方案了嗎?顯然不是。
在實(shí)際的開(kāi)發(fā)環(huán)境中,我們還需要考慮更多東西,比如模塊化開(kāi)發(fā)、自動(dòng)編譯和構(gòu)建等等,所以我們還需要一個(gè)更為強(qiáng)大的工具來(lái)升級(jí)我們的這套構(gòu)建流程。
Webpack圍觀群眾:我知道了!你是想說(shuō)Gulp對(duì)吧?!喂,醒醒!大清亡了!
在前端框架以及工程化大行其道的今天,想必大家對(duì)Webpack、Gulp等工具并不會(huì)感到陌生,配合它們我們可以輕松實(shí)現(xiàn)一個(gè)大型前端應(yīng)用的構(gòu)建、打包、發(fā)布的流程。
不過(guò)現(xiàn)在是2018年了,三大框架三足鼎立,而Gulp已經(jīng)稍顯老態(tài),作為它的晚輩,一個(gè)名叫Webpack的少年正在逐漸崛起。
這位少年,相信大家在使用Vue、React的過(guò)程中已經(jīng)或多或少接觸過(guò)它了。簡(jiǎn)而言之,它和Gulp在項(xiàng)目中的角色是一樣的,只不過(guò)配置更為簡(jiǎn)單,構(gòu)建更為高效,下面就讓我們來(lái)看看Webpack是怎么使用的吧。
如果你還沒(méi)有接觸過(guò)Webpack,那可以參考官方文檔,先對(duì)Webpack有一個(gè)大致的認(rèn)識(shí),我們這里不作過(guò)多介紹,只講解它的安裝與配置。
As usual,我們需要安裝它:
npm i webpack webpack-cli webpack-dev-server -D
使用它也非常簡(jiǎn)單,只需要建立一個(gè)名叫webpack.config.js的配置文件即可:
const path = require("path"); module.exports = { // 模式配置 mode: "development", // 入口文件 entry: {}, // 出口文件 output: {}, // 對(duì)應(yīng)的插件 plugins: [], // 處理對(duì)應(yīng)模塊 module: {} }
這個(gè)配置文件的主要部分有:入口、出口、插件、模塊,在具體配置它們之前,我們可以先理一理我們項(xiàng)目的打包構(gòu)建流程:
尋找到./src/es6/目錄下面的index.js項(xiàng)目入口文件
使用Babel編譯它及它所引用的所有依賴(如Scss、css文件等)
壓縮編譯完成后的js文件,配置為umd規(guī)范,重命名為csdwheels.min.js
清空dist-es6目錄
輸出至dist-es6目錄下
要使用清空目錄、壓縮代碼、解析css等功能,我們還需要安裝一下額外的包:
npm i clean-webpack-plugin uglifyjs-webpack-plugin css-loader style-loader node-sass sass-loader
要在配置中讓babel失效,還需要建立一個(gè).babelrc文件,并在其中指定編碼規(guī)則:
{ "presets": ["env"] }
最后,我們就能完成這個(gè)配置文件了:
const path = require("path"); const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); const CleanWebpackPlugin = require("clean-webpack-plugin"); //每次構(gòu)建清理dist目錄 module.exports = { // 模式配置 mode: "development", // 入口文件 entry: { pagination: "./src/es6/index.js" }, // 出口文件 output: { path: path.resolve(__dirname, "dist-es6"), filename: "csdwheels.min.js", libraryTarget: "umd", library: "csdwheels" }, // 對(duì)應(yīng)的插件 plugins: [ new CleanWebpackPlugin(["dist-es6"]), new UglifyJsPlugin({ test: /.js($|?)/i }) ], // 開(kāi)發(fā)服務(wù)器配置 devServer: {}, // 處理對(duì)應(yīng)模塊 module: { rules: [ { test: /.js$/, include: path.join(__dirname , "src/es6"), exclude: /node_modules/, use: ["babel-loader"] }, { test: /.scss$/, use: [{ loader: "style-loader" }, { loader: "css-loader" }, { loader: "sass-loader" }] } ] } }
光配置好還不夠,我們總需要用命令來(lái)運(yùn)行它吧,在package.json里配置:
"scripts": { "test": "node test/test.js", "dev": "webpack-dev-server", "build": "webpack && gulp mini && npm run test" }
這里使用dev可以啟動(dòng)一個(gè)服務(wù)器來(lái)展示項(xiàng)目,不過(guò)這里我們暫時(shí)不需要,而運(yùn)行npm run build命令就可以同時(shí)將我們的./src/es5和./src/es6目錄下的源碼打包好輸出到指定目錄了。
不是說(shuō)好不用Gulp的呢?嘛。。針對(duì)ES5的打包工作來(lái)說(shuō)Gulp還是挺好用的,真香警告!
ES6開(kāi)發(fā)所需要的環(huán)境終于配置完成,接下來(lái)就讓我們開(kāi)始代碼的重構(gòu)吧!
ES6-代碼重構(gòu)如果你想要入門(mén)ES6,強(qiáng)烈推薦阮一峰老師的教程相關(guān)的新語(yǔ)法和特性較多,不過(guò)要我們的項(xiàng)目要重構(gòu)為ES6暫時(shí)還用不了多少比較高級(jí)的特性,你只需要著重看完Class部分即可。
ES6引入的新特性中,最重要的一個(gè)就是Class了。有了它,我們不需要再像以前那樣用構(gòu)造函數(shù)去模擬面向?qū)ο蟮膶?xiě)法,因?yàn)樗荍avaScript原生支持的一種面向?qū)ο蟮恼Z(yǔ)法糖,雖然底層仍然是原型鏈,不過(guò)至少寫(xiě)出來(lái)的代碼看上去像是那么一回事了。
拿前面提到的插件模板來(lái)說(shuō),ES5的時(shí)候我們是這樣寫(xiě)的:
(function(root, factory) { if (typeof define === "function" && define.amd) { define([], factory); } else if (typeof module === "object" && module.exports) { module.exports = factory(); } else { root.Plugin = factory(); } }(typeof self !== "undefined" ? self : this, function() { "use strict"; // tool function extend(o, n, override) { for (var p in n) { if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override)) o[p] = n[p]; } } // plugin construct function function Plugin(selector, userOptions) { // Plugin() or new Plugin() if (!(this instanceof Plugin)) return new Plugin(selector, userOptions); this.init(selector, userOptions) } Plugin.prototype = { constructor: Plugin, // default option options: {}, init: function(selector, userOptions) { extend(this.options, userOptions, true); } }; return Plugin; }));
經(jīng)過(guò)Class這種新語(yǔ)法糖的改造后,它變成了下面這樣:
// ES6 插件模板 class Plugin { constructor(selector, options = {}) { this.options = {}; Object.assign(this.options, options); this.init(selector, options); } init(selector, options) {} } export default Plugin;
改造后的代碼,不僅在語(yǔ)法層面直接支持了構(gòu)造函數(shù)的寫(xiě)法,更是去掉了IIFE這種臃腫的寫(xiě)法,可以說(shuō)不管是看起來(lái)還是寫(xiě)起來(lái)都更為清晰流暢了。
利用內(nèi)置的Object.assign()方法,可以直接替換掉我們實(shí)現(xiàn)的extend函數(shù),功能可以說(shuō)完全一樣,而且更為強(qiáng)大
有了新的模板,我們就能直接開(kāi)始插件代碼的重構(gòu)了,這里只貼上變動(dòng)比較大的幾個(gè)地方,其余部分可參考源碼
import "../../../style/pagination/pagination.scss" class Pagination { static CLASS_NAME = { ITEM: "pagination-item", LINK: "pagination-link" } static PAGE_INFOS = [{ id: "first", content: "首頁(yè)" }, { id: "prev", content: "前一頁(yè)" }, { id: "next", content: "后一頁(yè)" }, { id: "last", content: "尾頁(yè)" }, { id: "", content: "..." } ] constructor(selector, options = {}) { // 默認(rèn)配置 this.options = { curr: 1, pageShow: 2, ellipsis: true, hash: false }; Object.assign(this.options, options); this.init(selector); } changePage () { let pageElement = this.pageElement; this.addEvent(pageElement, "click", (ev) => { let e = ev || window.event; let target = e.target || e.srcElement; if (target.nodeName.toLocaleLowerCase() == "a") { if (target.id === "prev") { this.prevPage(); } else if (target.id === "next") { this.nextPage(); } else if (target.id === "first") { this.firstPage(); } else if (target.id === "last") { this.lastPage(); } else if (target.id === "page") { this.goPage(parseInt(target.innerHTML)); } else { return; } this.renderPages(); this.options.callback && this.options.callback({ curr: this.pageNumber, limit: this.options.limit, isFirst: false }); this.pageHash(); } }); } init(selector) { // 分頁(yè)器元素 this.pageElement = this.$(selector)[0]; // 數(shù)據(jù)總數(shù) this.dataCount = this.options.count; // 當(dāng)前頁(yè)碼 this.pageNumber = this.options.curr; // 總頁(yè)數(shù) this.pageCount = Math.ceil(this.options.count / this.options.limit); // 渲染 this.renderPages(); // 執(zhí)行回調(diào)函數(shù) this.options.callback && this.options.callback({ curr: this.pageNumber, limit: this.options.limit, isFirst: true }); // 改變頁(yè)數(shù)并觸發(fā)事件 this.changePage(); } } export default Pagination;
總結(jié)起來(lái),這次改造用到的語(yǔ)法就這么幾點(diǎn):
const、let替換var
用constructor實(shí)現(xiàn)構(gòu)造函數(shù)
箭頭函數(shù)替換function
除此之外,在安裝了Sass的編譯插件后,我們還能直接在這個(gè)js文件中把樣式import進(jìn)來(lái),這樣打包壓縮后的js中也會(huì)包含進(jìn)我們的樣式代碼,使用的時(shí)候就不需要額外再引入樣式文件了。
最后,由于ES6并不支持類的靜態(tài)屬性,所以還需要用到ES7新提案的static語(yǔ)法。我們可以安裝對(duì)應(yīng)的babel包:
npm i babel-preset-stage-0 -D
安裝后,在.babelrc文件中添加它即可:
{ "presets": ["env", "stage-0"] }
現(xiàn)在萬(wàn)事俱備,你只需要運(yùn)行npm run build,然后就可以看到我們打包完成后的csdwheels.min.js文件了。
打包后,我們還可以發(fā)布這個(gè)npm包,運(yùn)行如下命令即可:(有關(guān)npm的發(fā)布流程,這里就不啰嗦了)
npm loginnpm publish
要使用發(fā)布后的插件,只需要安裝這個(gè)npm包,并import對(duì)應(yīng)的插件:
npm i csdwheels -D
import { Pagination } from "csdwheels";更新(2018-08-01) Vue插件版本
按照原定開(kāi)發(fā)計(jì)劃,其實(shí)是不想馬上更新Vue版本的,畢竟這個(gè)系列的“賣(mài)點(diǎn)”是原生開(kāi)發(fā),不過(guò)最近用Vue做的項(xiàng)目和自己的博客都恰好用到了分頁(yè)這個(gè)組件,所以我決定一鼓作氣把這個(gè)插件的Vue版本寫(xiě)出來(lái),正好也利用這個(gè)機(jī)會(huì)學(xué)學(xué)Vue插件的開(kāi)發(fā)。
開(kāi)發(fā)規(guī)范既然是框架,那肯定有它自己的開(kāi)發(fā)規(guī)范了,類似于我們自己寫(xiě)的插件一樣,它也會(huì)給我們提供各式各樣的API接口,讓我們能定制自己的插件模塊。
簡(jiǎn)單來(lái)說(shuō),我們的插件在Vue中需要掛載到全局上,這樣才能直接在任何地方引入插件:
import Pagination from "./components/vue-wheels-pagination" const VueWheelsPagination = { install (Vue, options) { Vue.component(Pagination.name, Pagination) } } if (typeof window !== "undefined" && window.Vue) { window.Vue.use(VueWheelsPagination) } export { VueWheelsPagination }
vue-wheels-pagination是我們即將要開(kāi)發(fā)的單文件組件,引入后通過(guò)install方法把它掛載上去,然后在外部就可以use這個(gè)插件了,最后導(dǎo)出這個(gè)掛載了我們插件的對(duì)象。(如果檢測(cè)到瀏覽器環(huán)境后,可以直接掛載它)
這差不多就是一個(gè)最簡(jiǎn)單的插件模板了,更詳細(xì)的配置可參考官方文檔。
將這個(gè)入口用Webpack打包后,就可以在你Vue項(xiàng)目中的main.js中全局加載這個(gè)插件了:
import { VueWheelsPagination } from "vue-wheels" Vue.use(VueWheelsPagination)
接下來(lái),就讓我們來(lái)看看用Vue的方式是怎么完成這個(gè)分頁(yè)插件的吧!
DOM渲染利用現(xiàn)代MVVM框架雙向綁定的特性,我們已經(jīng)不必再用原生JS的API去直接操作DOM了,取而代之的,可以在DOM結(jié)構(gòu)上利用框架提供的API間接進(jìn)行DOM的渲染及交互:
如上,我們直接在單文件組件的template標(biāo)簽中就完成了這個(gè)插件大部分的渲染邏輯。相對(duì)原生JS實(shí)現(xiàn)的版本,不僅輕松省去了事件監(jiān)聽(tīng)、DOM操作等步驟,而且讓我們能只關(guān)注插件本身具體的交互邏輯,可以說(shuō)大大減輕了開(kāi)發(fā)難度,并提升了頁(yè)面性能。剩下的數(shù)據(jù)部分的邏輯及交互處理,在JS中完成即可。
交互邏輯export default { name: "VueWheelsPagination", props: { count: { type: Number, required: true }, limit: { type: Number, required: true }, curr: { type: Number, required: false, default: 1 }, max: { type: Number, required: false, default: 2 }, ellipsis: { type: Boolean, required: false, default: true }, info: { type: Object, required: false, default: { firstInfo: "首頁(yè)", prevInfo: "前一頁(yè)", nextInfo: "后一頁(yè)", lastInfo: "尾頁(yè)" } } }, data () { return { pageNumber: this.curr } }, watch: { curr (newVal) { this.pageNumber = newVal } }, computed: { pageData () { let pageData = [] for (let index = 1; index <= this.max; index++) { pageData.push(index) } return pageData }, rPageData () { return this.pageData.slice(0).reverse() }, pageDataFront () { let pageDataFront = [] for (let index = 1; index <= this.max * 2 + 1; index++) { pageDataFront.push(index) } return pageDataFront }, pageDataCenter () { let pageDataCenter = [] for (let index = this.pageCount - this.max * 2; index <= this.pageCount; index++) { pageDataCenter.push(index) } return pageDataCenter }, pageDataBehind () { let pageDataBehind = [] for (let index = this.pageNumber - this.max; index <= this.pageNumber + this.max; index++) { pageDataBehind.push(index) } return pageDataBehind }, pageCount () { return Math.ceil(this.count / this.limit) } }, methods: { goFirst () { this.pageNumber = 1 this.$emit("pageChange", 1) }, goPrev () { this.pageNumber-- this.$emit("pageChange", this.pageNumber) }, goPage (pageNumber) { this.pageNumber = pageNumber this.$emit("pageChange", this.pageNumber) }, goNext () { this.pageNumber++ this.$emit("pageChange", this.pageNumber) }, goLast () { this.pageNumber = this.pageCount this.$emit("pageChange", this.pageNumber) } } }
總體分成幾個(gè)部分:
props屬性中對(duì)父組件傳遞的參數(shù)進(jìn)行類型、默認(rèn)值、是否必填等配置的定義
計(jì)算屬性中對(duì)分頁(yè)器本身所需數(shù)據(jù)進(jìn)行初始化
定義操作頁(yè)碼的方法,并向父組件傳遞當(dāng)前頁(yè)碼
在watch屬性中監(jiān)聽(tīng)頁(yè)碼的變化(主要應(yīng)用于不通過(guò)分頁(yè)而在其他地方改變頁(yè)碼的情況)
這樣,整個(gè)分頁(yè)插件的開(kāi)發(fā)就已經(jīng)完成了。相信大家可以感覺(jué)得到,關(guān)于分頁(yè)邏輯部分的代碼量是明顯減少了不少的,并且插件本身的邏輯也更清晰,和我們前面一步一步從底層實(shí)現(xiàn)起來(lái)的版本比較起來(lái),更易拓展和維護(hù)了。
在外層的組件上調(diào)用起來(lái)大概就像這樣:
export default { name: "app", data () { return { count: 162, limit: 5, info: { firstInfo: "<<", prevInfo: "<", nextInfo: ">", lastInfo: ">>" } } }, methods: { change (pageNumber) { console.log(pageNumber) } } }
傳入必填和選填的參數(shù),再監(jiān)聽(tīng)到子組件冒泡回來(lái)的頁(yè)碼值,最后在你自己定義的change()方法里進(jìn)行跳轉(zhuǎn)等對(duì)應(yīng)的邏輯處理就行了。
項(xiàng)目的打包流程和上一節(jié)提到的差不多,只不過(guò)在配置上額外增加了一個(gè)本地開(kāi)發(fā)環(huán)境服務(wù)器的啟動(dòng),可以參考我的源碼。打包完成后,同樣可以發(fā)布一個(gè)npm包,然后就可以在任何Vue項(xiàng)目中引入并使用了。
后面開(kāi)發(fā)的輪子不一定都會(huì)發(fā)布Vue版本,因?yàn)橐呀?jīng)給大家提供了一種重構(gòu)和包裝插件的思路,如果你有自己的需求,可自行利用框架的規(guī)范進(jìn)行插件開(kāi)發(fā)。
到止為止,我們第一個(gè)輪子的開(kāi)發(fā)就算真正結(jié)束了,所有源碼已同步更新到github,如果大家發(fā)現(xiàn)有bug或其他問(wèn)題,可以回復(fù)在項(xiàng)目的issue中,咱們后會(huì)有期!(挖坑不填,逃。。
To be continued...
參考內(nèi)容由匿名函數(shù)展開(kāi)的一系列知識(shí)點(diǎn)
自執(zhí)行函數(shù)(IIFE)
UMD (Universal Module Definition)
原生JavaScript插件編寫(xiě)指南
如何定義一個(gè)高逼格的原生JS插件
如何寫(xiě)一個(gè)簡(jiǎn)單的分頁(yè)
我總結(jié)的js性能優(yōu)化的小知識(shí)
起步 | webpack 中文網(wǎng)
webpack 項(xiàng)目構(gòu)建:(一)基本架構(gòu)搭建
webpack 項(xiàng)目構(gòu)建:(二)ES6 編譯環(huán)境搭建
Vue-Guide-插件
第一個(gè)Vue插件從封裝到發(fā)布
vue封裝插件并發(fā)布到npm上
vue封裝第三方插件并發(fā)布到npm
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/96282.html
摘要:綁定輪播事件然后是鼠標(biāo)移入移出事件的綁定鼠標(biāo)移入移出事件移入時(shí)停止輪播播放的定時(shí)器,移出后自動(dòng)開(kāi)始下一張的播放。 通過(guò)上一篇文章的學(xué)習(xí),我們基本掌握了一個(gè)輪子的封裝和開(kāi)發(fā)流程。那么這次將帶大家開(kāi)發(fā)一個(gè)更有難度的項(xiàng)目——輪播圖,希望能進(jìn)一步加深大家對(duì)于面向?qū)ο蟛寮_(kāi)發(fā)的理解和認(rèn)識(shí)。 So, Lets begin! 目前項(xiàng)目使用 ES5及UMD 規(guī)范封裝,所以在前端暫時(shí)只支持標(biāo)簽的引入方式...
摘要:模塊化是隨著前端技術(shù)的發(fā)展,前端代碼爆炸式增長(zhǎng)后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調(diào)也不等同于異步。將會(huì)討論安全的類型檢測(cè)惰性載入函數(shù)凍結(jié)對(duì)象定時(shí)器等話題。 Vue.js 前后端同構(gòu)方案之準(zhǔn)備篇——代碼優(yōu)化 目前 Vue.js 的火爆不亞于當(dāng)初的 React,本人對(duì)寫(xiě)代碼有潔癖,代碼也是藝術(shù)。此篇是準(zhǔn)備篇,工欲善其事,必先利其器。我們先在代...
摘要:靈活性和針對(duì)性。所以我覺(jué)得大部分組件還是自己封裝來(lái)的更為方便和靈活一些。動(dòng)手開(kāi)干接下來(lái)我們一起手摸手教改造包裝一個(gè)插件,只要幾分鐘就可以封裝一個(gè)專屬于你的。 項(xiàng)目地址:vue-countTo配套完整后臺(tái)demo地址:vue-element-admin系類文章一:手摸手,帶你用vue擼后臺(tái) 系列一(基礎(chǔ)篇)系類文章二:手摸手,帶你用vue擼后臺(tái) 系列二(登錄權(quán)限篇)系類文章三:手摸手,帶...
摘要:插件開(kāi)發(fā)前端掘金作者原文地址譯者插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡(jiǎn)單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內(nèi)優(yōu)雅的實(shí)現(xiàn)文件分片斷點(diǎn)續(xù)傳。 Vue.js 插件開(kāi)發(fā) - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡(jiǎn)單的方式。插....
閱讀 1042·2021-09-26 09:55
閱讀 3312·2021-09-22 15:36
閱讀 3079·2021-09-04 16:48
閱讀 3301·2021-09-01 11:41
閱讀 2663·2019-08-30 13:49
閱讀 1565·2019-08-29 18:46
閱讀 3616·2019-08-29 17:28
閱讀 3554·2019-08-29 14:11