将DOM页面中的一部分(动态生成的二维码、动态生成海报)转化为图片,甚至点击下载按钮,将这部分保存为图片下载到手机里或者电脑上,是一个非常常见的需求,而使用canvas转也是非常麻烦,于是找到html2canvas。
前提:npm下载html2canvas0.5.0-beta02版本的依赖包
html2canvas 中npm 包官方文档:www.npmjs.com/package/htm…
在项目中的使用
一、安装html2canvas
npm install --save html2canvas
二、在项目中使用
import html2canvas from 'html2canvas';
页面(saveImg组件)具体如下:
vue文件
<div class="canvasBox" v-show="isShareCards" @touchmove.prevent>
<!-- 需裁剪的位置 -->
<div class="canvasMain" ref="canvasMain" v-if="!img">
<div class="canvasContent">
<div class="top">
<div class="avatar">
<img :src="data.avatar_img ? data.avatar_img : default_img" alt="avatar" />
</div>
<p class="name">{{data.contact_name}}</p>
</div>
<div class="content">
<h3 class="title">{{lang.title}}</h3>
<div class="base_info">
<div class="tel">
<img src="../../assets/img/sharePromotion/popup/samll_phone.png" alt="phone_icon" />
<span>{{data.contact_number}}</span>
</div>
<div class="company">
<img src="../../assets/img/sharePromotion/popup/samll_company.png" alt="company_icon" />
<span>{{data.license_name}}</span>
</div>
</div>
<div class="address">
<img src="../../assets/img/sharePromotion/popup/samll_address.png" alt="address_icon" />
<span>{{data.contact_address}}</span>
</div>
<div class="business_scope">
<h3>{{lang.businessScopeTitle}}</h3>
<div class="business_content">
<p v-for="item,index in data.business_scope" :key="index">{{item}}</p>
</div>
</div>
<div class="more_tips">{{lang.moreTips}}</div>
</div>
</div>
<div class="kong"></div>
</div>
<!-- 存放裁剪base64图片的位置 -->
<img v-if="img" :class="['canvasimg', (isApp || isWeixin) ? '' : 'canvasimg-web' ]" :src="img" alt="">
<!-- 保存图片的按钮 -->
<div class="footer">
<div class="top">
<img src="../../assets/img/sharePromotion/popup/press_icon.png" alt="press_img" />
<span>{{lang.pressTips}}</span>
<div class="f-kong"></div>
</div>
<div class="bottom">
<div class="cancel_btn" :isShareShow="isShareCards" @click="cancelHandle">{{lang.cancel}}</div>
</div>
</div>
</div>
less文件
@import '../../assets/less/mixin.less';
.canvasBox {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, .8);
/* 需裁剪的位置 */
.canvasMain {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 2.56rem;
width: 5.5rem;
height: 7.7rem;
border-radius: 0.12rem;
margin: auto;
overflow: hidden;
.canvasContent {
width: 100%;
height: 100%;
margin: auto;
overflow: hidden;
// background-color: #fff;
// background-repeat: no-repeat;
// background-size: 100% 100%;
// background-position: center center;
background: #fff no-repeat center center / 100% 100%;
position: absolute;
z-index: 90;
font-size: 24px;
color: #333;
.top {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding-top: 28px;
.avatar {
width: 98px;
height: 98px;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.name {
width: 100%;
font-size: 26px;
font-weight: bold;
padding-top: 20px;
.textOverflow();
}
}
.content {
.title {
color: #666;
padding-top: 20px;
font-size: 26px;
font-weight: normal;
}
.base_info {
display: flex;
justify-content: space-between;
padding: 38px 30px 40px 30px;
border-bottom: 1px solid #efefef;
div {
text-align: left;
width: 50%;
display: flex;
justify-content: flex-start;
img {
width: 26px;
height: 26px;
}
span {
padding-left: 12px;
flex: 1;
line-height: 28px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.address {
display: flex;
height: 98px;
justify-content: flex-start;
padding: 20px 30px 25px 30px;
border-bottom: 1px solid #efefef;
box-sizing: border-box;
img {
width: 26px;
height: 26px;
}
span {
text-align: left;
line-height: 28px;
padding-left: 12px;
overflow: hidden;
text-overflow: ellipsis;
}
}
.business_scope {
height: 276px;
padding: 20px 30px 18px 30px;
border-bottom: 1px solid #efefef;
box-sizing: border-box;
overflow: hidden;
display: flex;
justify-content: flex-start;
flex-direction: column;
h3 {
color: #666;
font-size: 24px;
font-weight: normal;
height: 26px;
padding-bottom: 16px;
}
.business_content {
flex: 1;
p {
line-height: 28px;
text-align: left;
overflow: hidden;
padding-top: 2px;
}
overflow: hidden;
}
}
.more_tips {
text-align: left;
padding: 20px 30px 0px 30px;
}
}
}
.kong {
width: 100%;
height: 100%;
z-index: 9999;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
/* 存放裁剪base64图片的位置 */
.canvasimg {
z-index: 9999999;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 276px;
// position: fixed;
// left: 100px;
// bottom: 356px;
width: 550px;
height: 776px;
margin: auto;
pointer-events:auto;
-webkit-touch-callout: default;
}
.canvasimg-web {
width: 500px;
height: 726px;
}
/* 保存图片的按钮 */
.footer {
position: fixed;
bottom: 0;
left: 0;
height: 256px;
width: 100%;
background-color: #efefef;
color: #666;
font-size: 24px;
.top {
height: 158px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
img {
width: 34px;
padding-right: 10px;
}
.f-kong {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 999;
}
}
.bottom {
background-color: #fff;
height: 98px;
line-height: 98px;
.cancel_btn {
color: #333;
font-size: 30px;
}
}
}
}
js文件
/* eslint-disable */
import lang from '../../i18n/sharePromotion';
import toast from '../toast';
import loading from '../../components/loading/index';
import html2canvas from 'html2canvas';
export default {
name: 'saveImg',
data() {
return {
lang: lang,
img: '',
imgurl: '',
firstFlag: true,
default_img: require('../../assets/img/sharePromotion/default_avatar.png'),
canvas: null,
imgName: 'poster'
};
},
props: ['isShareCards', 'data', 'isApp', 'isWeixin'],
watch: {
data(newData) {
this.data = newData;
},
dataURL(newImg) {
this.img = newImg;
}
},
created () {
this.firstFlag = true
},
mounted() {
},
methods: {
// 取消
cancelHandle() {
this.$emit('update:isShareCards', false);
this.img = '';
},
/* canvas裁剪 */
getCanvas(imgUrl) {
loading.show();
const that = this;
const canEle = this.$refs.canvasMain; // 获取存放截图的包裹的上一级dom对象(原生)
const width = canEle.offsetWidth; // 获取dom宽
const height = canEle.offsetHeight; // 获取dom高
const canvas = document.createElement('canvas'); // 创建一个canvas节点
const scale = 2; // 定义任意放大倍数 支持小数
const context = canvas.getContext('2d');
canvas.width = width * scale; // 定义canvas 宽度 * 缩放
canvas.height = height * scale; // 定义canvas高度 *缩放
context.scale(scale, scale);
const rect = canEle.getBoundingClientRect(); //获取元素相对于视察的偏移量
context.translate(-rect.left, -rect.top); //设置context位置,值为相对于视窗的偏移量负值,让图片复位
const options = {
useCORS: true, // 【重要】开启跨域配置
tainttest: true, // 检测每张图片都已经加载完成
scale: scale, // 添加的scale 参数
backgroundColor: null, // 避免下载不全
canvas, // 自定义 canvas
width: width, // dom 原始宽度
height: height,
};
const imgs = new Image();
imgs.onload = function () {
html2canvas(canEle, options).then(canvas => {
that.canvas = canvas;
canvas.style.width = width+"px";
canvas.style.height = height+"px";
let dataURL = canvas.toDataURL("image/png");
that.img = dataURL;
that.firstFlag = false
const context = canvas.getContext('2d');
// 关闭抗锯齿 保证生成的分享图是清晰的
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
loading.hide();
});
};
imgs.onerror = () => {
// 安卓机canvas.toDataUrl的时候导出的base64图片没有base64前缀 走了 imgs.onerror
html2canvas(canEle, options).then(canvas => {
canvas.style.width = width+"px";
canvas.style.height = height+"px";
let dataURL = canvas.toDataURL("image/png");
that.img = dataURL;
that.firstFlag = false
const context = canvas.getContext('2d');
// 关闭抗锯齿 保证生成的分享图是清晰的
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
loading.hide();
});
}
imgs.src = imgUrl;
},
// 获取设备像素密度的方法
getPixelRatio(context){
var backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStore;
},
},
};
组件在具体页面上的使用:
// vue
<saveImg ref="child" :isApp.sync="isApp" :isWeixin.sync='isWeixin' :isShareCards.sync="isShareCards" :data.sync="data"></saveImg>
// js
import saveImg from '../../components/saveImg/saveImg.vue';
@Component({
components: {
saveImg,
}
});
export default class Sharepromotion extends Vue {
// 是否为本地app-web
@getter('isApp')
isApp: number;
// 是否是微信环境
@state('appInfo', 'isWeixin')
isWeixin: boolean;
@state('shareOperatorInfo', 'data')
data: any;
lang = lang;
isShareCards = false; // 是否保存图片弹窗
default_img = require('../../assets/img/sharePromotion/default_avatar.png');
shareCards() {
this.isShareCards = true; // 显示保存图片
if (this.isShareCards ) {
this.$nextTick(function() {
const handle: any = this.$refs.child;
handle.getCanvas((!this.data.avatar_img ? this.data.avatar_img : this.default_img));
});
}
}
}
遇到的Bug及对策
一、图片跨域无法加载问题
1、前端js设置:useCORS: true
这里有几个关键的地方:
(1)allowTaint: true 和 useCORS: true 都是解决跨域问题的方式,不同的是使用allowTaint 会对canvas造成污染,就无法读取其数据,不能使用画布的toBolb(),toDataURL()或getImageData()方法,否则会出错,所以这里不能使用allowTaint: true
(2)在跨域的图片里设置 crossOrigin="anonymous" 并且需要给imageUrl加上随机数
(3)canvas.toDataURL('image/jpg') 是将canvas转成base64图片格式。
2、服务端设置CORS
解决跨域最常用的方法是跨域资源共享,我们将图片服务器的response header
设置。
图片若存放在阿里云之类的,则也需要处理【接口的返回的图片地址是允许跨域的+项目的域名(不同环境)允许跨域,那么各个环境看都是可以运行的。】
开发过程中,前后端设置配置都OK,但是就是还是报错,那么一个关键点就是运行环境如何看的问题了。
使用的图片不能在本地,因为图片可能还在本地服务器上,你的代码也还在本地;
若放在服务器上了,是没有问题的。想要查看有无问题,则把浏览器的跨域给设置一下,然后跑本地代码,若没有问题则该问题也是OK的。
切记:图片url代码运行环境一定要保持一致!
二、下载最新依赖包报错
当解决了图片跨域问题(1、接口返回的图片是允许跨域的---由服务器那边控制;2、域名的控制:允许域名跨域),还是报错:
一开始使用html2canvas包的版本为1.0.0-rc.5
,减低至html2canvas@0.5.0-beta3
的包,但是用npm 指定版本下载:npm install html2canvas@0.5.0-beta3
实际下载下来的却是beta4
的版本;因此指定版本下载后还是有上述截图的问题;而后使用vue引入js插件来进行使用,竟然解决了。
1、在vue+ts项目中如何引入原生js插件来进行使用
在config/index.js
进行配置
此方式导致的问题:整个文件都打包到项目中,导致项目体积变大,须优化!
2、引入稳定的版本html2canvas 0.5.0-beta3,且解决了跨越问题;但如果截图区域有base64图片依然不成功。
原因是node包里面对解析的base64后面加了时间戳导致src不能识别,所以修改了config/templateInlineResources/html2canvas.js
包里面1263行
代码为:
if (src.match(/data:image\/.*;base64,/i)) {
self.image.src = src;
} else {
self.image.src = src +'?'+ new Date().getTime();
}
此刻完美解决报错~
3、优化
npm进行下载包时,无法正确对应的下载0.5.0-beta3的版本,下载下来的是0.5.0-beta4的版本;
直接引入html2canvas.js插件的方式,每个页面都会有这个插件在,导致项目体积过大;
npm 形式下载的话,在需要的时候才会用,所以最后决定降低到0.5.0-beta02
的版本尝试看看改包是否有用?最后beta2可以用
,就出现了位偏移的问题;加上需要改动包里边的内容,因此采取把该包直接拉取,然后进行改动后,放入自己git里边进行引用。
配置如下:
- package.json的文件
- package-log.json的文件
三、在安卓手机上截屏清晰度问题
主要是scale的参数作用。
1、使用设备像素密度
// 获取设备像素密度的方法
getPixelRatio(context){
var backingStoreRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return (window.devicePixelRatio || 1) / backingStoreRatio;
}
2、放大比例倍数设置为固定值
const scale = 2; // 定义任意放大倍数 支持小数
3、关闭抗锯齿 保证生成的分享图是清晰的
// 关闭抗锯齿 保证生成的分享图是清晰的
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
四、出现位偏移的问题
const context = canvas.getContext('2d');
var rect = canEle.getBoundingClientRect(); //获取元素相对于视察的偏移量
context.translate(-rect.left, -rect.top); //设置context位置,值为相对于视窗的偏移量负值,让图片复位