iframe 渲染请求到的 html (邮件预览), 避免样式污染 + 打印 iframe 邮件详情 + iframe 预览邮件时固定水平滚动条在视口底部

文章目录

    • iframe 渲染请求到的 html (邮件预览), 避免样式污染
    • 接上一条, 打印 iframe 邮件详情
    • 接上一条, iframe 预览邮件时, 要求固定水平滚动条在视口底部

iframe 渲染请求到的 html (邮件预览), 避免样式污染

背景:
之前弄了邮件系统, 但显示邮件内容时是直接 v-html , 导致邮件内容和项目样式互相污染; 之前代码是去掉邮件内容的样式文件, 结果导致部分内容显示错位, 现在想不改邮件内容, 用 iframe 包裹邮件内容显示

思路:

需要解决几个难点

  1. iframe 宽高如何随内容变化而变化
  2. iframe 如何与父级通讯
  3. 如何将不同类型的返回内容渲染成 html 放到 iframe 中

解决:

代码以 Vue 形式写的, 下面的代码是最终代码(iframe 渲染 + 自定义水平固定滚动条 + 打印 + 其他删除功能)拆分而来, 可能有些错漏/未删除变量

  1. 新建一个空白 HTML 页面, mailDetail.html , 只有最基本的 html 格式, 无任何内容

  2. 详情页新增 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>
    
  3. 请求到邮件内容, 并对邮件内容做了处理, 调用方法渲染邮件内容到 iframe 中

    // 处理邮件详情代码并渲染到iframe中
    renderCodeToIframe(mailContent) {/** handleMailHTML方法 和 renderMailHTML方法都来源mixins(mailContent) */// 生成 iframe documentElement 代码this.content = this.handleMailHTML(mailContent);// 渲染代码到 iframe 中this.renderMailHTML(this.mailFrameName, this.content);
    },
    
  4. 渲染相关方法, from mixins(mailContent), 可以理解为提取出公共的方法到某处, 方便其他地方复用

    1. 对原始邮件内容做一些处理, 返回 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;
      }
      
    2. 渲染 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 邮件详情

  1. 新增一个打印用的 iframe , 隐藏不显示

    <!-- 邮件打印的iframe容器 -->
    <iframe id="printf" name="printf" style="display: none;"></iframe>
    
  2. 调用下方打印方法打印

    // 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 继续向上移动

解决:

做一个模拟滚动条满足此需求, 其原理为:

  1. 邮件详情 iframe 本身不显示水平滚动条, 在 iframe 底部新增一个两层 div , 外部 div 宽度与 iframe 保持一致, 内部 div 宽度为 iframe 内部文档实际宽度
  2. 监听模拟滚动条容器(外层 div)的 scroll 事件, 同步将内部 div 的 scrollLeft 赋值给 iframe documentElement 的 scrollLeft
  3. 模拟滚动条容器默认设置为 position: absolute , 监听 iframeElement 的 offsetParent (其所在的 overflow div)的 scroll 事件, 当 iframe 的边界进入视口后, 设置 position 为 relative

具体代码如下:

  1. 滚动条 div

    <!-- 邮件详情 iframe --><!-- 自定义滚动条 -->
    <div @scroll="handleMailHorizontalScroll"ref="mailIframeScroll":style="mailScrollContainerStyleObj"><div :style="mailScrollInnerStyleObj"></div>
    </div>
    
  2. 相关变量/滚动监听器定义

    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)};
    }
    
  3. 监听 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);
    }
    
  4. 监听滚动条的宽度, 避免页面宽度足够, 不用显示水平滚动条时, 水平滚动条仍然占位, 导致底部无法点击

    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();}}}
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/875645.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] LYA的跳格子游戏(200分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题目在线…

手写spring简易版本,让你更好理解spring源码

首先我们要模拟spring&#xff0c;先搞配置文件&#xff0c;并配置bean 创建我们需要的类&#xff0c;beandefito&#xff0c;这个类是用来装解析后的bean&#xff0c;主要三个字段&#xff0c;id&#xff0c;class&#xff0c;scop&#xff0c;对应xml配置的属性 package org…

理解 Kotlin 中的 crossinline 关键字

理解 Kotlin 中的 crossinline 关键字 Kotlin 提供了丰富的功能&#xff0c;用于开发简洁且富有表现力的代码。这些特性包括高阶函数和 Lambda 表达式&#xff0c;它们是 Kotlin 设计的核心部分。在使用这些构造时&#xff0c;您可能会遇到 crossinline 关键字。在本文中&#…

第二讲:NJ网络配置

Ethernet/IP网络拓扑结构 一. NJ EtherNet/IP 1、网络端口位置 NJ的CPU上面有两个RJ45的网络接口,其中一个是EtherNet/IP网络端口(另一个是EtherCAT的网络端口) 2、网络作用 如图所示,EtherNet/IP网络既可以做控制器与控制器之间的通信,也可以实现与上位机系统的对接通…

MySQL --- 表的操作

在对表进行操作时&#xff0c;需要先选定操作的表所在的数据库&#xff0c;即先执行 use 数据库名; 一、创建表 create table 表名( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collate 校验规则 engine 存储引擎 ; 说明&#xff1a…

从零入门 AI for Science(AI+药物) #Datawhale AI 夏令营

使用平台 我的Notebook 魔搭社区 https://modelscope.cn/my/mynotebook/preset 主要操作 运行实例&#xff0c;如果有时长尽量选择方式二&#xff08;以下操作基于方式二的实例实现&#xff09; 创建文件夹&#xff0c;并重命名为 2.3siRNA 上传两个文件 到文件夹&#…

LC 128.最长连续序列

128.最长连续序列 给定一个未排序的整数数组 nums &#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 示例 1&#xff1a; 输入&#xff1a; nums [100,4,200,1,3,2]…

go标准库---net/http服务端

1、http简单使用 go的http标准库非常强大&#xff0c;调用了两个函数就能够实现一个简单的http服务&#xff1a; func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) func ListenAndServe(addr string, handler Handler) error handleFunc注册一个路…

BGP路由反射器

原理概述 缺省情况下&#xff0c;路由器从它的一个 IBGP对等体那里接收到的路由条目不会被该路由器再传递给其他IBGP对等体&#xff0c;这个原则称为BGP水平分割原则&#xff0c;该原则的根本作用是防止 AS内部的BGP路由环路。因此&#xff0c;在AS内部&#xff0c;一般需要每台…

LabVIEW做二次开发时应该注意哪些方面?

在使用LabVIEW进行二次开发时&#xff0c;以下几个方面需要特别注意&#xff1a; 需求明确化&#xff1a; 确认并详细记录客户的需求&#xff0c;明确系统的功能、性能、可靠性等要求。制定详细的需求文档&#xff0c;并与客户反复确认&#xff0c;避免后期的需求变更和误解。 …

【Android】数据存储方案——文件存储、SharedPreferences、SQLite数据库用法总结

文章目录 文件存储存储到文件读取文件 SharedPreferences存储存储获取SharedPreferences对象Context 类的 getSharedPreferences() 方法Activity 类的 getPreferences() 方法PreferenceManager 类中的 getDefaultSharedPreferences() 方法 示例 读取记住密码的功能 SQLite数据库…

4.Java Web开发模式(javaBean+servlet+MVC)

Java Web开发模式 一、Java Web开发模式 1.javaBean简介 JavaBeans是Java中一种特殊的类&#xff0c;可以将多个对象封装到一个对象&#xff08;bean&#xff09;中。特点是可序列化&#xff0c;提供无参构造器&#xff0c;提供getter方法和setter方法访问对象的属性。名称中…

JAVA代码审计JAVA0基础学习(需要WEB基础知识)DAY2

JAVA 在 SQL执行当中 分为3种写法&#xff1a; JDBC注入分析 Mybatis注入分析 Hibernate注入分析 JDBC 模式不安全JAVA代码示例部分特征 定义了一个 sql 参数 直接让用户填入id的内容 一个最简单的SQL语句就被执行了 使用安全语句却并没有被执行 Mybatis&#xff1a; #…

【MetaGPT系列】【MetaGPT完全实践宝典——多智能体实践】

目录 前言一、智能体1-1、Agent概述1-2、Agent与ChatGPT的区别 二、多智能体框架MetaGPT2-1、安装&配置2-2、使用已有的Agent&#xff08;ProductManager&#xff09;2-3、多智能体系统介绍2-4、多智能体案例分析2-4-1、构建智能体团队2-4-2、动作/行为 定义2-4-3、角色/智…

PyTorch和TensorFlow概念及对比

PyTorch和TensorFlow是两个流行的深度学习框架&#xff0c;用于构建和训练机器学习和深度学习模型。它们各自有一些独特的特点和优点&#xff1a; 一 、PyTorch 动态计算图&#xff1a; PyTorch使用动态计算图&#xff08;Dynamic Computation Graph&#xff09;&#xff0c;…

【OpenCV C++20 学习笔记】调节图片对比度和亮度(像素变换)

调节图片对比度和亮度&#xff08;像素变换&#xff09; 原理像素变换亮度和对比度调整 代码实现更简便的方法结果展示 γ \gamma γ校正及其实操案例线性变换的缺点 γ \gamma γ校正低曝光图片矫正案例代码实现 原理 关于OpenCV的配置和基础用法&#xff0c;请参阅本专栏的其…

五、工厂方法模式

文章目录 1 基本介绍2 案例2.1 Drink 抽象类2.2 Tea 类2.3 Coffee 类2.4 DrinkFactory 抽象类2.5 TeaFactory 类2.6 CoffeeFactory 类2.7 Client 类2.8 Client 类运行结果2.9 总结 3 各角色之间的关系3.1 角色3.1.1 Product ( 抽象产品 )3.1.2 ConcreteProduct ( 具体产品 )3.1…

生物信息学新突破:在英特尔 Gaudi 2 上实现 ProtST 蛋白质语言模型加速

引言 随着人工智能技术的快速发展&#xff0c;蛋白质结构预测和语言模型在生物信息学领域扮演着越来越重要的角色。ProtST作为一种新兴的蛋白质语言模型&#xff0c;其性能在英特尔 Gaudi 2 加速器的助力下得到了显著提升。本文将探讨如何利用英特尔 Gaudi 2 加速 ProtST 模型…

哈希表相关的力扣题和讲解和Java、C++常用的数据结构(哈希法)

20240725 一、什么时候适用什么样的结构。1.java中1.1 HashSet&#xff1a;1.2 TreeSet&#xff1a;1.3 LinkedHashSet&#xff1a;1.4 HashMap&#xff1a;1.5 TreeMap&#xff1a;1.6 LinkedHashMap&#xff1a;1.7 总结 2. c中2.1 std::unordered_set&#xff1a;2.2 std::s…

项目实战——外挂开发(30小时精通C++和外挂实战)

项目实战——外挂开发&#xff08;30小时精通C和外挂实战&#xff09; 外挂开发1-监控游戏外挂开发2-秒杀僵尸外挂开发3-阳光地址分析外挂开发4-模拟阳光外挂开发5-无限阳光 外挂开发1-监控游戏 外挂的本质 有两种方式 1&#xff0c;修改内存中的数据 2&#xff0c;更改内存中…