前端将html导出为word文件

介绍

需求:在开发过程中,遇到了产品要求将展示的可视化报告直接导出到word格式的需求,导出的html包括Echarts图、文字、表格。经过一番查找,找到一个插件html-docx-js。

功能实现

exportWord函数:主函数接受一个dom元素,通过a标签导出文件

ini 复制代码
import htmlDocx from 'html-docx-js/dist/html-docx'

export async function exportWord(ele) {
  let htmlContent = await wordStyleProcess(ele)
  const docxBlob = htmlDocx.asBlob(htmlContent)
  const URL = window.URL || window.webkitURL
  let a = document.createElement('a')
  a.download = 'name'
  a.rel = 'noopener'
  a.target = '_blank'
  a.href = URL.createObjectURL(docxBlob)
  a.click()
  URL.revokeObjectURL(a.href) // 20
}

wordStyleProcess函数:对元素进行样式处理。首先,复制元素,防止修改样式影响页面样式。使用shortId标记元素,通过id找到页面渲染的原始元素,对样式进行调整。

javascript 复制代码
export async function wordStyleProcess(ele) {
  if (!ele) return console.warn('未传入元素')
  // 设置标记, 方便复制样式,通过data-export-file-id标记,用于后续找到页面的真实渲染样式的元素
  Array.from(ele.querySelectorAll('*')).forEach((item) => {
    item.setAttribute('data-export-file-id', 'str' + stortId.generate())
  })
  //不影响页面样式,复制dom
  let exportFileEle = ele.cloneNode(true)

  // 遍历iframe中的元素,将类样式设置行内样式
  Array.from(exportFileEle.querySelectorAll('*')).forEach((item) => {
    let dataExportFileId = item.getAttribute('data-export-file-id')
    let originDom = ele.querySelector('[data-export-file-id="' + dataExportFileId + '"]') //原始dom才能获取真实样式
    if (originDom) {
      let originSty = getComputedStyle(originDom) //利用getComputedStyle获取真实页面dom的样式
      if (originSty.display === 'none' || originSty.opacity === '0') return item.remove()
      setStyle(item, originSty)
    }
    //处理table类型的元素
    if (item.getAttribute('data-transform-table')) {
      const wordTable = convertElTableToHtmlTableWord(item, ele)
      const table = document.createElement('div')
      const parent = item.parentNode
      parent.style.height = 'auto'
      table.innerHTML = wordTable
      if (item.nextSibling) {
        parent.insertBefore(table, item.nextSibling)
      } else {
        parent.appendChild(table)
      }
      item.remove()
    }
  })

  // 处理echarts类型元素:遍历真实页面中的元素,带有自定义属性data-transform-image的元素为需要转换的元素,真实页面的canvas转换为图片,替换到iframe中对应位置
    await convertHTMLToImage(Array.from(ele.querySelectorAll('[data-transform-image="true"]')), exportFileEle)  
  //
  const htmlContent = exportFileEle.innerHTML
  if (exportFileEle) {
    exportFileEle.remove()
  }
  return htmlContent
}

setStyle函数:获取到的元素没有类样式,通过getComputedStyle获取的页面真实样式赋值到导出的元素中。

css 复制代码
/**
 * 把元素类样式设置行内样式
 *
 * @param ele DOM元素
 * @param sty 包含样式属性的对象
 */
export function setStyle(ele, sty) {
  if (ele.nodeName.toLowerCase() !== 'img') {
    ele.setAttribute(
      'style',
      (ele.getAttribute('style') || '') +
        `;font-size: ${sty.fontSize};color: ${sty.color};font-style: ${sty.fontStyle};line-height: ${sty.lineHeight};font-weight: ${sty.fontWeight};
      font-family: ${sty.fontFamily};text-align: ${sty.textAlign};text-indent: ${sty.textIndent}; margin: ${sty.margin}; padding: ${sty.padding};width: ${sty.width}; height: ${sty.height};
      white-space:${sty.whiteSpace};word-break:${sty.wordBreak};display:${sty.display}; flex-direction:${sty.flexDirection};align-items: ${sty.alignItems}; `
    )
  }
}

convertHTMLToImage:对echarts图表元素导出的问题,一开始我使用的html2canvas,网上的资料的很多也是用的这个插件,但是这个插件实在的太太太太慢了!!!!,几个echarts图,居然十几秒。 后面我翻了一下资料,echarts实例是支持导出base64的,然后尝试了一下,直接秒导出,快太多了。 html2canvas版本:

javascript 复制代码
export async function convertHTMLToImage(elements, exportFileEle, canvasWidth) {
  // 使用html2canvas捕捉DOM元素
  for (let element of elements) {
    const option = {
      // 使用html2Canvas进行截图
      logging: false,
      allowTaint: true, // 允许跨域图片渲染
      useCORS: true, // 使用跨域资源
      imageTimeout: 0, // 图片加载延迟,默认延迟为0,单位毫秒
      scale: 1.2, // 设置缩放比例
      willReadFrequently: true,
    }
    const canvas = await html2canvas(element, option)
    canvas.setAttribute('data-export-file-id', element.getAttribute('data-export-file-id'))
    const url = canvas.toDataURL('image/jpg', 1.0)
    let img = new Image()
    img.src = url
    canvasWidth ? (img.width = canvasWidth) : (img.style.width = '100%')
    //移除原来的图表元素,替换为图片
    let canvasEle = exportFileEle.querySelector(`[data-export-file-id=${element.getAttribute('data-export-file-id')}]`)
    if (canvasEle) {
      const parent = canvasEle.parentNode
      if (canvasEle.nextSibling) {
        parent.insertBefore(img, canvasEle.nextSibling)
      } else {
        parent.appendChild(img)
      }
      canvasEle.remove()
    }
  }
}

使用echart的版本:从这里看好像也差不多,但是element有个data-url的自定义属性,这个需要在外部配合,将获取到的base64赋值给元素的自定义属性。

javascript 复制代码
export async function convertHTMLToImage(elements, exportFileEle, canvasWidth) {
  // 使用html2canvas捕捉DOM元素
  for (let element of elements) {
    const option = {
      // 使用html2Canvas进行截图
      logging: false,
      allowTaint: true, // 允许跨域图片渲染
      useCORS: true, // 使用跨域资源
      imageTimeout: 0, // 图片加载延迟,默认延迟为0,单位毫秒
      scale: 1.2, // 设置缩放比例
      willReadFrequently: true,
    }
    const url = element.getAttribute('data-url')   
    let img = new Image()
    img.src = url
    canvasWidth ? (img.width = canvasWidth) : (img.style.width = '100%')
    //移除原来的图表元素,替换为图片
    let canvasEle = exportFileEle.querySelector(`[data-export-file-id=${element.getAttribute('data-export-file-id')}]`)
    if (canvasEle) {
      const parent = canvasEle.parentNode
      if (canvasEle.nextSibling) {
        parent.insertBefore(img, canvasEle.nextSibling)
      } else {
        parent.appendChild(img)
      }
      canvasEle.remove()
    }
  }
}

echart组件核心代码:这里只简单写了一下,主要核心意思就是,在echarts渲染完成之后,生成base64,然后赋值到元素的自定义属性上。

kotlin 复制代码
<div ref="refEchartsContainer" class="echarts-container" :style="cardStyle" data-transform-image="true" :data-url="url"/>

this.chartInstance.on('finished', () => {
            this.url = this.chartInstance.getDataURL({ type: 'png' })
        })

convertElTableToHtmlTableWord:处理el-table表格

javascript 复制代码
export function convertElTableToHtmlTableWord(elTable, renderEle) {
  if (!elTable) return ''
  const tableEmptyBlockStyHeight = '100%'
  // 获取 el-table 的表头数据,包括多级表头
  const tableOptions = {
    tableStyle: 'border-collapse: collapse; width:100%',
    headerStyle: 'border: 1px solid black; width: 60px;height: 50px;',
    rowStyle: 'mso-yfti-irow:0; mso-yfti-firstrow:yes; mso-yfti-lastrow:yes; page-break-inside:avoid;height: 20px;',
    cellStyle: 'border: 1px solid black; min-width: 50px;max-width: 100px;height: 20px;',
  }
  const theadRows = elTable.querySelectorAll('thead tr') || []
  // 获取 el-table 的数据行
  const tbodyRows = elTable.querySelectorAll('tbody tr') || []

  // 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
  let htmlTable = `<table style="${tableOptions.tableStyle}"><thead>`
  let columnsNum = 0
  // 处理表头
  theadRows.forEach((row) => {
    htmlTable += `<tr style="${tableOptions.rowStyle}">`
    const columns = row.querySelectorAll('th') || []
    // 获取表头列数,用于后面暂无数据的colspan属性
    columnsNum = columns.length || 0
    columns.forEach((column) => {
      if (column.style.display !== 'none') {
        const colspan = column.getAttribute('colspan') || '1'
        const rowspan = column.getAttribute('rowspan') || '1'
        htmlTable += `<th colspan="${colspan}" rowspan="${rowspan}" style="${tableOptions.headerStyle}">${column.innerText}</th>`
      }
    })
    htmlTable += '</tr>'
  })
  htmlTable += '</thead><tbody>'

  // 构建数据
  if (tbodyRows && tbodyRows.length > 0) {
    tbodyRows.forEach((row) => {
      htmlTable += `<tr style="${tableOptions.rowStyle}">`

      const cells = row.querySelectorAll('td') || []
      cells.forEach((cell) => {
        if (cell.querySelector('div')) {
          htmlTable += `<td style="${tableOptions.cellStyle}">${cell.querySelector('div').innerHTML}</td>`
        } else {
          htmlTable += `<td style="${tableOptions.cellStyle}">${cell.innerText}</td>`
        }
      })
      htmlTable += '</tr>'
    })
  } else {
    htmlTable += `<tr style="${tableOptions.rowStyle}"><td colspan="${columnsNum}" style="width: 100%; border: 1px solid black; height: ${tableEmptyBlockStyHeight};"><div style="display: inline-block;width: 100%; text-align: center;">暂无数据</div></td></tr>`
  }
  htmlTable += '</tbody></table>'

  return htmlTable
}
相关推荐
前端付豪8 小时前
如何使用 Vuex 设计你的数据流
前端·javascript·vue.js
李雨泽8 小时前
通过 Prisma 将结构推送到数据库
前端
前端小万8 小时前
使用 AI 开发一款聊天工具
前端·全栈
咖啡の猫9 小时前
Vue消息订阅与发布
前端·javascript·vue.js
GIS好难学9 小时前
Three.js 粒子特效实战③:粒子重组效果
开发语言·前端·javascript
申阳9 小时前
Day 2:我用了2小时,上线了一个还算凑合的博客站点
前端·后端·程序员
刺客_Andy9 小时前
React 第四十七节 Router 中useLinkClickHandler使用详解及开发注意事项案例
前端·javascript·react.js
爱分享的鱼鱼9 小时前
Java实践之路(一):记账程序
前端·后端
爱编码的傅同学10 小时前
【HTML教学】成为前端大师的入门教学
前端·html