作為一個前端er,如果不會寫一個小插件,都不好意思說自己是混前端界的。寫還不能依賴jquery之類的工具庫,否則裝得不夠高端。那么,如何才能裝起來讓自己看起來逼格更高呢?當(dāng)然是利用js純原生的寫法啦。以前一直說,掌握了js原生,就基本上可以解決前端的所有腳本交互工作了,這話大體上是有些浮夸了。不過,也從側(cè)面說明了原生js在前端中占著多么重要的一面。好了。廢話不多說。咱們就來看一下怎么去做一個自己的js插件吧。
插件的需求
我們寫代碼,并不是所有的業(yè)務(wù)或者邏輯代碼都要抽出來復(fù)用。首先,我們得看一下是否需要將一部分經(jīng)常重復(fù)的代碼抽象出來,寫到一個單獨(dú)的文件中為以后再次使用。再看一下我們的業(yè)務(wù)邏輯是否可以為團(tuán)隊(duì)服務(wù)。
插件不是隨手就寫成的,而是根據(jù)自己業(yè)務(wù)邏輯進(jìn)行抽象。沒有放之四海而皆準(zhǔn)的插件,只有對插件,之所以叫做插件,那么就是開箱即用,或者我們只要添加一些配置參數(shù)就可以達(dá)到我們需要的結(jié)果。如果都符合了這些情況,我們才去考慮做一個插件。
插件封裝的條件
一個可復(fù)用的插件需要滿足以下條件:
- 插件自身的作用域與用戶當(dāng)前的作用域相互獨(dú)立,也就是插件內(nèi)部的私有變量不能影響使用者的環(huán)境變量;
- 插件需具備默認(rèn)設(shè)置參數(shù);
- 插件除了具備已實(shí)現(xiàn)的基本功能外,需提供部分API,使用者可以通過該API修改插件功能的默認(rèn)參數(shù),從而實(shí)現(xiàn)用戶自定義插件效果;
- 插件支持鏈?zhǔn)秸{(diào)用;
- 插件需提供監(jiān)聽入口,及針對指定元素進(jìn)行監(jiān)聽,使得該元素與插件響應(yīng)達(dá)到插件效果。
關(guān)于插件封裝的條件,可以查看一篇文章:原生JavaScript插件編寫指南
而我想要說明的是,如何一步一步地實(shí)現(xiàn)我的插件封裝。所以,我會先從簡單的方法函數(shù)來做起。
插件的外包裝
用函數(shù)包裝
所謂插件,其實(shí)就是封裝在一個閉包中的一種函數(shù)集。我記得剛開始寫js的時候,我是這樣干的,將我想要的邏輯,寫成一個函數(shù),然后再根據(jù)不同需要傳入不同的參數(shù)就可以了。
比如,我想實(shí)現(xiàn)兩個數(shù)字相加的方法:
function add(n1,n2) {
return n1 + n2;
}
// 調(diào)用
add(1,2)
// 輸出:3
這就是我們要的功能的簡單實(shí)現(xiàn)。如果僅僅只不過實(shí)現(xiàn)這么簡單的邏輯,那已經(jīng)可以了,沒必要弄一些花里胡哨的東西。js函數(shù)本身就可以解決絕大多數(shù)的問題。不過我們在實(shí)際工作與應(yīng)用中,一般情況的需求都是比較復(fù)雜得多。
如果這時,產(chǎn)品來跟你說,我不僅需要兩個數(shù)相加的,我還要相減,相乘,相除,求余等等功能。這時候,我們怎么辦呢?
當(dāng)然,你會想,這有什么難的。直接將這堆函數(shù)都寫出來不就完了。然后都放在一個js文件里面。需要的時候,就調(diào)用它就好了。
// 加
function add(n1,n2) {
return n1 + n2;
}
// 減
function sub(n1,n2) {
return n1 - n2;
}
// 乘
function mul(n1,n2) {
return n1 * n2;
}
// 除
function div(n1,n2) {
return n1 / n2;
}
// 求余
function sur(n1,n2) {
return n1 % n2;
}
OK,現(xiàn)在已經(jīng)實(shí)現(xiàn)我們所需要的所有功能。并且我們也把這些函數(shù)都寫到一個js里面了。如果是一個人在用,那么可以很清楚知道自己是否已經(jīng)定義了什么,并且知道自己寫了什么內(nèi)容,我在哪個頁面需要,那么就直接引入這個js文件就可以搞定了。
不過,如果是兩個人以上的團(tuán)隊(duì),或者你與別人一起協(xié)作寫代碼,這時候,另一個人并不知道你是否寫了add方法,這時他也定義了同樣的add方法。那么你們之間就會產(chǎn)生命名沖突,一般稱之為變量的 全局污染
用全局對象包裝
為了解決這種全局變量污染的問題。這時,我們可以定義一個js對象來接收我們這些工具函數(shù)。
var plugin = {
add: function(n1,n2){...},//加
sub: function(n1,n2){...},//減
mul: function(n1,n2){...},//乘
div: function(n1,n2){...},//除
sur: function(n1,n2){...} //余
}
// 調(diào)用
plugin.add(1,2)
上面的方式,約定好此插件名為plugin,讓團(tuán)隊(duì)成員都要遵守命名規(guī)則,在一定程度上已經(jīng)解決了全局污染的問題。在團(tuán)隊(duì)協(xié)作中只要約定好命名規(guī)則了,告知其它同學(xué)即可以。當(dāng)然不排除有個別人,接手你的項(xiàng)目,并不知道此全局變量已經(jīng)定義,則他又定義了一次并賦值,這時,就會把你的對象覆蓋掉。當(dāng)然,可能你會這么干來解決掉命名沖突問題:
if(!plugin){ //這里的if條件也可以用: (typeof plugin == 'undefined')
var plugin = {
// 以此寫你的函數(shù)邏輯
}
}
或者也可以這樣寫:
var plugin;
if(!plugin){
plugin = {
// ...
}
}
這樣子,就不會存在命名上的沖突了。
也許有同學(xué)會疑問,為什么可以在此聲明plugin變量?實(shí)際上js的解釋執(zhí)行,會把所有聲明都提前。如果一個變量已經(jīng)聲明過,后面如果不是在函數(shù)內(nèi)聲明的,則是沒有影響的。所以,就算在別的地方聲明過var plugin,我同樣也以可以在這里再次聲明一次。關(guān)于聲明的相關(guān)資料可以看阮一鋒的如何判斷Javascript對象是否存在。
基本上,這就可以算是一個插件了。解決了全局污染問題,方法函數(shù)可以抽出來放到一單獨(dú)的文件里面去。
利用閉包包裝
上面的例子,雖然可以實(shí)現(xiàn)了插件的基本上的功能。不過我們的plugin對象,是定義在全局域里面的。我們知道,js變量的調(diào)用,從全局作用域上找查的速度會比在私有作用域里面慢得多得多。所以,我們最好將插件邏輯寫在一個私有作用域中。
實(shí)現(xiàn)私有作用域,最好的辦法就是使用閉包。可以把插件當(dāng)做一個函數(shù),插件內(nèi)部的變量及函數(shù)的私有變量,為了在調(diào)用插件后依舊能使用其功能,閉包的作用就是延長函數(shù)(插件)內(nèi)部變量的生命周期,使得插件函數(shù)可以重復(fù)調(diào)用,而不影響用戶自身作用域。
故需將插件的所有功能寫在一個立即執(zhí)行函數(shù)中:
;(function(global,undefined) {
var plugin = {
add: function(n1,n2){...}
...
}
// 最后將插件對象暴露給全局對象
'plugin' in global && (global.plugin = plugin);
})(window);
對上面的代碼段傳參問題進(jìn)行解釋一下:
- 在定義插件之前添加一個分號,可以解決js合并時可能會產(chǎn)生的錯誤問題;
- undefined在老一輩的瀏覽器是不被支持的,直接使用會報錯,js框架要考慮到兼容性,因此增加一個形參undefined,就算有人把外面的
undefined定義了,里面的 undefined 依然不受影響; - 把window對象作為參數(shù)傳入,是避免了函數(shù)執(zhí)行的時候到外部去查找。
其實(shí),我們覺得直接傳window對象進(jìn)去,我覺得還是不太妥當(dāng)。我們并不確定我們的插件就一定用于瀏覽器上,也有可能使用在一些非瀏覽端上。所以我們還可以這么干,我們不傳參數(shù),直接取當(dāng)前的全局this對象為作頂級對象用。
;(function(global,undefined) {
"use strict" //使用js嚴(yán)格模式檢查,使語法更規(guī)范
var _global;
var plugin = {
add: function(n1,n2){...}
...
}
// 最后將插件對象暴露給全局對象
_global = (function(){ return this || (0, eval)('this'); }());
!('plugin' in _global) && (_global.plugin = plugin);
}());
如此,我們不需要傳入任何參數(shù),并且解決了插件對環(huán)境的依事性。如此我們的插件可以在任何宿主環(huán)境上運(yùn)行了。
上面的代碼段中有段奇怪的表達(dá)式:
(0, eval)('this'),實(shí)際上(0,eval)是一個表達(dá)式,這個表達(dá)式執(zhí)行之后的結(jié)果就是eval這一句相當(dāng)于執(zhí)行eval('this')的意思,詳細(xì)解釋看此篇:(0,eval)('this')釋義或者看一下這篇(0,eval)('this')
關(guān)于立即自執(zhí)行函數(shù),有兩種寫法:
// 寫法一
(function(){})()
//寫法二
(function(){}())
上面的兩種寫法是沒有區(qū)別的。都是正確的寫法。個人建議使用第二種寫法。這樣子更像一個整體。
附加一點(diǎn)知識:
js里面()括號就是將代碼結(jié)構(gòu)變成表達(dá)式,被包在()里面的變成了表達(dá)式之后,則就會立即執(zhí)行,js中將一段代碼變成表達(dá)式有很多種方式,比如:
void function(){...}();
// 或者
!function foo(){...}();
// 或者
+function foot(){...}();
當(dāng)然,我們不推薦你這么用。而且亂用可能會產(chǎn)生一些歧義。
到這一步,我們的插件的基礎(chǔ)結(jié)構(gòu)就已經(jīng)算是完整的了。
使用模塊化的規(guī)范包裝
雖然上面的包裝基本上已經(jīng)算是ok了的。但是如果是多個人一起開發(fā)一個大型的插件,這時我們要該怎么辦呢?多人合作,肯定會產(chǎn)生多個文件,每個人負(fù)責(zé)一個小功能,那么如何才能將所有人開發(fā)的代碼集合起來呢?這是一個討厭的問題。要實(shí)現(xiàn)協(xié)作開發(fā)插件,必須具備如下條件:
- 每功能互相之間的依賴必須要明確,則必須嚴(yán)格按照依賴的順序進(jìn)行合并或者加載
- 每個子功能分別都要是一個閉包,并且將公共的接口暴露到共享域也即是一個被主函數(shù)暴露的公共對象
關(guān)鍵如何實(shí)現(xiàn),有很多種辦法。最笨的辦法就是按順序加載js
<script type="text/javascript" src="part1.js"></script>
<script type="text/javascript" src="part2.js"></script>
<script type="text/javascript" src="part3.js"></script>
...
<script type="text/javascript" src="main.js"></script>
但是不推薦這么做,這樣做與我們所追求的插件的封裝性相背。
不過現(xiàn)在前端界有一堆流行的模塊加載器,比如require、seajs,或者也可以像類似于Node的方式進(jìn)行加載,不過在瀏覽器端,我們還得利用打包器來實(shí)現(xiàn)模塊加載,比如browserify。不過在此不談如何進(jìn)行模塊化打包或者加載的問題,如有問題的同學(xué)可以去上面的鏈接上看文檔學(xué)習(xí)。
為了實(shí)現(xiàn)插件的模塊化并且讓我們的插件也是一個模塊,我們就得讓我們的插件也實(shí)現(xiàn)模塊化的機(jī)制。
我們實(shí)際上,只要判斷是否存在加載器,如果存在加載器,我們就使用加載器,如果不存在加載器。我們就使用頂級域?qū)ο蟆?/p>
if (typeof module !== "undefined" && module.exports) {
module.exports = plugin;
} else if (typeof define === "function" && define.amd) {
define(function(){return plugin;});
} else {
_globals.plugin = plugin;
}
這樣子我們的完整的插件的樣子應(yīng)該是這樣子的:
// plugin.js
;(function(undefined) {
"use strict"
var _global;
var plugin = {
add: function(n1,n2){ return n1 + n2; },//加
sub: function(n1,n2){ return n1 - n2; },//減
mul: function(n1,n2){ return n1 * n2; },//乘
div: function(n1,n2){ return n1 / n2; },//除
sur: function(n1,n2){ return n1 % n2; } //余
}
// 最后將插件對象暴露給全局對象
_global = (function(){ return this || (0, eval)('this'); }());
if (typeof module !== "undefined" && module.exports) {
module.exports = plugin;
} else if (typeof define === "function" && define.amd) {
define(function(){return plugin;});
} else {
!('plugin' in _global) && (_global.plugin = plugin);
}
}());
我們引入了插件之后,則可以直接使用plugin對象。
with(plugin){
console.log(add(2,1)) // 3
console.log(sub(2,1)) // 1
console.log(mul(2,1)) // 2
console.log(div(2,1)) // 2
console.log(sur(2,1)) // 0
}
插件的API
插件的默認(rèn)參數(shù)
我們知道,函數(shù)是可以設(shè)置默認(rèn)參數(shù)這種說法,而不管我們是否傳有參數(shù),我們都應(yīng)該返回一個值以告訴用戶我做了怎樣的處理,比如:
function add(param){
var args = !!param ? Array.prototype.slice.call(arguments) : [];
return args.reduce(function(pre,cur){
return pre + cur;
}, 0);
}
console.log(add()) //不傳參,結(jié)果輸出0,則這里已經(jīng)設(shè)置了默認(rèn)了參數(shù)為空數(shù)組
console.log(add(1,2,3,4,5)) //傳參,結(jié)果輸出15
則作為一個健壯的js插件,我們應(yīng)該把一些基本的狀態(tài)參數(shù)添加到我們需要的插件上去。
假設(shè)還是上面的加減乘除余的需求,我們?nèi)绾螌?shí)現(xiàn)插件的默認(rèn)參數(shù)呢?道理其實(shí)是一樣的。
// plugin.js
;(function(undefined) {
"use strict"
var _global;
function result(args,fn){
var argsArr = Array.prototype.slice.call(args);
if(argsArr.length > 0){
return argsArr.reduce(fn);
} else {
return 0;
}
}
var plugin = {
add: function(){
return result(arguments,function(pre,cur){
return pre + cur;
});
},//加
sub: function(){
return result(arguments,function(pre,cur){
return pre - cur;
});
},//減
mul: function(){
return result(arguments,function(pre,cur){
return pre * cur;
});
},//乘
div: function(){
return result(arguments,function(pre,cur){
return pre / cur;
});
},//除
sur: function(){
return result(arguments,function(pre,cur){
return pre % cur;
});
} //余
}
// 最后將插件對象暴露給全局對象
_global = (function(){ return this || (0, eval)('this'); }());
if (typeof module !== "undefined" && module.exports) {
module.exports = plugin;
} else if (typeof define === "function" && define.amd) {
define(function(){return plugin;});
} else {
!('plugin' in _global) && (_global.plugin = plugin);
}
}());
// 輸出結(jié)果為:
with(plugin){
console.log(add()); // 0
console.log(sub()); // 0
console.log(mul()); // 0
console.log(div()); // 0
console.log(sur()); // 0
console.log(add(2,1)); // 3
console.log(sub(2,1)); // 1
console.log(mul(2,1)); // 2
console.log(div(2,1)); // 2
console.log(sur(2,1)); // 0
}
實(shí)際上,插件都有自己的默認(rèn)參數(shù),就以我們最為常見的表單驗(yàn)證插件為例:validate.js
(function(window, document, undefined) {
// 插件的默認(rèn)參數(shù)
var defaults = {
messages: {
required: 'The %s field is required.',
matches: 'The %s field does not match the %s field.',
"default": 'The %s field is still set to default, please change.',
valid_email: 'The %s field must contain a valid email address.',
valid_emails: 'The %s field must contain all valid email addresses.',
min_length: 'The %s field must be at least %s characters in length.',
max_length: 'The %s field must not exceed %s characters in length.',
exact_length: 'The %s field must be exactly %s characters in length.',
greater_than: 'The %s field must contain a number greater than %s.',
less_than: 'The %s field must contain a number less than %s.',
alpha: 'The %s field must only contain alphabetical characters.',
alpha_numeric: 'The %s field must only contain alpha-numeric characters.',
alpha_dash: 'The %s field must only contain alpha-numeric characters, underscores, and dashes.',
numeric: 'The %s field must contain only numbers.',
integer: 'The %s field must contain an integer.',
decimal: 'The %s field must contain a decimal number.',
is_natural: 'The %s field must contain only positive numbers.',
is_natural_no_zero: 'The %s field must contain a number greater than zero.',
valid_ip: 'The %s field must contain a valid IP.',
valid_base64: 'The %s field must contain a base64 string.',
valid_credit_card: 'The %s field must contain a valid credit card number.',
is_file_type: 'The %s field must contain only %s files.',
valid_url: 'The %s field must contain a valid URL.',
greater_than_date: 'The %s field must contain a more recent date than %s.',
less_than_date: 'The %s field must contain an older date than %s.',
greater_than_or_equal_date: 'The %s field must contain a date that\'s at least as recent as %s.',
less_than_or_equal_date: 'The %s field must contain a date that\'s %s or older.'
},
callback: function(errors) {
}
};
var ruleRegex = /^(.+?)\[(.+)\]$/,
numericRegex = /^[0-9]+$/,
integerRegex = /^\-?[0-9]+$/,
decimalRegex = /^\-?[0-9]*\.?[0-9]+$/,
emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
alphaRegex = /^[a-z]+$/i,
alphaNumericRegex = /^[a-z0-9]+$/i,
alphaDashRegex = /^[a-z0-9_\-]+$/i,
naturalRegex = /^[0-9]+$/i,
naturalNoZeroRegex = /^[1-9][0-9]*$/i,
ipRegex = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i,
base64Regex = /[^a-zA-Z0-9\/\+=]/i,
numericDashRegex = /^[\d\-\s]+$/,
urlRegex = /^((http|https):\/\/(\w+:{0,1}\w*@)?(\S+)|)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,
dateRegex = /\d{4}-\d{1,2}-\d{1,2}/;
... //省略后面的代碼
})(window,document);
/*
* Export as a CommonJS module
*/
if (typeof module !== 'undefined' && module.exports) {
module.exports = FormValidator;
}
當(dāng)然,參數(shù)既然是默認(rèn)的,那就意味著我們可以隨意修改參數(shù)以達(dá)到我們的需求。插件本身的意義就在于具有復(fù)用性。
如表單驗(yàn)證插件,則就可以new一個對象的時候,修改我們的默認(rèn)參數(shù):
var validator = new FormValidator('example_form', [{
name: 'req',
display: 'required',
rules: 'required'
}, {
name: 'alphanumeric',
rules: 'alpha_numeric'
}, {
name: 'password',
rules: 'required'
}, {
name: 'password_confirm',
display: 'password confirmation',
rules: 'required|matches[password]'
}, {
name: 'email',
rules: 'valid_email'
}, {
name: 'minlength',
display: 'min length',
rules: 'min_length[8]'
}, {
names: ['fname', 'lname'],
rules: 'required|alpha'
}], function(errors) {
if (errors.length > 0) {
// Show the errors
}
});
插件的鉤子
我們知道,設(shè)計一下插件,參數(shù)或者其邏輯肯定不是寫死的,我們得像函數(shù)一樣,得讓用戶提供自己的參數(shù)去實(shí)現(xiàn)用戶的需求。則我們的插件需要提供一個修改默認(rèn)參數(shù)的入口。
如上面我們說的修改默認(rèn)參數(shù),實(shí)際上也是插件給我們提供的一個API。讓我們的插件更加的靈活。如果大家對API不了解,可以百度一下API
通常我們用的js插件,實(shí)現(xiàn)的方式會有多種多樣的。最簡單的實(shí)現(xiàn)邏輯就是一個方法,或者一個js對象,又或者是一個構(gòu)造函數(shù)等等。
** 然我們插件所謂的API,實(shí)際就是我們插件暴露出來的所有方法及屬性。 **
我們需求中,加減乘除余插件中,我們的API就是如下幾個方法:
...
var plugin = {
add: function(n1,n2){ return n1 + n2; },
sub: function(n1,n2){ return n1 - n2; },
mul: function(n1,n2){ return n1 * n2; },
div: function(n1,n2){ return n1 / n2; },
sur: function(n1,n2){ return n1 % n2; }
}
...
可以看到plubin暴露出來的方法則是如下幾個API:
- add
- sub
- mul
- div
- sur
在插件的API中,我們常常將容易被修改和變動的方法或?qū)傩越y(tǒng)稱為鉤子(Hook),方法則直接叫鉤子函數(shù)。這是一種形象生動的說法,就好像我們在一條繩子上放很多掛鉤,我們可以按需要在上面掛東西。
實(shí)際上,我們即知道插件可以像一條繩子上掛東西,也可以拿掉掛的東西。那么一個插件,實(shí)際上就是個形象上的鏈。不過我們上面的所有鉤子都是掛在對象上的,用于實(shí)現(xiàn)鏈并不是很理想。
插件的鏈?zhǔn)秸{(diào)用(利用當(dāng)前對象)
插件并非都是能鏈?zhǔn)秸{(diào)用的,有些時候,我們只是用鉤子來實(shí)現(xiàn)一個計算并返回結(jié)果,取得運(yùn)算結(jié)果就可以了。但是有些時候,我們用鉤子并不需要其返回結(jié)果。我們只利用其實(shí)現(xiàn)我們的業(yè)務(wù)邏輯,為了代碼簡潔與方便,我們常常將插件的調(diào)用按鏈?zhǔn)降姆绞竭M(jìn)行調(diào)用。
最常見的jquery的鏈?zhǔn)秸{(diào)用如下:
$(<id>).show().css('color','red').width(100).height(100)....
那,如何才能將鏈?zhǔn)秸{(diào)用運(yùn)用到我們的插件中去呢?假設(shè)我們上面的例子,如果是要按照plugin這個對象的鏈?zhǔn)竭M(jìn)行調(diào)用,則可以將其業(yè)務(wù)結(jié)構(gòu)改為:
...
var plugin = {
add: function(n1,n2){ return this; },
sub: function(n1,n2){ return this; },
mul: function(n1,n2){ return this; },
div: function(n1,n2){ return this; },
sur: function(n1,n2){ return this; }
}
...
顯示,我們只要將插件的當(dāng)前對象this直接返回,則在下一下方法中,同樣可以引用插件對象plugin的其它勾子方法。然后調(diào)用的時候就可以使用鏈?zhǔn)搅恕?/p>
plugin.add().sub().mul().div().sur() //如此調(diào)用顯然沒有任何實(shí)際意義
顯然這樣做并沒有什么意義。我們這里的每一個鉤子函數(shù)都只是用來計算并且獲取返回值而已。而鏈?zhǔn)秸{(diào)用本身的意義是用來處理業(yè)務(wù)邏輯的。
插件的鏈?zhǔn)秸{(diào)用(利用原型鏈)
JavaScript中,萬物皆對象,所有對象都是繼承自原型。JS在創(chuàng)建對象(不論是普通對象還是函數(shù)對象)的時候,都有一個叫做__proto__的內(nèi)置屬性,用于指向創(chuàng)建它的函數(shù)對象的原型對象prototype。關(guān)于原型問題,感興趣的同學(xué)可以看這篇:js原型鏈
在上面的需求中,我們可以將plugin對象改為原型的方式,則需要將plugin寫成一個構(gòu)造方法,我們將插件名換為Calculate避免因?yàn)镻lugin大寫的時候與Window對象中的API沖突。
...
function Calculate(){}
Calculate.prototype.add = function(){return this;}
Calculate.prototype.sub = function(){return this;}
Calculate.prototype.mul = function(){return this;}
Calculate.prototype.div = function(){return this;}
Calculate.prototype.sur = function(){return this;}
...
當(dāng)然,假設(shè)我們的插件是對初始化參數(shù)進(jìn)行運(yùn)算并只輸出結(jié)果,我們可以稍微改一下:
// plugin.js
// plugin.js
;(function(undefined) {
"use strict"
var _global;
function result(args,type){
var argsArr = Array.prototype.slice.call(args);
if(argsArr.length == 0) return 0;
switch(type) {
case 1: return argsArr.reduce(function(p,c){return p + c;});
case 2: return argsArr.reduce(function(p,c){return p - c;});
case 3: return argsArr.reduce(function(p,c){return p * c;});
case 4: return argsArr.reduce(function(p,c){return p / c;});
case 5: return argsArr.reduce(function(p,c){return p % c;});
default: return 0;
}
}
function Calculate(){}
Calculate.prototype.add = function(){console.log(result(arguments,1));return this;}
Calculate.prototype.sub = function(){console.log(result(arguments,2));return this;}
Calculate.prototype.mul = function(){console.log(result(arguments,3));return this;}
Calculate.prototype.div = function(){console.log(result(arguments,4));return this;}
Calculate.prototype.sur = function(){console.log(result(arguments,5));return this;}
// 最后將插件對象暴露給全局對象
_global = (function(){ return this || (0, eval)('this'); }());
if (typeof module !== "undefined" && module.exports) {
module.exports = Calculate;
} else if (typeof define === "function" && define.amd) {
define(function(){return Calculate;});
} else {
!('Calculate' in _global) && (_global.Calculate = Calculate);
}
}());
這時調(diào)用我們寫好的插件,則輸出為如下:
var plugin = new Calculate();
plugin
.add(2,1)
.sub(2,1)
.mul(2,1)
.div(2,1)
.sur(2,1);
// 結(jié)果:
// 3
// 1
// 2
// 2
// 0
上面的例子,可以并沒有太多的現(xiàn)實(shí)意義。不過在網(wǎng)頁設(shè)計中,我們的插件基本上都是服務(wù)于UI層面,利用js腳本實(shí)現(xiàn)一些可交互的效果。這時我們編寫一個UI插件,實(shí)現(xiàn)過程也是可以使用鏈?zhǔn)竭M(jìn)行調(diào)用。
編寫UI組件
一般情況,如果一個js僅僅是處理一個邏輯,我們稱之為插件,但如果與dom和css有關(guān)系并且具備一定的交互性,一般叫做組件。當(dāng)然這沒有什么明顯的區(qū)分,只是一種習(xí)慣性叫法。
利用原型鏈,可以將一些UI層面的業(yè)務(wù)代碼封裝在一個小組件中,并利用js實(shí)現(xiàn)組件的交互性。
現(xiàn)有一個這樣的需求:
- 實(shí)現(xiàn)一個彈層,此彈層可以顯示一些文字提示性的信息;
- 彈層右上角必須有一個關(guān)閉按扭,點(diǎn)擊之后彈層消失;
- 彈層底部必有一個“確定”按扭,然后根據(jù)需求,可以配置多一個“取消”按扭;
- 點(diǎn)擊“確定”按扭之后,可以觸發(fā)一個事件;
- 點(diǎn)擊關(guān)閉/“取消”按扭后,可以觸發(fā)一個事件。
根據(jù)需求,我們先寫出dom結(jié)構(gòu):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<div class="mydialog">
<span class="close">×</span>
<div class="mydialog-cont">
<div class="cont">hello world!</div>
</div>
<div class="footer">
<span class="btn">確定</span>
<span class="btn">取消</span>
</div>
</div>
<script src="index.js"></script>
</body>
</html>
寫出css結(jié)構(gòu):
* { padding: 0; margin: 0; }
.mydialog { background: #fff; box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3); overflow: hidden; width: 300px; height: 180px; border: 1px solid #dcdcdc; position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; }
.close { position: absolute; right: 5px; top: 5px; width: 16px; height: 16px; line-height: 16px; text-align: center; font-size: 18px; cursor: pointer; }
.mydialog-cont { padding: 0 0 50px; display: table; width: 100%; height: 100%; }
.mydialog-cont .cont { display: table-cell; text-align: center; vertical-align: middle; width: 100%; height: 100%; }
.footer { display: table; table-layout: fixed; width: 100%; position: absolute; bottom: 0; left: 0; border-top: 1px solid #dcdcdc; }
.footer .btn { display: table-cell; width: 50%; height: 50px; line-height: 50px; text-align: center; cursor: pointer; }
.footer .btn:last-child { display: table-cell; width: 50%; height: 50px; line-height: 50px; text-align: center; cursor: pointer; border-left: 1px solid #dcdcdc; }
接下來,我們開始編寫我們的交互插件。
我們假設(shè)組件的彈出層就是一個對象。則這個對象是包含了我們的交互、樣式、結(jié)構(gòu)及渲染的過程。于是我們定義了一個構(gòu)造方法:
function MyDialog(){} // MyDialog就是我們的組件對象了
對象MyDialog就相當(dāng)于一個繩子,我們只要往這個繩子上不斷地掛上鉤子就是一個組件了。于是我們的組件就可以表示為:
function MyDialog(){}
MyDialog.prototype = {
constructor: this,
_initial: function(){},
_parseTpl: function(){},
_parseToDom: function(){},
show: function(){},
hide: function(){},
css: function(){},
...
}
然后就可以將插件的功能都寫上。不過中間的業(yè)務(wù)邏輯,需要自己去一步一步研究。無論如何寫,我們最終要做到通過實(shí)例化一個MyDialog對象就可以使用我們的插件了。
在編寫的過程中,我們得先做一些工具函數(shù):
1.對象合并函數(shù)
// 對象合并
function extend(o,n,override) {
for(var key in n){
if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){
o[key]=n[key];
}
}
return o;
}
2.自定義模板引擎解釋函數(shù)
// 自定義模板引擎
function templateEngine(html, data) {
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var r=[];\n',
cursor = 0;
var match;
var add = function(line, js) {
js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
(code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
}
while (match = re.exec(html)) {
add(html.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
add(html.substr(cursor, html.length - cursor));
code += 'return r.join("");';
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
}
3.查找class獲取dom函數(shù)
// 通過class查找dom
if(!('getElementsByClass' in HTMLElement)){
HTMLElement.prototype.getElementsByClass = function(n, tar){
var el = [],
_el = (!!tar ? tar : this).getElementsByTagName('*');
for (var i=0; i<_el.length; i++ ) {
if (!!_el[i].className && (typeof _el[i].className == 'string') && _el[i].className.indexOf(n) > -1 ) {
el[el.length] = _el[i];
}
}
return el;
};
((typeof HTMLDocument !== 'undefined') ? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass;
}
結(jié)合工具函數(shù),再去實(shí)現(xiàn)每一個鉤子函數(shù)具體邏輯結(jié)構(gòu):
// plugin.js
;(function(undefined) {
"use strict"
var _global;
...
// 插件構(gòu)造函數(shù) - 返回數(shù)組結(jié)構(gòu)
function MyDialog(opt){
this._initial(opt);
}
MyDialog.prototype = {
constructor: this,
_initial: function(opt) {
// 默認(rèn)參數(shù)
var def = {
ok: true,
ok_txt: '確定',
cancel: false,
cancel_txt: '取消',
confirm: function(){},
close: function(){},
content: '',
tmpId: null
};
this.def = extend(def,opt,true);
this.tpl = this._parseTpl(this.def.tmpId);
this.dom = this._parseToDom(this.tpl)[0];
this.hasDom = false;
},
_parseTpl: function(tmpId) { // 將模板轉(zhuǎn)為字符串
var data = this.def;
var tplStr = document.getElementById(tmpId).innerHTML.trim();
return templateEngine(tplStr,data);
},
_parseToDom: function(str) { // 將字符串轉(zhuǎn)為dom
var div = document.createElement('div');
if(typeof str == 'string') {
div.innerHTML = str;
}
return div.childNodes;
},
show: function(callback){
var _this = this;
if(this.hasDom) return ;
document.body.appendChild(this.dom);
this.hasDom = true;
document.getElementsByClass('close',this.dom)[0].onclick = function(){
_this.hide();
};
document.getElementsByClass('btn-ok',this.dom)[0].onclick = function(){
_this.hide();
};
if(this.def.cancel){
document.getElementsByClass('btn-cancel',this.dom)[0].onclick = function(){
_this.hide();
};
}
callback && callback();
return this;
},
hide: function(callback){
document.body.removeChild(this.dom);
this.hasDom = false;
callback && callback();
return this;
},
modifyTpl: function(template){
if(!!template) {
if(typeof template == 'string'){
this.tpl = template;
} else if(typeof template == 'function'){
this.tpl = template();
} else {
return this;
}
}
// this.tpl = this._parseTpl(this.def.tmpId);
this.dom = this._parseToDom(this.tpl)[0];
return this;
},
css: function(styleObj){
for(var prop in styleObj){
var attr = prop.replace(/[A-Z]/g,function(word){
return '-' + word.toLowerCase();
});
this.dom.style[attr] = styleObj[prop];
}
return this;
},
width: function(val){
this.dom.style.width = val + 'px';
return this;
},
height: function(val){
this.dom.style.height = val + 'px';
return this;
}
}
_global = (function(){ return this || (0, eval)('this'); }());
if (typeof module !== "undefined" && module.exports) {
module.exports = MyDialog;
} else if (typeof define === "function" && define.amd) {
define(function(){return MyDialog;});
} else {
!('MyDialog' in _global) && (_global.MyDialog = MyDialog);
}
}());
到這一步,我們的插件已經(jīng)達(dá)到了基礎(chǔ)需求了。我們可以在頁面這樣調(diào)用:
<script type="text/template" id="dialogTpl">
<div class="mydialog">
<span class="close">×</span>
<div class="mydialog-cont">
<div class="cont"><% this.content %></div>
</div>
<div class="footer">
<% if(this.cancel){ %>
<span class="btn btn-ok"><% this.ok_txt %></span>
<span class="btn btn-cancel"><% this.cancel_txt %></span>
<% } else{ %>
<span class="btn btn-ok" style="width: 100%"><% this.ok_txt %></span>
<% } %>
</div>
</div>
</script>
<script src="index.js"></script>
<script>
var mydialog = new MyDialog({
tmpId: 'dialogTpl',
cancel: true,
content: 'hello world!'
});
mydialog.show();
</script>
插件的監(jiān)聽
彈出框插件我們已經(jīng)實(shí)現(xiàn)了基本的顯示與隱藏的功能。不過我們在怎么時候彈出,彈出之后可能進(jìn)行一些操作,實(shí)際上還是需要進(jìn)行一些可控的操作。就好像我們進(jìn)行事件綁定一樣,只有用戶點(diǎn)擊了按扭,才響應(yīng)具體的事件。那么,我們的插件,應(yīng)該也要像事件綁定一樣,只有執(zhí)行了某些操作的時候,調(diào)用相應(yīng)的事件響應(yīng)。
這種js的設(shè)計模式,被稱為 訂閱/發(fā)布模式,也被叫做 觀察者模式。我們插件中的也需要用到觀察者模式,比如,在打開彈窗之前,我們需要先進(jìn)行彈窗的內(nèi)容更新,執(zhí)行一些判斷邏輯等,然后執(zhí)行完成之后才顯示出彈窗。在關(guān)閉彈窗之后,我們需要執(zhí)行關(guān)閉之后的一些邏輯,處理業(yè)務(wù)等。這時候我們需要像平時綁定事件一樣,給插件做一些“事件”綁定回調(diào)方法。
我們jquery對dom的事件響應(yīng)是這樣的:
$(<dom>).on("click",function(){})
我們照著上面的方式設(shè)計了對應(yīng)的插件響應(yīng)是這樣的:
mydialog.on('show',function(){})
則,我們需要實(shí)現(xiàn)一個事件機(jī)制,以達(dá)到監(jiān)聽插件的事件效果。關(guān)于自定義事件監(jiān)聽,可以參考一篇博文:漫談js自定義事件、DOM/偽DOM自定義事件。在此不進(jìn)行大篇幅講自定義事件的問題。
最終我們實(shí)現(xiàn)的插件代碼為:
// plugin.js
;(function(undefined) {
"use strict"
var _global;
// 工具函數(shù)
// 對象合并
function extend(o,n,override) {
for(var key in n){
if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){
o[key]=n[key];
}
}
return o;
}
// 自定義模板引擎
function templateEngine(html, data) {
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var r=[];\n',
cursor = 0;
var match;
var add = function(line, js) {
js ? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
(code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
}
while (match = re.exec(html)) {
add(html.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
add(html.substr(cursor, html.length - cursor));
code += 'return r.join("");';
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
}
// 通過class查找dom
if(!('getElementsByClass' in HTMLElement)){
HTMLElement.prototype.getElementsByClass = function(n){
var el = [],
_el = this.getElementsByTagName('*');
for (var i=0; i<_el.length; i++ ) {
if (!!_el[i].className && (typeof _el[i].className == 'string') && _el[i].className.indexOf(n) > -1 ) {
el[el.length] = _el[i];
}
}
return el;
};
((typeof HTMLDocument !== 'undefined') ? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass;
}
// 插件構(gòu)造函數(shù) - 返回數(shù)組結(jié)構(gòu)
function MyDialog(opt){
this._initial(opt);
}
MyDialog.prototype = {
constructor: this,
_initial: function(opt) {
// 默認(rèn)參數(shù)
var def = {
ok: true,
ok_txt: '確定',
cancel: false,
cancel_txt: '取消',
confirm: function(){},
close: function(){},
content: '',
tmpId: null
};
this.def = extend(def,opt,true); //配置參數(shù)
this.tpl = this._parseTpl(this.def.tmpId); //模板字符串
this.dom = this._parseToDom(this.tpl)[0]; //存放在實(shí)例中的節(jié)點(diǎn)
this.hasDom = false; //檢查dom樹中dialog的節(jié)點(diǎn)是否存在
this.listeners = []; //自定義事件,用于監(jiān)聽插件的用戶交互
this.handlers = {};
},
_parseTpl: function(tmpId) { // 將模板轉(zhuǎn)為字符串
var data = this.def;
var tplStr = document.getElementById(tmpId).innerHTML.trim();
return templateEngine(tplStr,data);
},
_parseToDom: function(str) { // 將字符串轉(zhuǎn)為dom
var div = document.createElement('div');
if(typeof str == 'string') {
div.innerHTML = str;
}
return div.childNodes;
},
show: function(callback){
var _this = this;
if(this.hasDom) return ;
if(this.listeners.indexOf('show') > -1) {
if(!this.emit({type:'show',target: this.dom})) return ;
}
document.body.appendChild(this.dom);
this.hasDom = true;
this.dom.getElementsByClass('close')[0].onclick = function(){
_this.hide();
if(_this.listeners.indexOf('close') > -1) {
_this.emit({type:'close',target: _this.dom})
}
!!_this.def.close && _this.def.close.call(this,_this.dom);
};
this.dom.getElementsByClass('btn-ok')[0].onclick = function(){
_this.hide();
if(_this.listeners.indexOf('confirm') > -1) {
_this.emit({type:'confirm',target: _this.dom})
}
!!_this.def.confirm && _this.def.confirm.call(this,_this.dom);
};
if(this.def.cancel){
this.dom.getElementsByClass('btn-cancel')[0].onclick = function(){
_this.hide();
if(_this.listeners.indexOf('cancel') > -1) {
_this.emit({type:'cancel',target: _this.dom})
}
};
}
callback && callback();
if(this.listeners.indexOf('shown') > -1) {
this.emit({type:'shown',target: this.dom})
}
return this;
},
hide: function(callback){
if(this.listeners.indexOf('hide') > -1) {
if(!this.emit({type:'hide',target: this.dom})) return ;
}
document.body.removeChild(this.dom);
this.hasDom = false;
callback && callback();
if(this.listeners.indexOf('hidden') > -1) {
this.emit({type:'hidden',target: this.dom})
}
return this;
},
modifyTpl: function(template){
if(!!template) {
if(typeof template == 'string'){
this.tpl = template;
} else if(typeof template == 'function'){
this.tpl = template();
} else {
return this;
}
}
this.dom = this._parseToDom(this.tpl)[0];
return this;
},
css: function(styleObj){
for(var prop in styleObj){
var attr = prop.replace(/[A-Z]/g,function(word){
return '-' + word.toLowerCase();
});
this.dom.style[attr] = styleObj[prop];
}
return this;
},
width: function(val){
this.dom.style.width = val + 'px';
return this;
},
height: function(val){
this.dom.style.height = val + 'px';
return this;
},
on: function(type, handler){
// type: show, shown, hide, hidden, close, confirm
if(typeof this.handlers[type] === 'undefined') {
this.handlers[type] = [];
}
this.listeners.push(type);
this.handlers[type].push(handler);
return this;
},
off: function(type, handler){
if(this.handlers[type] instanceof Array) {
var handlers = this.handlers[type];
for(var i = 0, len = handlers.length; i < len; i++) {
if(handlers[i] === handler) {
break;
}
}
this.listeners.splice(i, 1);
handlers.splice(i, 1);
return this;
}
},
emit: function(event){
if(!event.target) {
event.target = this;
}
if(this.handlers[event.type] instanceof Array) {
var handlers = this.handlers[event.type];
for(var i = 0, len = handlers.length; i < len; i++) {
handlers[i](event);
return true;
}
}
return false;
}
}
// 最后將插件對象暴露給全局對象
_global = (function(){ return this || (0, eval)('this'); }());
if (typeof module !== "undefined" && module.exports) {
module.exports = MyDialog;
} else if (typeof define === "function" && define.amd) {
define(function(){return MyDialog;});
} else {
!('MyDialog' in _global) && (_global.MyDialog = MyDialog);
}
}());
然后調(diào)用的時候就可以直接使用插件的事件綁定了。
var mydialog = new MyDialog({
tmpId: 'dialogTpl',
cancel: true,
content: 'hello world!'
});
mydialog.on('confirm',function(ev){
console.log('you click confirm!');
// 寫你的確定之后的邏輯代碼...
});
document.getElementById('test').onclick = function(){
mydialog.show();
}
給出此例子的demo,有需要具體實(shí)現(xiàn)的同學(xué)可以去查閱。
插件發(fā)布
我們寫好了插件,實(shí)際上還可以將我們的插件發(fā)布到開源組織去分享給更多人去使用(代碼必須是私人擁有所有支配權(quán)限)。我們將插件打包之后,就可以發(fā)布到開源組織上去供別人下載使用了。
我們熟知的npm社區(qū)就是一個非常良好的發(fā)布插件的平臺。具體可以如下操作:
寫初始化包的描述文件:
$ npm init
注冊包倉庫帳號
$ npm adduser
Username: <帳號>
Password: <密碼>
Email:(this IS public) <郵箱>
Logged in as <帳號> on https://registry.npmjs.org/.
上傳包
$ npm publish
安裝包
$ npm install mydialog
到此,我們的插件就可以直接被更多人去使用了。
結(jié)論
寫了這么多,比較啰嗦,我在此做一下總結(jié):
關(guān)于如何編寫出一個好的js原生插件,需要平時在使用別人的插件的同時,多查看一下api文檔,了解插件的調(diào)用方式,然后再看一下插件的源碼的設(shè)計方式?;旧衔覀兛梢源_定大部分插件都是按照原型的方式進(jìn)行設(shè)計的。而我從上面的例子中,就使用了好多js原生的知識點(diǎn),函數(shù)的命名沖突、閉包、作用域,自定義工具函數(shù)擴(kuò)展對象的鉤子函數(shù),以及對象的初始化、原型鏈繼承,構(gòu)造函數(shù)的定義及設(shè)計模式,還有事件的自定義,js設(shè)計模式的觀察者模式等知識。這些內(nèi)容還是需要初學(xué)者多多了解才能進(jìn)行一些高層次一些的插件開發(fā)。