用面向对象封装一个模态框插件
最近在面试,看了很多有关面向对象编程的资料,原来主要是为了应付面试的,哪知道不晓得为啥突然Get到了我的点,越来越感兴趣,便想着自己深入研究下用在工作上,渣渣平时都是面向谷歌编程的,这次要换一下思路了,这两天看了些使用面向对象实现的插件的源码,主要是bootstrap-dialog和circles,也照着这思路写一个类似bootstrap-dialog的模态框组件,比较简单,api也比较少,比较适合新接触面向对象编程的老手(可能并没有),没多少js经验的可能你得先练练基础,本文主要是讨论下如何用面向对象的思路思考实现以及设计api和配置。
目录
-1.效果
-2.实现思路
-3.实现过程
-4.结果和新的问题
模态框说白了就是弹出框,效果如下效果
可以控制弹出框的打开和关闭,修改标题和内容,自定义按钮个数和相应处理逻辑,并且自带钩子函数和扩展接口。
实现思路
如果是以前那样的面向过程编程的方式,肯定是定义一个html字符串然后生成一个div元素,添加到页面去了,然后再绑定点击事件,最后再控制下整个div的display是否为none就能打开和隐藏了,类似下面这样;
//面向过程伪代码
function buildDialog () {
var dom = document.createElement('div');
var html =
`
当然你也可以将三个部分分开,使用createElement生成dom,绑定事件后再添加到页面中,这是一般面向过程的方法,这样做有什么问题呢?
最大的问题就是没法扩展和修改:所有元素全部耦合在一起,要改,基本上要考虑到对互相的影响,同时没有留一点缝隙给外部的接口,无法扩展;
让我们看看用面向对象的方式的实现思路。
实现过程
1.根据需要设计配置内容:
首先先列出我们需要实现的api,毕竟也是需要按照使用者的配置来生成的,配置就是一个json对象,把整个模态框的生成当作一个对象。根据我们的了解,一般的对象都是有属性和方法构成的,而这个json就决定了模态框生成这个对象的对象的方法,下面分析下我们需要什么属性和方法。
需要的属性:
1.模态框的头部_title,主体_body,
2.底部为若干个点击按钮,这个又有自己的属性和方法,所以传入的按钮应该也是一个对象,属性有显示的文字:label,点击的回调函数action;
3.如果页面上已经有了一个div用来生成模态框,那么可以把这个div的id名传过来ele;
需要的方法:
1.我们的模态框的生成需要有一个初始化方法,命名为_init()方法;
2.我们需要模态框的打开和收起,也就是:_open(),_close();
3.还有一些钩子函数_hook();
以上就是基本的属性和方法;
这样看起来,用户传进来的json配置应该是这样:
var option = {
'ele':'content',
'title':'提示',
'body':'我是内容',
'buttons':[
{
label:'确定',
action:function() {
console.log('click enter')
}
},
{
label:'取消',
action:function() {
buildContent._close()
}
},
],
'onClosed':function () {
console.log('close!')
}
}
后面的onClosed是关闭模态框的钩子函数,现在先不管,好了api写好了,你可以跟其他人去宣传使用了(误)
2.根据传入的内容写代码
首先就是新建一个对象,把所有的熟悉全部赋值给this;
function BuildDialog (option) {
this._ele = document.querySelector('#' + option.ele) || addElement();
this._title = option.title;
this._body = option.body;
this._btn = option.buttons;
this._type = option.type;
this._hook = new BuildHook();
this._hook.onClosed = option.onClosed || function (){};
this._hook.onMounted = option.onMounted || function (){};
this._hook.onOpened = option.onOpened || function (){};
//私有
function addElement () {
var newElemnt = document.createElement('div')
document.querySelector('body').append(newElemnt)
return newElemnt
}
}
下面那个私有方法是如果用户没有传入一个div,就自己生成一个。那几个hook的钩子函数可以先不要在意。
好了属性我们给对象设好了,下面就是我们的方法,方法一般是写在原型上,这样无论new多少次对象,这个原型都不会被复制而是用的同一个,减少了内存的占用,不过一般一个页面就一个模态框,一般不会出现这样的情况,但为了养成好习惯,先这样写。
BuildDialog.prototype = {
\\可以不写
constructor : BuildDialog,
_init(): function () {
},
_open : function () {
this._ele.style.display = 'block';
},
_close : function () {
this._ele.style.display = 'none';
},
}
好了基本的对外的api都挂上去了,下面就是核心的init(),也就是生成的设计,在这里我介绍一个设计原则SOLID,感兴趣的可以上网搜一下,里面有一个原则就是单一职责原则,意思就是一个对象就负责做一件事,如果我们直接将整个框交给一个对象去生成和修改,那不还是回到原来那个面向过程那样的高度耦合化的情况么。所以我们需要拆分init(),也就是简单拆分为3个部分,头部header,主体body,footer,各自有相对应的build对象,各自的对象有自己的方法这样组合起来成为一个总体的init()方法。
//组成头部,其他类似
function BuildHeader(html){
//
}
每一个部分都有各自的creat()方法生成都没,而由于我们需要支持各部分的热修改,这就需要每一个部分都有方法直接取到生成的dom元素,当然不是直接在页面取,这时候还没有添加到页面,取不到(添加到页面的职责在BuildDialog对象上)。然后我们需要一个方法将生产的dom赋值到this上,还需要一个get()方法从this上获得这个dom,各个方法互相独立解耦,各有各的职责,这也体现了单一职责原则,只是公用同一个this 而已,也就是:
function BuildHeader(html){
this.creatHeader = function () {
var Dom = document.createElement('div');
Dom.innerHTML = `${html}`
return Dom
}
this.getHeader = function () {
return this.$hearder
}
this.setHeader = function ($html) {
this.$hearder = $html;
return this
}
}
好了这东西怎么给BuildDialog用呢?首先不能直接用,init()只是初始化,各部分的生成需要有另外的方法调用;_BuildHeader,_buildBody,_buildBody
各自的build方法是这样的:
_BuildHeader : function () {
var html = this._title;
var initHeader = new BuildHeader(html);
var HeaderDom = initHeader.setHeader(initHeader.creatHeader(html)).getHeader();
this._ele.append(HeaderDom)
return HeaderDom
},
讲解一下,取得title传给BuildHeader对象,这个对象就是为了生成header部分,下一行就是生成过程,先生成dom对象在set到this中作为一个内部属性供调用,之所以能够用类似jQuery那样的链式调用方法是因为我们在set方法中最后把this这个对象return出来了,这样就相当于一个将前一个函数的返回值(这里入参是this)作为后一个函数的入参,get函数中就可以从this中取得当前dom了;
然后init()函数就直接这样调用
_init : function(){
this.HeaderDom = this._BuildHeader();
this.BodyDom = this._buildBody();
this.Footer = this._buildFooter();
return this
},
这样在_init()方法中就能按照各自的职责各自调用对应的build方法,而对应的方法中又调用对应的对象,每个对象负责一部分职责,最后组合起来,现在我们的各部分对象的生成的元素比较简单,但后面可以相对应添加不同的元素,比如body部分添加输入框,选择框等复杂的表单元素组合,footer部分还需要添加多个点击按钮,每一个按钮需要绑定不同的点击回调函数,这些都只在相对应那部分职责的对象中完成。主体对象负责调用建造主体,各部分负责建造各部分的细节,最终合成一个整体,这有点像设计模式中的建造者模式。
好了我们现在就开始完成一些细节,我们知道footer部分的dom比较复杂,他是需要又若干个按钮的,需要单独处理,处理的思路也是一样的,拆分,新建一个BuildBtn,负责建造一个button对象,在BuildFooter对象中调用若干次生成完整的footer,同样在BuildBtn中也需要有creat,get和set方法
function BuildBtn (html) {
this.creatBtn = function () {
var Dom = document.createElement('button');
Dom.innerHTML = html.label;
//用户的action就作为点击的回调函数
Dom.onclick = html.action;
return Dom
}
this.getBtn = function () {
return this.$Btn
}
this.setBtn = function ($html) {
this.$Btn= $html
return this
}
}
BuildFooter中这样调用
function BuildFooter(html){
this.creatFooter = function () {
var Dom = document.createElement('div');
for(let item of html){
//html就是buttons这个数组
var initBtn = new BuildBtn(item);
var btnHtml = initBtn.setBtn(initBtn.creatBtn(item)).getBtn()
Dom.append(btnHtml);
}
return Dom
}
this.getFooter = function () {
return this.$Footer
}
this.setFooter = function ($html) {
this.$Footer = $html;
return this
}
}
这样就生成了若干个按钮,而且点击事件都在各自的对象中绑定了,init()中直接调用BuildFooter就好了,好了到这里我们的基本初始化就完成了,对比面向过程的方式写的代码还是比较多的,但是下一步的实现就方便了。
首先就是各个部分的热修改。在BuildDialog对象中的原型中加一个updateHeader方法;
_updateHeader : function (newTitle) {
this._title = newTitle;
//内部就已经缓存了header这个dom对象,不需要再取了;甚至可以直接使用对象.HeaderDom.innerHTML的方式访问修改,但这样又造成了职责的不清晰
this.HeaderDom.innerHTML = `${this._title}`
return this
},
其他部分可以照着写。
然后就是一些钩子函数和拓展:
新设一个钩子属性:
this.hook = {};
//'||'后面是为了让他有值,不至于调用一个underfine出错;
this._hook.onClosed = option.onClosed || function (){};
this._hook.onMounted = option.onMounted || function (){};this._hook.onOpened = option.onOpened || function (){}
//然后在各个需要的地方调用,比如onClosed;
_close : function () {
this._ele.style.display = 'none';
this.Modal.style.display = 'none';
this._hook.onClosed(this);
},
扩展就简单了,用户可以自定义某些方法和属性:
BuildDialog.prototype.addMethod = function (name, fn) {
this.prototype[name] = fn;
return this;
}
BuildDialog.prototype.addProp = function (name, prop) {
this.prototype[name] = prop;
return this;
}
//用户可以这样用
var newFn = function () {}
BuildDialog.addMethod('newFn', newFn);
BuildDialog.addProp('newProp', 'newProp');
var Dialog = new BuildDialog();
Dialog.newFn();
console.log(Dialog.newProp);
结果和新的问题
好了这样一个模态框就实现了,我们咋使用呢?在html中这样使用就行了
第一次用面向对象方式写东西还是问题很多的,比如生成header和body那部分有很多代码是一样的,其实我们可以建立一个buildHtml这样一个基类,然后继承出三个部分的build方法,有兴趣可以自己改下。
`
dom.innerHTML = html;
document.querySelector('body').append(dom);
var buttonList = document.querySelector('button')
buttonList[0].onclick = function (){
}
buttonList[1].onclick = function (){
}
}