怎樣用原生JS封裝自己需要的插件

今天想和大家討論一下我們在工作中對於插件的需求,我們寫代碼,並不是所有的業務或者邏輯代碼都要抽出來複用。首先,我們得看一下是否需要將一部分經常重複的代碼抽象出來,寫到一個單獨的文件中爲以後再次使用。再看一下我們的業務邏輯是否可以爲團隊服務。插件不是隨手就寫成的,而是根據自己業務邏輯進行抽象。沒有放之四海而皆準的插件,只有對插件,之所以叫做插件,那麼就是開箱即用,或者我們只要添加一些配置參數就可以達到我們需要的結果。如果都符合了這些情況,我們纔去考慮做一個插件。

插件封裝的條件

一個可複用的插件需要滿足以下條件:

插件自身的作用域與用戶當前的作用域相互獨立,也就是插件內部的私有變量不能影響使用者的環境變量;

插件需具備默認設置參數;

插件除了具備已實現的基本功能外,需提供部分API,使用者可以通過該API修改插件功能的默認參數,從而實現用戶自定義插件效果;

插件支持鏈式調用;

插件需提供監聽入口,及針對指定元素進行監聽,使得該元素與插件響應達到插件效果。

關於插件封裝的條件,可以查看一篇文章:原生JavaScript插件編寫指南
而我想要說明的是,如何一步一步地實現我的插件封裝。所以,我會先從簡單的方法函數來做起。

插件的外包裝

用函數包裝

所謂插件,其實就是封裝在一個閉包中的一種函數集。我記得剛開始寫js的時候,我是這樣乾的,將我想要的邏輯,寫成一個函數,然後再根據不同需要傳入不同的參數就可以了。
比如,我想實現兩個數字相加的方法:

1

2

3

4

5

6

function add(n1,n2) {

    return n1 + n2;

}

// 調用

add(1,2)

// 輸出:3

這就是我們要的功能的簡單實現。如果僅僅只不過實現這麼簡單的邏輯,那已經可以了,沒必要弄一些花裏胡哨的東西。js函數本身就可以解決絕大多數的問題。不過我們在實際工作與應用中,一般情況的需求都是比較複雜得多。
如果這時,產品來跟你說,我不僅需要兩個數相加的,我還要相減,相乘,相除,求餘等等功能。這時候,我們怎麼辦呢?
當然,你會想,這有什麼難的。直接將這堆函數都寫出來不就完了。然後都放在一個js文件裏面。需要的時候,就調用它就好了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// 加

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,現在已經實現我們所需要的所有功能。並且我們也把這些函數都寫到一個js裏面了。如果是一個人在用,那麼可以很清楚知道自己是否已經定義了什麼,並且知道自己寫了什麼內容,我在哪個頁面需要,那麼就直接引入這個js文件就可以搞定了。
不過,如果是兩個人以上的團隊,或者你與別人一起協作寫代碼,這時候,另一個人並不知道你是否寫了add方法,這時他也定義了同樣的add方法。那麼你們之間就會產生命名衝突,一般稱之爲變量的 全局污染

用全局對象包裝

爲了解決這種全局變量污染的問題。這時,我們可以定義一個js對象來接收我們這些工具函數。

1

2

3

4

5

6

7

8

9

var plugin = {

    add: function(n1,n2){...},//加

    sub: function(n1,n2){...},//減

    mul: function(n1,n2){...},//乘

    div: function(n1,n2){...},//除

    sur: function(n1,n2){...} //餘

}

// 調用

plugin.add(1,2)

上面的方式,約定好此插件名爲plugin,讓團隊成員都要遵守命名規則,在一定程度上已經解決了全局污染的問題。在團隊協作中只要約定好命名規則了,告知其它同學即可以。當然不排除有個別人,接手你的項目,並不知道此全局變量已經定義,則他又定義了一次並賦值,這時,就會把你的對象覆蓋掉。當然,可能你會這麼幹來解決掉命名衝突問題:

1

2

3

if(!plugin){ //這裏的if條件也可以用: (typeof plugin == 'undefined')

    var plugin = {        // 以此寫你的函數邏輯    }

}

或者

1

2

3

4

5

6

var plugin;

if(!plugin){

    plugin = {

        // ...

    }

}

這樣子,就不會存在命名上的衝突了。

基本上,這就可以算是一個插件了。解決了全局污染問題,方法函數可以抽出來放到一單獨的文件裏面去。

利用閉包包裝

上面的例子,雖然可以實現了插件的基本上的功能。不過我們的plugin對象,是定義在全局域裏面的。我們知道,js變量的調用,從全局作用域上找查的速度會比在私有作用域裏面慢得多得多。所以,我們最好將插件邏輯寫在一個私有作用域中。
實現私有作用域,最好的辦法就是使用閉包。可以把插件當做一個函數,插件內部的變量及函數的私有變量,爲了在調用插件後依舊能使用其功能,閉包的作用就是延長函數(插件)內部變量的生命週期,使得插件函數可以重複調用,而不影響用戶自身作用域。
故需將插件的所有功能寫在一個立即執行函數中:

1

2

3

4

5

6

7

8

;(function(global,undefined) {

    var plugin = {

        add: function(n1,n2){...}

        ...

    }

    // 最後將插件對象暴露給全局對象

    'plugin' in global && global.plugin = plugin;

})(window);

對上面的代碼段傳參問題進行解釋一下:

在定義插件之前添加一個分號,可以解決js合併時可能會產生的錯誤問題;

undefined在老一輩的瀏覽器是不被支持的,直接使用會報錯,js框架要考慮到兼容性,因此增加一個形參undefined,就算有人把外面的 undefined 定義了,裏面的 undefined 依然不受影響;

window對象作爲參數傳入,是避免了函數執行的時候到外部去查找。

其實,我們覺得直接傳window對象進去,我覺得還是不太妥當。我們並不確定我們的插件就一定用於瀏覽器上,也有可能使用在一些非瀏覽端上。所以我們還可以這麼幹,我們不傳參數,直接取當前的全局this對象爲作頂級對象用。

1

2

3

4

5

6

7

8

9

10

11

;(function(global,undefined) {

    "use strict" //使用js嚴格模式檢查,使語法更規範

    var _global;

    var plugin = {

        add: function(n1,n2){...}

        ...

    }

    // 最後將插件對象暴露給全局對象

    _global = (function(){ return this || (0, eval)('this'); }());

    !('plugin' in _global) && (_global.plugin = plugin);

}());

如此,我們不需要傳入任何參數,並且解決了插件對環境的依事性。如此我們的插件可以在任何宿主環境上運行了。

關於立即自執行函數,有兩種寫法:

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

寫法二

(function(){}())

使用模塊化的規範包裝

雖然上面的包裝基本上已經算是ok了的。但是如果是多個人一起開發一個大型的插件,這時我們要該怎麼辦呢?多人合作,肯定會產生多個文件,每個人負責一個小功能,那麼如何才能將所有人開發的代碼集合起來呢?這是一個討厭的問題。要實現協作開發插件,必須具備如下條件:

每功能互相之間的依賴必須要明確,則必須嚴格按照依賴的順序進行合併或者加載

每個子功能分別都要是一個閉包,並且將公共的接口暴露到共享域也即是一個被主函數暴露的公共對象

關鍵如何實現,有很多種辦法。最笨的辦法就是按順序加載js

1

2

3

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

但是不推薦這麼做,這樣做與我們所追求的插件的封裝性相背。
不過現在前端界有一堆流行的模塊加載器,比如require、seajs,或者也可以像類似於Node的方式進行加載,不過在瀏覽器端,我們還得利用打包器來實現模塊加載,比如browserify。不過在此不談如何進行模塊化打包或者加載的問題,如有問題的同學可以去上面的鏈接上看文檔學習。
爲了實現插件的模塊化並且讓我們的插件也是一個模塊,我們就得讓我們的插件也實現模塊化的機制。
我們實際上,只要判斷是否存在加載器,如果存在加載器,我們就使用加載器,如果不存在加載器。我們就使用頂級域對象。

1

2

3

4

5

6

7

if (typeof module !== "undefined" && module.exports) {

    module.exports = plugin;

} else if (typeof define === "function" && define.amd) {

    define(function(){return plugin;});

} else {

    _globals.plugin = plugin;

}

這樣子我們的完整的插件的樣子應該是這樣子的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

// 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對象。

1

2

3

4

5

6

7

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

插件的默認參數

我們知道,函數是可以設置默認參數這種說法,而不管我們是否傳有參數,我們都應該返回一個值以告訴用戶我做了怎樣的處理,比如:

1

2

3

4

5

6

7

function add(param){   

        var args = !!param ? Array.prototype.slice.call(arguments) : [];

            return args.reduce(function(pre,cur){       

            return pre + cur;

    }, 0);

}

console.log(add()) //不傳參,結果輸出0,則這裏已經設置了默認了參數爲空數組console.log(add(1,2,3,4,5)) //傳參,結果輸出15

則作爲一個健壯的js插件,我們應該把一些基本的狀態參數添加到我們需要的插件上去。
假設還是上面的加減乘除餘的需求,我們如何實現插件的默認參數呢?道理其實是一樣的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

// 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);

    }

}());

// 輸出結果爲:

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

}

實際上,插件都有自己的默認參數,就以我們最爲常見的表單驗證插件爲例:validate.js

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

(function(window, document, undefined) {

    // 插件的默認參數

    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;

}

當然,參數既然是默認的,那就意味着我們可以隨意修改參數以達到我們的需求。插件本身的意義就在於具有複用性。
如表單驗證插件,則就可以new一個對象的時候,修改我們的默認參數:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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

    }

});

插件的鉤子

我們知道,設計一下插件,參數或者其邏輯肯定不是寫死的,我們得像函數一樣,得讓用戶提供自己的參數去實現用戶的需求。則我們的插件需要提供一個修改默認參數的入口。
如上面我們說的修改默認參數,實際上也是插件給我們提供的一個API。讓我們的插件更加的靈活。如果大家對API不瞭解,可以百度一下API
通常我們用的js插件,實現的方式會有多種多樣的。最簡單的實現邏輯就是一個方法,或者一個js對象,又或者是一個構造函數等等。
** 然我們插件所謂的API,實際就是我們插件暴露出來的所有方法及屬性。 **
我們需求中,加減乘除餘插件中,我們的API就是如下幾個方法:

1

2

3

4

5

6

7

8

9

...

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中,我們常常將容易被修改和變動的方法或屬性統稱爲鉤子(Hook),方法則直接叫鉤子函數。這是一種形象生動的說法,就好像我們在一條繩子上放很多掛鉤,我們可以按需要在上面掛東西。
實際上,我們即知道插件可以像一條繩子上掛東西,也可以拿掉掛的東西。那麼一個插件,實際上就是個形象上的鏈。不過我們上面的所有鉤子都是掛在對象上的,用於實現鏈並不是很理想。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章