最近在做一个基于zepto的h5项目,整个技术栈比较老旧,因此需要自己动手实现模态窗。现在已经是react、vue这些mvvm框架的天下了,请不要嘲笑我2018年还在使用zepto开发,我也是很无奈呀~。不过无论这些轮子框架发展到多么先进的地步,基础知识还是需要熟悉的嘛。本篇总结会介绍下模态窗的概念以及如何实现。
什么是模态(modal)
不知道大家对模态这个词怎么看,反正我最开始看到是一脸懵逼,不知道是什么意思,只是隐约觉得这种弹窗后面一般都有一个灰色蒙层。我是个喜欢追根溯源的人,一般碰到这种情况都会很纠结,非的弄明白为什么叫模态窗。
后来查了下,发现模态窗这个词是软件设计领域的词汇。咱们可以先看看普通的软件使用流程,在使用软件的时候,我们一般都会按照自己的思路以及软件内在的逻辑一步步操作,比如我们在使用一个购物系统,我们会按照我们对这个软件的固有理解来执行自己脑中的流程:选购商品,加入购物车,下单付款等等。这些流程可以说是我们使用软件时的一种正常操作状态。
而模态其实来源于单词modal,表示一种特定模式的状态,那么是什么特定模式呢?其实是一种阻塞正常操作的模式。模态窗弹出时会使主窗口变的不可用,只有在模态窗内的操作完成后才能关闭模态窗并返回正常主窗口。关机模态窗更加详细的介绍可以参考维基百科Modal window。
cssfixed绝对定位
这里再介绍下css中的绝对定位。为什么要介绍这个概念呢?因为我要实现的模态窗需要保证水平&垂直同时居中(大部分简单的模态窗都是水平、垂直居中的),其实水平&垂直同时居中的模态窗的实现很简单,就是利用css的fixed绝对定位来实现的。
css中position包括absolute、fixed、relative以及static等定位方式,这里我们只介绍fixed定位方式,其他的定位方式跟这篇总结的主题没关系,不过多讨论。
其实css中fixed定位是相对浏览器视口,即view point来定位。浏览器视口其实就是浏览器抛掉了操作栏、边框后的窗口区域,如下图红色区域所示:

因此我们只需要将
背景蒙层利用fixed定位设置为完全覆盖浏览器视口,模态窗利用fixed定位将弹窗进行居中定位即可:
.modal-layer {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
visibility: hidden;
transform: scale(1.1);
transition: opacity 0.25s 0s, transform 0.25s;
}
.modal-layer.show {
visibility: visible;
transform: scale(1.0);
transition: opacity 0.25s 0s, transform 0.25s;
}
.modal-dialog-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 0.5rem;
}
注意我们是通过show这个className来控制模态窗以及背景蒙层的展示与隐藏。
实现
由于我们是基于原生js来实现模态窗,因此我们最好将弹窗封装称为一套工具函数:
- init,用来初始化
模态窗; - destroy,用来在页面退出时销毁
模态窗; - popup,弹出
模态窗; - close,关闭
模态窗。
function importFromBelow() {
/**
*
* @param {object} options init options
* @param {string} options.id dialog id
* @param {string} options.contentHtml dialog content html
* @param {boolean} options.closeWhenClickLayer whether close dialog when click layer
*/
function init(options) {
const {id: dialogId, contentHtml, closeWhenClickLayer = true} = options;
const layerEle = document.createElement('div');
layerEle.setAttribute('class', 'modal-layer');
layerEle.setAttribute('id', dialogId);
const contentContainerEle = document.createElement('div');
contentContainerEle.setAttribute('class', 'modal-dialog-container');
contentContainerEle.innerHTML = contentHtml;
layerEle.appendChild(contentContainerEle);
document.body.appendChild(layerEle);
if (closeWhenClickLayer) {
document.onclick = function (e) {
if (e.target === layerEle) {
close(dialogId);
}
}
}
}
/**
* destroy modal dialog
* @param {string} dialogId dialog id
*/
function destroy(dialogId) {
const dialogEle = document.querySelector(`#${dialogId}`);
dialogEle.parentNode.removeChild(dialogEle);
}
function popup(dialogId) {
const dialogEle = document.querySelector(`#${dialogId}`);
dialogEle.classList.add('show');
}
function close(dialogId) {
const dialogEle = document.querySelector(`#${dialogId}`);
dialogEle.classList.remove('show');
}
return {init, destroy, popup, close};
}
实现完成这套模态窗工具函数后我们可以这样使用:
const dialogUtil = importFromBelow();
const DIALOG_ID = 'my-modal-dialog';
const contentHtml = `
<div class="modal-dialog-content" id="modal-dialog-content">
<div>这里是弹窗内容</div>
<div>
<button id="dialog-close-button">close modal dialog</button>
</div>
</div
`;
dialogUtil.init({
id: DIALOG_ID,
contentHtml,
// closeWhenClickLayer: false
});
const closeButtonEle = document.querySelector('#dialog-close-button');
closeButtonEle.onclick = function (e) {
dialogUtil.close(DIALOG_ID);
};
const triggerButton = document.querySelector('#trigger-button');
triggerButton.onclick = function (e) {
dialogUtil.popup(DIALOG_ID);
};
现在让我们看下效果:

完整的实现代码见这里。