几种常见下载方式总结&html2canvas截图相关

下载方法一

window.open('https://xxx.xxx');

最简单的下载方式,表现为新开窗口访问服务器上的文件执行下载。

  • 优点: 代码量最少。

  • 缺点:

  1. 只支持get方法,而且参数太长可能会出现问题。

  2. 浏览器新开窗口开始下载后tab页会瞬间关闭,可能交互不太友好,这个看设计要求了。

下载方法二

使用form表单提交。基本流程为创建form元素,在form内创建隐藏的input元素,input的name属性为下载接口传参的字段名,value属性为字段对应的值。form的method属性代表接口请求方式(一般是get或post),action属性为接口地址。最后调用form元素的submit方法执行下载

可以动态创建form和input元素

// 需要传给后台的参数
const downloadObj = {
    name: 'a',
    value: 1
};
const downLoadForm = document.createElement('form');
document.body.appendChild(downLoadForm);
Object.keys(downloadObj).forEach(key => {
    const input = document.createElement('input');
    input.type = 'hidden';
    input.name = key;
    input.value = downloadObj[key];
    downLoadForm.appendChild(input);
});
downLoadForm.method = 'get'; // get/post
downLoadForm.action = 'https://xxxx.xxxx';
downLoadForm.submit();
document.body.removeChild(downLoadForm);

这种方法的优点是支持post方法下载,同时下载过程不会新开窗口。不过如果下载文件过大或者接口响应过慢可能会导致用户点了下载之后不仔细看会觉得发现页面没什么变化...(仔细看的话左下角会有一个浏览器自带的提示文字)

我之前做过的下载基本都是用的以上两种方式,而且纵观一些大型网站的下载基本也是以上两种居多。直到最近做的一个项目已经不适合使用上面两种方式了。需求是这样的,用户点击下载,前端生成页面内容截图(因为有很多echarts图表)发给后台,后台生成pdf返回给前端下载。首先传图片给后台的话使用的是文件流,其次这次是和兄弟部门合作开发,后台服务器不保存文件(有些历史原因),只返回文件流给前端。由这两点导致以上两种下载方法都没法使用了。于是我综合网络各种前辈资料总结出了第三种下载方法:

下载方法三

流程是使用常规的接口请求拿到后台返回的文件流,由前端通过Blob对象转化为所需文件,使用动态创建a标签并执行点击事件完成下载

直接上代码(使用vue+axios)
2020.07.31更新兼容IE


// import {postPDFDownData} from 'api.js';
// element的loading
const loading = this.$loading({
    lock: true,
    text: '拼命下载中...',
    spinner: 'el-icon-loading'
});
// ...
// 使用封装好的方法获得图片文件对象
const imgFile = convertBase64UrlToFile(base64);
const formData = new FormData();
formData.append('img', imgFile);
formData.append('fileName', 'report');
// 传给后台的是formData对象
postPDFDownData(formData)
    .then(res => {
        let blob = new Blob([res], { type: 'application/pdf' }); // 根据要求转化为不同的二进制对象
        // 兼容IE
        if (window.navigator.msSaveBlob) {
          window.navigator.msSaveBlob(blob, `${fileName}.pdf`);
        } else {
          const a = document.createElement('a');
          a.href = window.URL.createObjectURL(blob);
          a.download = fileName || timestamp + '.pdf';
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
          window.URL.revokeObjectURL(a.href); // 释放掉blob对象
        }
    })
    .catch(() => {
        this.$message.error('暂无数据,请稍后重试');
    })
    .finally(() => {
        loading.close();
    });
// ----------------------------------
// request.js 将axios封装了下
// api.js 下载pdf的方法
export function postPDFDownData(data) {
    return request({
        url: `xxxx/xxx`,
        method: 'post',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, // 根据接口要求设置请求头
        responseType: 'blob', // 设置返回类型,必须
        data: data
    });
}

这种下载方式优点很明显,过程可以控制,体验较好,但是下载的文件过大可能等待的时间会比较长(下载文件流加上转换),而页面可能整体都在转圈,会导致用户无法进行其他操作。

补充convertBase64UrlToFile方法

/**
 * @description: 将以base64的图片url数据转换为File文件对象
 * @param {String} [urlData] 用url方式表示的base64图片数据
 * @param {String} [fileName] 上传时的文件名
 */
export function convertBase64UrlToFile(urlData, fileName) {
  const arr = urlData.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  // return new File([u8arr], fileName, { type: mime });
  // 返回Blob以兼容IE
  return new Blob([u8arr], { type: mime });
}

html2canvas生成页面截图踩坑

介绍:html2canvas能够实现在用户浏览器端直接对整个或部分页面进行截屏,大致的原理是通过读取DOM将页面所需的内容渲染成canvas元素。

前排提醒:高版本的html2canvas截图可能存在bug,我在项目中使用的版本是0.5.0-alpha1

正是通过这个插件我才能够将页面截图下来传给后台。当然,一系列问题也就随之而来...

  • 首先,它生成的是一个canvas元素,要想获取传给后台的文件流或者base64,可以调用toDataURL方法转化为base64,至于base64如何转化为文件对象本文不作介绍,网上有很多这方面的资料。

  • 其次,由于页面上截图区域过大,出现了滚动条,直接下载的话滚动条后面的内容直接显示的是空白。这个问题当时困扰了我很久,网上也没有我能用的解决方案,比较常见的是使用cloneNode方法将截图区域克隆一份放到body下然后将克隆的节点转化为canvas,但是我的页面上大部分是echarts图,这种已经是canvas元素的克隆之后会出现无法显示的情况。这个时候我找到了另一个方法getImageData,它可以复制canvas,但是网上评论说这个方法复制缓慢且不推荐在浏览器上使用,再则我当时用了一下这个方法并没有成功,而且页面上的图表很多也不能确定数量,所以我果断放弃了这个方案。最后我在一篇文章的评论中看到有人说他当时也是两页,但是图表都在第二页,所以直接将截图的节点放到了body中可以完整下载。于是我灵机一动,计上心来。将整个截图区域从它的父元素中移除,并放到body中,待下载完成后再将截图元素还原。因为下载过程整个页面都是loading,所以对节点的操作不会让用户觉得有问题。最后一试,可行!

关键代码如下

export function initImage(id, boxid, cb) {
    const originNode = document.getElementById(id); // 截图区域
    const boxNode = document.getElementById(boxid); // 截图区的父节点
    boxNode.removeChild(originNode); // “移花接木”
    document.body.appendChild(originNode);
    html2canvas(originNode).then(canvas => {
        document.body.removeChild(originNode); // 还原
        boxNode.appendChild(originNode);
        return cb(canvas.toDataURL());
    });
}

// 调用
initImage('reportPart', 'reortPartBox', base64 => {
 // 里面就是上文写的下载的代码
}
  • 眼看问题都解决了,但是测试的时候又出现的新的问题,因为我做的项目中是可以图和表格切换的,当切换到表格的时候,如果表格被压缩到很小(没错,项目里面还带拖拽编辑功能),表格出现了滚动条之后,点击下载,会把表面上overflow的内容全部显示出来,内容全部错乱,结果就是做了半天的下载可能都白做了。。。于是乎我在截取节点时改变了一下overflow,perfect!

最后代码长这样(当然,时间仓促,方法的通用性几乎没有,凑合用了)

export function initImage(id, boxid, cb) {
    const originNode = document.getElementById(id);
    const boxNode = document.getElementById(boxid);
    const chartBoxDOM = document.querySelectorAll('.chart-component');
    // 解决表格出现滚动条时截图出现的问题
    chartBoxDOM.forEach(v => {
        v.style.overflow = 'hidden';
    })
    boxNode.removeChild(originNode);
    document.body.appendChild(originNode);
    html2canvas(originNode).then(canvas => {
        chartBoxDOM.forEach(v => {
            v.style.overflow = 'auto';
        })
        document.body.removeChild(originNode);
        boxNode.appendChild(originNode);
        return cb(canvas.toDataURL());
    });
}

html2canvas还有些其他的坑,由于我的项目没有涉及到,所以没有研究,总之有问题看issues大半都能得到解决,剩下的。。办法也都是人想出来的,不是吗。

最后,关于下载方式的选择还是得看需求和后台的情况而定。目前我觉得也没有一个最优的解决方案。


2020.6.30更新

  • html2canvas @1.0.0-alpha.12版本比较好用,解决了不在可视区无法截图的问题,所以之前用的移花接木的旁门左道也不需要了。

  • 0.5.0-alpha1版本存在一个问题,element的progress组件无法正确在截图中显示,1.0.0-alpha.12版本可解决这个问题。

  • 高版本截图存在的问题,我的页面有一个灰色的底色,中间部分的内容居中显示,背景为fff白色带有box-shadow,使用高版本截图时,内容部分的背景色会出现问题(灰色白色重叠)

  • https://www.jsdelivr.com/ 这个网站的cdn比较好,还能看到插件使用率高的版本

你可能感兴趣的:(几种常见下载方式总结&html2canvas截图相关)