用面向?qū)ο蠓庋b一個(gè)模態(tài)框插件
最近在面試,看了很多有關(guān)面向?qū)ο缶幊痰馁Y料,原來主要是為了應(yīng)付面試的饲宿,哪知道不曉得為啥突然Get到了我的點(diǎn)乖仇,越來越感興趣,便想著自己深入研究下用在工作上类缤,渣渣平時(shí)都是面向谷歌編程的,這次要換一下思路了,這兩天看了些使用面向?qū)ο髮?shí)現(xiàn)的插件的源碼扑毡,主要是bootstrap-dialog和circles,也照著這思路寫一個(gè)類似bootstrap-dialog的模態(tài)框組件盛险,比較簡單瞄摊,api也比較少,比較適合新接觸面向?qū)ο缶幊痰睦鲜郑赡懿]有)苦掘,沒多少js經(jīng)驗(yàn)的可能你得先練練基礎(chǔ)换帜,本文主要是討論下如何用面向?qū)ο蟮乃悸匪伎紝?shí)現(xiàn)以及設(shè)計(jì)api和配置。
目錄
-1.效果
-2.實(shí)現(xiàn)思路
-3.實(shí)現(xiàn)過程
-4.結(jié)果和新的問題
模態(tài)框說白了就是彈出框鹤啡,效果如下效果
可以控制彈出框的打開和關(guān)閉惯驼,修改標(biāo)題和內(nèi)容,自定義按鈕個(gè)數(shù)和相應(yīng)處理邏輯递瑰,并且自帶鉤子函數(shù)和擴(kuò)展接口祟牲。
實(shí)現(xiàn)思路
如果是以前那樣的面向過程編程的方式,肯定是定義一個(gè)html字符串然后生成一個(gè)div元素抖部,添加到頁面去了说贝,然后再綁定點(diǎn)擊事件,最后再控制下整個(gè)div的display是否為none就能打開和隱藏了慎颗,類似下面這樣乡恕;
//面向過程偽代碼
function buildDialog () {
var dom = document.createElement('div');
var html =
`<div class ='header'><div>
<div class = 'body'></div>
<div class ='footer'>
<button>確定</button><button>取消</button>
</div>
`
dom.innerHTML = html;
document.querySelector('body').append(dom);
var buttonList = document.querySelector('button')
buttonList[0].onclick = function (){
}
buttonList[1].onclick = function (){
}
}
當(dāng)然你也可以將三個(gè)部分分開言询,使用createElement生成dom,綁定事件后再添加到頁面中几颜,這是一般面向過程的方法倍试,這樣做有什么問題呢?
最大的問題就是沒法擴(kuò)展和修改:所有元素全部耦合在一起蛋哭,要改县习,基本上要考慮到對(duì)互相的影響,同時(shí)沒有留一點(diǎn)縫隙給外部的接口谆趾,無法擴(kuò)展躁愿;
讓我們看看用面向?qū)ο蟮姆绞降膶?shí)現(xiàn)思路。
實(shí)現(xiàn)過程
1.根據(jù)需要設(shè)計(jì)配置內(nèi)容:
首先先列出我們需要實(shí)現(xiàn)的api沪蓬,畢竟也是需要按照使用者的配置來生成的彤钟,配置就是一個(gè)json對(duì)象,把整個(gè)模態(tài)框的生成當(dāng)作一個(gè)對(duì)象跷叉。根據(jù)我們的了解逸雹,一般的對(duì)象都是有屬性和方法構(gòu)成的,而這個(gè)json就決定了模態(tài)框生成這個(gè)對(duì)象的對(duì)象的方法云挟,下面分析下我們需要什么屬性和方法梆砸。
需要的屬性:
1.模態(tài)框的頭部_title,主體_body园欣,
2.底部為若干個(gè)點(diǎn)擊按鈕帖世,這個(gè)又有自己的屬性和方法,所以傳入的按鈕應(yīng)該也是一個(gè)對(duì)象沸枯,屬性有顯示的文字:label日矫,點(diǎn)擊的回調(diào)函數(shù)action;
3.如果頁面上已經(jīng)有了一個(gè)div用來生成模態(tài)框绑榴,那么可以把這個(gè)div的id名傳過來ele哪轿;
需要的方法:
1.我們的模態(tài)框的生成需要有一個(gè)初始化方法,命名為_init()方法彭沼;
2.我們需要模態(tài)框的打開和收起缔逛,也就是:_open(),_close();
3.還有一些鉤子函數(shù)_hook();
以上就是基本的屬性和方法姓惑;
這樣看起來,用戶傳進(jìn)來的json配置應(yīng)該是這樣:
var option = {
'ele':'content',
'title':'提示',
'body':'我是內(nèi)容',
'buttons':[
{
label:'確定',
action:function() {
console.log('click enter')
}
},
{
label:'取消',
action:function() {
buildContent._close()
}
},
],
'onClosed':function () {
console.log('close!')
}
}
后面的onClosed是關(guān)閉模態(tài)框的鉤子函數(shù)按脚,現(xiàn)在先不管于毙,好了api寫好了,你可以跟其他人去宣傳使用了(誤)
2.根據(jù)傳入的內(nèi)容寫代碼
首先就是新建一個(gè)對(duì)象,把所有的熟悉全部賦值給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
}
}
下面那個(gè)私有方法是如果用戶沒有傳入一個(gè)div唯沮,就自己生成一個(gè)脖旱。那幾個(gè)hook的鉤子函數(shù)可以先不要在意。
好了屬性我們給對(duì)象設(shè)好了介蛉,下面就是我們的方法萌庆,方法一般是寫在原型上,這樣無論new多少次對(duì)象币旧,這個(gè)原型都不會(huì)被復(fù)制而是用的同一個(gè)践险,減少了內(nèi)存的占用,不過一般一個(gè)頁面就一個(gè)模態(tài)框吹菱,一般不會(huì)出現(xiàn)這樣的情況巍虫,但為了養(yǎng)成好習(xí)慣,先這樣寫鳍刷。
BuildDialog.prototype = {
\\可以不寫
constructor : BuildDialog,
_init(): function () {
},
_open : function () {
this._ele.style.display = 'block';
},
_close : function () {
this._ele.style.display = 'none';
},
}
好了基本的對(duì)外的api都掛上去了占遥,下面就是核心的init(),也就是生成的設(shè)計(jì)输瓜,在這里我介紹一個(gè)設(shè)計(jì)原則SOLID瓦胎,感興趣的可以上網(wǎng)搜一下,里面有一個(gè)原則就是單一職責(zé)原則尤揣,意思就是一個(gè)對(duì)象就負(fù)責(zé)做一件事搔啊,如果我們直接將整個(gè)框交給一個(gè)對(duì)象去生成和修改,那不還是回到原來那個(gè)面向過程那樣的高度耦合化的情況么芹缔。所以我們需要拆分init()坯癣,也就是簡單拆分為3個(gè)部分,頭部header最欠,主體body示罗,footer,各自有相對(duì)應(yīng)的build對(duì)象芝硬,各自的對(duì)象有自己的方法這樣組合起來成為一個(gè)總體的init()方法蚜点。
//組成頭部,其他類似
function BuildHeader(html){
//
}
每一個(gè)部分都有各自的creat()方法生成都沒拌阴,而由于我們需要支持各部分的熱修改绍绘,這就需要每一個(gè)部分都有方法直接取到生成的dom元素,當(dāng)然不是直接在頁面取迟赃,這時(shí)候還沒有添加到頁面陪拘,取不到(添加到頁面的職責(zé)在BuildDialog對(duì)象上)。然后我們需要一個(gè)方法將生產(chǎn)的dom賦值到this上纤壁,還需要一個(gè)get()方法從this上獲得這個(gè)dom左刽,各個(gè)方法互相獨(dú)立解耦,各有各的職責(zé)酌媒,這也體現(xiàn)了單一職責(zé)原則欠痴,只是公用同一個(gè)this 而已迄靠,也就是:
function BuildHeader(html){
this.creatHeader = function () {
var Dom = document.createElement('div');
Dom.innerHTML = `<span>${html}</span>`
return Dom
}
this.getHeader = function () {
return this.$hearder
}
this.setHeader = function ($html) {
this.$hearder = $html;
return this
}
}
好了這東西怎么給BuildDialog用呢?首先不能直接用喇辽,init()只是初始化掌挚,各部分的生成需要有另外的方法調(diào)用;_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對(duì)象旦委,這個(gè)對(duì)象就是為了生成header部分奇徒,下一行就是生成過程,先生成dom對(duì)象在set到this中作為一個(gè)內(nèi)部屬性供調(diào)用缨硝,之所以能夠用類似jQuery那樣的鏈?zhǔn)秸{(diào)用方法是因?yàn)槲覀冊趕et方法中最后把this這個(gè)對(duì)象return出來了摩钙,這樣就相當(dāng)于一個(gè)將前一個(gè)函數(shù)的返回值(這里入?yún)⑹莟his)作為后一個(gè)函數(shù)的入?yún)ⅲ琯et函數(shù)中就可以從this中取得當(dāng)前dom了查辩;
然后init()函數(shù)就直接這樣調(diào)用
_init : function(){
this.HeaderDom = this._BuildHeader();
this.BodyDom = this._buildBody();
this.Footer = this._buildFooter();
return this
},
這樣在_init()方法中就能按照各自的職責(zé)各自調(diào)用對(duì)應(yīng)的build方法胖笛,而對(duì)應(yīng)的方法中又調(diào)用對(duì)應(yīng)的對(duì)象,每個(gè)對(duì)象負(fù)責(zé)一部分職責(zé)宜岛,最后組合起來长踊,現(xiàn)在我們的各部分對(duì)象的生成的元素比較簡單,但后面可以相對(duì)應(yīng)添加不同的元素萍倡,比如body部分添加輸入框身弊,選擇框等復(fù)雜的表單元素組合,footer部分還需要添加多個(gè)點(diǎn)擊按鈕列敲,每一個(gè)按鈕需要綁定不同的點(diǎn)擊回調(diào)函數(shù)阱佛,這些都只在相對(duì)應(yīng)那部分職責(zé)的對(duì)象中完成。主體對(duì)象負(fù)責(zé)調(diào)用建造主體戴而,各部分負(fù)責(zé)建造各部分的細(xì)節(jié)凑术,最終合成一個(gè)整體,這有點(diǎn)像設(shè)計(jì)模式中的建造者模式所意。
好了我們現(xiàn)在就開始完成一些細(xì)節(jié)淮逊,我們知道footer部分的dom比較復(fù)雜,他是需要又若干個(gè)按鈕的扶踊,需要單獨(dú)處理泄鹏,處理的思路也是一樣的,拆分秧耗,新建一個(gè)BuildBtn命满,負(fù)責(zé)建造一個(gè)button對(duì)象,在BuildFooter對(duì)象中調(diào)用若干次生成完整的footer绣版,同樣在BuildBtn中也需要有creat胶台,get和set方法
function BuildBtn (html) {
this.creatBtn = function () {
var Dom = document.createElement('button');
Dom.innerHTML = html.label;
//用戶的action就作為點(diǎn)擊的回調(diào)函數(shù)
Dom.onclick = html.action;
return Dom
}
this.getBtn = function () {
return this.$Btn
}
this.setBtn = function ($html) {
this.$Btn= $html
return this
}
}
BuildFooter中這樣調(diào)用
function BuildFooter(html){
this.creatFooter = function () {
var Dom = document.createElement('div');
for(let item of html){
//html就是buttons這個(gè)數(shù)組
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
}
}
這樣就生成了若干個(gè)按鈕,而且點(diǎn)擊事件都在各自的對(duì)象中綁定了杂抽,init()中直接調(diào)用BuildFooter就好了诈唬,好了到這里我們的基本初始化就完成了,對(duì)比面向過程的方式寫的代碼還是比較多的缩麸,但是下一步的實(shí)現(xiàn)就方便了铸磅。
首先就是各個(gè)部分的熱修改。在BuildDialog對(duì)象中的原型中加一個(gè)updateHeader方法杭朱;
_updateHeader : function (newTitle) {
this._title = newTitle;
//內(nèi)部就已經(jīng)緩存了header這個(gè)dom對(duì)象阅仔,不需要再取了;甚至可以直接使用對(duì)象.HeaderDom.innerHTML的方式訪問修改弧械,但這樣又造成了職責(zé)的不清晰
this.HeaderDom.innerHTML = `<span>${this._title}</span>`
return this
},
其他部分可以照著寫八酒。
然后就是一些鉤子函數(shù)和拓展:
新設(shè)一個(gè)鉤子屬性:
this.hook = {};
//'||'后面是為了讓他有值,不至于調(diào)用一個(gè)underfine出錯(cuò)刃唐;
this._hook.onClosed = option.onClosed || function (){};
this._hook.onMounted = option.onMounted || function (){};this._hook.onOpened = option.onOpened || function (){}
//然后在各個(gè)需要的地方調(diào)用羞迷,比如onClosed;
_close : function () {
this._ele.style.display = 'none';
this.Modal.style.display = 'none';
this._hook.onClosed(this);
},
擴(kuò)展就簡單了画饥,用戶可以自定義某些方法和屬性:
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);
結(jié)果和新的問題
好了這樣一個(gè)模態(tài)框就實(shí)現(xiàn)了衔瓮,我們咋使用呢?在html中這樣使用就行了
<script>
window.onload = function() {
var option = {'url':'',
'ele':'content',
'title':'提示',
'body':'我是內(nèi)容',
'buttons':[
{
label:'確定',
action:function() {
console.log('click enter')
}
},
{
label:'取消',
action:function() {
buildContent._close()
}
},
],
'onClosed':function () {
console.log('close!')
}
}
var buildContent = new BuildDialog(option);
Dialog = buildContent._init();
Dialog._open();
}
</script>
第一次用面向?qū)ο蠓绞綄憱|西還是問題很多的抖甘,比如生成header和body那部分有很多代碼是一樣的热鞍,其實(shí)我們可以建立一個(gè)buildHtml這樣一個(gè)基類,然后繼承出三個(gè)部分的build方法衔彻,有興趣可以自己改下薇宠。