wxml-to-canvas使用过坑记录

1. 介绍wxml-to-canvas

官网介绍

这是一个类似html2canvas的插件,在canvas上绘制页面,但是有段时间没维护了。

我的需求是将一个长页面截图并保存。

2.使用

如果是原生微信小程序,就按照官网整就行了。

这里说一下uniapp的方式(我是这么做的,当然还有其他方法):

  1. 新建一个代码片段
  2. npm install --save wxml-to-canvas
  3. 构建npm
  4. 复制miniprogram_npm中的widget-uiwxml-to-canvas到项目中的wxcomponents文件夹。
  5. page.json配置
json 复制代码
"usingComponents": {  
    "wxml-to-canvas": "/wxcomponents/wxml-to-canvas"  
}
  1. 页面中的使用就和文档一样了。
html 复制代码
 <wxml-to-canvas
      ref="wxml2canvas"
      class="wxml2canvas"
      :width="width"
      :height="height"/>
js 复制代码
Page({
  onLoad() {
  // 获取自定义组件实例
    this.widget = this.selectComponent('.widget')
    // 绘制页面
    this.widget.renderToCanvas({ 
        // 模板
        wxml: "<view class='container'><text class='text'>hello</text><view>", 
        // 样式
        style: {
            container: {
                backgroundColor: '#f50'
            },
            text: {
                width: 30,
                fontSize: 16
            }
        }
    })
    // 导出图片
    this.widget.canvasToTempFilePath()
  },
})

3.原理

这个插件原理还是比较简单,就是把模板解析成一个个节点,然后根据样式绘制不同的元素。比如view -> rectimage -> drawImagetext -> drawText。实际上也就只支持这三个元素。

插件中使用的widget-ui就是解析模板和样式的:

js 复制代码
// 源代码中能看到renderToCanvas的流程
    async renderToCanvas(args) {
    // 传进来的模板和样式
      const {wxml, style} = args
      const ctx = this.ctx
      const canvas = this.canvas
    // 解析wxml
      const {root: xom} = xmlParse(wxml)
      const widget = new Widget(xom, style)
      const container = widget.init()

      const draw = new Draw(ctx, canvas, use2dCanvas)
    // 绘制节点
      await draw.drawNode(container)

      return Promise.resolve(container)
    },

然后遍历节点树根据标签配置分别绘制:

js 复制代码
  async drawNode(element) {
    const {layoutBox, computedStyle, name} = element
    const {src, text} = element.attributes
    if (name === 'view') {
     // 绘制view
    } else if (name === 'image') {
      // 绘制image
    } else if (name === 'text') {
       // 绘制text
    }
    // 遍历节点树
    const childs = Object.values(element.children)
    for (const child of childs) {
      await this.drawNode(child)
    }
  }

所以很多插件不支持的样式标签,你可以自己加上去。

4. 坑

这个插件还是蛮多的,这里记录下我使用的过程中遇到的。

4.0. 获取元素尺寸

这个插件最重要的一点就是最好最好所有的元素都得有宽高尺寸,不然到时候显示不出来,都没办法调试,不知道怎么回事。

获取元素尺寸我用的就是createSelectorQuery

js 复制代码
  const query = uni.createSelectorQuery()
  query
      .selectAll('.class1,.class2,.class3')
      .boundingClientRect()
      .exec(res => console.log(res))
 // 获取到尺寸就可以放进样式中了

4.1. 无法显示本地图片

html 复制代码
<!-- 你可能想要这样引入 -->
<image src="./img/icon.png"/>

但是这样肯定不行,因为没有经过处理,这个路径是无法识别的。

解决方案:

  1. 使用base64,这种方案只适合use2dCanvastrue的时候。
  2. 先写入文件再调用,这种方法适合use2dCanvasfalse的时候。

4.1.1 方案一

这个方案简单粗暴但是还有点问题,源码中没有针对base64做处理,所以需要改下源码。

js 复制代码
async drawImage(img, box, style) {
    await new Promise((resolve, reject) => {
        // ....
      const _drawImage = (img) => {
   // ....
      }
   // ....
      const isTempFile = /^wxfile:\/\//.test(img)
      const isNetworkFile = /^https?:\/\//.test(img)
      // 主要就是加这里
      const isBase64 = img.includes(';base64,')
      // 之前是没有判断的,会直接报错
      if (isTempFile || isBase64) {
        _drawImage(img)
      } else if (isNetworkFile) {
   // ....
      } else {
        reject(new Error(`image format error: ${img}`))
      }
    })
  }

4.1.2 方案二

这个方案适合use2dCanvas=false的时候,也就是使用老版canvas接口的时候,因为老版接口不支持使用base64

js 复制代码
// 设置图片
const filePath = `${wx.env.USER_DATA_PATH}/export-bg1.png`
const fs = wx.getFileSystemManager()
await (() => new Promise((resolve) => {
  fs.access({
    path: filePath,
    success(res) {
      // 文件存在
      resolve()
    },
    fail(res) {
      // 文件不存在或其他错误
      fs.writeFile({
        filePath: filePath,
        // 我这里将base64的数据存储为文件
        data: '/9j/4AAQSkZJRg',
        encoding: 'base64',
        success(res) {
          resolve()
        },
        fail(res) {
          console.error(res)
        },
      })
    }
  })
}))()
// 模板里面就可以使用这个路径了
wxml: `
    <image src="${filePath}"/>
`

4.2. 无法显示文字

多半是尺寸没设置,最重要的是设置text的宽度,其实这里的textview没有什么区别,在canvas中就只是一个图形而已。

最好不要搞嵌套text,就像上面说的,对于canvas来讲没有什么块级元素,行内元素,排版是通过计算得出的x,y,所以嵌套text会有问题。最好就是平级的text,设置一下父节点的flex就行了。

js 复制代码
{
    wxml: `
        <view class="container">
            <text class="text1">100</text>
            <text class="text2">㎡</text>
        </view>
    `,
    style:{
        container: {
            flexDirection: 'row'
        },
        text1: {
            width: 30,
            height:30,
        },
        text2: {
            width: 20,
            height: 20
        }
    }
}

4.3. 文字加粗

改一下源码,把样式加上

js 复制代码
  drawText(text, box, style) {
    const ctx = this.ctx
    let {
      left: x, top: y, width: w, height: h
    } = box
    let {
      color = '#000',
      lineHeight = '1em',
      fontSize = 14,
      textAlign = 'left',
      verticalAlign = 'top',
      backgroundColor = 'transparent',
      // 加一个fontWeight
      fontWeight = 'normal'
    } = style
    // 这里也加一个${fontWeight}
    // 补充一下fontSize在老版本的canvas中不能有小数,所以这里也判断了一下
    ctx.font = `${fontWeight} ${this.use2dCanvas ? fontSize : Math.round(fontSize)}px sans-serif`
    // ....
  }

4.4. 动态设置宽高

由于源码中只初始化了一次,所以后续canvas宽高发生变化是没有反应的。我懒得加上监听了,所以偷懒了一下,只加了一个初始完成的事件。宽高变化了重新加载一次组件就完事了。

js 复制代码
// 源码中初始化部分
  lifetimes: {
    attached() {
    // ....
      this.setData({use2dCanvas}, () => {
        if (use2dCanvas) {
          const query = this.createSelectorQuery()
          query.select(`#${canvasId}`)
            .fields({node: true, size: true})
            .exec(res => {
              const canvas = res[0].node
              const ctx = canvas.getContext('2d')
              canvas.width = res[0].width * dpr
              canvas.height = res[0].height * dpr
              ctx.scale(dpr, dpr)
              this.ctx = ctx
              this.canvas = canvas
              // 加了一个初始化完成的事件
              this.triggerEvent('load')
            })
        } else {
          this.ctx = wx.createCanvasContext(canvasId, this)
        // 加了一个初始化完成的事件
          this.triggerEvent('load')
        }
      })
    }
  },

4.5. flex问题

父元素要设置宽高才能应用justifyContentalignItems

flexDirection: 'row'时子元素必须设置宽度,可以使用flex: 1,但是不会自动扩展尺寸

4.6. 单位转换

canvas使用的单位是px,需要转化rpx

js 复制代码
/**
 * rpx转px
 */
export const rpx2px = (rpx) => {
// 这里的deviceInfo实际上就是getSystemInfo接口获取的数据
  const {
    devicePixelRatio,
    screenWidth
  } = store.state.deviceInfo

  const px = (screenWidth / 750) * rpx
  // 小于1像素的处理
  if(Math.floor(px) === 0) {
    if(devicePixelRatio === 1) {
      return 1
    }else {
      return 0.5
    }
  }
  return px

}

4.7.设置边框

四周边框的原理实际上是套了一个更大一点的矩形。所以内部需要指定背景色,不然就都是边框的颜色。

js 复制代码
drawView(box, style) {  
    const ctx = this.ctx  
    const {  
    left: x, top: y, width: w, height: h  
    } = box  
    const {  
        borderRadius = 0,  
        borderWidth = 0,  
        borderColor,  
        color = '#000',  
        backgroundColor = 'transparent',  
    } = style  
    ctx.save()  
    // 外环  
    if (borderWidth > 0) {  
        ctx.fillStyle = borderColor || color  
        this.roundRect(x, y, w, h, borderRadius)  
    }  
  
    // 内环  
    ctx.fillStyle = backgroundColor  
    const innerWidth = w - 2 * borderWidth  
    const innerHeight = h - 2 * borderWidth  
    const innerRadius = borderRadius - borderWidth >= 0 ? borderRadius - borderWidth : 0  
    this.roundRect(x + borderWidth, y + borderWidth, innerWidth, innerHeight, innerRadius)  
    ctx.restore()  
}

4.8. 截取超长图

新版的canvas是有尺寸限制的:

官网

js 复制代码
// 大概就是这么多,可能就5000px左右
deviceInfo.pixelRatio * 1365

对于高dpr的设备这个尺寸绘制长图还是有点费劲,所以要绘制长图最好是使用老接口。

老版本我试了一下,截取20000px都没有问题,当然这个和设备还是有关系,不过对于一般的业务也是足够的。

4.9. 使用老版本canvas的问题

绘制图片不能用base64,先写入文件 这个前面说了。

注意高度不要算出什么NaN之类的,这个在新接口中好像不会有问题,在老版本的接口中会出现高度不正常。

4.10. renderToCanvas异步不可靠

源码中对renderToCanvas做了异步处理,代码逻辑没问题,但是实际绘制耗时任务的时候renderToCanvaspromise结束了但是绘制有可能还没完成。如果没完成的时候导出图片,就会是残缺样式的。

我的解决方法是在导出前延迟1s。虽然不是很保险,但是基本上没问题。

4.11. 隐藏canvas

这个主要针对使用老版本canvas的时候,因为新版本可以随便修改层级了。

解决方法:

包裹一个view,设置宽高0,定位到屏幕外。

css 复制代码
.wxml2canvas {  
    position: absolute;  
    top: -9999px;  
    left: 0;  
    width: 0;  
    height: 0;  
    overflow: hidden;  
}

5. 效果

我在项目中实现的截图效果大概就是这样:

说实话就这么一个表格都要拼接好久的模板,复杂的截图用这个插件根本无法做到,只有期待谁可以完善这套东西,增加一些标签和样式。

相关推荐
web150850966413 小时前
在uniapp Vue3版本中如何解决webH5网页浏览器跨域的问题
前端·uni-app
.生产的驴5 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
汤姆yu11 小时前
基于微信小程序的乡村旅游系统
微信小程序·旅游·乡村旅游
曲辒净11 小时前
微信小程序实现二维码海报保存分享功能
微信小程序·小程序
何极光13 小时前
uniapp小程序样式穿透
前端·小程序·uni-app
User_undefined1 天前
uniapp Native.js 调用安卓arr原生service
android·javascript·uni-app
流氓也是种气质 _Cookie1 天前
uniapp blob格式转换为video .mp4文件使用ffmpeg工具
ffmpeg·uni-app
oil欧哟1 天前
🤔认真投入一个月做的小程序,能做成什么样子?有人用吗?
前端·vue.js·微信小程序
爱笑的眼睛111 天前
uniapp 极速上手鸿蒙开发
华为·uni-app·harmonyos
汤姆yu1 天前
基于微信小程序的消防隐患在线举报系统
微信小程序·小程序·消防隐患