序
在平时中,经常会用组件来定义弹窗,因为弹窗可以用一个div元素浮在页面上。在事实上,弹窗可以简单也可以复杂,简单到只有一个标题,一个文本描述;复杂到可以和一个app一样,拥有前进后退,可以实现复杂业务逻辑。如果按照往常的思路,就会很难扩展。
在弹窗的日常使用中,主要有如下几个情况:
- 类似广告弹窗,一直挂在页面上,除非用户手动点击关闭按钮;
- 类似提示框,它会停留在页面角落几秒,然后消失。不管页面是否切换它都会在,没有手动关闭按钮;
- 类似输入窗口,它会常驻页面上,往往会有个遮罩,告诉用户要把这个窗口完成了才能做其他事情,或者取消,它的优先级很高,无法通过后退键关闭(有时候后退键代表切换页面),后退只会切换页面;
- 类似上面的情况,它可以通过用后退键关闭弹窗;
- 上面的弹窗拥有随时切换内容的功能,可能是一个弹窗,点击的时候变成一个大表单等;
- 弹窗可能会生成弹窗的功能,可以拥有没有上限的弹窗数,通过关闭最底层的弹窗来关闭所有它延伸的弹窗。
上述可能是常见的弹窗例子,还有很多非常规复杂的情况,比如用pc端弹窗模拟移动端窗口等。
在这里我把弹窗分为2类:
- 它是由App实例对象弹出来的,不依赖当前显示页面而存在,用户点击后退不会影响到它。它可以通过定时关闭,比如上面的第二种情况,也可以通过用户手动关闭,比如第一种情况。也可以是第三章情况,用户完成任务后才关闭;
- 它是由Page实例对象弹出来的,它依赖于当前页面,与history相关的,因此它是可以通过按后退键进行关闭的,上面的3,4,5情况。
弹窗都是可以更改里面的内容的,因此我们把Page页面放在弹窗里面,同时它也有不变的部分。因此,它和之前讨论的App对象是很像的,而且弹窗里面的页面切换,也是会影响整体的history。这样,弹窗就变得灵活性十足。因为Page页面可以弹出弹窗,所以也满足了弹窗弹出新弹窗的需求,而且,很容易应付除此以外的非常复杂的情况(因为弹窗对象和App对象继承于同一个对象,App对象能做的,它都能做);
需求
我们要实现拥有App对象类似功能的PopUp对象,但是它是无法单独存在的,必须依附App实例对象或Page实例对象,它是为了辅助业务开发而存在。但是它拥有管理页面、渲染页面、history对象等功能。
实现思路
我们抽象出一个ReplaceProto对象,它是主要特点就是可以切换页面。然后把App对象和PopUp对象都继承于这个对象。在基类中实现了页面的切换逻辑,页面缓存等基础操作,代码如下
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; // 关闭后的回调函数,主要是清理历史记录
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); // 转换为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);
});
});
})
});
},
// 弹窗关闭
hidden: function (option, bk) {
var that = this;
if (this.isHidden) return; // 防止多次点关闭
this.isHidden = true;
// 如果它有子弹窗,子弹窗先关闭,再关闭它, 保证关闭是一个异步操作
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(); // 弹窗内部引用清除,待垃圾回收
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方法来判断。
实际应用
与当前页面交互
可以在页面中自定义事件,弹窗通过触发自定义事件,并且传递数据进行交互。在页面上定义
this.attachDiyEvent(eventName, handler);
然后在弹窗的页面上触发, this表示当前弹窗的Page对象,parent代表着PopUp对象
this.parent.dispatchEventByName(eventName, data);
弹窗也是按需引入的,因此需要通过配置引入
{
name: "strdatePicker",
js: "/public/ui/popup/datePicker/index.js"
}
案例地址
总结
这里主要介绍了PopUp对象的原理,然而弹窗的创建是最复杂的,需要一个PopUp对象和两个Page对象,好在Page对象可以随意切换,有时候创建一个PopUp对象,可以复用不同的Page,灵活性十足,在strui框架中,也有很多不错的弹窗的案例。
推广
底层框架开源地址:https:///string-for-100w/string
演示网站: https://www./
|