html2canvas在vue2中的应用-移动端

3,088 阅读7分钟

将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));
            });
        }
    }
}

03.png

遇到的Bug及对策

一、图片跨域无法加载问题

04.png

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设置。

图片若存放在阿里云之类的,则也需要处理【接口的返回的图片地址是允许跨域的+项目的域名(不同环境)允许跨域,那么各个环境看都是可以运行的。】

08.png

开发过程中,前后端设置配置都OK,但是就是还是报错,那么一个关键点就是运行环境如何看的问题了。

使用的图片不能在本地,因为图片可能还在本地服务器上,你的代码也还在本地;

若放在服务器上了,是没有问题的。想要查看有无问题,则把浏览器的跨域给设置一下,然后跑本地代码,若没有问题则该问题也是OK的。

切记:图片url代码运行环境一定要保持一致!

二、下载最新依赖包报错

当解决了图片跨域问题(1、接口返回的图片是允许跨域的---由服务器那边控制;2、域名的控制:允许域名跨域),还是报错:

07.png

一开始使用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进行配置

config.png

此方式导致的问题:整个文件都打包到项目中,导致项目体积变大,须优化!

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();
 }

05.png 此刻完美解决报错~

3、优化

npm进行下载包时,无法正确对应的下载0.5.0-beta3的版本,下载下来的是0.5.0-beta4的版本;

直接引入html2canvas.js插件的方式,每个页面都会有这个插件在,导致项目体积过大;

npm 形式下载的话,在需要的时候才会用,所以最后决定降低到0.5.0-beta02的版本尝试看看改包是否有用?最后beta2可以用,就出现了位偏移的问题;加上需要改动包里边的内容,因此采取把该包直接拉取,然后进行改动后,放入自己git里边进行引用。

配置如下:

  • package.json的文件

config_package01.png

  • package-log.json的文件

cofing_package02.png

三、在安卓手机上截屏清晰度问题

主要是scale的参数作用。

1、使用设备像素密度

// 获取设备像素密度的方法
getPixelRatio(context){
       var backingStoreRatio = context.webkitBackingStorePixelRatio ||
            context.mozBackingStorePixelRatio ||
            context.msBackingStorePixelRatio ||
            context.oBackingStorePixelRatio ||
            context.backingStorePixelRatio || 1;
       return  (window.devicePixelRatio || 1) / backingStoreRatio; 
  }

config_canvas01.png

2、放大比例倍数设置为固定值

const scale = 2; // 定义任意放大倍数 支持小数

config_canvas02.png

3、关闭抗锯齿 保证生成的分享图是清晰的

// 关闭抗锯齿 保证生成的分享图是清晰的
 context.mozImageSmoothingEnabled = false;
 context.webkitImageSmoothingEnabled = false;
 context.msImageSmoothingEnabled = false;
 context.imageSmoothingEnabled = false;

config_canvas04.png

四、出现位偏移的问题

const context = canvas.getContext('2d');
var rect = canEle.getBoundingClientRect(); //获取元素相对于视察的偏移量
context.translate(-rect.left, -rect.top); //设置context位置,值为相对于视窗的偏移量负值,让图片复位

config_canvas03.png