微信小程序实现截图并下载以及wxml2Canvas踩坑记录

7,981 阅读5分钟

(本文只是个人的学习记录,请大佬指出错误。🙊)最近在工作里,遇到一个微信小程序的简单需求,需要把页面截图并下载。虽然我的第一想法是就不能让用户自己手动截图吗,当然也就自己想想吧😥。之前做过一个生成海报的功能,但跟这次有点不一样,涉及到动态页面数据和样式,所以尝试了其他方法。

目前我用过的方法,大致有三种:

1. 直接用canvas自己手动绘制;

2. 用wxml2Canvas等工具帮助生成canvas对象并生成图片

3. 页面嵌入webview,利用小程序的JSSDK,保存图片.

先说下我的个人总结:像布局和数据不会有变化、不复杂的情况下,直接用canvas会很方便;但如果数据是动态的,页面的一些样式也会随之变化,比较难做适配的话,建议用webview的方法。其实复杂情况下,用方法1也是可以实现的,比如多行数据不知道高度的情况下,可以通过先在页面上显示元素,然后获取所需要的样式参数等。但同时工作量也会大大增加,后期的调试和适配也是个让人头疼的问题,所以弊端还是存在的😖。最后的重点:不推荐使用wxml2Canvas、wxmlToCanvas等工具,坑比较多,某些样式也会变形。下面我对这三个方法简单说一下:

1. 直接使用小程序的canvas生成分享海报

分享海报大致分为三个步骤:一张图作为背景、图上画一个白底圆形、将生成的带参数二维码画到白色圆形里。

首先定义一个canvas,由于图片的比例可能不可知,canvas的宽高根据图片的宽高去确定。

<canvas class='canvas' :style="{height:canvasH+'px',width:canvasW+'px'}" 
canvas-id="mycanvas"/>

获取图片的宽高比例的方法,就可以确定你要画的图的尺寸了。(比如可以以屏幕宽度为基础尺寸来决定你的图片尺寸)

<image :src = "imageUrl" mode = "widthFix" @load= "onImgLoad" v-show = "false"> </image> 
onImgLoad(e)
{
    let imgW = e.detail.width; //图片原始宽度 
    let imgH = e.detail.height; //图片原始高度 
    this.scale = imgW / imgH; //比例计算 
}

然后,获取后端返回的带参数二维码(base64格式),转为图片保存至本地(canvas不能直接绘制网络图片,背景图也是要先保存在本地)

//声明文件系统
const fs = wx.getFileSystemManager();
//随机定义路径名称
var times = new Date().getTime();
var codeimg = wx.env.USER_DATA_PATH + '/' + times + '.png';

//将base64图片写入
fs.writeFile({
    filePath: codeimg,
    data: data.data.wxacode, // base64
    encoding: 'base64',
    success: () => {
        //写入成功了的话,新的图片路径就能用了
        that.localCodeUrl = codeimg
        // 创建canvas图片
        that.createNewImg();
    }
});

最后一步,就是绘制了

createNewImg(){
    let imgH = this.canvasH //前面定义的canvas的高度
    let imgW = this.canvasW //canvas的宽度
    let ctx = wx.createCanvasContext('mycanvas', this)
// 绘制白色背景
    ctx.setFillStyle("#fff")
    ctx.fillRect(x, y, imgW, imgH) // context.fillRect(x,y,width,height);
//绘制图片
    ctx.drawImage(this.localBackgroundUrl, x, y, imgW, imgH) // 绘制背景图
// 绘制标题
    ctx.setFontSize(18)
    ctx.setFillStyle('#333')

// 绘制二维码,以图片的宽高,通过比例确定二维码的位置,防止错位
    let xyCircle = [1.2, 0.79, 0.64, 0.18]
    let xyCode = [1.05, 1.14, 0.31]
// 画出白色圆形背景
    ctx.arc(imgW * xyCircle[0], imgH * xyCircle[1] + imgW * xyCircle[2], imgW * xyCircle[3], 0, 2 * Math.PI)
    ctx.setFillStyle('#FFFFFF')
    ctx.fill()
// 绘制二维码
    ctx.drawImage(this.localCodeUrl, imgW * xyCode[0], imgH * xyCode[1], imgW * xyCode[2], imgW * xyCode[2])
// 显示绘制
    ctx.draw()
//将生成好的图片保存到本地,需要延迟一会,绘制期间耗时
    this.timer = setTimeout(() => {
        wx.canvasToTempFilePath({
            canvasId: 'mycanvas',
            x:x, // 画布区域左上角坐标
            y:y, // 画布区域左上角坐标
            width:imgW,
            height:imgH,
            // destWidth: imgW*2,  // 决定了保存图片的清晰程度
            // destHeight: imgH*2,  // 决定了保存图片的清晰程度
            success: function(res) {
                let tempFilePath = res.tempFilePath
                that.loadImagePath = tempFilePath // 本地图片地址,现在可以下载了
            },
            fail: function(res) {
                console.log(res)
            },
            complete:function(){
                that.canvasType = true
            }
        }, that)
    }, 500)
}

2. wxml2Canvas的使用和踩坑记录

该工具是将wxml元素转换成canvas元素,从而生成图片。它支持两种格式,一个是JSON形式,根据type将元素一个个画上去,比如:

{
    type: 'rect',
    x: 10,
    y: 10,
    style: {
        width: 150,
        height: 80,
        }
}

这样显然也是比较麻烦的,这显然不符合我的要求。还有一种就是wxml转换的方式,根据wxml里面的样式,自动绘制出你想要的元素模块。我这边就是用的这种方式。

因为不推荐,就上一段简单的代码,详细的用法可以看git地址github.com/wg-front/wx…

let drawImage = new Wxml2Canvas({
    element: 'share', // canvas节点的id,
    obj: this, // 在组件中使用时,需要传入当前组件的this
    width: this.width, // 宽高
    height: this.height,
    background: '#FF9652', // 默认背景色
    progress(percent) { // 绘制进度
    },
    finish(url) {
        wx.showShareImageMenu({
            path: url,
            success: function (res) {
                wx.showToast({
                    title: '分享成功',
                    icon: 'success',
                    duration: 2500
                })
            },
        })
    },
    error(res) {
        console.log(res);
    }
}, this);
let data = {
    //直接获取wxml数据
    list: [{
        type: 'wxml',
        class: '.draw_canvas', // 需要绘制的元素,必须加上这个class名
        limit: '.outer_class' // 只在此class内的元素才会被绘制
    },]
}
//传入数据,画制canvas图片
drawImage.draw(data);

注意点:

1. Wxml2Canvas内容都不显示:

每个需要的绘制的元素,draw_canvas、data-type、data-text或者data-url等缺一不可。

<view style="display: inline-block;width: 100rpx;" class="draw_canvas" data-type="text"
data-text="hello"> hello </view>

2. Wxml2Canvas的文字设置粗体,安卓真机不显示:

好像是小程序的canvas一直存在问题,只有设置了 cts.font = 'normal bold 14px 'font-family'' 才可以。我们可以修改源码中对应设置字体的地方:

this.ctx.font = (`${style.fontWeight > 500 ? 'normal bold' : 'normal'} ${fontSize}px ${style.fontFamily || 'PingFang SC'}`);

还有所有没有设置font的this.ctx.fillText()方法前面,应该是主要在_drawText方法里面,也需要加上这一句。

3. Wxml2Canvas的wxml转换方式里,怎么实现超过两行省略号:

如果直接写上css样式,在这边的是不生效的。但它的JSON格式方法里的text是支持的,所以我们可以在对应设置lineClamp参数的地方,加上一个判断

if (style['-webkit-line-clamp'] !== 'none') { // 如果style的元素里有-webkit-line-clamp属性,则说明需要实现转行省略号功能。
    style.lineClamp = style['-webkit-line-clamp']
}

同时,在 _getWxml 方法获取 style 内元素的数组 computedStyle 里需要加上 -webkit-line-clamp ,这样才能被获取到。

4. Wxml2Canvas的图片有时候不显示:

网络图片最好是下载到本地,在确保图片下载完之后再去生成图片,这边可以写个简易的promise方法来实现。

5. 奇怪的现象,有时候某些元素不显示,可能是某些行或者某些字,再重新进几次可能又好了。

简单看了下源码也没找到原因,有大神知道可以告知下(主要是我菜,懒得一行行去看了)。黑科技(不推荐):偶然间发现,页面上下滑动的距离,可以影响到此情况。直接上代码

if (resPhone.platform === 'ios') { // ios的情况下,生成截图前,页面需要先置顶
    wx.pageScrollTo({
        scrollTop: 0
    })
} else if (resPhone.platform === 'android') { // android的情况下,生成截图前,页面需要先到底部
    wx.pageScrollTo({
        scrollTop: res[0].height // 当前页面的高度
    })
}

3. 在小程序里使用webview生成截图

在文章开头就讲到了不推荐方法2,主要原因就是真机生成的图片,变形很严重,样式也会变化,总之就不是我们页面上的样式,再加上坑很多,还是建议别用了。 用webview其实很简单,我这边使用了webviewbindmessage方法和微信小程序的JSSDK、html2canvas实现的。

1. 引入JSSDK,此处涉及的功能不需要配置wx.config()

npm i weixin-js-sdk --save 
import wx from 'weixin-js-sdk'

2. 引入html2canvas

npm install --save html2canvas //useCORS、allowTaint解决图片跨域

3. 使用

<web-view :src="url" @message="bindmessage"></web-view> // 网页向小程序 postMessage 时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。此处uniapp的用法
bindmessage(e){
    var save = wx.getFileSystemManager(); // 获取文件管理器对象
    var number = Math.random();
    save.writeFile({
        filePath: wx.env.USER_DATA_PATH + '/pic' + number + '.png', // 表示生成一个临时文件名
        data: e.detail.data[0].replace('data:image/png;base64,',''), // 此方法需要将data:image/png;base64,去掉
        encoding: 'base64',
        success: res => {
            wx.saveImageToPhotosAlbum({
                filePath: wx.env.USER_DATA_PATH + '/pic' + number + '.png',
                success: function (res) {
                    wx.showToast({
                        title: '保存成功',
                    })
                },
                fail: function (err) {
                    console.log(err)
                }
            })
        }, fail: err => {
        }
    })
}

参数放到url里传给web端,web端获取到参数后,调接口拿数据,然后触发saveImage方法。

saveImage(divText)
{
    let canvasID = this.$refs[divText];
    html2canvas(canvasID, {useCORS: true, allowTaint: true,}).then(canvas => {
        let base = canvas.toDataURL('image/png')
        wx.miniProgram.postMessage({data: base}) // 注册postMessage方法,参数是base64格式
        wx.miniProgram.navigateBack() // 小程序返回上级,通过销毁页面,触发postMessage
    });
}

4. 踩坑

  1. webview中使用html2canvas时,在IOS15及以上的系统里,不支持rem、根据屏幕宽度决定尺寸的适配方案。由canvas.toDataURL('image/png')方法得到的,不是正确的base64数据。 可以在调用方法之前,先将需要的DOM的rem转换成px。
  2. 微信小程序的PC端不支持postMessage方法,可以选择通过wx.config()获取wx.downloadImage方法的权限来实现本地文件下载。

总结:

其实,如果有管理后台的话,可以在信息新增或修改的时候,就将图片生成并上传,此时只需要连同图片地址存起来就可以了,这种方法也是很省时省力的。上面介绍的都是一些很简单的内容,也是我本身的一个记录和学习过程。希望有大佬指出错误,有更好的方法的话,欢迎补充!😂