我们知道,设计一下插件,参数或者其逻辑肯定不是写死的,我们得像函数一样,得让用户提供自己的参数去实现用户的需求。则我们的插件需要提供一个修改默认参数的入口。
如上面我们说的修改默认参数,实际上也是插件给我们提供的一个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的链式调用如下:
$().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实现组件的交互性。
现有一个这样的需求:
实现一个弹层,此弹层可以显示一些文字提示性的信息;
弹层右上角必须有一个关闭按扭,点击之后弹层消失;
弹层底部必有一个“确定”按扭,然后根据需求,可以配置多一个“取消”按扭;
点击“确定”按扭之后,可以触发一个事件;
点击关闭/“取消”按扭后,可以触发一个事件。
根据需求,我们先写出dom结构:
写出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) { // 将字符串转为domvar 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);
}
}());
到这一步,我们的插件已经达到了基础需求了。我们可以在页面这样调用:
插件的监听
弹出框插件我们已经实现了基本的显示与隐藏的功能。不过我们在怎么时候弹出,弹出之后可能进行一些操作,实际上还是需要进行一些可控的操作。就好像我们进行事件绑定一样,只有用户点击了按扭,才响应具体的事件。那么,我们的插件,应该也要像事件绑定一样,只有执行了某些操作的时候,调用相应的事件响应。
这种js的设计模式,被称为 订阅/发布模式,也被叫做 观察者模式。我们插件中的也需要用到观察者模式,比如,在打开弹窗之前,我们需要先进行弹窗的内容更新,执行一些判断逻辑等,然后执行完成之后才显示出弹窗。在关闭弹窗之后,我们需要执行关闭之后的一些逻辑,处理业务等。这时候我们需要像平时绑定事件一样,给插件做一些“事件”绑定回调方法。
我们jquery对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) { // 将字符串转为domvar 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, confirmif(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设计模式的观察者模式等知识。这些内容还是需要初学者多多了解才能进行一些高层次一些的插件开发。
【版权与免责声明】如发现内容存在版权问题,烦请提供相关信息发邮件至 lnkj@3173.top ,我们将及时沟通与处理。 本站内容除了3117站长服务平台( www.3117.cn )原创外,其它均为网友转载内容,涉及言论、版权与本站无关。