前端将html导出为word文档

06-01 1226阅读

前言

本次功能的引出是因为博主所做的功能业务为统计分析页面,需要将图表及分析数据导出到word文档,使系统使用人员可以在页面显示的基础上进行修改。按理说页面导出的数据是不允许修改的,所以博主向产品经理提出直接将页面导出为图片,在博主的据理力争下,成功的被否决了,那就来好好研究一下吧

正文

在通过多方搜索以及实践之下,终于找到了解决方案,原理就是先将页面上的元素样式转换为行内样式,然后再加上可以被word文档识别的标头,然后通过html-docx-js将html文档转为blob流,这样就可以通过点击a标签的形式将该文件导出

将页面元素样式转化为行内样式

首先,需要生成一个空iframe元素 我们操作的数据都在这个元素内,这样就不会影响页面上的数据

    window.contIframe = document.createElement('iframe');
    window.contIframe.style = 'display: none; width:100%;';
    document.body.appendChild(window.contIframe);

然后将需要导出的元素插入到这个iframe中

  // contEl 需要导出的元素
  let cloneEl = contEl.cloneNode(true);
  cloneEl.style.width = getComputedStyle(contEl).width;
  window.contIframe.contentDocument.body.appendChild(cloneEl);

删除隐藏元素并将元素样式转换为行内样式

 
let domWrap = cloneEl.cloneNode(true)
  // 1. 删除隐藏的元素, 并且将元素样式设置为行内样式, 方便word识别
  Array.from(domWrap.querySelectorAll('*')).forEach(item => {
  // 这个是需要特殊处理的元素 这个是需要将这个元素转换为canvas然后再导出,这里只提供这种思路 如若需要可以按我这个方法走,不然就直接删掉这个if分支
    if (item.className.includes('fishbone_main') && canvas) {
      item.childNodes[0].remove();
      item.appendChild(canvas);
    }
    let attr = item.getAttribute('data-toword');
    let originItem = contEl.querySelector('[data-toword="' + attr + '"]');
    if (originItem) {
      let sty = getComputedStyle(originItem);
      if (sty.display == 'none ') return item.remove();
      if (sty.opacity === '0') return item.remove();
      setStyle(item, sty);
    }
   // 当页面中存在表格时 需要稍微处理一下,因为直接导出会导致单元格过宽,本次使用的是element的table表格
    if (item.className.includes('department_content_table')) {
      const table = convertElTableToHtmlTableWord(item.childNodes[0], '');
      item.childNodes[0].remove();
      item.innerHTML = table;
    }
  });
  
 function setStyle(ele, sty) {
  if (ele.nodeName.toLowerCase() != 'img') {
    // let sty = getComputedStyle(ele)
    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}`
    );
  }
}
// 处理table转换为html
function convertElTableToHtmlTableWord(elTable, title, infoLeft = '', infoRight = '') {
  if (!elTable) return '';
  // 获取 el-table 的表头数据,包括多级表头
  const theadRows = elTable.querySelectorAll('thead tr');
  // 获取 el-table 的数据行
  const tbodyRows = elTable.querySelectorAll('tbody tr');
  let length = getTotalColumnCount(theadRows[0]);
  // 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
  let htmlTable = '
'; htmlTable += ''; if (infoRight != '') { htmlTable += ``; } else { htmlTable += ``; } htmlTable += ''; // 处理多级表头 theadRows.forEach(row => { htmlTable += ''; const columns = row.querySelectorAll('th'); columns.forEach(column => { const colspan = column.getAttribute('colspan') || '1'; const rowspan = column.getAttribute('rowspan') || '1'; htmlTable += ``; }); htmlTable += ''; }); htmlTable += ''; // 构建数据行 tbodyRows.forEach(row => { htmlTable += ''; const cells = row.querySelectorAll('td'); cells.forEach(cell => { if (cell.querySelector('div')) { htmlTable += ``; } else { htmlTable += ``; } }); htmlTable += ''; }); htmlTable += '
单位:${infoRight}
${column.innerText}
${cell.querySelector('div').innerHTML}${cell.innerText}
'; return htmlTable; }

这样整个html文档的样式就转换为行内样式了

下面就需要将图片转换为base64用于导出

let imgList = domWrap.querySelectorAll('img');
  console.log('加载图片数量: ', imgList.length);
  await Promise.all(
    Array.from(imgList)
      .filter(x => !x.src.startsWith('data'))
      .map(tempimg => {
        let img = new Image();
        img.setAttribute('crossOrigin', 'anonymous');
        img.src = options.proxyHost ? tempimg.src.replace(location.host, options.proxyHost) : tempimg.src;
        return new Promise((resolve, reject) => {
          try {
            img.onload = function() {
              img.onload = null;
              const cw = Math.min(img.width, options.maxWidth);
              const ch = img.height * (cw / img.width);
              const canvas = document.createElement('CANVAS');
              canvas.width = cw;
              canvas.height = ch;
              const context = canvas.getContext('2d');
              context?.drawImage(img, 0, 0, cw, ch);
              const uri = canvas.toDataURL('image/jpg', 0.8);
              tempimg.src = uri;
              const w = Math.min(img.width, 550, options.maxWidth); // word图片最大宽度
              tempimg.width = w;
              tempimg.height = img.height * (w / img.width);
              console.log('img onload...', options.fileName, img.src, img.width, img.height, cw, ch, w, tempimg.height);
              canvas.remove();
              resolve(img.src);
            };
            img.onerror = function() {
              console.log('img load error, ', img.src);
              resolve('');
            };
          } catch (e) {
            console.log(e);
            resolve('');
          }
        });
      })
  );

还需要将canvas转换为 base64,其实可以利用这一步将页面中不想让用户更改的元素先转化为canvas,然后再导出

 
  let canvasList = domWrap.querySelectorAll('canvas');
  console.log('加载canvas数量: ', canvasList.length);
  await Promise.all(
    Array.from(canvasList).map(tempCanvas => {
      let img = new Image();
      img.setAttribute('crossOrigin', 'anonymous');
      return new Promise((resolve, reject) => {
        try {
          let attr = tempCanvas.getAttribute('data-toword');
          let cvs = contEl.querySelector('[data-toword="' + attr + '"]');
          // 由于该canvas是我再导出之前生成,所以需要单独处理一下
          if (!cvs && tempCanvas.className === 'fishbone_canvas') {
            cvs = tempCanvas;
          }
          if (!cvs) return resolve();
          img.src = cvs.toDataURL('image/jpg', 0.8);
          const w = Math.min(cvs.width, options.maxWidth);
          const h = cvs.height * (w / cvs.width);
          img.width = w;
          img.height = h;
          const parent = tempCanvas.parentNode;
          if (tempCanvas.nextSibling) {
            parent.insertBefore(img, tempCanvas.nextSibling);
          } else {
            parent.appendChild(img);
          }
          tempCanvas.remove();
          resolve('');
        } catch (e) {
          console.log(e);
          resolve('');
        }
      });
    })
  );

至此准备工作完全结束 下面就需要将文件导出为word文档 首先需要引入html-docx-js包

 
  const htmlContent = domWrap.innerHTML;
  // 将html数据转化为blob
  const docxBlob = htmlDocx.asBlob(htmlContent, { tableCellMargin: 0 });
  console.log('即将生成文件大小: ', docxBlob.size, (docxBlob.size / 1024 / 1024).toFixed(2) + 'M');
  // 移除iframe内部元素, 方便下次导出
  if (!window.devs) domWrap.remove();
  saveAs(docxBlob, options.fileName + '.docx');
  
  //--------------
  
  function saveAs(blob, fileName) {
  var URL = window.URL || window.webkitURL;
  var a = document.createElement('a');
  fileName = fileName || blob.name || 'download';
  a.download = fileName;
  a.rel = 'noopener';
  a.target = '_blank';
  if (typeof blob === 'string') {
    a.href = blob;
    a.click();
  } else {
    a.href = URL.createObjectURL(blob);
    setTimeout(() => a.click(), 0);
    setTimeout(() => URL.revokeObjectURL(a.href), 2e4); // 20s
  }
}

报错问题 

With statements cannot be used with the "esm" output format due to strict mode

博主在使用html-docx-js包时发现了报错问题导致整个流程卡在了最后一步,在经过长达一天的研究之后,发现可以通过script方式引入 第一步 先将包下载到本地 npm install html-docx-js 将包内关键js文件拿到本地前端将html导出为word文档

前端将html导出为word文档 博主是拿出之后并修改了名字, 然后在index.html文件中导入该js文件 一定要在head标签内导入 不然是不生效的前端将html导出为word文档

自此导出完毕

效果展示前端将html导出为word文档

前端将html导出为word文档

完整代码

toWord.js完整代码

import { formatDate, setStyle, saveAs, convertElTableToHtmlTableWord } from './util';
// maxWidth?: number, title?: string, fileName?: string, time?: string, proxyHost?: string, exclude?: array
export async function toWord(contEl, option, canvas) {
  let options = Object.assign(
    {
      fileName: `word_${formatDate('yyyy-MM-dd hh:mm:ss')}`, // 导出文件名
      maxWidth: 550, // 图片最大宽度,
      title: '', // 导出添加一级标题
      time: '', // 导出添加文章时间
      blob: false, // 返回结果为blob
      exclude: [] // 排除元素选择器
    },
    option || {}
  );
  if (!contEl) return console.warn('未传入导出元素');
  if (typeof contEl === 'string') contEl = document.getElementById(contEl) || document.querySelector(contEl);
  // 设置标记, 方便复制样式
  Array.from(contEl.querySelectorAll('*')).forEach(item => {
    item.setAttribute(
      'data-toword',
      Math.random()
        .toString(32)
        .slice(-5)
    );
  });
  if (!window.contIframe) {
    window.contIframe = document.createElement('iframe');
    window.contIframe.style = 'display: none; width:100%;';
    document.body.appendChild(window.contIframe);
  }
  let cloneEl = contEl.cloneNode(true);
  cloneEl.style.width = getComputedStyle(contEl).width;
  window.contIframe.contentDocument.body.appendChild(cloneEl);
  let domWrap = cloneEl; // .cloneNode(true)
  // 1. 删除隐藏的元素, 并且将元素样式设置为行内样式, 方便word识别
  Array.from(domWrap.querySelectorAll('*')).forEach(item => {
    if (item.className.includes('fishbone_main') && canvas) {
      item.childNodes[0].remove();
      item.appendChild(canvas);
    }
    let attr = item.getAttribute('data-toword');
    let originItem = contEl.querySelector('[data-toword="' + attr + '"]');
    if (originItem) {
      let sty = getComputedStyle(originItem);
      if (sty.display == 'none ') return item.remove();
      if (sty.opacity === '0') return item.remove();
      setStyle(item, sty);
    }
    if (item.className.includes('department_content_table')) {
      const table = convertElTableToHtmlTableWord(item.childNodes[0], '');
      item.childNodes[0].remove();
      item.innerHTML = table;
    }
  });
  //
  // 1.1 删除排除的元素
  if (Array.isArray(options.exclude) && options.exclude.length) {
    options.exclude.forEach(ext => {
      Array.from(domWrap.querySelectorAll(ext)).forEach(item => item.remove());
    });
  }
  // 2. 将图片转为Base64编码, 方便word保存
  let imgList = domWrap.querySelectorAll('img');
  console.log('加载图片数量: ', imgList.length);
  await Promise.all(
    Array.from(imgList)
      .filter(x => !x.src.startsWith('data'))
      .map(tempimg => {
        let img = new Image();
        img.setAttribute('crossOrigin', 'anonymous');
        img.src = options.proxyHost ? tempimg.src.replace(location.host, options.proxyHost) : tempimg.src;
        return new Promise((resolve, reject) => {
          try {
            img.onload = function() {
              img.onload = null;
              const cw = Math.min(img.width, options.maxWidth);
              const ch = img.height * (cw / img.width);
              const canvas = document.createElement('CANVAS');
              canvas.width = cw;
              canvas.height = ch;
              const context = canvas.getContext('2d');
              context?.drawImage(img, 0, 0, cw, ch);
              const uri = canvas.toDataURL('image/jpg', 0.8);
              tempimg.src = uri;
              const w = Math.min(img.width, 550, options.maxWidth); // word图片最大宽度
              tempimg.width = w;
              tempimg.height = img.height * (w / img.width);
              console.log('img onload...', options.fileName, img.src, img.width, img.height, cw, ch, w, tempimg.height);
              canvas.remove();
              resolve(img.src);
            };
            img.onerror = function() {
              console.log('img load error, ', img.src);
              resolve('');
            };
          } catch (e) {
            console.log(e);
            resolve('');
          }
        });
      })
  );
  // 3. 将canvas转为Base64编码, 方便word保存
  let canvasList = domWrap.querySelectorAll('canvas');
  console.log('加载canvas数量: ', canvasList.length);
  await Promise.all(
    Array.from(canvasList).map(tempCanvas => {
      let img = new Image();
      img.setAttribute('crossOrigin', 'anonymous');
      return new Promise((resolve, reject) => {
        try {
          let attr = tempCanvas.getAttribute('data-toword');
          let cvs = contEl.querySelector('[data-toword="' + attr + '"]');
          if (!cvs && tempCanvas.className === 'fishbone_canvas') {
            cvs = tempCanvas;
          }
          if (!cvs) return resolve();
          img.src = cvs.toDataURL('image/jpg', 0.8);
          const w = Math.min(cvs.width, options.maxWidth);
          const h = cvs.height * (w / cvs.width);
          img.width = w;
          img.height = h;
          const parent = tempCanvas.parentNode;
          if (tempCanvas.nextSibling) {
            parent.insertBefore(img, tempCanvas.nextSibling);
          } else {
            parent.appendChild(img);
          }
          tempCanvas.remove();
          resolve('');
        } catch (e) {
          console.log(e);
          resolve('');
        }
      });
    })
  );
  Array.from(contEl.querySelectorAll('*')).forEach(item => {
    item.removeAttribute('data-toword');
  });
  const htmlContent = domWrap.innerHTML;
  const docxBlob = htmlDocx.asBlob(htmlContent, { tableCellMargin: 0 });
  console.log('即将生成文件大小: ', docxBlob.size, (docxBlob.size / 1024 / 1024).toFixed(2) + 'M');
  // 移除iframe内部元素, 方便下次导出
  if (!window.devs) domWrap.remove();
  saveAs(docxBlob, options.fileName + '.docx');
}

 

util完整代码

 
export function formatDate(fmt, date) {
  if (!date) date = new Date();
  if (!fmt) fmt = 'yyyy-MM-dd hh:mm:ss';
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  }
  const o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  };
  for (const k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      const str = o[k] + '';
      fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? str : ('00' + str).substr(str.length));
    }
  }
  return fmt;
}
export function setStyle(ele, sty) {
  if (ele.nodeName.toLowerCase() != 'img') {
    // let sty = getComputedStyle(ele)
    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}`
    );
  }
}
export function saveAs(blob, fileName) {
  var URL = window.URL || window.webkitURL;
  var a = document.createElement('a');
  fileName = fileName || blob.name || 'download';
  a.download = fileName;
  a.rel = 'noopener';
  a.target = '_blank';
  if (typeof blob === 'string') {
    a.href = blob;
    a.click();
  } else {
    a.href = URL.createObjectURL(blob);
    setTimeout(() => a.click(), 0);
    setTimeout(() => URL.revokeObjectURL(a.href), 2e4); // 20s
  }
}
//导出word   e-table转html
export function convertElTableToHtmlTableWord(elTable, title, infoLeft = '', infoRight = '') {
  if (!elTable) return '';
  // 获取 el-table 的表头数据,包括多级表头
  const theadRows = elTable.querySelectorAll('thead tr');
  // 获取 el-table 的数据行
  const tbodyRows = elTable.querySelectorAll('tbody tr');
  let length = getTotalColumnCount(theadRows[0]);
  // 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
  let htmlTable = '
'; htmlTable += ''; if (infoRight != '') { htmlTable += ``; } else { htmlTable += ``; } htmlTable += ''; // 处理多级表头 theadRows.forEach(row => { htmlTable += ''; const columns = row.querySelectorAll('th'); columns.forEach(column => { const colspan = column.getAttribute('colspan') || '1'; const rowspan = column.getAttribute('rowspan') || '1'; htmlTable += ``; }); htmlTable += ''; }); htmlTable += ''; // 构建数据行 tbodyRows.forEach(row => { htmlTable += ''; const cells = row.querySelectorAll('td'); cells.forEach(cell => { if (cell.querySelector('div')) { htmlTable += ``; } else { htmlTable += ``; } }); htmlTable += ''; }); htmlTable += '
单位:${infoRight}
${column.innerText}
${cell.querySelector('div').innerHTML}${cell.innerText}
'; return htmlTable; } //获取总列数 function getTotalColumnCount(theadRows) { let rowColumns = 0; const columns = theadRows.querySelectorAll('th'); columns.forEach(column => { const colspan = column.getAttribute('colspan') || 1; rowColumns += Number(colspan); }); return rowColumns; }

 

结语

本文结束,如果有更好的导出方式欢迎评论区讨论

原文:https://juejin.cn/post/7439556363103584271

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码