如何定義一個(gè)高逼格的原生JS插件

作為一個(gè)前端er荆残,如果不會(huì)寫一個(gè)小插件,都不好意思說(shuō)自己是混前端界的亿笤。寫還不能依賴jquery之類的工具庫(kù)翎迁,否則裝得不夠高端。那么责嚷,如何才能裝起來(lái)讓自己看起來(lái)逼格更高呢鸳兽?當(dāng)然是利用js純?cè)膶懛ɡ病R郧耙恢闭f(shuō)罕拂,掌握了js原生揍异,就基本上可以解決前端的所有腳本交互工作了全陨,這話大體上是有些浮夸了。不過(guò)衷掷,也從側(cè)面說(shuō)明了原生js在前端中占著多么重要的一面辱姨。好了。廢話不多說(shuō)戚嗅。咱們就來(lái)看一下怎么去做一個(gè)自己的js插件吧雨涛。

插件的需求

我們寫代碼,并不是所有的業(yè)務(wù)或者邏輯代碼都要抽出來(lái)復(fù)用懦胞。首先替久,我們得看一下是否需要將一部分經(jīng)常重復(fù)的代碼抽象出來(lái),寫到一個(gè)單獨(dú)的文件中為以后再次使用躏尉。再看一下我們的業(yè)務(wù)邏輯是否可以為團(tuán)隊(duì)服務(wù)蚯根。
插件不是隨手就寫成的,而是根據(jù)自己業(yè)務(wù)邏輯進(jìn)行抽象胀糜。沒有放之四海而皆準(zhǔn)的插件颅拦,只有對(duì)插件,之所以叫做插件教藻,那么就是開箱即用距帅,或者我們只要添加一些配置參數(shù)就可以達(dá)到我們需要的結(jié)果。如果都符合了這些情況括堤,我們才去考慮做一個(gè)插件碌秸。

插件封裝的條件

一個(gè)可復(fù)用的插件需要滿足以下條件:

  1. 插件自身的作用域與用戶當(dāng)前的作用域相互獨(dú)立,也就是插件內(nèi)部的私有變量不能影響使用者的環(huán)境變量痊臭;
  2. 插件需具備默認(rèn)設(shè)置參數(shù)哮肚;
  3. 插件除了具備已實(shí)現(xiàn)的基本功能外,需提供部分API广匙,使用者可以通過(guò)該API修改插件功能的默認(rèn)參數(shù)允趟,從而實(shí)現(xiàn)用戶自定義插件效果;
  4. 插件支持鏈?zhǔn)秸{(diào)用鸦致;
  5. 插件需提供監(jiān)聽入口潮剪,及針對(duì)指定元素進(jìn)行監(jiān)聽,使得該元素與插件響應(yīng)達(dá)到插件效果分唾。

關(guān)于插件封裝的條件抗碰,可以查看一篇文章:原生JavaScript插件編寫指南
而我想要說(shuō)明的是,如何一步一步地實(shí)現(xiàn)我的插件封裝绽乔。所以弧蝇,我會(huì)先從簡(jiǎn)單的方法函數(shù)來(lái)做起。

插件的外包裝

用函數(shù)包裝

所謂插件,其實(shí)就是封裝在一個(gè)閉包中的一種函數(shù)集看疗。我記得剛開始寫js的時(shí)候沙峻,我是這樣干的,將我想要的邏輯两芳,寫成一個(gè)函數(shù)摔寨,然后再根據(jù)不同需要傳入不同的參數(shù)就可以了。
比如怖辆,我想實(shí)現(xiàn)兩個(gè)數(shù)字相加的方法:

function add(n1,n2) {
    return n1 + n2;
}
// 調(diào)用
add(1,2)
// 輸出:3

這就是我們要的功能的簡(jiǎn)單實(shí)現(xiàn)是复。如果僅僅只不過(guò)實(shí)現(xiàn)這么簡(jiǎn)單的邏輯,那已經(jīng)可以了竖螃,沒必要弄一些花里胡哨的東西淑廊。js函數(shù)本身就可以解決絕大多數(shù)的問(wèn)題。不過(guò)我們?cè)趯?shí)際工作與應(yīng)用中斑鼻,一般情況的需求都是比較復(fù)雜得多蒋纬。
如果這時(shí),產(chǎn)品來(lái)跟你說(shuō)坚弱,我不僅需要兩個(gè)數(shù)相加的,我還要相減关摇,相乘荒叶,相除,求余等等功能输虱。這時(shí)候些楣,我們?cè)趺崔k呢?
當(dāng)然宪睹,你會(huì)想愁茁,這有什么難的。直接將這堆函數(shù)都寫出來(lái)不就完了亭病。然后都放在一個(gè)js文件里面鹅很。需要的時(shí)候,就調(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ù)都寫到一個(gè)js里面了整袁。如果是一個(gè)人在用菠齿,那么可以很清楚知道自己是否已經(jīng)定義了什么,并且知道自己寫了什么內(nèi)容坐昙,我在哪個(gè)頁(yè)面需要绳匀,那么就直接引入這個(gè)js文件就可以搞定了。
不過(guò),如果是兩個(gè)人以上的團(tuán)隊(duì)疾棵,或者你與別人一起協(xié)作寫代碼盗飒,這時(shí)候,另一個(gè)人并不知道你是否寫了add方法陋桂,這時(shí)他也定義了同樣的add方法逆趣。那么你們之間就會(huì)產(chǎn)生命名沖突,一般稱之為變量的 全局污染

用全局對(duì)象包裝

為了解決這種全局變量污染的問(wèn)題嗜历。這時(shí)宣渗,我們可以定義一個(gè)js對(duì)象來(lái)接收我們這些工具函數(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)解決了全局污染的問(wèn)題暴匠。在團(tuán)隊(duì)協(xié)作中只要約定好命名規(guī)則了鞍恢,告知其它同學(xué)即可以。當(dāng)然不排除有個(gè)別人每窖,接手你的項(xiàng)目帮掉,并不知道此全局變量已經(jīng)定義,則他又定義了一次并賦值窒典,這時(shí)蟆炊,就會(huì)把你的對(duì)象覆蓋掉。當(dāng)然瀑志,可能你會(huì)這么干來(lái)解決掉命名沖突問(wèn)題:

if(!plugin){ //這里的if條件也可以用: (typeof plugin == 'undefined')
    var plugin = {
        // 以此寫你的函數(shù)邏輯
    }
}

或者也可以這樣寫:

var plugin;
if(!plugin){
    plugin = {
        // ...
    }
}

這樣子涩搓,就不會(huì)存在命名上的沖突了。

也許有同學(xué)會(huì)疑問(wèn)劈猪,為什么可以在此聲明plugin變量昧甘?實(shí)際上js的解釋執(zhí)行,會(huì)把所有聲明都提前战得。如果一個(gè)變量已經(jīng)聲明過(guò)充边,后面如果不是在函數(shù)內(nèi)聲明的,則是沒有影響的贡避。所以痛黎,就算在別的地方聲明過(guò)var plugin,我同樣也以可以在這里再次聲明一次刮吧。關(guān)于聲明的相關(guān)資料可以看阮一鋒的如何判斷Javascript對(duì)象是否存在湖饱。

基本上,這就可以算是一個(gè)插件了杀捻。解決了全局污染問(wèn)題井厌,方法函數(shù)可以抽出來(lái)放到一單獨(dú)的文件里面去。

利用閉包包裝

上面的例子,雖然可以實(shí)現(xiàn)了插件的基本上的功能仅仆。不過(guò)我們的plugin對(duì)象器赞,是定義在全局域里面的。我們知道墓拜,js變量的調(diào)用港柜,從全局作用域上找查的速度會(huì)比在私有作用域里面慢得多得多。所以咳榜,我們最好將插件邏輯寫在一個(gè)私有作用域中夏醉。
實(shí)現(xiàn)私有作用域,最好的辦法就是使用閉包涌韩∨先幔可以把插件當(dāng)做一個(gè)函數(shù),插件內(nèi)部的變量及函數(shù)的私有變量臣樱,為了在調(diào)用插件后依舊能使用其功能靶擦,閉包的作用就是延長(zhǎng)函數(shù)(插件)內(nèi)部變量的生命周期,使得插件函數(shù)可以重復(fù)調(diào)用雇毫,而不影響用戶自身作用域玄捕。
故需將插件的所有功能寫在一個(gè)立即執(zhí)行函數(shù)中:

;(function(global,undefined) {
    var plugin = {
        add: function(n1,n2){...}
        ...
    }
    // 最后將插件對(duì)象暴露給全局對(duì)象
    'plugin' in global && (global.plugin = plugin);
})(window);

對(duì)上面的代碼段傳參問(wèn)題進(jìn)行解釋一下:

  1. 在定義插件之前添加一個(gè)分號(hào),可以解決js合并時(shí)可能會(huì)產(chǎn)生的錯(cuò)誤問(wèn)題嘴拢;
  2. undefined在老一輩的瀏覽器是不被支持的桩盲,直接使用會(huì)報(bào)錯(cuò),js框架要考慮到兼容性席吴,因此增加一個(gè)形參undefined,就算有人把外面的 undefined 定義了捞蛋,里面的 undefined 依然不受影響孝冒;
  3. 把window對(duì)象作為參數(shù)傳入,是避免了函數(shù)執(zhí)行的時(shí)候到外部去查找拟杉。

其實(shí)庄涡,我們覺得直接傳window對(duì)象進(jìn)去,我覺得還是不太妥當(dāng)搬设。我們并不確定我們的插件就一定用于瀏覽器上穴店,也有可能使用在一些非瀏覽端上。所以我們還可以這么干拿穴,我們不傳參數(shù)泣洞,直接取當(dāng)前的全局this對(duì)象為作頂級(jí)對(duì)象用。

;(function(global,undefined) {
    "use strict" //使用js嚴(yán)格模式檢查默色,使語(yǔ)法更規(guī)范
    var _global;
    var plugin = {
        add: function(n1,n2){...}
        ...
    }
    // 最后將插件對(duì)象暴露給全局對(duì)象
    _global = (function(){ return this || (0, eval)('this'); }());
    !('plugin' in _global) && (_global.plugin = plugin);
}());

如此球凰,我們不需要傳入任何參數(shù),并且解決了插件對(duì)環(huán)境的依事性。如此我們的插件可以在任何宿主環(huán)境上運(yùn)行了呕诉。

上面的代碼段中有段奇怪的表達(dá)式:(0, eval)('this')缘厢,實(shí)際上(0,eval)是一個(gè)表達(dá)式,這個(gè)表達(dá)式執(zhí)行之后的結(jié)果就是eval這一句相當(dāng)于執(zhí)行eval('this')的意思甩挫,詳細(xì)解釋看此篇:(0,eval)('this')釋義或者看一下這篇(0,eval)('this')

關(guān)于立即自執(zhí)行函數(shù)贴硫,有兩種寫法:

// 寫法一
(function(){})()

//寫法二
(function(){}())

上面的兩種寫法是沒有區(qū)別的。都是正確的寫法伊者。個(gè)人建議使用第二種寫法英遭。這樣子更像一個(gè)整體。

附加一點(diǎn)知識(shí):
js里面()括號(hào)就是將代碼結(jié)構(gòu)變成表達(dá)式删壮,被包在()里面的變成了表達(dá)式之后贪绘,則就會(huì)立即執(zhí)行,js中將一段代碼變成表達(dá)式有很多種方式央碟,比如:

void function(){...}();
// 或者
!function foo(){...}();
// 或者
+function foot(){...}();

當(dāng)然税灌,我們不推薦你這么用。而且亂用可能會(huì)產(chǎn)生一些歧義亿虽。

到這一步菱涤,我們的插件的基礎(chǔ)結(jié)構(gòu)就已經(jīng)算是完整的了。

使用模塊化的規(guī)范包裝

雖然上面的包裝基本上已經(jīng)算是ok了的洛勉。但是如果是多個(gè)人一起開發(fā)一個(gè)大型的插件粘秆,這時(shí)我們要該怎么辦呢?多人合作收毫,肯定會(huì)產(chǎn)生多個(gè)文件攻走,每個(gè)人負(fù)責(zé)一個(gè)小功能,那么如何才能將所有人開發(fā)的代碼集合起來(lái)呢此再?這是一個(gè)討厭的問(wèn)題昔搂。要實(shí)現(xiàn)協(xié)作開發(fā)插件,必須具備如下條件:

  • 每功能互相之間的依賴必須要明確输拇,則必須嚴(yán)格按照依賴的順序進(jìn)行合并或者加載
  • 每個(gè)子功能分別都要是一個(gè)閉包摘符,并且將公共的接口暴露到共享域也即是一個(gè)被主函數(shù)暴露的公共對(duì)象

關(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>

但是不推薦這么做逛裤,這樣做與我們所追求的插件的封裝性相背。
不過(guò)現(xiàn)在前端界有一堆流行的模塊加載器猴抹,比如require带族、seajs,或者也可以像類似于Node的方式進(jìn)行加載洽糟,不過(guò)在瀏覽器端炉菲,我們還得利用打包器來(lái)實(shí)現(xiàn)模塊加載堕战,比如browserify。不過(guò)在此不談如何進(jìn)行模塊化打包或者加載的問(wèn)題拍霜,如有問(wèn)題的同學(xué)可以去上面的鏈接上看文檔學(xué)習(xí)嘱丢。
為了實(shí)現(xiàn)插件的模塊化并且讓我們的插件也是一個(gè)模塊,我們就得讓我們的插件也實(shí)現(xiàn)模塊化的機(jī)制祠饺。
我們實(shí)際上越驻,只要判斷是否存在加載器,如果存在加載器道偷,我們就使用加載器缀旁,如果不存在加載器。我們就使用頂級(jí)域?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; } //余
    }
    // 最后將插件對(duì)象暴露給全局對(duì)象
    _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對(duì)象并巍。

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ù)這種說(shuō)法换途,而不管我們是否傳有參數(shù)懊渡,我們都應(yīng)該返回一個(gè)值以告訴用戶我做了怎樣的處理,比如:

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

則作為一個(gè)健壯的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;
            });
        } //余
    }

    // 最后將插件對(duì)象暴露給全局對(duì)象
    _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一個(gè)對(duì)象的時(shí)候示血,修改我們的默認(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è)計(jì)一下插件救拉,參數(shù)或者其邏輯肯定不是寫死的难审,我們得像函數(shù)一樣,得讓用戶提供自己的參數(shù)去實(shí)現(xiàn)用戶的需求亿絮。則我們的插件需要提供一個(gè)修改默認(rèn)參數(shù)的入口告喊。
如上面我們說(shuō)的修改默認(rèn)參數(shù)麸拄,實(shí)際上也是插件給我們提供的一個(gè)API。讓我們的插件更加的靈活黔姜。如果大家對(duì)API不了解拢切,可以百度一下API
通常我們用的js插件,實(shí)現(xiàn)的方式會(huì)有多種多樣的秆吵。最簡(jiǎn)單的實(shí)現(xiàn)邏輯就是一個(gè)方法淮椰,或者一個(gè)js對(duì)象,又或者是一個(gè)構(gòu)造函數(shù)等等纳寂。
** 然我們插件所謂的API主穗,實(shí)際就是我們插件暴露出來(lái)的所有方法及屬性。 **
我們需求中毙芜,加減乘除余插件中忽媒,我們的API就是如下幾個(gè)方法:

...
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暴露出來(lái)的方法則是如下幾個(gè)API:

  • add
  • sub
  • mul
  • div
  • sur

在插件的API中,我們常常將容易被修改和變動(dòng)的方法或?qū)傩越y(tǒng)稱為鉤子(Hook)腋粥,方法則直接叫鉤子函數(shù)晦雨。這是一種形象生動(dòng)的說(shuō)法,就好像我們?cè)谝粭l繩子上放很多掛鉤灯抛,我們可以按需要在上面掛東西金赦。
實(shí)際上,我們即知道插件可以像一條繩子上掛東西对嚼,也可以拿掉掛的東西夹抗。那么一個(gè)插件,實(shí)際上就是個(gè)形象上的纵竖。不過(guò)我們上面的所有鉤子都是掛在對(duì)象上的漠烧,用于實(shí)現(xiàn)鏈并不是很理想。

插件的鏈?zhǔn)秸{(diào)用(利用當(dāng)前對(duì)象)

插件并非都是能鏈?zhǔn)秸{(diào)用的靡砌,有些時(shí)候已脓,我們只是用鉤子來(lái)實(shí)現(xiàn)一個(gè)計(jì)算并返回結(jié)果,取得運(yùn)算結(jié)果就可以了通殃。但是有些時(shí)候度液,我們用鉤子并不需要其返回結(jié)果。我們只利用其實(shí)現(xiàn)我們的業(yè)務(wù)邏輯画舌,為了代碼簡(jiǎn)潔與方便堕担,我們常常將插件的調(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這個(gè)對(duì)象的鏈?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)前對(duì)象this直接返回膜楷,則在下一下方法中,同樣可以引用插件對(duì)象plugin的其它勾子方法贞奋。然后調(diào)用的時(shí)候就可以使用鏈?zhǔn)搅恕?/p>

plugin.add().sub().mul().div().sur()  //如此調(diào)用顯然沒有任何實(shí)際意義

顯然這樣做并沒有什么意義术裸。我們這里的每一個(gè)鉤子函數(shù)都只是用來(lái)計(jì)算并且獲取返回值而已呼渣。而鏈?zhǔn)秸{(diào)用本身的意義是用來(lái)處理業(yè)務(wù)邏輯的。

插件的鏈?zhǔn)秸{(diào)用(利用原型鏈)

JavaScript中,萬(wàn)物皆對(duì)象锰瘸,所有對(duì)象都是繼承自原型遏暴。JS在創(chuàng)建對(duì)象(不論是普通對(duì)象還是函數(shù)對(duì)象)的時(shí)候霍转,都有一個(gè)叫做__proto__的內(nèi)置屬性偿衰,用于指向創(chuàng)建它的函數(shù)對(duì)象的原型對(duì)象prototype。關(guān)于原型問(wèn)題漫拭,感興趣的同學(xué)可以看這篇:js原型鏈
在上面的需求中亚兄,我們可以將plugin對(duì)象改為原型的方式,則需要將plugin寫成一個(gè)構(gòu)造方法采驻,我們將插件名換為Calculate避免因?yàn)镻lugin大寫的時(shí)候與Window對(duì)象中的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è)我們的插件是對(duì)初始化參數(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;}


    // 最后將插件對(duì)象暴露給全局對(duì)象
    _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);
    }
}());

這時(shí)調(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í)意義痘系。不過(guò)在網(wǎng)頁(yè)設(shè)計(jì)中菲嘴,我們的插件基本上都是服務(wù)于UI層面,利用js腳本實(shí)現(xiàn)一些可交互的效果汰翠。這時(shí)我們編寫一個(gè)UI插件龄坪,實(shí)現(xiàn)過(guò)程也是可以使用鏈?zhǔn)竭M(jìn)行調(diào)用。

編寫UI組件

一般情況复唤,如果一個(gè)js僅僅是處理一個(gè)邏輯健田,我們稱之為插件,但如果與dom和css有關(guān)系并且具備一定的交互性佛纫,一般叫做組件妓局。當(dāng)然這沒有什么明顯的區(qū)分,只是一種習(xí)慣性叫法呈宇。
利用原型鏈跟磨,可以將一些UI層面的業(yè)務(wù)代碼封裝在一個(gè)小組件中,并利用js實(shí)現(xiàn)組件的交互性攒盈。
現(xiàn)有一個(gè)這樣的需求:

  1. 實(shí)現(xiàn)一個(gè)彈層,此彈層可以顯示一些文字提示性的信息哎榴;
  2. 彈層右上角必須有一個(gè)關(guān)閉按扭型豁,點(diǎn)擊之后彈層消失僵蛛;
  3. 彈層底部必有一個(gè)“確定”按扭,然后根據(jù)需求迎变,可以配置多一個(gè)“取消”按扭充尉;
  4. 點(diǎn)擊“確定”按扭之后,可以觸發(fā)一個(gè)事件衣形;
  5. 點(diǎn)擊關(guān)閉/“取消”按扭后驼侠,可以觸發(fā)一個(gè)事件。

根據(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; }

接下來(lái)倒源,我們開始編寫我們的交互插件。
我們假設(shè)組件的彈出層就是一個(gè)對(duì)象句狼。則這個(gè)對(duì)象是包含了我們的交互笋熬、樣式、結(jié)構(gòu)及渲染的過(guò)程腻菇。于是我們定義了一個(gè)構(gòu)造方法:

function MyDialog(){} // MyDialog就是我們的組件對(duì)象了

對(duì)象MyDialog就相當(dāng)于一個(gè)繩子胳螟,我們只要往這個(gè)繩子上不斷地掛上鉤子就是一個(gè)組件了。于是我們的組件就可以表示為:

function MyDialog(){}
MyDialog.prototype = {
    constructor: this,
    _initial: function(){},
    _parseTpl: function(){},
    _parseToDom: function(){},
    show: function(){},
    hide: function(){},
    css: function(){},
    ...
}

然后就可以將插件的功能都寫上筹吐。不過(guò)中間的業(yè)務(wù)邏輯糖耸,需要自己去一步一步研究。無(wú)論如何寫丘薛,我們最終要做到通過(guò)實(shí)例化一個(gè)MyDialog對(duì)象就可以使用我們的插件了嘉竟。
在編寫的過(guò)程中,我們得先做一些工具函數(shù):

1.對(duì)象合并函數(shù)

// 對(duì)象合并
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ù)

// 通過(guò)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)每一個(gè)鉤子函數(shù)具體邏輯結(jié)構(gòu):

// plugin.js
;(function(undefined) {
    "use strict"
    var _global;

    ...

    // 插件構(gòu)造函數(shù) - 返回?cái)?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ǔ)需求了。我們可以在頁(yè)面這樣調(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)了基本的顯示與隱藏的功能凰兑。不過(guò)我們?cè)谠趺磿r(shí)候彈出妥粟,彈出之后可能進(jìn)行一些操作,實(shí)際上還是需要進(jìn)行一些可控的操作吏够。就好像我們進(jìn)行事件綁定一樣勾给,只有用戶點(diǎn)擊了按扭,才響應(yīng)具體的事件锅知。那么播急,我們的插件,應(yīng)該也要像事件綁定一樣售睹,只有執(zhí)行了某些操作的時(shí)候桩警,調(diào)用相應(yīng)的事件響應(yīng)。
這種js的設(shè)計(jì)模式昌妹,被稱為 訂閱/發(fā)布模式捶枢,也被叫做 觀察者模式握截。我們插件中的也需要用到觀察者模式,比如烂叔,在打開彈窗之前谨胞,我們需要先進(jìn)行彈窗的內(nèi)容更新,執(zhí)行一些判斷邏輯等蒜鸡,然后執(zhí)行完成之后才顯示出彈窗胯努。在關(guān)閉彈窗之后,我們需要執(zhí)行關(guān)閉之后的一些邏輯逢防,處理業(yè)務(wù)等叶沛。這時(shí)候我們需要像平時(shí)綁定事件一樣,給插件做一些“事件”綁定回調(diào)方法胞四。
我們jquery對(duì)dom的事件響應(yīng)是這樣的:

$(<dom>).on("click",function(){})

我們照著上面的方式設(shè)計(jì)了對(duì)應(yīng)的插件響應(yīng)是這樣的:

mydialog.on('show',function(){})

則恬汁,我們需要實(shí)現(xiàn)一個(gè)事件機(jī)制,以達(dá)到監(jiān)聽插件的事件效果辜伟。關(guān)于自定義事件監(jiān)聽氓侧,可以參考一篇博文:漫談js自定義事件、DOM/偽DOM自定義事件导狡。在此不進(jìn)行大篇幅講自定義事件的問(wèn)題约巷。
最終我們實(shí)現(xiàn)的插件代碼為:

// plugin.js
;(function(undefined) {
    "use strict"
    var _global;

    // 工具函數(shù)
    // 對(duì)象合并
    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);
    }
    // 通過(guò)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ù) - 返回?cái)?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;
        }
    }

    // 最后將插件對(duì)象暴露給全局對(duì)象
    _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)用的時(shí)候就可以直接使用插件的事件綁定了旱捧。

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ū)就是一個(gè)非常良好的發(fā)布插件的平臺(tái)贪婉。具體可以如下操作:
寫初始化包的描述文件:

$ npm init

注冊(cè)包倉(cāng)庫(kù)帳號(hào)

$ npm adduser
Username: <帳號(hào)>
Password: <密碼>
Email:(this IS public) <郵箱>
Logged in as <帳號(hào)> on https://registry.npmjs.org/.

上傳包

$ npm publish

安裝包

$ npm install mydialog

到此,我們的插件就可以直接被更多人去使用了卢肃。

結(jié)論

寫了這么多疲迂,比較啰嗦,我在此做一下總結(jié):
關(guān)于如何編寫出一個(gè)好的js原生插件莫湘,需要平時(shí)在使用別人的插件的同時(shí)尤蒿,多查看一下api文檔,了解插件的調(diào)用方式幅垮,然后再看一下插件的源碼的設(shè)計(jì)方式腰池。基本上我們可以確定大部分插件都是按照原型的方式進(jìn)行設(shè)計(jì)的。而我從上面的例子中巩螃,就使用了好多js原生的知識(shí)點(diǎn)演怎,函數(shù)的命名沖突、閉包避乏、作用域,自定義工具函數(shù)擴(kuò)展對(duì)象的鉤子函數(shù)甘桑,以及對(duì)象的初始化拍皮、原型鏈繼承,構(gòu)造函數(shù)的定義及設(shè)計(jì)模式跑杭,還有事件的自定義铆帽,js設(shè)計(jì)模式的觀察者模式等知識(shí)。這些內(nèi)容還是需要初學(xué)者多多了解才能進(jìn)行一些高層次一些的插件開發(fā)德谅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末爹橱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子窄做,更是在濱河造成了極大的恐慌愧驱,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椭盏,死亡現(xiàn)場(chǎng)離奇詭異组砚,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)掏颊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門糟红,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人乌叶,你說(shuō)我怎么就攤上這事盆偿。” “怎么了准浴?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵事扭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我兄裂,道長(zhǎng)句旱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任晰奖,我火速辦了婚禮谈撒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匾南。我一直安慰自己啃匿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著溯乒,像睡著了一般夹厌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裆悄,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天矛纹,我揣著相機(jī)與錄音,去河邊找鬼光稼。 笑死或南,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的艾君。 我是一名探鬼主播采够,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼冰垄!你這毒婦竟也來(lái)了蹬癌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤虹茶,失蹤者是張志新(化名)和其女友劉穎逝薪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體写烤,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翼闽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了洲炊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片感局。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖暂衡,靈堂內(nèi)的尸體忽然破棺而出询微,到底是詐尸還是另有隱情,我是刑警寧澤狂巢,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布撑毛,位于F島的核電站,受9級(jí)特大地震影響唧领,放射性物質(zhì)發(fā)生泄漏藻雌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一斩个、第九天 我趴在偏房一處隱蔽的房頂上張望胯杭。 院中可真熱鬧,春花似錦受啥、人聲如沸做个。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)居暖。三九已至顽频,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間太闺,已是汗流浹背糯景。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留省骂,地道東北人莺奸。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像冀宴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子温学,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,517評(píng)論 25 707
  • js文檔加載完畢有哪幾種寫法 1:js加載完畢有哪幾種寫法 一略贮、當(dāng)不引入jQuery框架,只寫原生JS代碼時(shí)仗岖,需要...
    不高興_d651閱讀 1,618評(píng)論 0 2
  • 還記得以前看過(guò)的一個(gè)笑話轧拄。有人問(wèn)揽祥,你覺得這個(gè)世界上最棒的發(fā)明是什么?一個(gè)男人答道檩电,當(dāng)然是面膜啦拄丰!提問(wèn)者非常驚奇,女...
    自由譯員Scar閱讀 1,561評(píng)論 4 3
  • 董沛沛 洛陽(yáng) 焦點(diǎn)講師班三期 堅(jiān)持原創(chuàng)分享第160天 我從未見過(guò)懶惰的人: 我見過(guò)有個(gè)人有時(shí)在下午睡覺俐末, 在雨天不...
    緣源流長(zhǎng)閱讀 208評(píng)論 0 0
  • 39W4D料按。3D。 其實(shí)卓箫,按照近期丁大姐給我算的預(yù)產(chǎn)期载矿,今天是39周2天,距離預(yù)產(chǎn)期5天烹卒。 周一凌晨掛的今天的號(hào)闷盔。...
    林培閱讀 333評(píng)論 0 0