做为一个前端er,若是不会写一个小插件,都很差意思说本身是混前端界的。写还不能依赖jquery之类的工具库,不然装得不够高端。那么,如何才能装起来让本身看起来逼格更高呢?固然是利用js纯原生的写法啦。之前一直说,掌握了js原生,就基本上能够解决前端的全部脚本交互工做了,这话大致上是有些浮夸了。不过,也从侧面说明了原生js在前端中占着多么重要的一面。好了。废话很少说。我们就来看一下怎么去作一个本身的js插件吧。javascript
咱们写代码,并非全部的业务或者逻辑代码都要抽出来复用。首先,咱们得看一下是否须要将一部分常常重复的代码抽象出来,写到一个单独的文件中为之后再次使用。再看一下咱们的业务逻辑是否能够为团队服务。
插件不是随手就写成的,而是根据本身业务逻辑进行抽象。没有放之四海而皆准的插件,只有对插件,之因此叫作插件,那么就是开箱即用,或者咱们只要添加一些配置参数就能够达到咱们须要的结果。若是都符合了这些状况,咱们才去考虑作一个插件。css
一个可复用的插件须要知足如下条件:html
关于插件封装的条件,能够查看一篇文章:原生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);
对上面的代码段传参问题进行解释一下:
undefined
定义了,里面的 undefined 依然不受影响;其实,咱们以为直接传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>
可是不推荐这么作,这样作与咱们所追求的插件的封装性相背。
不过如今前端界有一堆流行的模块加载器,好比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 }
咱们知道,函数是能够设置默认参数这种说法,而无论咱们是否传有参数,咱们都应该返回一个值以告诉用户我作了怎样的处理,好比:
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:
在插件的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插件,实现过程也是可使用链式进行调用。
通常状况,若是一个js仅仅是处理一个逻辑,咱们称之为插件,但若是与dom和css有关系而且具有必定的交互性,通常叫作组件。固然这没有什么明显的区分,只是一种习惯性叫法。
利用原型链,能够将一些UI层面的业务代码封装在一个小组件中,并利用js实现组件的交互性。
现有一个这样的需求:
根据需求,咱们先写出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">
弹出框插件咱们已经实现了基本的显示与隐藏的功能。不过咱们在怎么时候弹出,弹出以后可能进行一些操做,实际上仍是须要进行一些可控的操做。就好像咱们进行事件绑定同样,只有用户点击了按扭,才响应具体的事件。那么,咱们的插件,应该也要像事件绑定同样,只有执行了某些操做的时候,调用相应的事件响应。
这种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',