根据iframe的内容调整其大小

在做邮件预览的功能时,需要将邮件的HTML内容显示出来。一开始的做法是sanitized之后直接渲染出来。但是CSS样式要尽量保留,结果就是这些样式连外面不属于邮件的部分也影响了。

于是把邮件的内容放到一个iframe里面去,隔离它的样式。而iframe一般要写死高度和宽度,这样如果邮件内容过长过宽,就会出现滚动条。理想的情况是根据iframe的内容调整其大小。

这里记录一下我是怎么做的,直接上代码:

<template>
  <iframe
    :id="iframe_id"
    :src="iframe.src"
    :style="iframe.style"
    @load="onIframeLoaded"
    >
  </iframe>
</template>

<script>
  import HtmlSanitizer from '@/assets/HtmlSanitizer.js'
  export default {
    name: "vue-iframe",
    data() {
      return {
        iframe: {
          src: '',
          style: {}
        },
        bloburl: ''
      }
    },
    props: {
      id: {default: 'default'},
      raw_html: {default: ''},
      url: {default: ''},
      height: {default: ''},
      width: {default: '100%'},
      innerStyle: {type: String}
    },
    computed: {
      iframe_id: function() {
        return 'iframe-' + this.id + '-' + Math.floor(Math.random() * 10E8)
      },
      isHeightSet: function() {
        return !!this.height && !!this.height.length
      },
      // 判断传进来的url是否同源
      isSameOrigin: function() {
        if (!this.url || !this.url.length) {
          return true
        }
        let loc = new URL(window.location.href)
        let url = new URL(this.url)
        return loc.origin === url.origin
      }
    },
    watch: {
      raw_html: function(val, oldVal) {
        this.initIframe()
      },
      url: function(val, oldVal) {
        this.initIframe()
      }
    },
    methods: {
      initIframe() {
        this.iframe.style.height = '0'  // 重置iframe高度,否则iframe不会缩小
        if (this.url && this.url.length) {
          this.setIframeUrl(this.url)
        }
        else {
          let html = HtmlSanitizer.SanitizeHtml(this.raw_html)
          this.setIframeContent(html)
        }
      },
      setIframeUrl(url) {
        this.iframe.src = url
      },
      setIframeContent(content) {
        let ua = window.navigator.userAgent
        if (ua.indexOf('Trident/') > -1) {  // IE
          let doc = document.getElementById(this.iframe_id).contentWindow.document
          doc.open().write(content)
          doc.close()
        }
        else {
          window.URL.revokeObjectURL(this.bloburl)
          let blob = new Blob(['\uFEFF', content], {type: 'text/html'})
          this.bloburl = window.URL.createObjectURL(blob)
          this.iframe.src = this.bloburl
        }
      },
      onIframeLoaded() {
        this.setDocStyle()
        this.setIframeHeight()
        this.$emit('loaded', this.iframe.style.height)

        // 设置iframe的高度可能会让父元素出现滚动条,反过来影响iframe内容的尺寸
        // 监听iframe contentWindow的resize事件重新计算内容高度
        let iframe = document.getElementById(this.iframe_id)
        iframe.contentWindow.addEventListener('resize', this.setIframeHeight)
      },
      setIframeHeight() {
        if (this.isHeightSet) {
          this.iframe.style.height = this.height
        }
        else if (this.isSameOrigin) {
          let doc = document.getElementById(this.iframe_id).contentWindow.document
          let height = this.getDocHeight(doc)
          if (height === 0) {  // 有时高度会计算错误
            height = 40
          }
          else if (doc.body && doc.body.clientWidth < doc.body.scrollWidth) {
            height += 17  // 滚动条的高度
          }
          this.iframe.style.height = height + 'px'
        }
        else {
          this.iframe.style.height = '100%'
        }
        // this.$el.scrollIntoView({behavior: 'smooth', block: 'nearest', inline: 'start'})
      },
      getDocHeight(doc) {
        doc = doc || document
        let body = doc.body
        let html = doc.documentElement
        if (!body || !html) return 0
        let height = Math.max(body.scrollHeight, body.offsetHeight,
                              html.clientHeight, html.scrollHeight, html.offsetHeight)
        return height
      },
      // 设置iframe内容的基本样式
      setDocStyle() {
        if (this.isSameOrigin) {
          let doc = document.getElementById(this.iframe_id).contentWindow.document
          if (doc) {
            doc.body.style.margin = '0'
            doc.body.style.backgroundColor = 'transparent'
            let css = 'body {font-family: Calibri, Arial, Helvetica, Hiragino Sans GB, Microsoft YaHei, sans-serif; font-size: 14px;}'
            if (this.innerStyle) css += this.innerStyle
            let style = document.createElement('style')
            style.appendChild(document.createTextNode(css))
            doc.head.insertBefore(style, doc.head.firstChild)
          }
        }
      }
    },
    created() {
      this.iframe.style = {
        position: 'relative',
        height: this.height,
        width: this.width,
        border: 0
      }
    },
    mounted() {
      this.initIframe()
    }
  }
</script>

可以看到,这里封装的组件可以处理两种情况,一种是传进来url,另一种是传进来裸html。基本的思路是等iframe加载完成之后,计算一下里面的高度和宽度,然后相应地设置iframe的大小。

用法:

<vue-iframe url="xxx"></vue-iframe>

<vue-iframe raw_html="<html>...</html>"></vue-iframe>

可以看到,经过封装后的组件使用非常简单,只要把显示的内容传进去就可以了。

最后就是这个方法的缺点,那就是加载的url必须是同源的,否则获取不到内容的大小。 不过由于需求是显示裸html string,这个缺点问题不大。