第九十六期:一个小程序生成海报的问题

·  阅读 383

这里记录工作中遇到的技术点,以及自己对生活的一些思考,周三或周五发布。

封面图

一个小程序生成海报的问题

问题背景:

最近还是在做基于ts+taro的小程序开发,有个前端生成海报的需求。本来想着这个需求很简单,因为之前写过这个功能,基本的逻辑就是产品图片地址和小程序码图片地址下载到本地,然后通过createCanvasContext()这个方法拿到canvas的上下文,进行绘制即可。

而且代码也是现成的,想着直接移植一下不就行了吗,但是整整花了一天,也没整明白。

原先的生成海报代码

原先的项目是用taro+redux结合taro-ui进行开发,没有复杂的类型和封装。

且整体版本较高。

  • "@tarojs/taro": "3.2.1",
  • "@tarojs/components": "3.2.1",
  • "taro-ui": "^3.0.0-alpha.4"

海报组件的代码如下:

// 生成海报
import React, { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
import { View, Image, Canvas } from '@tarojs/components'
import './index.scss'

export default ({ ...props }) => {
  const { posterUrl, qrCodeUrl, title = '分享海报', desc = '作者张超', remark = '', className } = props
  const [combinedImg, setCombinedImg] = useState('');
  const [localImgs, setLocalImgs] = useState({})
  // 绘制之前下载道本地
  const transToLocal = async (url) => {
    return Taro.downloadFile({
      url: url, //图片url
      success: function (res) {
        console.log(res);
      },
      error: (err) => {
        console.log(err)
      }
    })
  }

  const saveToLocal = async (imgUrl, type) => {
    let { tempFilePath } = await transToLocal(imgUrl)
    // let obj = {}
    localImgs[type] = tempFilePath
    setLocalImgs(localImgs)
    console.log('localImgs---', localImgs)
    if (Object.keys(localImgs).length > 1) {
      genCompositeImg(localImgs)
    }
  }

  const genCompositeImg = (obj) => {
    console.log('obj---', obj)
    const ctx = Taro.createCanvasContext('cvs');
    // 背景白色
    ctx.setStrokeStyle('#fff');
    ctx.setLineWidth(10);
    // 750 1336
    ctx.rect(0, 0, 750, 1336);
    ctx.setFillStyle('#fff');
    ctx.fill();
    ctx.stroke();
    // 参考写 标题设置
    ctx.restore()
    ctx.setTextBaseline('top');
    ctx.setFillStyle('rgba(51,51,51,1)');
    ctx.setFontSize(32);
    ctx.fillText(title, 32, 1169);
    // 参考写 推广语
    ctx.restore()
    ctx.setTextBaseline('top');
    ctx.setFillStyle('rgba(153,153,153,1)');
    ctx.setFontSize(24);
    if (desc.length <= 20) {
      ctx.fillText(desc, 32, 1218);
    } else if (desc.length <= 40) {
      let context1 = desc.slice(0, 20);
      let context2 = desc.slice(20, desc.length);
      ctx.fillText(context1, 32, 1218);
      ctx.fillText(context2, 32, 1251);
    } else {
      let context1 = desc.slice(0, 20);
      let context2 = desc.slice(20, 39) + '...';
      ctx.fillText(context1, 32, 1218);
      ctx.fillText(context2, 32, 1251);
    }
    // 背景图
    ctx.drawImage(obj.poster, 0, 0, 750, 1120);
    // 二维码
    ctx.drawImage(obj.qrcode, 570, 1152, 148, 148);

    // 绘制
    ctx.save();
    ctx.draw(true, function (e) {
      Taro.canvasToTempFilePath({
        x: 0,
        y: 0,
        width: 750,
        height: 1336,
        destWidth: 750,
        destHeight: 1336,
        canvasId: 'cvs',
        success(res) {
          setCombinedImg(res.tempFilePath)
          props.genCompositeImgSucc(res.tempFilePath);
          // Taro.hideLoading();
        }
      })
    });
  }

  useEffect(() => {
    if (posterUrl && qrCodeUrl) {
      saveToLocal(posterUrl, 'poster')
      saveToLocal(qrCodeUrl, 'qrcode')
    }
  }, [posterUrl, qrCodeUrl])

  return (
    <View className={className ? className + ' ' + 'canvas-wrap' : 'canvas-wrap'}>
      <View className='canvas-wrap-cvsbox'>
        <Canvas style='width: 750px; height: 1336px;' canvasId='cvs' />
      </View>
      <View className='cvsDoneWrap'>
        <Image className='cvsDoneWrap-img' mode='widthFix' src={combinedImg} />
      </View>
    </View>
  );
}

这个组件在原有的项目中正常运行。

现在的项目

现在的项目整体是基于taro2.x结合ts的架构。

  • "@tarojs/cli": "2.2.1",
  • "@tarojs/components": "2.2.1",
  • "@tarojs/mobx": "2.2.1",

页面的大体逻辑是实现了 taro/mobx中的 ITaroComponent的类型。

然后自己实现了一个viewModal层,用来做数据的处理和传递。

/**
 * 基础model返回值
 */
export default interface CreateOptions<P, S> {
  useCallBackState: (defaultState: S) => [S, (state: Partial<S>, callback?: (state: S) => void) => void],

  useInitialize: (hook: () => void) => void,

  props?: P
}

按照正常的逻辑,原先生成海报能够正常执行,移植过来问题也应该不大。

但是最初遇到的问题是 ctx.draw()回调不执行。

去掉 ctx.draw() ,直接导出canvas图片地址:

 Taro.canvasToTempFilePath({
        x: 0,
        y: 0,
        width: 750,
        height: 1336,
        destWidth: 750,
        destHeight: 1336,
        canvasId: 'myCanvas',
        success(res) {
          setCombinedImg(res.tempFilePath)
          props.genCompositeImgSucc(res.tempFilePath);
          // Taro.hideLoading();
        }
      })

报错:

{errMsg: "canvasToTempFilePath: fail canvas is empty"}

canvas根本没有绘制相关的内容。

问题出在哪呢?

代码中获取上下文本,使用的是:

const ctx = Taro.createCanvasContext('cvs');

看了文档,wx.createCanvasContext(string canvasId, Object this)从基础库 2.9.0 开始,改接口已经停止维护,文档让用 Canvas 进行代替。

好吧,换成Canvas,用Canvas.getContext(string contextType)获取 Canvas 的绘图上下文。

需要用到查找dom节点的API。

Taro.createSelectorQuery().in(useScope())
  .select('#myCanvas').fields({
  node: true,
  size: true,
 }).exec(async (res) => {
  console.log('res====>', res)
  let canInstance = res[0].node
  let canvasCtx = res[0].node.getContext('2d')
  })

然后重新进行绘制。

发现相关的API差异比较大。

比如drawImage(imageResource),原先第一个参数,文档上说:

CanvasContext.drawImage(string imageResource, number sx, number sy, number sWidth, number sHeight, number dx, number dy, number dWidth, number dHeight)。
string imageResource
所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载)

此时的imageResource需要是个image对象。

const img = canvas.createImage()
img.scr = "url"
img.onload = () {
  canvasCtx.drawImage(img,0,0)
}

这样才能勉强将图片绘制到画布上。

然而,事情并没有结束。

canvas.createImage()创建的图片对象,img的onload方法会不停的重复执行,似乎进入了一个死循环。

最终还是暂时放弃前端生成海报,暂时由服务端去合成。

很奇怪的问题,使用wx.createCanvasContext(),绘制方法不起作用。

使用Canvas 2D,drawImage()又陷入死循环。

但是以前的项目又一切正常,这给我整的有点不会了。

不甘心的我又翻了翻github上相关的issue。有一个最接近的问题是:

CanvasRenderingContext2D 对象通过canvas.getContext("2d")创建出来,无法使用drawImage方法绘制图片 #5881

但是目前没有解决方法。

感觉问题应该还是出在系统或者版本上,API更新了,文档没更新。或者canvas相关的API没有完全和HTML Canvas 2D Context 同步。

最后

  • 公众号《JavaScript高级程序设计》
  • 公众号内回复”vue-router“ 或 ”router“即可收到 VueRouter源码分析的文档。
  • 回复”vuex“ 或 ”Vuex“即可收到 Vuex 源码分析的文档。

全文完,如果喜欢。

请点赞和"在看"吧,最好也加个"关注",或者分享到朋友圈。

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改