研究背景:最近做的项目中有功能项是导出PDF报告,由于页面内容包含多种统计图,后台人员不好实现,那就前端来呗~ 由于是内网项目,图片都读取服务器,导致下载pdf中图片出不来,我太南了。
---------------------------------------------------------------------------分割线------------------------------------------------------------------------------
导Pdf方法大家应该都知道了,先把html转成canvas,再转成pdf。
由于项目用的vue框架+typescript语言开发,提前在这里声明一下,方便各位朋友先做语法了解。
用到了第三方库 html2canvas和jspdf,网上都有这里就不贴地址了(或者没事的时候再贴)。
先说一下发现问题的思路吧:
1.一开始啥都不提示,我以为被盖住了,但是把pdf背景色去掉,还是没有,说明图片是真没导出来。
2.用本地项目图片试试,成功了(这很重要,下面会讲);用网络图片试试,也导出来了,那会不会是跨域的问题?
3.找网上资料,有说修改 useCORS 改为true允许跨域,allowTaint改为true的,现在把图片的路径固定写死一张服务器图片,还真就提示了
破案了破案了,嫌犯终于承认了。哎~别慌,怎么回事呢,去看第二条
html2canvas没拿到图片的资源。
由此,大体知道了症结:
普通的img标签src属性链接服务器图片,是不会引起跨域问题的,这也是一开始没想到的原因,那咋会报跨域?稍微看看(万一没看懂说错了呢)就能知道,html转canvas时对html里的图片、echartscanvas又单独做了一次处理,他用js方式引的图片,才造成了跨域问题。
但是咋搞呢,推荐一款神奇的网站,名叫【百度一下,你就知道】(咳咳,彦宏老哥,记得打钱)
在网上搜了如下内容
作者用下载到本地再引用的方法,在此向作者致敬,把跨域请求改成本地文件,这不就解决了?但我不想搞这么麻烦,有没有更好的办法?
用过vue开发的小伙伴都知道,vue引用本地图片的方式是require('项目相对路径'),到了页面其实就是转成了base64码
我能不能直接把base64码放在图片的src属性上,这样相当于动态存放到我本地,就不会出现跨域了?导出看看,结果是成功了,那么我只需要在每次导出时把动态图片地址转成base64码就可以了,do it。
上代码
data(){
imgSrc: null
}
mounted(){
//...执行你的查询方法获取图片网络路径,仅为了演示所以写成回调方式
this.search().then(()=>{
this.toBase64('图片全路径')
})
}
methods(){
toBase64(imgUrl: any) {
try {
let image = new Image()
// 解决跨域问题
image.setAttribute('crossOrigin', 'anonymous')
let imageUrl = imgUrl
image.src = imageUrl
// image.onload为异步加载
image.onload = () => {
let canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
let context: any = canvas.getContext('2d')
context.drawImage(image, 0, 0, image.width, image.height)
let quality = 0.8
// 这里的dataurl就是base64类型
// 使用toDataUrl将图片转换成jpeg的格式,不要把图片压缩成png,因为压缩成png后base64的字符串可能比不转换前的长!
let dataurl = canvas.toDataURL('image/jpeg', quality)
this.imgSrc = dataurl
}
} catch (e) {
console.log(e)
}
}
}
页面上直接赋值即可
<img :src="imgSrc">
完全不会影响到html2pdf代码,这样导出的内容就能看到图片了。
到这里,我们站在巨人们的肩膀上,搞定这个问题啦,和巨人们的思路都是化跨域为本地,不过vue框架的好处就体现出来了,哈哈哈。
下面上vue+TS的关键代码
import html2Canvas from 'html2canvas'
export default class SingleHealthAnalysisModal extends Vue{
imgSrc: any = null
created() {
this.initBasicInfo(this.equipId)
}
render(){
<div>
<img
style={{ width: '100%', height: '100%' }}
src={this.imgSrc ? this.imgSrc : require('@/assets/images/default_pic.jpg')}>
</img>
<a-button
type="primary"
on-click={() => {
this.$AntMessage.loading({ duration: 0, content: '文件导出中...' })
this.printOut('装备健康情况分析')
}}
>
导出PDF
</a-button>
</div>
}
initBasicInfo(equipId:any){
// 查询详情请求
.then((res:any)=>{
if (res.code === window.CROSS_CODE) {
this.imgSrc = null // 重置
this.equipBasicInfo = res.map
if (this.equipBasicInfo.equipmentImage && this.equipBasicInfo.equipmentImage !== 'null') {
this.toBase64(window.config.imgShowUrl + this.equipBasicInfo.equipmentImage.split(',')[0])
}
}
})
}
printOut(name: any) {
try {
let shareContent: any = document.querySelector('.allLifeDetail') //需要截图的包裹的(原生的)DOM 对象
let width = shareContent.clientWidth //获取dom 宽度
let height = shareContent.clientHeight //获取dom 高度
let canvas: any = document.createElement('canvas') //创建一个canvas节点
let scale = 1 //定义任意放大倍数 支持小数
canvas.width = width * scale //定义canvas 宽度 * 缩放
canvas.height = height * scale //定义canvas高度 *缩放
canvas.style.width = shareContent.clientWidth * scale + 'px'
canvas.style.height = shareContent.clientHeight * scale + 'px'
canvas.getContext('2d').scale(scale, scale) //获取context,设置scale
let opts = {
scale: scale, // 添加的scale 参数
canvas: canvas, //自定义 canvas
logging: false, //日志开关,便于查看html2canvas的内部执行流程
width: width, //dom 原始宽度
height: height,
useCORS: true, // 【重要】开启跨域配置
// allowTaint: true,
// taintTest: true,
// logging: true,
}
html2Canvas(shareContent, opts).then(() => {
var contentWidth = canvas.width
var contentHeight = canvas.height
//一页pdf显示html页面生成的canvas高度;
var pageHeight = (contentWidth / 592.28) * 841.89
//未生成pdf的html页面高度
var leftHeight = contentHeight
//页面偏移
var position = 0
//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
var imgWidth = 595.28
var imgHeight = (592.28 / contentWidth) * contentHeight
var pageData = canvas.toDataURL('image/jpeg', 1.0)
var PDF = new jsPDF('', 'pt', 'a4')
if (leftHeight < pageHeight) {
PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
} else {
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight
position -= 841.89
if (leftHeight > 0) {
PDF.addPage()
}
}
}
this.$AntMessage.destroy()
PDF.save(name + '.pdf') // 这里是导出的文件名
})
} catch (error) {
this.$AntMessage.destroy()
this.$AntMessage.error('导出失败')
}
}
toBase64(imgUrl: any) {
try {
imgUrl = imgUrl.replace('\\', '/')
// 一定要设置为let,不然图片不显示
let image = new Image()
// 解决跨域问题
image.setAttribute('crossOrigin', 'anonymous')
let imageUrl = imgUrl
image.src = imageUrl
// image.onload为异步加载
image.onload = () => {
var canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
var context: any = canvas.getContext('2d')
context.drawImage(image, 0, 0, image.width, image.height)
var quality = 0.8
// 这里的dataurl就是base64类型
// 使用toDataUrl将图片转换成jpeg的格式,不要把图片压缩成png,因为压缩成png后base64的字符串可能比不转换前的长!
let dataurl = canvas.toDataURL('image/jpeg', quality)
this.imgSrc = dataurl
}
} catch (e) {
console.log(e)
}
}
}
参考文章:
Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may n