如何定义原生JS插件

插件的需求

我们写代码,并不是所有的业务或者逻辑代码都要抽出来复用。首先,我们得看一下是否需要将一部分经常重复的代码抽象出来,写到一个单独的文件中为以后再次使用。再看一下我们的业务逻辑是否可以为团队服务。

插件不是随手就写成的,而是根据自己业务逻辑进行抽象。没有放之四海而皆准的插件,只有对插件,之所以叫做插件,那么就是开箱即用,或者我们只要添加一些配置参数就可以达到我们需要的结果。如果都符合了这些情况,我们才去考虑做一个插件。

插件封装的条件

一个可复用的插件需要满足以下条件:

插件自身的作用域与用户当前的作用域相互独立,也就是插件内部的私有变量不能影响使用者的环境变量;

插件需具备默认设置参数;

插件除了具备已实现的基本功能外,需提供部分API,使用者可以通过该API修改插件功能的默认参数,从而实现用户自定义插件效果;

插件支持链式调用;

插件需提供监听入口,及针对指定元素进行监听,使得该元素与插件响应达到插件效果。

关于插件封装的条件,可以查看一篇文章:原生JavaScript插件编写指南

而我想要说明的是,如何一步一步地实现我的插件封装。所以,我会先从简单的方法函数来做起。

插件的外包装

用函数包装

所谓插件,其实就是封装在一个闭包中的一种函数集。我记得刚开始写js的时候,我是这样干的,将我想要的逻辑,写成一个函数,然后再根据不同需要传入不同的参数就可以了。

比如,我想实现两个数字相加的方法:

functionadd(n1,n2){returnn1 + n2;}

// 调用add(1,2)

// 输出:3

这就是我们要的功能的简单实现。如果仅仅只不过实现这么简单的逻辑,那已经可以了,没必要弄一些花里胡哨的东西。js函数本身就可以解决绝大多数的问题。不过我们在实际工作与应用中,一般情况的需求都是比较复杂得多。

如果这时,产品来跟你说,我不仅需要两个数相加的,我还要相减,相乘,相除,求余等等功能。这时候,我们怎么办呢?

当然,你会想,这有什么难的。直接将这堆函数都写出来不就完了。然后都放在一个js文件里面。需要的时候,就调用它就好了。

// 加functionadd(n1,n2){returnn1 + n2;}

// 减functionsub(n1,n2){returnn1 - n2;}

// 乘functionmul(n1,n2){returnn1 * n2;}

/ 除functiondiv(n1,n2){returnn1 / n2;}

// 求余functionsur(n1,n2){returnn1 % n2;}

OK,现在已经实现我们所需要的所有功能。并且我们也把这些函数都写到一个js里面了。如果是一个人在用,那么可以很清楚知道自己是否已经定义了什么,并且知道自己写了什么内容,我在哪个页面需要,那么就直接引入这个js文件就可以搞定了。

不过,如果是两个人以上的团队,或者你与别人一起协作写代码,这时候,另一个人并不知道你是否写了add方法,这时他也定义了同样的add方法。那么你们之间就会产生命名冲突,一般称之为变量的全局污染

用全局对象包装

为了解决这种全局变量污染的问题。这时,我们可以定义一个js对象来接收我们这些工具函数。

varplugin = {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')varplugin = {// 以此写你的函数逻辑}}

或者也可以这样写:

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

这样子,就不会存在命名上的冲突了。

也许有同学会疑问,为什么可以在此声明plugin变量?实际上js的解释执行,会把所有声明都提前。如果一个变量已经声明过,后面如果不是在函数内声明的,则是没有影响的。所以,就算在别的地方声明过var plugin,我同样也以可以在这里再次声明一次。关于声明的相关资料可以看阮一锋的如何判断Javascript对象是否存在

基本上,这就可以算是一个插件了。解决了全局污染问题,方法函数可以抽出来放到一单独的文件里面去。

利用闭包包装

上面的例子,虽然可以实现了插件的基本上的功能。不过我们的plugin对象,是定义在全局域里面的。我们知道,js变量的调用,从全局作用域上找查的速度会比在私有作用域里面慢得多得多。所以,我们最好将插件逻辑写在一个私有作用域中。

实现私有作用域,最好的办法就是使用闭包。可以把插件当做一个函数,插件内部的变量及函数的私有变量,为了在调用插件后依旧能使用其功能,闭包的作用就是延长函数(插件)内部变量的生命周期,使得插件函数可以重复调用,而不影响用户自身作用域。

故需将插件的所有功能写在一个立即执行函数中:

;(function(global,undefined){

var plugin = {

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

}// 最后将插件对象暴露给全局对象'plugin'inglobal && (global.plugin = plugin);}

)(window);

对上面的代码段传参问题进行解释一下:

在定义插件之前添加一个分号,可以解决js合并时可能会产生的错误问题;

undefined在老一辈的浏览器是不被支持的,直接使用会报错,js框架要考虑到兼容性,因此增加一个形参undefined,就算有人把外面的undefined定义了,里面的 undefined 依然不受影响;

把window对象作为参数传入,是避免了函数执行的时候到外部去查找。

其实,我们觉得直接传window对象进去,我觉得还是不太妥当。我们并不确定我们的插件就一定用于浏览器上,也有可能使用在一些非浏览端上。所以我们还可以这么干,我们不传参数,直接取当前的全局this对象为作顶级对象用。

;(function(global,undefined){    "use strict"//使用js严格模式检查,使语法更规范var_global;varplugin = {add:function(n1,n2){...}        ...    }// 最后将插件对象暴露给全局对象_global = (function(){returnthis|| (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中将一段代码变成表达式有很多种方式,比如:

voidfunction(){...}();// 或者!functionfoo(){...}();// 或者+functionfoot(){...}();

当然,我们不推荐你这么用。而且乱用可能会产生一些歧义。

到这一步,我们的插件的基础结构就已经算是完整的了。

使用模块化的规范包装

虽然上面的包装基本上已经算是ok了的。但是如果是多个人一起开发一个大型的插件,这时我们要该怎么办呢?多人合作,肯定会产生多个文件,每个人负责一个小功能,那么如何才能将所有人开发的代码集合起来呢?这是一个讨厌的问题。要实现协作开发插件,必须具备如下条件:

每功能互相之间的依赖必须要明确,则必须严格按照依赖的顺序进行合并或者加载

每个子功能分别都要是一个闭包,并且将公共的接口暴露到共享域也即是一个被主函数暴露的公共对象

关键如何实现,有很多种办法。最笨的办法就是按顺序加载js

...

但是不推荐这么做,这样做与我们所追求的插件的封装性相背。

不过现在前端界有一堆流行的模块加载器,比如requireseajs,或者也可以像类似于Node的方式进行加载,不过在浏览器端,我们还得利用打包器来实现模块加载,比如browserify。不过在此不谈如何进行模块化打包或者加载的问题,如有问题的同学可以去上面的链接上看文档学习。

为了实现插件的模块化并且让我们的插件也是一个模块,我们就得让我们的插件也实现模块化的机制。

我们实际上,只要判断是否存在加载器,如果存在加载器,我们就使用加载器,如果不存在加载器。我们就使用顶级域对象。

if(typeofmodule!=="undefined"&&module.exports) {module.exports = plugin;}elseif(typeofdefine ==="function"&& define.amd) {    define(function(){returnplugin;});}else{    _globals.plugin = plugin;}

这样子我们的完整的插件的样子应该是这样子的:

// plugin.js;(function(undefined){   

"use strict"

var_global;

varplugin = {

add:function(n1,n2){returnn1 + n2; },//加

sub:function(n1,n2){returnn1 - n2; },//减

mul:function(n1,n2){returnn1 * n2; },//乘

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

sur:function(n1,n2){returnn1 % n2; }//余

}// 最后将插件对象暴露给全局对象

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

if(typeofmodule!=="undefined"&&module.exports) {module.exports = plugin;    }

elseif(typeofdefine ==="function"&& define.amd) {       

define(function(){returnplugin;});    }

else{       

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

}}());


插件的链式调用(利用当前对象)

插件并非都是能链式调用的,有些时候,我们只是用钩子来实现一个计算并返回结果,取得运算结果就可以了。但是有些时候,我们用钩子并不需要其返回结果。我们只利用其实现我们的业务逻辑,为了代码简洁与方便,我们常常将插件的调用按链式的方式进行调用。

最常见的jquery的链式调用如下:

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

那,如何才能将链式调用运用到我们的插件中去呢?假设我们上面的例子,如果是要按照plugin这个对象的链式进行调用,则可以将其业务结构改为:

...var plugin = {add:function(n1,n2){returnthis; },sub:function(n1,n2){returnthis; },mul:function(n1,n2){returnthis; },div:function(n1,n2){returnthis; },sur:function(n1,n2){returnthis; } }... 关于原型问题,感兴趣的同学可以看这篇: 链式链接

显示,我们只要将插件的当前对象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(){returnthis;}Calculate.prototype.sub =function(){returnthis;}Calculate.prototype.mul =function(){returnthis;}Calculate.prototype.div =function(){returnthis;}Calculate.prototype.sur =function(){returnthis;}...

当然,假设我们的插件是对初始化参数进行运算并只输出结果,我们可以稍微改一下:

// plugin.js// plugin.js;(function(undefined){    "use strict"var_global;functionresult(args,type){varargsArr =Array.prototype.slice.call(args);if(argsArr.length ==0)return0;switch(type) {case1:returnargsArr.reduce(function(p,c){returnp + c;});case2:returnargsArr.reduce(function(p,c){returnp - c;});case3:returnargsArr.reduce(function(p,c){returnp * c;});case4:returnargsArr.reduce(function(p,c){returnp / c;});case5:returnargsArr.reduce(function(p,c){returnp % c;});default:return0;        }    }functionCalculate(){}    Calculate.prototype.add =function(){console.log(result(arguments,1));returnthis;}    Calculate.prototype.sub =function(){console.log(result(arguments,2));returnthis;}    Calculate.prototype.mul =function(){console.log(result(arguments,3));returnthis;}    Calculate.prototype.div =function(){console.log(result(arguments,4));returnthis;}    Calculate.prototype.sur =function(){console.log(result(arguments,5));returnthis;}// 最后将插件对象暴露给全局对象_global = (function(){returnthis|| (0,eval)('this'); }());if(typeofmodule!=="undefined"&&module.exports) {module.exports = Calculate;    }elseif(typeofdefine ==="function"&& define.amd) {        define(function(){returnCalculate;});    }else{        !('Calculate'in_global) && (_global.Calculate = Calculate);    }}());

这时调用我们写好的插件,则输出为如下:

varplugin =newCalculate();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实现组件的交互性。

现有一个这样的需求:

实现一个弹层,此弹层可以显示一些文字提示性的信息;

弹层右上角必须有一个关闭按扭,点击之后弹层消失;

弹层底部必有一个“确定”按扭,然后根据需求,可以配置多一个“取消”按扭;

点击“确定”按扭之后,可以触发一个事件;

点击关闭/“取消”按扭后,可以触发一个事件。

根据需求,我们先写出dom结构:

<!DOCTYPE html>index×hello world!确定取消

写出css结构:

* {padding:0;margin:0; }.mydialog{background:#fff;box-shadow:01px10px0rgba(0, 0, 0, 0.3);overflow: hidden;width:300px;height:180px;border:1pxsolid#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:0050px;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:1pxsolid#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:1pxsolid#dcdcdc; }

接下来,我们开始编写我们的交互插件。

我们假设组件的弹出层就是一个对象。则这个对象是包含了我们的交互、样式、结构及渲染的过程。于是我们定义了一个构造方法:

functionMyDialog(){}// MyDialog就是我们的组件对象了

对象MyDialog就相当于一个绳子,我们只要往这个绳子上不断地挂上钩子就是一个组件了。于是我们的组件就可以表示为:

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

然后就可以将插件的功能都写上。不过中间的业务逻辑,需要自己去一步一步研究。无论如何写,我们最终要做到通过实例化一个MyDialog对象就可以使用我们的插件了。

在编写的过程中,我们得先做一些工具函数:

1.对象合并函数

// 对象合并functionextend(o,n,override){for(varkeyinn){if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){            o[key]=n[key];        }    }returno;}

2.自定义模板引擎解释函数

// 自定义模板引擎functiontemplateEngine(html, data){varre =/<%([^%>]+)?%>/g,        reExp =/(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,        code ='var r=[];\n',        cursor =0;varmatch;varadd =function(line, js){        js ? (code += line.match(reExp) ? line +'\n':'r.push('+ line +');\n') :            (code += line !=''?'r.push("'+ line.replace(/"/g,'\\"') +'");\n':'');returnadd;    }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("");';returnnewFunction(code.replace(/[\r\t\n]/g,'')).apply(data);}

3.查找class获取dom函数

// 通过class查找domif(!('getElementsByClass'inHTMLElement)){    HTMLElement.prototype.getElementsByClass =function(n, tar){varel = [],            _el = (!!tar ? tar :this).getElementsByTagName('*');for(vari=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];            }        }returnel;    };    ((typeofHTMLDocument !=='undefined') ? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass;}

结合工具函数,再去实现每一个钩子函数具体逻辑结构:

// plugin.js;(function(undefined){    "use strict"var_global;    ...// 插件构造函数 - 返回数组结构functionMyDialog(opt){this._initial(opt);    }    MyDialog.prototype = {constructor:this,_initial:function(opt){// 默认参数vardef = {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){// 将模板转为字符串vardata =this.def;vartplStr =document.getElementById(tmpId).innerHTML.trim();returntemplateEngine(tplStr,data);        },_parseToDom:function(str){// 将字符串转为domvardiv =document.createElement('div');if(typeofstr =='string') {                div.innerHTML = str;            }returndiv.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();returnthis;        },hide:function(callback){document.body.removeChild(this.dom);this.hasDom =false;            callback && callback();returnthis;        },modifyTpl:function(template){if(!!template) {if(typeoftemplate =='string'){this.tpl = template;                }elseif(typeoftemplate =='function'){this.tpl = template();                }else{returnthis;                }            }// this.tpl = this._parseTpl(this.def.tmpId);this.dom =this._parseToDom(this.tpl)[0];returnthis;        },css:function(styleObj){for(varpropinstyleObj){varattr = prop.replace(/[A-Z]/g,function(word){return'-'+ word.toLowerCase();                });this.dom.style[attr] = styleObj[prop];            }returnthis;        },width:function(val){this.dom.style.width = val +'px';returnthis;        },height:function(val){this.dom.style.height = val +'px';returnthis;        }    }    _global = (function(){returnthis|| (0,eval)('this'); }());if(typeofmodule!=="undefined"&&module.exports) {module.exports = MyDialog;    }elseif(typeofdefine ==="function"&& define.amd) {        define(function(){returnMyDialog;});    }else{        !('MyDialog'in_global) && (_global.MyDialog = MyDialog);    }}());

到这一步,我们的插件已经达到了基础需求了。我们可以在页面这样调用:

×<%this.content%><%if(this.cancel){ %><%this.ok_txt%><%this.cancel_txt%><%}else{ %><%this.ok_txt%><%} %>varmydialog =newMyDialog({        tmpId:'dialogTpl',        cancel:true,        content:'hello world!'});    mydialog.show();

插件的监听

弹出框插件我们已经实现了基本的显示与隐藏的功能。不过我们在怎么时候弹出,弹出之后可能进行一些操作,实际上还是需要进行一些可控的操作。就好像我们进行事件绑定一样,只有用户点击了按扭,才响应具体的事件。那么,我们的插件,应该也要像事件绑定一样,只有执行了某些操作的时候,调用相应的事件响应。

这种js的设计模式,被称为订阅/发布模式,也被叫做观察者模式。我们插件中的也需要用到观察者模式,比如,在打开弹窗之前,我们需要先进行弹窗的内容更新,执行一些判断逻辑等,然后执行完成之后才显示出弹窗。在关闭弹窗之后,我们需要执行关闭之后的一些逻辑,处理业务等。这时候我们需要像平时绑定事件一样,给插件做一些“事件”绑定回调方法。

我们jquery对dom的事件响应是这样的:

$().on("click",function(){})

我们照着上面的方式设计了对应的插件响应是这样的:

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

则,我们需要实现一个事件机制,以达到监听插件的事件效果。关于自定义事件监听,可以参考一篇博文:漫谈js自定义事件、DOM/伪DOM自定义事件。在此不进行大篇幅讲自定义事件的问题。

最终我们实现的插件代码为:

// plugin.js;(function(undefined){    "use strict"var_global;// 工具函数// 对象合并functionextend(o,n,override){for(varkeyinn){if(n.hasOwnProperty(key) && (!o.hasOwnProperty(key) || override)){                o[key]=n[key];            }        }returno;    }// 自定义模板引擎functiontemplateEngine(html, data){varre =/<%([^%>]+)?%>/g,            reExp =/(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,            code ='var r=[];\n',            cursor =0;varmatch;varadd =function(line, js){            js ? (code += line.match(reExp) ? line +'\n':'r.push('+ line +');\n') :                (code += line !=''?'r.push("'+ line.replace(/"/g,'\\"') +'");\n':'');returnadd;        }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("");';returnnewFunction(code.replace(/[\r\t\n]/g,'')).apply(data);    }// 通过class查找domif(!('getElementsByClass'inHTMLElement)){        HTMLElement.prototype.getElementsByClass =function(n){varel = [],                _el =this.getElementsByTagName('*');for(vari=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];                }            }returnel;        };        ((typeofHTMLDocument !=='undefined') ? HTMLDocument : Document).prototype.getElementsByClass = HTMLElement.prototype.getElementsByClass;    }// 插件构造函数 - 返回数组结构functionMyDialog(opt){this._initial(opt);    }    MyDialog.prototype = {constructor:this,_initial:function(opt){// 默认参数vardef = {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){// 将模板转为字符串vardata =this.def;vartplStr =document.getElementById(tmpId).innerHTML.trim();returntemplateEngine(tplStr,data);        },_parseToDom:function(str){// 将字符串转为domvardiv =document.createElement('div');if(typeofstr =='string') {                div.innerHTML = str;            }returndiv.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})            }returnthis;        },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})            }returnthis;        },modifyTpl:function(template){if(!!template) {if(typeoftemplate =='string'){this.tpl = template;                }elseif(typeoftemplate =='function'){this.tpl = template();                }else{returnthis;                }            }this.dom =this._parseToDom(this.tpl)[0];returnthis;        },css:function(styleObj){for(varpropinstyleObj){varattr = prop.replace(/[A-Z]/g,function(word){return'-'+ word.toLowerCase();                });this.dom.style[attr] = styleObj[prop];            }returnthis;        },width:function(val){this.dom.style.width = val +'px';returnthis;        },height:function(val){this.dom.style.height = val +'px';returnthis;        },on:function(type, handler){// type: show, shown, hide, hidden, close, confirmif(typeofthis.handlers[type] ==='undefined') {this.handlers[type] = [];            }this.listeners.push(type);this.handlers[type].push(handler);returnthis;        },off:function(type, handler){if(this.handlers[type]instanceofArray) {varhandlers =this.handlers[type];for(vari =0, len = handlers.length; i < len; i++) {if(handlers[i] === handler) {break;                    }                }this.listeners.splice(i,1);                handlers.splice(i,1);returnthis;            }        },emit:function(event){if(!event.target) {                event.target =this;            }if(this.handlers[event.type]instanceofArray) {varhandlers =this.handlers[event.type];for(vari =0, len = handlers.length; i < len; i++) {                    handlers[i](event);returntrue;                }            }returnfalse;        }    }// 最后将插件对象暴露给全局对象_global = (function(){returnthis|| (0,eval)('this'); }());if(typeofmodule!=="undefined"&&module.exports) {module.exports = MyDialog;    }elseif(typeofdefine ==="function"&& define.amd) {        define(function(){returnMyDialog;});    }else{        !('MyDialog'in_global) && (_global.MyDialog = MyDialog);    }}());

然后调用的时候就可以直接使用插件的事件绑定了。

varmydialog =newMyDialog({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 adduserUsername: <帐号>Password: <密码>Email:(this IS public) <邮箱>Loggedinas <帐号> on https://registry.npmjs.org/.

上传包

$ npm publish

安装包

$ npm install mydialog

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

结论

写了这么多,比较啰嗦,我在此做一下总结:

关于如何编写出一个好的js原生插件,需要平时在使用别人的插件的同时,多查看一下api文档,了解插件的调用方式,然后再看一下插件的源码的设计方式。基本上我们可以确定大部分插件都是按照原型的方式进行设计的。而我从上面的例子中,就使用了好多js原生的知识点,函数的命名冲突、闭包、作用域,自定义工具函数扩展对象的钩子函数,以及对象的初始化、原型链继承,构造函数的定义及设计模式,还有事件的自定义,js设计模式的观察者模式等知识。这些内容还是需要初学者多多了解才能进行一些高层次一些的插件开发。

作者:绰号陆拾柒

链接:https://www.jianshu.com/p/e65c246beac1

来源:简书

简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,667评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,361评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,700评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,027评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,988评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,230评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,705评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,366评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,496评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,405评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,453评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,725评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,803评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,015评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,514评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,111评论 2 341

推荐阅读更多精彩内容

  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,047评论 1 10
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,885评论 1 4
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,316评论 0 2
  • 作为一个前端er,如果不会写一个小插件,都不好意思说自己是混前端界的。写还不能依赖jquery之类的工具库,否则装...
    绰号陆拾柒阅读 46,034评论 42 261
  • 源站:http://fengyuanchen.github.io/viewer/ 应用: html: 源码上是正常...
    羊绘霖阅读 5,475评论 0 2