js原生高逼格插件

如何定義一個高逼格的原生JS插件

做爲一個前端er,若是不會寫一個小插件,都很差意思說本身是混前端界的。寫還不能依賴jquery之類的工具庫,不然裝得不夠高端。那麼,如何才能裝起來讓本身看起來逼格更高呢?固然是利用js純原生的寫法啦。之前一直說,掌握了js原生,就基本上能夠解決前端的全部腳本交互工做了,這話大致上是有些浮誇了。不過,也從側面說明了原生js在前端中佔着多麼重要的一面。好了。廢話很少說。我們就來看一下怎麼去作一個本身的js插件吧。javascript

插件的需求

咱們寫代碼,並非全部的業務或者邏輯代碼都要抽出來複用。首先,咱們得看一下是否須要將一部分常常重複的代碼抽象出來,寫到一個單獨的文件中爲之後再次使用。再看一下咱們的業務邏輯是否能夠爲團隊服務。
插件不是隨手就寫成的,而是根據本身業務邏輯進行抽象。沒有放之四海而皆準的插件,只有對插件,之因此叫作插件,那麼就是開箱即用,或者咱們只要添加一些配置參數就能夠達到咱們須要的結果。若是都符合了這些狀況,咱們纔去考慮作一個插件。css

插件封裝的條件

一個可複用的插件須要知足如下條件:html

  1. 插件自身的做用域與用戶當前的做用域相互獨立,也就是插件內部的私有變量不能影響使用者的環境變量;
  2. 插件需具有默認設置參數;
  3. 插件除了具有已實現的基本功能外,需提供部分API,使用者能夠經過該API修改插件功能的默認參數,從而實現用戶自定義插件效果;
  4. 插件支持鏈式調用;
  5. 插件需提供監聽入口,及針對指定元素進行監聽,使得該元素與插件響應達到插件效果。

關於插件封裝的條件,能夠查看一篇文章:原生JavaScript插件編寫指南
而我想要說明的是,如何一步一步地實現個人插件封裝。因此,我會先從簡單的方法函數來作起。前端

插件的外包裝

用函數包裝

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

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

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

// 加 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方法。那麼大家之間就會產生命名衝突,通常稱之爲變量的 全局污染jquery

用全局對象包裝

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

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,讓團隊成員都要遵照命名規則,在必定程度上已經解決了全局污染的問題。在團隊協做中只要約定好命名規則了,告知其它同窗便可以。固然不排除有個別人,接手你的項目,並不知道此全局變量已經定義,則他又定義了一次並賦值,這時,就會把你的對象覆蓋掉。固然,可能你會這麼幹來解決掉命名衝突問題:github

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

或者也能夠這樣寫:npm

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

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

也許有同窗會疑問,爲何能夠在此聲明plugin變量?實際上js的解釋執行,會把全部聲明都提早。若是一個變量已經聲明過,後面若是不是在函數內聲明的,則是沒有影響的。因此,就算在別的地方聲明過var plugin,我一樣也以能夠在這裏再次聲明一次。關於聲明的相關資料能夠看阮一鋒的如何判斷Javascript對象是否存在

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

利用閉包包裝

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

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

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

  1. 在定義插件以前添加一個分號,能夠解決js合併時可能會產生的錯誤問題;
  2. undefined在老一輩的瀏覽器是不被支持的,直接使用會報錯,js框架要考慮到兼容性,所以增長一個形參undefined,就算有人把外面的 undefined 定義了,裏面的 undefined 依然不受影響;
  3. 把window對象做爲參數傳入,是避免了函數執行的時候到外部去查找。

其實,咱們以爲直接傳window對象進去,我以爲仍是不太穩當。咱們並不肯定咱們的插件就必定用於瀏覽器上,也有可能使用在一些非瀏覽端上。因此咱們還能夠這麼幹,咱們不傳參數,直接取當前的全局this對象爲做頂級對象用。

;(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); }()); 

如此,咱們不須要傳入任何參數,而且解決了插件對環境的依事性。如此咱們的插件能夠在任何宿主環境上運行了。

上面的代碼段中有段奇怪的表達式:(0, eval)('this'),實際上(0,eval)是一個表達式,這個表達式執行以後的結果就是eval這一句至關於執行eval('this')的意思,詳細解釋看此篇:(0,eval)('this')釋義或者看一下這篇(0,eval)('this')

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

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

上面的兩種寫法是沒有區別的。都是正確的寫法。我的建議使用第二種寫法。這樣子更像一個總體。

附加一點知識:
js裏面()括號就是將代碼結構變成表達式,被包在()裏面的變成了表達式以後,則就會當即執行,js中將一段代碼變成表達式有不少種方式,好比:

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

固然,咱們不推薦你這麼用。並且亂用可能會產生一些歧義。

到這一步,咱們的插件的基礎結構就已經算是完整的了。

使用模塊化的規範包裝

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

  • 每功能互相之間的依賴必需要明確,則必須嚴格按照依賴的順序進行合併或者加載
  • 每一個子功能分別都要是一個閉包,而且將公共的接口暴露到共享域也便是一個被主函數暴露的公共對象

關鍵如何實現,有不少種辦法。最笨的辦法就是按順序加載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> 

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

if (typeof module !== "undefined" && module.exports) { module.exports = plugin; } else if (typeof define === "function" && define.amd) { define(function(){return plugin;}); } else { _globals.plugin = plugin; } 

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

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

插件的默認參數

咱們知道,函數是能夠設置默認參數這種說法,而無論咱們是否傳有參數,咱們都應該返回一個值以告訴用戶我作了怎樣的處理,好比:

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插件,咱們應該把一些基本的狀態參數添加到咱們須要的插件上去。
假設仍是上面的加減乘除餘的需求,咱們如何實現插件的默認參數呢?道理實際上是同樣的。

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

(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一個對象的時候,修改咱們的默認參數:

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就是以下幾個方法:

...
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),方法則直接叫鉤子函數。這是一種形象生動的說法,就好像咱們在一條繩子上放不少掛鉤,咱們能夠按須要在上面掛東西。
實際上,咱們即知道插件能夠像一條繩子上掛東西,也能夠拿掉掛的東西。那麼一個插件,實際上就是個形象上的鏈。不過咱們上面的全部鉤子都是掛在對象上的,用於實現鏈並非很理想。

插件的鏈式調用(利用當前對象)

插件並不是都是能鏈式調用的,有些時候,咱們只是用鉤子來實現一個計算並返回結果,取得運算結果就能夠了。可是有些時候,咱們用鉤子並不須要其返回結果。咱們只利用其實現咱們的業務邏輯,爲了代碼簡潔與方便,咱們經常將插件的調用按鏈式的方式進行調用。
最多見的jquery的鏈式調用以下:

$(<id>).show().css('color','red').width(100).height(100).... 

那,如何才能將鏈式調用運用到咱們的插件中去呢?假設咱們上面的例子,若是是要按照plugin這個對象的鏈式進行調用,則能夠將其業務結構改成:

...
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; } } ... 

顯示,咱們只要將插件的當前對象this直接返回,則在下一下方法中,一樣能夠引用插件對象plugin的其它勾子方法。而後調用的時候就可使用鏈式了。

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

顯然這樣作並無什麼意義。咱們這裏的每個鉤子函數都只是用來計算而且獲取返回值而已。而鏈式調用自己的意義是用來處理業務邏輯的。

插件的鏈式調用(利用原型鏈)

JavaScript中,萬物皆對象,全部對象都是繼承自原型。JS在建立對象(不管是普通對象仍是函數對象)的時候,都有一個叫作__proto__的內置屬性,用於指向建立它的函數對象的原型對象prototype。關於原型問題,感興趣的同窗能夠看這篇:js原型鏈
在上面的需求中,咱們能夠將plugin對象改成原型的方式,則須要將plugin寫成一個構造方法,咱們將插件名換爲Calculate避免由於Plugin大寫的時候與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;} ... 

固然,假設咱們的插件是對初始化參數進行運算並只輸出結果,咱們能夠稍微改一下:

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

這時調用咱們寫好的插件,則輸出爲以下:

var plugin = new Calculate(); plugin .add(2,1) .sub(2,1) .mul(2,1) .div(2,1) .sur(2,1); // 結果: // 3 // 1 // 2 // 2 // 0 

上面的例子,能夠並無太多的現實意義。不過在網頁設計中,咱們的插件基本上都是服務於UI層面,利用js腳本實現一些可交互的效果。這時咱們編寫一個UI插件,實現過程也是可使用鏈式進行調用。

編寫UI組件

通常狀況,若是一個js僅僅是處理一個邏輯,咱們稱之爲插件,但若是與dom和css有關係而且具有必定的交互性,通常叫作組件。固然這沒有什麼明顯的區分,只是一種習慣性叫法。
利用原型鏈,能夠將一些UI層面的業務代碼封裝在一個小組件中,並利用js實現組件的交互性。
現有一個這樣的需求:

  1. 實現一個彈層,此彈層能夠顯示一些文字提示性的信息;
  2. 彈層右上角必須有一個關閉按扭,點擊以後彈層消失;
  3. 彈層底部必有一個「肯定」按扭,而後根據需求,能夠配置多一個「取消」按扭;
  4. 點擊「肯定」按扭以後,能夠觸發一個事件;
  5. 點擊關閉/「取消」按扭後,能夠觸發一個事件。

根據需求,咱們先寫出dom結構:

<!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結構:

* { 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; } 

接下來,咱們開始編寫咱們的交互插件。
咱們假設組件的彈出層就是一個對象。則這個對象是包含了咱們的交互、樣式、結構及渲染的過程。因而咱們定義了一個構造方法:

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

對象MyDialog就至關於一個繩子,咱們只要往這個繩子上不斷地掛上鉤子就是一個組件了。因而咱們的組件就能夠表示爲:

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

而後就能夠將插件的功能都寫上。不過中間的業務邏輯,須要本身去一步一步研究。不管如何寫,咱們最終要作到經過實例化一個MyDialog對象就可使用咱們的插件了。
在編寫的過程當中,咱們得先作一些工具函數:

1.對象合併函數

// 對象合併 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.自定義模板引擎解釋函數

// 自定義模板引擎 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函數

// 經過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; } 

結合工具函數,再去實現每個鉤子函數具體邏輯結構:

// plugin.js ;(function(undefined) {  "use strict" var _global; ... // 插件構造函數 - 返回數組結構 function MyDialog(opt){ this._initial(opt); } MyDialog.prototype = { constructor: this, _initial: function(opt) { // 默認參數 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) { // 將模板轉爲字符串 var data = this.def; var tplStr = document.getElementById(tmpId).innerHTML.trim(); return templateEngine(tplStr,data); }, _parseToDom: function(str) { // 將字符串轉爲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); } }()); 

到這一步,咱們的插件已經達到了基礎需求了。咱們能夠在頁面這樣調用:

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

插件的監聽

彈出框插件咱們已經實現了基本的顯示與隱藏的功能。不過咱們在怎麼時候彈出,彈出以後可能進行一些操做,實際上仍是須要進行一些可控的操做。就好像咱們進行事件綁定同樣,只有用戶點擊了按扭,才響應具體的事件。那麼,咱們的插件,應該也要像事件綁定同樣,只有執行了某些操做的時候,調用相應的事件響應。
這種js的設計模式,被稱爲 訂閱/發佈模式,也被叫作 觀察者模式。咱們插件中的也須要用到觀察者模式,好比,在打開彈窗以前,咱們須要先進行彈窗的內容更新,執行一些判斷邏輯等,而後執行完成以後才顯示出彈窗。在關閉彈窗以後,咱們須要執行關閉以後的一些邏輯,處理業務等。這時候咱們須要像平時綁定事件同樣,給插件作一些「事件」綁定回調方法。
咱們jquery對dom的事件響應是這樣的:

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

咱們照着上面的方式設計了對應的插件響應是這樣的:

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

則,咱們須要實現一個事件機制,以達到監聽插件的事件效果。關於自定義事件監聽,能夠參考一篇博文:漫談js自定義事件、DOM/僞DOM自定義事件。在此不進行大篇幅講自定義事件的問題。
最終咱們實現的插件代碼爲:

// plugin.js ;(function(undefined) {  "use strict" var _global; // 工具函數 // 對象合併 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; } // 插件構造函數 - 返回數組結構 function MyDialog(opt){ this._initial(opt); } MyDialog.prototype = { constructor: this, _initial: function(opt) { // 默認參數 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; //檢查dom樹中dialog的節點是否存在 this.listeners = []; //自定義事件,用於監聽插件的用戶交互 this.handlers = {}; }, _parseTpl: function(tmpId) { // 將模板轉爲字符串 var data = this.def; var tplStr = document.getElementById(tmpId).innerHTML.trim(); return templateEngine(tplStr,data); }, _parseToDom: function(str) { // 將字符串轉爲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); } }()); 

而後調用的時候就能夠直接使用插件的事件綁定了。

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,有須要具體實現的同窗能夠去查閱。

插件發佈

咱們寫好了插件,實際上還能夠將咱們的插件發佈到開源組織去分享給更多人去使用(代碼必須是私人擁有全部支配權限)。咱們將插件打包以後,就能夠發佈到開源組織上去供別人下載使用了。
咱們熟知的npm社區就是一個很是良好的發佈插件的平臺。具體能夠以下操做:
寫初始化包的描述文件:

$ npm init

註冊包倉庫賬號

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

上傳包

$ npm publish

安裝包

$ npm install mydialog

到此,咱們的插件就能夠直接被更多人去使用了。

結論

寫了這麼多,比較囉嗦,我在此作一下總結:關於如何編寫出一個好的js原生插件,須要平時在使用別人的插件的同時,多查看一下api文檔,瞭解插件的調用方式,而後再看一下插件的源碼的設計方式。基本上咱們能夠肯定大部分插件都是按照原型的方式進行設計的。而我從上面的例子中,就使用了好多js原生的知識點,函數的命名衝突、閉包、做用域,自定義工具函數擴展對象的鉤子函數,以及對象的初始化、原型鏈繼承,構造函數的定義及設計模式,還有事件的自定義,js設計模式的觀察者模式等知識。這些內容仍是須要初學者多多瞭解才能進行一些高層次一些的插件開發。

相關文章
相關標籤/搜索