文章目录
- iframe 渲染请求到的 html (邮件预览), 避免样式污染
- 接上一条, 打印 iframe 邮件详情
- 接上一条, iframe 预览邮件时, 要求固定水平滚动条在视口底部
iframe 渲染请求到的 html (邮件预览), 避免样式污染
背景:
之前弄了邮件系统, 但显示邮件内容时是直接 v-html , 导致邮件内容和项目样式互相污染; 之前代码是去掉邮件内容的样式文件, 结果导致部分内容显示错位, 现在想不改邮件内容, 用 iframe 包裹邮件内容显示
思路:
需要解决几个难点
- iframe 宽高如何随内容变化而变化
- iframe 如何与父级通讯
- 如何将不同类型的返回内容渲染成 html 放到 iframe 中
解决:
代码以 Vue 形式写的, 下面的代码是最终代码(iframe 渲染 + 自定义水平固定滚动条 + 打印 + 其他删除功能)拆分而来, 可能有些错漏/未删除变量
-
新建一个空白 HTML 页面, mailDetail.html , 只有最基本的 html 格式, 无任何内容
-
详情页新增 iframe 框, 引用此 html , 但先设置 height=“0” , 不显示内容
<!-- publicPath: process.env.BASE_URL --> <!-- mailFrameName: 'mailContentFrame' --> <iframe :src="`${publicPath}mailDetail.html`":name="mailFrameName"width="100%"height="0"style="border: none;"></iframe>
-
请求到邮件内容, 并对邮件内容做了处理, 调用方法渲染邮件内容到 iframe 中
// 处理邮件详情代码并渲染到iframe中 renderCodeToIframe(mailContent) {/** handleMailHTML方法 和 renderMailHTML方法都来源mixins(mailContent) */// 生成 iframe documentElement 代码this.content = this.handleMailHTML(mailContent);// 渲染代码到 iframe 中this.renderMailHTML(this.mailFrameName, this.content); },
-
渲染相关方法, from mixins(mailContent), 可以理解为提取出公共的方法到某处, 方便其他地方复用
-
对原始邮件内容做一些处理, 返回 html 字符串
handleMailHTML(mailContent) {// 解析邮件内容为 Document 对象const parser = new DOMParser();const doc = parser.parseFromString(mailContent, 'text/html');// 邮件内容中的 base 标签会导致页面跳转时,指向 base 标签指定的地址,而非当前系统的页面,因此去除全部的 base 标签Array.from(doc.querySelectorAll('base')).forEach(node => {node.remove();});// 邮件详情页的正文中超链接更改为新标签窗口打开Array.from(doc.querySelectorAll('a')).forEach(node => {if (node.target && node.target !== '_blank') {node.target = '_blank';}});// 设置 body margin 默认为 0 , 避免浏览器默认样式给 body 加上 margindoc.body.style.margin = '0';// 设置 body overflow-x hidden , 不允许出现横向滚动条 --- 外部模拟水平滚动条doc.body.style['overflow-x'] = 'hidden';// 设置 body overflow-y hidden + 去掉 body 的高度限制, 避免出现右侧滚动条doc.body.style['overflow-y'] = 'hidden';doc.body.style['min-height'] = 'auto';doc.body.style['max-height'] = 'auto';doc.body.style.height = 'auto';// 添加高度自适应 scriptconst heightWatcher = doc.createElement('script');heightWatcher.type = 'text/javascript';heightWatcher.innerHTML = ` // 监听元素高度变化(200ms 定时查询元素 offsetHeight 是否发生变化) // 注意, 不同浏览器, 不同版本, 对各种 height 实现不同, documentElement 和其他元素也有区别 // 这里是用 documentElement.offsetHeight 来获取整个文档高度, 别的元素的行为不确定, 可能要用 scrollHeight 来获取高度 function onElementHeightChange(elm, callback){var lastHeight = elm.offsetHeight, newHeight;(function run(){newHeight = elm.offsetHeight;if( lastHeight != newHeight ) {callback(newHeight, lastHeight);}lastHeight = newHeight;if( elm.onElementHeightChangeTimer ) {clearTimeout(elm.onElementHeightChangeTimer);}// 更新 hash 值, 供外部监听获取相应传参// iframe document 实际宽度var hashStr = 'documentWidth=' + elm.scrollWidth + ';'// iframe 元素宽度hashStr += 'iframeWidth=' + window.frameElement.clientWidth + ';'// 转码, 赋值location.hash = encodeURIComponent(hashStr);elm.onElementHeightChangeTimer = setTimeout(run, 200);})(); }// 监听 documentElement offsetHeight 变化, 变化后设置父页面 frame 元素 height 属性为变化后的高度 onElementHeightChange(document.documentElement, function(newHeight, oldHeight){console.error('onElementHeightChange', newHeight, oldHeight)if (window.frameElement) {// 设置 frame height 为变化后的新高度 + 滚动条高度, 以避免元素底部出现水平滚动条时垂直方向不能占满window.frameElement.height = (newHeight || 50) + (window.innerWidth - document.documentElement.clientWidth);} });// 初次加载完成时, 设置父页面 frame 元素 height 属性为 documentElement.offsetHeight window.addEventListener('DOMContentLoaded', function(e) {console.error('DOMContentLoaded');if (window.frameElement) {// 设置 frame height 为页面高度 + 滚动条高度, 以避免元素底部出现水平滚动条时垂直方向不能占满window.frameElement.height = document.documentElement.offsetHeight + (window.innerWidth - document.documentElement.clientWidth);} })`;doc.body.append(heightWatcher);// 设置 DOCTYPE 以避免页面内容缩小时, iframe 高度不变, 导致多出空白区域(参考 https://segmentfault.com/a/1190000014586956#item-3)const docType = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';// 返回最终的 HTML 字符串return docType + doc.documentElement.outerHTML; }
-
渲染 html 到 iframe 中
renderMailHTML(frameName, strHTML, callBack) {// 不加 $nextTick 或 $nextTick 位置放错(参见 git 文件提交日志), 可能导致内容不显示 --- 实际原因是多了一个 iframe , 不清楚咋出现的this.$nextTick(() => {// 获取指定 iframe 的 windowlet ifr = window.frames[frameName];if (!ifr) {return;}// 清除原有 iframe , 避免其内容对新 iframe 造成影响, 同时也避免原有 iframe 中的各种监听之类的残留const ifrElm = ifr.frameElement;const newIfrElm = ifr.frameElement.cloneNode();ifrElm.parentElement.replaceChild(newIfrElm, ifrElm);// 写入新 iframe 内容ifr = window.frames[frameName];if (ifr) {// 写入 HTMLifr.document.open();ifr.document.write(strHTML);ifr.document.close();// 触发回调函数if (callBack) {callBack();}}}); }
-
接上一条, 打印 iframe 邮件详情
-
新增一个打印用的 iframe , 隐藏不显示
<!-- 邮件打印的iframe容器 --> <iframe id="printf" name="printf" style="display: none;"></iframe>
-
调用下方打印方法打印
// strHTML: 原始邮件内容 // containerNode: 邮件完整内容(包括 iframe 和其他信息如收件人发件人等)所在的 node // frameName: 邮件详情页 iframe 的 name // printFrameName: 之前初始化的打印用 iframe 的 name printMailHtml(strHTML, containerNode, frameName, printFrameName) {// 初始化打印 Documentconst parser = new DOMParser();// 指定打印样式和 onload 打印const doc = parser.parseFromString(`<html><head><style media="print">* {word-wrap: break-word; word-break: break-word;}ul li {font-size: 12px;line-height: 18px;font-weight: 400; list-style-type:none;}</style></head><body οnlοad="window.print()">${containerNode.innerHTML}</body></html>`, 'text/html');// 替换邮件内容 iframe 为 iframe 内部文档const ifr = doc.querySelector(`iframe[name=${frameName}]`);if (ifr) {ifr.outerHTML = strHTML;}// 写入数据到打印 iframe 中, 打印const printWin = window.frames[printFrameName];if (printWin) {printWin.document.write(doc.documentElement.outerHTML);printWin.document.close();} }
接上一条, iframe 预览邮件时, 要求固定水平滚动条在视口底部
背景:
邮件过长时, 页面要滚动到最底部才能拖拽 iframe 的水平滚动条, 操作不方便; 因此希望水平滚动条固定显示在视口底部, 用户可以直接拖拽查看详情; 并且, 在 iframe 垂直方向滚动到底后, 水平滚动条应取消固定, 随着 iframe 继续向上移动
解决:
做一个模拟滚动条满足此需求, 其原理为:
- 邮件详情 iframe 本身不显示水平滚动条, 在 iframe 底部新增一个两层 div , 外部 div 宽度与 iframe 保持一致, 内部 div 宽度为 iframe 内部文档实际宽度
- 监听模拟滚动条容器(外层 div)的 scroll 事件, 同步将内部 div 的 scrollLeft 赋值给 iframe documentElement 的 scrollLeft
- 模拟滚动条容器默认设置为 position: absolute , 监听 iframeElement 的 offsetParent (其所在的 overflow div)的 scroll 事件, 当 iframe 的边界进入视口后, 设置 position 为 relative
具体代码如下:
-
滚动条 div
<!-- 邮件详情 iframe --><!-- 自定义滚动条 --> <div @scroll="handleMailHorizontalScroll"ref="mailIframeScroll":style="mailScrollContainerStyleObj"><div :style="mailScrollInnerStyleObj"></div> </div>
-
相关变量/滚动监听器定义
data() {return {// 邮件自定义水平滚动条样式 -- 外部与 iframe 等宽 div 的样式mailScrollContainerStyleObj: {// 固定属性// 允许出现水平滚动条, 此水平滚动条即为最终显示的水平滚动条'overflow-x': 'auto',// 尽量减少滚动条占位高度'line-height': '0',// 背景透明'background-color': 'transparent',// 变动属性// 控制鼠标穿透, 确保滚动条不显示时鼠标不会误触滚动条'pointer-events': 'none',// 滚动条外部宽度, 因为显示区域和 offsetParent 不一定等宽, 这个也是要调整的, 避免滚动条从固定变为正常时宽度发生变化width: '100%',// 固定显示时 absolute, 正常显示时 relativeposition: 'relative',bottom: '0'},// 邮件自定义水平滚动条样式 -- 内部与 iframe documentElement 等宽 div 的样式mailScrollInnerStyleObj: {// 固定属性// 高度尽可能小height: '1px',// 背景透明'background-color': 'transparent',// 变动属性// 模拟 iframe 内部文档宽度, 保证外部 div 滚动条显示逻辑和 iframe 系统水平滚动条逻辑一致width: '0'},// 监听: iframe 所在 overflow div 发生垂直滚动 ($debounce 是自己写的防抖方法)handleMailVerticalScroll: this.$debounce(() => {// 暂存 iframe 元素const ifrEle = document.querySelector(`iframe[name="${this.mailFrameName}"]`);if (ifrEle) {// 获取 iframe rect.bottom 和其 offsetParent rect.bottom , 以判断 iframe 底部是否在其 offsetParent 下方(还要算上指定 bottom , 避免 offsetParent 和滚动容器位置不一致)const ifrRec = ifrEle.getBoundingClientRect();const scrollRec = ifrEle.offsetParent.getBoundingClientRect();if (ifrRec.bottom > scrollRec.bottom + this.scrollBarBottom) {// iframe 底部在其 offsetParent 下方// 固定显示水平滚动条在 offsetParent 底部this.mailScrollContainerStyleObj.position = 'absolute';this.mailScrollContainerStyleObj.bottom = `${this.scrollBarBottom}px`;} else {// iframe 底部不在其 offsetParent 下方// 水平滚动条正常显示在原位置(iframe 之下)this.mailScrollContainerStyleObj.position = 'relative';this.mailScrollContainerStyleObj.bottom = '0';}}}, 10),// 监听: iframe 下方模拟水平滚动条 发生水平滚动handleMailHorizontalScroll: this.$debounce((e) => {// 暂存 iframe windowconst ifr = window.frames[this.mailFrameName];// 控制 iframe documentElement 左偏移量if (ifr && ifr.document && ifr.document.documentElement) {ifr.document.documentElement.scrollLeft = e.target.scrollLeft;}}, 10)}; }
-
监听 iframe hash 值变化(之前的渲染方法里写了, 文档宽度变化时更新数据到 hash 中), 调整自定义滚动条和其容器的 width
// 之前 renderMailHtml 方法预留有参数 callBack , 调用时给此参数传入下面的方法就行了// 监听: iframe hash 值变化 handleIframeHashChange() {setTimeout(() => {// 暂存 iframe windowconst ifr = window.frames[this.mailFrameName];ifr.onhashchange = () => {// hash 值解码const hashVal = decodeURIComponent(ifr.location.hash);if (hashVal) {// 取到 iframe clientWidthlet temp = hashVal.match(/iframeWidth=(.*?);/);// 设置模拟滚动条外部 div 宽度为 iframe clientWidthif (temp[1]) {this.mailScrollContainerStyleObj.width = `${temp[1]}px`;}// 取到 iframe document scrollWidthtemp = hashVal.match(/documentWidth=(.*?);/);// 设置模拟滚动条内部 div 宽度为 iframe documentElement scrollWidthif (temp[1]) {this.mailScrollInnerStyleObj.width = `${temp[1]}px`;}}};}, 100); }
-
监听滚动条的宽度, 避免页面宽度足够, 不用显示水平滚动条时, 水平滚动条仍然占位, 导致底部无法点击
watch: {'mailScrollInnerStyleObj.width': {handler(val) {// 获取模拟滚动条容器const scrollBarDiv = this.$refs.mailIframeScroll;if (scrollBarDiv) {// 模拟滚动条内部 div 宽度大于容器宽度时, 才允许鼠标点击滚动条区域(避免用户想点击邮件内容却点中滚动条, 导致点击无效)this.mailScrollContainerStyleObj['pointer-events'] = parseFloat(val) > scrollBarDiv.clientWidth ? 'auto' : 'none';// 主动触发垂直滚动方法, 判断当前滚动条应该固定显示还是正常显示this.handleMailVerticalScroll();}}} }