工作记录-前端——contentWindow的使用及手写elementUI的四种弹窗
创作场景阅读前提contentWindow详解ready方法onload方法例子说明ready和onload的区别 四种弹窗的实现
创作场景
因公司项目时间较长且比较古老,准备对所有的弹窗进行升级,样式按照elementUI的四种提示框,且效果与其保持相同:鼠标悬浮不消失,鼠标离开4秒后自动消失,点击关闭图标弹窗消失。过程中也遇到了很多的问题,其中最烦人的就是如果弹窗提示结束后,页面进行了跳转(也就是iframe进行了变换),绑定在弹窗上的所有DOM事件就没了,因为刷新会冲刷掉所有的dom元素事件,在此记录并分享一下源码。
阅读前提
本文是对elementUI的弹窗进行仿写,使用纯js和jQuery方式,在实际项目中运行良好。主要分享的知识点为contentWindow
,重点还是如何在页面跳转时弹窗还能正常展示的思路
,因为页面跳转意味着页面会进行刷新,如何在刷新后依旧保持DOM和DOM上的事件
。
contentWindow详解
contentWindow对象是一个只读对象,说白了就是一个Window对象,一般页面上是有Window对象的,为什么还要有这么一个对象,它是怎么获取的?又是干什么的呢?
这个对象一般在有多个iframe时使用,作用就是你可以通过一个DOM对象获取当前iframe的Window对象,那Window对象上有document元素,这里面有所有的DOM元素,你就可以获取iframe中的元素进而操作了。
<body><script src="https://code.jquery.com/jquery-3.1.1.min.js"></script><iframe name="iframe" src="test111.html" width="200px" height="200px"></iframe><script> var iframe = $("iframe") console.log("iframe", iframe.get(0).contentWindow)</script></body>
上面是一个很简单的例子,表明了contentWindow确实是一个Window对象,有了这个Window对象就可以干很多事情了,你可以拿到这个iframe的URL,获取URL的参数啊等等,我们就来个最简单的,改一下iframe框中元素的样式。
var iframeList1 = $("iframe") var framesList = window.frames $("#button1").bind('click', function () { var iframe; $.each(iframeList1, function (i, v) { if (v.name === "iframe1") { iframe = v; return false } }) var div = iframe.contentWindow.$("#test111") console.log("button1点击:更改颜色为红色", div.css("background", "red")) }) $("#button2").bind('click', function () { // 很明确第一个就是iframe1,所以不用for循环了 var div = framesList[0].$("#test111") console.log("button2点击:更改颜色为蓝色", div.css("background", "blue")) })
上面列举了两种方式,给大家一一解读一下。
首先第一种就是我们的contentWindow对象
,使用jQuery选择器获取页面所有的iframe(注意,这种方式只能获取第一层的iframe,如果是iframe中嵌套的iframe则无法获取
),之后通过遍历找到我们对应name的iframe,然后上面的contentWindow属性就可以拿来用啦。
第二种方式是官方的一种方式,Window对象中有一个属性是frames
,它能返回页面中所有的iframe对象
(同样,仅限第一层,有点漏洞),是一个数组
,而这个数组中其实就是每个iframe对应的Window对象
,我们可以直接调用$方法查找元素,相对方便一些。
扩展:如果想要达到获取所有的iframe,包括iframe中嵌套的,简单写一个递归函数就可以实现。
var iframeListAll = [] function getIframe (windowParam) { var w; w = windowParam ? windowParam : window var frames = w.frames if (frames && frames.length > 0) { // ES5写法,使用apply函数进行数组合并 // ES6写法:iframeListAll.push(... frames) iframeListAll.push.apply(iframeListAll, frames) for (var i = 0; i < frames.length; i++) { getIframe(frames[i]) } } } $("#button3").bind('click', function () { getIframe() console.log("all", iframeListAll) })
如果对于apply函数不理解的可以看我的另一篇博客。
JS高级使用1.0——this的使用以及函数apply()和call()的解释
细心的伙伴可能发现了一个问题,为什么不直接在script中执行函数呢?一般这种操作都需要在初始化的时候执行,没毛病,这里再跟大家分享两个知识点,jQuery的ready
和Window的onload
方法。
ready方法
jQuery的ready方法就是让我们在页面加载完成时执行某些操作
,我们习惯将初始化操作写在ready方法中,官方定义为:
ready方法表示文档结构已经加载完成
(不包含图片等非文字媒体文件),文档结构也就是DOM的元素已经挂载到DOM树,在页面上啦,同时这个函数可调用多次,其中的操作依次执行。
举例说明:
// 1$().ready(function () { })// 2$(document).ready(function () {})// 3$(function () { })
个人还是比较喜欢第二种,虽然麻烦点,但可读性确实很高。
onload方法
这种是js中自带的方法,有时候是一定要用这个方法的,首先来看官方定义:
load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。
简单来说,就是不管你干什么了,这个事件一定是在所有操作结束了之后才会执行,就像小时候一定要等作业写完在吃饭一样,不然不得劲。下面说两种写法:
// 1window.addEventListener("load", (event) => { console.log("page is fully loaded");});// 2window.onload = (event) => { console.log("page is fully loaded");};
个人喜欢第一种,还是那句话,可读性比较高,就是麻烦点。
例子说明ready和onload的区别
继续回到我们的第一个例子,在页面初始化的时候就获取所有的iframe元素,开整。
$(document).ready(function () { $("#button3").css("background", "ready") getIframe() console.log("ready方法", iframeListAll) // 因为ready方法先执行,执行完成后清空数组,不然会重复 iframeListAll = [] }) window.onload = function () { getIframe() console.log("onload方法", iframeListAll) }
我们页面上一共有3个iframe,其中一个是在iframe里面的嵌套iframe,如果用ready方法只能获取到两个,而onload可以获取到全部,这是为什么呢?
还是最开始的定义,ready只管你的DOM元素挂载
,试想,把iframe当做一个DOM元素,事实上它已经挂载完毕了,可是人家自己还能渲染啊,本身就是一个页面,你ready管不了我自己怎么弄吧,而onload则是要等所有的事情干完,包括iframe内部的渲染
。
大家不要有一个误区,觉得onload会在异步请求结束后执行,那都算页面加载结束后执行的操作了。
四种弹窗的实现
MsgUtil = {// 新弹窗绑定事件bindAlertMsgEvent: function (alertDom, duration) {// 重新获取弹窗元素alertDom = $(".xxx-message")// 添加弹窗整体hover事件alertDom.unbind('mouseover').bind('mouseover', function () {if (alertMessageTimeOut) {window.clearTimeout(alertMessageTimeOut)}})alertDom.unbind('mouseout').bind('mouseout', function () {alertMessageTimeOut = window.setTimeout(function () {alertDom.css("opacity", "0").css("top", "0")}, duration)})// 添加关闭图标hover事件alertDom.children(".xxx-message-close-icon").hover(function () {$(this).css('color', '#909399');},function () {$(this).css('color', '#C0C4CC');})// 关闭图标添加点击事件alertDom.children(".xxx-message-close-icon").bind('click', function () {alertDom.css("opacity", "0").css("top", "0")})// 动画结束后移除元素alertDom.unbind('transitionend').bind('transitionend', function () {if (alertDom.css('opacity') == 0) {alertDom.remove()}})// 警告和失败提示时间边长if (alertDom.hasClass("error") || alertDom.hasClass("warning")) {var message = alertDom.children(".xxx-message-content").text()if (message.length > 20) {duration = (Math.ceil(message.length / 20) - 1) * 1000 + duration}}// 自动消失alertMessageTimeOut = window.setTimeout(function () {alertDom.css("opacity", "0").css("top", "0")}, duration)},}// ------------------------------------------------------------------------------------------------ 新版弹窗/** 创建基础的弹窗html */function createBaseAlertMsgHtml() {// 目前效果是两个图标与第一行文字对齐,如果要改为垂直居中,将外层div添加align-items: center;关闭图标的top值改为50%var html ='<div class="xxx-message" style="min-width: 220px; left: 50%; top: -20px; display: flex; display: -webkit-flex;' +'transform: translateX(-50%); padding: 12px; z-index: 99999999999999; overflow:hidden; opacity: 0;' +'line-height: 1.6; position: fixed; border:1px solid; border-radius: 4px;' +'transition: opacity .3s ease-in-out, transform .4s linear , top .4s linear ;">' +'<span class="xxx-message-type-icon iconfont-pc" style="font-size: 18px; position: absolute; top: 9px"></span>' +'<p class="xxx-message-content" style="font-size: 14px; text-align: left; padding: 0; margin: 0 20px 0 30px;"></p>' +'<span class="xxx-message-close-icon iconfont-pc" ' +'style="font-size: 14px; color: #C0C4CC; text-align: left; padding: 0;position: absolute;top: 24px;right: 15px;transform: translateY(-50%);cursor: pointer;"></span>' +'</div>'return html}/** 检测是否存在旧弹窗 */function checkAlertMsg(body) {var flag = truevar oldTip = body.children(".xxx-message")// 移除之前的弹窗if (oldTip.length > 0) {// 这种情况可能是弹窗因bug未移除if (oldTip.css("opacity") == 0) {oldTip.remove()} else {// 这种情况为弹窗依旧存在,前一个提示未消失flag = false}}return flag}var topIframe = parent.$("iframe")/** * 获取最上级的iframeContent元素 * @param frame * @param parentNode * @returns {*} */function getFrameTop(frame, parentNode) {if (frame.attr('id') !== "iframeContent") {if (parentNode) {frame = parentNode.parent.$("iframe")} else {frame = parent.$("iframe")}getFrameTop(frame, parent)} else {topIframe = framereturn frame}}/** * 通过类型参数调用提示框,也可以直接调用四种提示框 * 已兼容单模块页面(页面不存在面包屑等组件) * @param message提示的文字 * @param type弹窗类型:success成功、error失败、waring警告、info信息 * @param duration弹窗自动消失时间,不传则默认3秒 */function alertMsgByType(message, type, duration) {if ($.isBlank(message)) return;if ($.isBlank(type)) type = "info"var singleFlag = falsetry {getFrameTop($("iframe"))} catch (e) {singleFlag = trueconsole.warn("单模块页面")}var body;try {body = $(topIframe).parents(".layui-layout")} catch (e) {console.warn("单模块页面")}body = $(body)if (singleFlag) body = $("body")if (!checkAlertMsg(body)) {return}var baseHtml = createBaseAlertMsgHtml();if (!duration) duration = 4000body.append(baseHtml)var alertDom = body.children('.xxx-message')// 添加图标和修改背景颜色等switch (type) {case "success":alertDom.css("background-color", "#f0f9eb").css("border-color", "#e1f3d8")alertDom.children(".xxx-message-type-icon").html("").css("color", "#67C23A")alertDom.children(".xxx-message-content").css("color", "#67C23A")alertDom.addClass("success")break;case "error":alertDom.css("background-color", "#fef0f0").css("border-color", "#fde2e2")alertDom.children(".xxx-message-type-icon").html("").css("color", "#F56C6C")alertDom.children(".xxx-message-content").css("color", "#F56C6C")alertDom.addClass("error")break;case "warning":alertDom.css("background-color", "#fdf6ec").css("border-color", "#faecd8")alertDom.children(".xxx-message-type-icon").html("").css("color", "#E6A23C")alertDom.children(".xxx-message-content").css("color", "#E6A23C")alertDom.addClass("warning")break;case "info":alertDom.css("background-color", "#edf2fc").css("border-color", "#EBEEF5")alertDom.children(".xxx-message-type-icon").html("").css("color", "#909399")alertDom.children(".xxx-message-content").css("color", "#909399")alertDom.addClass("info")break;}// 添加提示语alertDom.children(".xxx-message-content").text(message)// 使用父级对象绑定事件,防止弹窗后跳转页面弹窗不消失// 通过topIframe.get(0).contentWindow找到顶级iframe的window对象,在通过parent找到祖宗页面的window对象if (!singleFlag && topIframe.get(0).contentWindow.parent.MsgUtil.hasOwnProperty("bindAlertMsgEvent")) {// 兼容写法topIframe.get(0).contentWindow.parent.MsgUtil.bindAlertMsgEvent(alertDom, duration)} else {MsgUtil.bindAlertMsgEvent(alertDom, duration)}// 展示弹窗alertDom.css("opacity", "1").css("top", "50px")}/** 成功提示框 */function alertMsgSuccess(message) {alertMsgByType(message, 'success')}/** 失败提示框 */function alertMsgError(message) {alertMsgByType(message, 'error')}/** 警告提示框 */function alertMsgWaring(message) {alertMsgByType(message, 'warning')}/** 信息提示框 */function alertMsgInfo(message) {alertMsgByType(message, 'info')}// ------------------------------------------------------------------------------------------------新版弹窗
上面是所有的源码,也没多少,这里跟大家简单唠唠。
alertDom.children(".xxx-message-type-icon").html()
这个方法中那些个字符串实际就是图标,可以参考阿里的iconfront图标库,我们公司的UI也自己搞了一套。xxx
就是你们公司项目的缩写了,类似el-message啊。其中用到了contentWindow
的知识点,其中的解释都在代码上啦。 稍微解释一下为啥要调用parent的方法,很简单,一般的公用js在每个页面都会存在一份,也就是不同的iframe中,如果你跳转页面之后,原来的iframe就不在了,但是你的老祖宗(也就是浏览器页面)还在,它是不会刷新的,所以也就不存在弹窗会消失和DOM事件消失的问题了。
这段代码经历了一段时间的正常运行,修正了多个版本,完全不用担心它的兼容问题,ie也可以正常运行,因为是用纯jQuery写的,甚至不需要引入任何的css文件,其实这样写不太标准,css应该单独提出来,我这样是为了方便。
今天就到这里,如有疑问可评论或私信俺。