今天想跟大家討論一下我們在工作中對於插件的需求,我們寫程式碼,並不是所有的業務或邏輯程式碼都要抽出來重複使用。首先,我們得看一下是否需要將一部分經常重複的程式碼抽象化出來,寫到一個單獨的檔案中為以後再次使用。再看一下我們的業務邏輯是否可以為團隊服務。插件不是隨手就寫成的,而是根據自己業務邏輯進行抽象化。沒有放諸四海皆準的插件,只有對插件,之所以叫做插件,那麼就是開箱即用,或者我們只要添加一些配置參數就可以達到我們需要的結果。如果都符合了這些情況,我們才去考慮做一個插件。
外掛程式封裝的條件
一個可重複使用的外掛程式需要滿足以下條件:
外掛程式本身的作用域與使用者目前的作用域相互獨立,也就是插件內部的私有變數無法影響使用者的環境變數;
外掛程式需具備預設設定參數;
外掛程式除了具備已實現的基本功能外,需提供部分API,使用者可以透過此API修改插件功能的預設參數,從而實現用戶自訂插件效果;
插件支援鍊式呼叫;
插件需提供監聽入口,及針對指定元素進行監聽,使得該元素與插件響應達到插件效果。
關於外掛程式封裝的條件,可以查看一篇文章:原生JavaScript外掛程式編寫指南
而我想要說明的是,如何一步一步實現我的外掛程式封裝。所以,我會先從簡單的方法函數來做。
插件的外包裝
用函數包裝
所謂插件,其實就是封裝在一個閉包中的一種函數集。我記得剛開始寫js的時候,我是這樣乾的,將我想要的邏輯,寫成一個函數,然後再根據不同需要傳入不同的參數就可以了。
例如,我想實作兩個數字相加的方法:
function add(n1,n2) { return n1 + n2; } // 调用 add(1,2) // 输出:3
這就是我們要的函數的簡單實作。如果只是實現這麼簡單的邏輯,那已經可以了,沒必要弄一些花俏的東西。 js函數本身就可以解決絕大多數的問題。不過我們在實際工作與應用中,一般情況的需求都比較複雜得多。
如果這時,產品來跟你說,我不只需要兩個數相加的,我還要相減,相乘,相除,求餘等等功能。這時候,我們要怎麼辦呢?
當然,你會想,這有什麼難的。直接將這堆函數都寫出來不就完了。然後都放在一個js檔裡面。需要的時候,就呼叫它就好了。
// 加 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物件來接收我們這些工具函數。
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,讓團隊成員都要遵守命名規則,在某種程度上已經解決了全域污染的問題。在團隊協作中只要約定好命名規則了,告知其它同學即可以。當然不排除有個別人,接手你的項目,並不知道此全域變數已經定義,則他又定義了一次並賦值,這時,就會把你的物件覆蓋掉。當然,可能你會這麼做來解決掉命名衝突問題:
if(!plugin){ //这里的if条件也可以用: (typeof plugin == 'undefined') var plugin = { // 以此写你的函数逻辑 } }
或
var plugin; if(!plugin){ plugin = { // ... } }
這樣子,就不會存在命名上的衝突了。
基本上,這就可以算是一個插件了。解決了全域污染問題,方法函數可以抽出來放到單獨的文件裡面去。
利用閉包包裝
上面的例子,虽然可以实现了插件的基本上的功能。不过我们的plugin对象,是定义在全局域里面的。我们知道,js变量的调用,从全局作用域上找查的速度会比在私有作用域里面慢得多得多。所以,我们最好将插件逻辑写在一个私有作用域中。
实现私有作用域,最好的办法就是使用闭包。可以把插件当做一个函数,插件内部的变量及函数的私有变量,为了在调用插件后依旧能使用其功能,闭包的作用就是延长函数(插件)内部变量的生命周期,使得插件函数可以重复调用,而不影响用户自身作用域。
故需将插件的所有功能写在一个立即执行函数中:
;(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对象为作顶级对象用。
;(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
<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。不过在此不谈如何进行模块化打包或者加载的问题,如有问题的同学可以去上面的链接上看文档学习。
为了实现插件的模块化并且让我们的插件也是一个模块,我们就得让我们的插件也实现模块化的机制。
我们实际上,只要判断是否存在加载器,如果存在加载器,我们就使用加载器,如果不存在加载器。我们就使用顶级域对象。
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),方法则直接叫钩子函数。这是一种形象生动的说法,就好像我们在一条绳子上放很多挂钩,我们可以按需要在上面挂东西。
实际上,我们即知道插件可以像一条绳子上挂东西,也可以拿掉挂的东西。那么一个插件,实际上就是个形象上的链。不过我们上面的所有钩子都是挂在对象上的,用于实现链并不是很理想。
相信看了这些案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
相关阅读:
在html里怎么添加flash视频格式(flv、swf)文件
怎样通过disabled和readonly将input设置为只读效果
以上是怎樣用原生JS封裝自己需要的插件的詳細內容。更多資訊請關注PHP中文網其他相關文章!