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