序
在平時中,經(jīng)常會用組件來定義彈窗,因為彈窗可以用一個div元素浮在頁面上。在事實上,彈窗可以簡單也可以復(fù)雜,簡單到只有一個標題,一個文本描述;復(fù)雜到可以和一個app一樣,擁有前進后退,可以實現(xiàn)復(fù)雜業(yè)務(wù)邏輯。如果按照往常的思路,就會很難擴展。
在彈窗的日常使用中,主要有如下幾個情況:
- 類似廣告彈窗,一直掛在頁面上,除非用戶手動點擊關(guān)閉按鈕;
- 類似提示框,它會停留在頁面角落幾秒,然后消失。不管頁面是否切換它都會在,沒有手動關(guān)閉按鈕;
- 類似輸入窗口,它會常駐頁面上,往往會有個遮罩,告訴用戶要把這個窗口完成了才能做其他事情,或者取消,它的優(yōu)先級很高,無法通過后退鍵關(guān)閉(有時候后退鍵代表切換頁面),后退只會切換頁面;
- 類似上面的情況,它可以通過用后退鍵關(guān)閉彈窗;
- 上面的彈窗擁有隨時切換內(nèi)容的功能,可能是一個彈窗,點擊的時候變成一個大表單等;
- 彈窗可能會生成彈窗的功能,可以擁有沒有上限的彈窗數(shù),通過關(guān)閉最底層的彈窗來關(guān)閉所有它延伸的彈窗。
上述可能是常見的彈窗例子,還有很多非常規(guī)復(fù)雜的情況,比如用pc端彈窗模擬移動端窗口等。
在這里我把彈窗分為2類:
- 它是由App實例對象彈出來的,不依賴當前顯示頁面而存在,用戶點擊后退不會影響到它。它可以通過定時關(guān)閉,比如上面的第二種情況,也可以通過用戶手動關(guān)閉,比如第一種情況。也可以是第三章情況,用戶完成任務(wù)后才關(guān)閉;
- 它是由Page實例對象彈出來的,它依賴于當前頁面,與history相關(guān)的,因此它是可以通過按后退鍵進行關(guān)閉的,上面的3,4,5情況。
彈窗都是可以更改里面的內(nèi)容的,因此我們把Page頁面放在彈窗里面,同時它也有不變的部分。因此,它和之前討論的App對象是很像的,而且彈窗里面的頁面切換,也是會影響整體的history。這樣,彈窗就變得靈活性十足。因為Page頁面可以彈出彈窗,所以也滿足了彈窗彈出新彈窗的需求,而且,很容易應(yīng)付除此以外的非常復(fù)雜的情況(因為彈窗對象和App對象繼承于同一個對象,App對象能做的,它都能做);
需求
我們要實現(xiàn)擁有App對象類似功能的PopUp對象,但是它是無法單獨存在的,必須依附App實例對象或Page實例對象,它是為了輔助業(yè)務(wù)開發(fā)而存在。但是它擁有管理頁面、渲染頁面、history對象等功能。
實現(xiàn)思路
我們抽象出一個ReplaceProto對象,它是主要特點就是可以切換頁面。然后把App對象和PopUp對象都繼承于這個對象。在基類中實現(xiàn)了頁面的切換邏輯,頁面緩存等基礎(chǔ)操作,代碼如下
function ReplaceProto(name, staticName, currentName) {
BaseProto.call(this);
this.name = name;
this.history = null; // 無論App還是PopUp都是與History掛鉤
this.options = {};
this.currentPage = null; // 當前顯示的Page對象
this.staticPage = null; // 布局Page對象
this.changeArea = null;
this.data = {};
// 其它屬性
}
// 主要方法
ReplaceProto.prototype = create(BaseProto.prototype, {
// 渲染頁面
_show: function (bk) {
var app = this._getApp(), that = this, len = 2;
function feeback() {
// 保證是個異步的過程
requestAnimationFrame(function () {
bk(that.staticPage, that.currentPage, app)
});
}
[this.staticName, this.currentName].forEach(function (name, index) {
app.getPageByName(name, function (outPage, opt) {
var page = new outPage();
if (index == 0) that.staticPage = page;
else that.currentPage = page;
page.baseUrl = getBaseUrl(opt.js);
for (var key in opt) {
if (["title", "js", "name", "url"].indexOf(key) === -1)
page.data[key] = opt[key];
}
if (--len === 0) feeback();
})
}))
},
// 切換頁面
render: function (pagename, isReplace, option) {
if (this.isRender) return false; // 防止多次渲染
this.isRender = true;
var currentPage = this.currentPage, that = this;
if (currentPage.popUp) {
currentPage.popUp.hidden(null, function () {
that._render(pagename, isReplace, option,
that._renderComplete.bind(that));
})
}
else {
this._render(pagename, isReplace, option, that._renderComplete.bind(this));
}
},
});
由此定義一個PopUp對象,代碼如下
function PopUp(name, staticName, currentName) {
ReplaceProto.call(this, name, staticName, currentName);
this.history = new HistoryStorage("popup"); // 歷史記錄
this.isShow = false; // 是否已彈出,防止多次彈出
this.hideBack = null; // 關(guān)閉后的回調(diào)函數(shù),主要是清理歷史記錄
this.popDiv = document.createElement("div"); // 彈窗的包圍容器
this.relativeDom = null;
this.showTarget = null; // 是由哪個目標彈出來的
};
// PopUp的主要方法
PopUp.prototype = create(ReplaceProto.prototype, {
constructor: PopUp,
// 顯示彈窗
show: function (dom, config, target, isDismisBeforeShow) {
var that = this, popDiv = this.popDiv
this.relativeDom = dom;
this.parent = target;
this._show(function (staticPage, currentPage, app) {
if (target.constructor === Page) {
app.GlobalHistory.addPopUp(that); // 轉(zhuǎn)換為Popup的歷史記錄
}
staticPage.parent = that;
currentPage.parent = that;
that.isShow = true;
dom.parentNode.appendChild(popDiv);
staticPage.render(function (html) {
staticPage.initialize(popDiv, html, null, function () {
that.changeArea = staticPage.domList.pageContainer || popDiv;
currentPage.render(function (htmlstr) {
if (target.constructor === Page)
that.history.replaceState(currentPage, config);
currentPage.initialize(changeDom, htmlstr);
});
});
})
});
},
// 彈窗關(guān)閉
hidden: function (option, bk) {
var that = this;
if (this.isHidden) return; // 防止多次點關(guān)閉
this.isHidden = true;
// 如果它有子彈窗,子彈窗先關(guān)閉,再關(guān)閉它, 保證關(guān)閉是一個異步操作
if (this.currentPage.popUp) {
this.currentPage.popUp.hidden(null, function () {
requestAnimationFrame(function () {
that._hidden(option, bk);
});
});
}
else {
requestAnimationFrame(function () {
that._hidden(option, bk);
});
}
},
});
因為彈窗只能由Page實例對象和App對象彈出,它們的處理方式不一樣的,代碼如下
-
Page的showPopUp方法
showPopUp: function (popupName, data, isDismisBeforeShow, bk) {
data = data || {};
if (this.isShowPop) { // 防止一個頁面點出多個彈窗
return false;
}
this.isShowPop = true;
var app = this._getApp(), that = this;
// 只能通過當前的currentPage彈出
if (this.parent.currentPage !== this || app.isLock) {
this.isShowPop = false;
return false;
}
app.getPopUpByName(popupName, function (popup) {
var popUp = new popup(data.resetConfig);
that._showPopUp(app, popUp, data, isDismisBeforeShow, bk);
});
return true;
},
_showPopUp: function (app, popUp, data, isDismisBeforeShow, bk) {
var that = this;
this.isShowPop = false;
popUp.data = data;
if (this.popUp) {
this.popUp.hidden(false, hiddenBack);
} else {
hiddenBack();
}
function hiddenBack() {
that.popUp = popUp;
if (popUp.show(app.changeArea || app.staticPage.domList.pageContainer,
data.in, that, isDismisBeforeShow)) {
if (typeof bk === "function") bk(popUp);
popUp.hideBack = function (bk) {
app.removePopUpHistory(bk); // 歷史記錄清除
that.popUp.destroy(); // 彈窗內(nèi)部引用清除,待垃圾回收
that.popUp = null; // 引用彈窗清除
}
}
}
},
-
App的showPopUp方法
showPopUp: function (popupName, data, isBack, isDismisBeforeShow, bk) {
var that = this, data = data || {};
this.getPopUpByName(popupName, function (popup) {
var popUp = new popup(data.resetConfig);
that._showPopUp(popUp, data, isBack, isDismisBeforeShow, bk);
});
return true;
},
_showPopUp: function (popUp, data, isBack, isDismisBeforeShow, bk) {
var that = this;
popUp.data = data;
if (popUp.show(this.changeArea || this.staticPage.domList.pageContainer,
data, this, isDismisBeforeShow)) {
if (typeof bk === "function") bk(popUp);
// 彈窗列表中添加
this.showPopups.push({
back: isBack,
popUp: popUp
});
popUp.hideBack = function (bk) {
popUp.destroy();
for (var i = 0; i < that.showPopups.length; i++) {
if (popUp === that.showPopups[i].popUp) that.showPopups.splice(i, 1);
}
if (typeof bk === "function") bk();
}
}
}
Page實例是否在PopUp中,可以通過isInPopUp方法來判斷。
實際應(yīng)用
與當前頁面交互
可以在頁面中自定義事件,彈窗通過觸發(fā)自定義事件,并且傳遞數(shù)據(jù)進行交互。在頁面上定義
this.attachDiyEvent(eventName, handler);
然后在彈窗的頁面上觸發(fā), this表示當前彈窗的Page對象,parent代表著PopUp對象
this.parent.dispatchEventByName(eventName, data);
彈窗也是按需引入的,因此需要通過配置引入
{
name: "strdatePicker",
js: "/public/ui/popup/datePicker/index.js"
}
案例地址
總結(jié)
這里主要介紹了PopUp對象的原理,然而彈窗的創(chuàng)建是最復(fù)雜的,需要一個PopUp對象和兩個Page對象,好在Page對象可以隨意切換,有時候創(chuàng)建一個PopUp對象,可以復(fù)用不同的Page,靈活性十足,在strui框架中,也有很多不錯的彈窗的案例。
推廣
底層框架開源地址:https:///string-for-100w/string
演示網(wǎng)站: https://www./
|