微信小程序海报生成分享功能的深度剖析与优化方案

写在前面

最近收到一个有关微信小程序的需求:

要求某个场景下,点击一个按钮,生成一个宣传海报,此海报可以转发给朋友、收藏、保存到相册。通过扫描海报上的二维码可以直达某个网址/小程序。

将此功能封装成组件后,写此文章记录一下,方便后续再用到的时候查阅。

1. 实现思路与构想

  1. 模块化设计

整个海报生成分享功能采用了模块化设计,将不同功能封装在独立的文件中。例如,painter 负责绘图,downloader 负责资源下载,pen 提供绘图工具类,qrcode 生成二维码,gradient 处理渐变色,sha1 和 util 提供工具函数。这种设计使得代码易于维护和扩展。

  1. 异步处理与状态管理

为了保证用户体验,所有耗时操作(如图片下载、绘图等)都采用异步处理。同时,通过 wx.showLoading 和 wx.hideLoading 来显示加载提示,提升用户体验。

  1. 性能优化
  • **LRU 缓存: **downloader.js 中实现了 LRU 缓存策略,确保图片资源的高效加载,减少重复下载。
  • **渐变色与二维码生成: **gradient.js 和 qrcode.js 提供了高效的渐变色和二维码生成算法,确保绘图性能。
  • **多层画布: **painter.wxml 中使用了多个 canvas 标签,分别用于背景、底部、全局、顶部和前台绘图,避免了单个画布的性能瓶颈。
  1. 用户交互与反馈
  • **用户交互: **painter.js 中处理了用户的点击、拖动等交互事件,提供了丰富的交互体验。
  • **错误处理: **在绘图过程中,通过 onImgErr 和 onImgOK 回调函数处理绘图成功和失败的情况,提供及时的用户反馈。

2. 目录结构即文件概述

--components			组件库
    --shareBox			海报生成组件	
        --index.js
  		--index.json
  		--index.wxml
  		--index.wxss
--pages				业务页面
    --index				首页
        --index.js
  		--index.json
  		--index.wxml
  		--index.wxss
--painter				绘图组件
    --lib				辅助库文件
        --downloader.js	
  		--gradient.js
  		--pen.js
  		--qrcode.js
  		--sha1.js
  		--util.js
    --painter.js
    --painter.json
    --painter.wxml
    --painter.wxss
  1. index.js 和 index.wxml
  • index.js: 这是海报生成组件的核心逻辑文件。它定义了一个自定义组件,负责处理用户交互和数据绑定。主要功能包括:
    • 监听属性变化(如是否开始绘图、用户信息等)。
    • 调用 handleStartDrawImg 方法启动绘图过程。
    • 处理绘图成功 (onImgOK) 和失败 (onImgErr) 的回调。
  • index.wxml: 定义了组件的结构,使用了 painter 组件来绘制海报,并通过 palette 属性传递绘图配置。
  1. painter.js 和 painter.wxml
  • painter.js: 这是绘图组件的核心逻辑文件,负责实际的绘图操作。它实现了以下功能:
    • 初始化画布并设置样式。
    • 下载图片资源并进行缓存管理。
    • 处理用户交互(如点击、拖动等)。
    • 触发绘图完成事件并返回结果。
  • painter.wxml: 定义了绘图组件的结构,包含多个 canvas 标签用于不同的绘图层(背景、底部、全局、顶部、前台),并通过事件绑定处理用户交互。
  1. 辅助库文件
  • downloader.js: 负责下载图片资源并进行本地缓存管理。提供了 LRU 缓存策略,确保图片资源的高效加载。
  • gradient.js: 实现渐变色的生成,支持线性渐变和径向渐变。
  • pen.js: 提供绘图工具类,封装了具体的绘图逻辑,如绘制矩形、文本、图片等。
  • qrcode.js: 实现二维码的生成,支持多种纠错级别。
  • sha1.js: 提供 SHA-1 哈希算法,用于生成唯一的文件名或标识符。
  • util.js: 提供一些通用工具函数,如 URL 验证、对象深比较等。
  1. 文件调用关系
    1. index.js 调用 painter:
    • 当 isCanDraw 属性变为 true 时,触发 handleStartDrawImg 方法。
    • handleStartDrawImg 方法中设置了 imgDraw 数据,并通过 setData 更新视图,触发 painter 组件的绘制。
    1. painter.js 内部调用:
    • painter 组件监听 palette 属性的变化,当有新的绘图配置时,调用 startPaint 方法。
    • startPaint 方法中下载图片资源并调用 Pen 类进行实际绘图。
    • Pen 类在绘图过程中会根据配置调用 gradient.js、qrcode.js 等辅助库来处理特定类型的元素。
    1. 辅助库之间的调用:
    • downloader.js 被 painter.js 调用来下载图片资源。
    • gradient.js 和 qrcode.js 被 pen.js 调用来处理渐变色和二维码的绘制。
    • sha1.js 和 util.js 提供了工具函数,被多个文件调用以增强功能。

3. 关键方法剖析

  1. handleStartDrawImg (index.js)
handleStartDrawImg() {
  wx.showLoading({
    title: '生成中'
  })
  this.setData({
    imgDraw: {
      width: '750rpx',
      height: '1334rpx',
      background: 'https://example.com/staticimages/wechat/poster-bg.png',
      views: [
        // 各种视图元素配置
      ]
    }
  })
}
  • 功能: 当用户触发开始绘图时,显示加载提示,并设置 imgDraw 数据来启动绘图过程。
  • 流程:
    • 使用 wx.showLoading 显示加载提示。
    • 调用 this.setData 更新组件的数据,其中 imgDraw 包含了画布尺寸、背景图片和多个视图元素(如文本、图片等)的配置。
  • 重要性: 这是整个绘图流程的起点,负责初始化绘图配置并更新视图。
  1. startPaint (painter.js)
startPaint() {
  this.initScreenK();

  this.downloadImages(this.properties.palette).then((palette) => {
    const { width, height } = palette;

    if (!width || !height) {
      console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
      return;
    }

    // 生成图片时,根据设置的像素值重新绘制
    this.canvasWidthInPx = width.toPx();
    if (this.properties.widthPixels) {
      setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx)
      this.canvasWidthInPx = this.properties.widthPixels
    }

    this.canvasHeightInPx = height.toPx();
    this.setData({
      photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
    });
    this.photoContext || (this.photoContext = wx.createCanvasContext('photo', this));

    new Pen(this.photoContext, palette).paint(() => {
      this.saveImgToLocal();
    });
    setStringPrototype(this.screenK, this.properties.scaleRatio);
  });
}
  • 功能: 初始化屏幕比例,下载图片资源,并调用 Pen 类进行实际绘图。完成后保存图片到本地。
  • 流程:
    • 调用 initScreenK 初始化屏幕比例。
    • 使用 downloadImages 下载所有需要的图片资源。
    • 设置画布宽度和高度,并创建 photoContext。
    • 创建 Pen 实例并调用 paint 方法开始绘图。
    • 绘图完成后调用 saveImgToLocal 将图片保存到本地。
  • 关键点:
    • 异步处理: 使用 Promise 确保图片下载完成后才开始绘图。
    • 画布配置: 根据传入的配置动态调整画布大小。
    • 绘图工具类: 使用 Pen 类封装具体的绘图逻辑,提高代码复用性和可维护性。
  1. download (downloader.js)
download(url, lru) {
  return new Promise((resolve, reject) => {
    if (!(url && util.isValidUrl(url))) {
      resolve(url);
      return;
    }
    const fileName = getFileName(url);
    if (!lru) {
      // 无 LRU 情况下直接判断临时文件是否存在
      wx.getFileInfo({
        filePath: fileName,
        success: () => {
          resolve(url);
        },
        fail: () => {
          downloadFile(url, lru).then((path) => {
            resolve(path);
          }, () => {
            reject();
          });
        },
      })
      return
    }

    const file = getFile(fileName);

    if (file) {
      // 检查文件是否正常,不正常需要重新下载
      wx.getSavedFileInfo({
        filePath: file[KEY_PATH],
        success: (res) => {
          resolve(file[KEY_PATH]);
        },
        fail: (error) => {
          downloadFile(url, lru).then((path) => {
            resolve(path);
          }, () => {
            reject();
          });
        },
      });
    } else {
      downloadFile(url, lru).then((path) => {
        resolve(path);
      }, () => {
        reject();
      });
    }
  });
}
  • **功能: **下载图片资源,支持 LRU 缓存策略,确保图片资源的高效加载。
  • 流程:
    • 验证 URL 的有效性。
    • 如果未启用 LRU 缓存,则检查临时文件是否存在,存在则直接返回路径,否则下载文件。
    • 如果启用了 LRU 缓存,则检查缓存文件是否存在且有效,无效则重新下载。
    • 使用 wx.getSavedFileInfo 和 wx.downloadFile 分别检查和下载文件。
  • 关键点:
    • LRU 缓存: 提供高效的缓存管理,减少重复下载。
    • 错误处理: 对于无效或不存在的文件,提供合理的错误处理机制。
    • 异步操作: 使用 Promise 处理异步操作,确保流程顺畅。
  1. doGradient (gradient.js)
doGradient(bg, width, height, ctx) {
  if (bg.startsWith('linear')) {
    linearEffect(width, height, bg, ctx);
  } else if (bg.startsWith('radial')) {
    radialEffect(width, height, bg, ctx);
  }
}
  • **功能: **根据背景配置生成渐变色效果,支持线性渐变和径向渐变。
  • 流程:
    • 判断背景配置类型(线性渐变或径向渐变)。
    • 根据类型调用相应的函数 (linearEffect 或 radialEffect) 来生成渐变色。
  • 关键点:
    • 条件分支: 根据背景配置选择不同的渐变生成方式。
    • 封装性: 将具体实现封装在 linearEffect 和 radialEffect 函数中,提高了代码的可读性和可维护性。
  1. draw (pen.js)
draw(callback, isMoving, movingCache) {
  this.style = {
    width: this.data.width.toPx(),
    height: this.data.height.toPx(),
  };
  if (isMoving) {
    this.isMoving = true
    this.movingCache = movingCache
  }
  this._background();
  for (const view of this.data.views) {
    this._drawAbsolute(view);
  }
  this.ctx.draw(false, () => {
    callback && callback(this.callbackInfo);
  });
}
  • **功能: **负责具体的绘图操作,包括背景绘制、元素绘制等。
  • 流程:
    • 计算画布的实际宽度和高度。
    • 如果有移动操作,更新状态和缓存。
    • 调用 _background 方法绘制背景。
    • 遍历 views 数组,调用 _drawAbsolute 方法绘制每个元素。
    • 完成绘图后调用 this.ctx.draw 并执行回调函数。
  • 关键点:
    • 状态管理: 通过 isMoving 和 movingCache 管理绘图状态。
    • 遍历绘制: 使用循环遍历 views 数组,逐个绘制元素。
    • 回调机制: 绘图完成后执行回调函数,通知外部绘图完成。
  1. genframe (qrcode.js)
function genframe(instring) {
  var x, y, k, t, v, i, j, m;

  // 找到最小版本号
  do {
    version++;
    k = (ecclevel - 1) * 4 + (version - 1) * 16;
    neccblk1 = eccblocks[k++];
    neccblk2 = eccblocks[k++];
    datablkw = eccblocks[k++];
    eccblkwid = eccblocks[k];
    k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
    if (t <= k)
      break;
  } while (version < 40);

  // 初始化数据结构
  width = 17 + 4 * version;

  // 插入定位符、校准图案、单黑点、定时器间隙、格式信息区、定时行/列、版本块等固定部分
  // ...

  // 将字符串转换为位流,并计算最大字符串长度
  v = strinbuf.length;

  // 填充到末尾的填充模式
  i = v + 3 - (version < 10);
  while (i < x) {
    strinbuf[i++] = 0xec;
    if (i == x) break;
    strinbuf[i++] = 0x11;
  }

  // 计算并附加 ECC
  // ...

  // 打包位流到帧,避免掩码区域
  // ...

  // 应用掩码并评估图像质量
  // ...

  // 添加最终的掩码/ECC级别字节
  // ...

  return qrframe;
}
  • **功能: **生成二维码,包括计算版本、插入固定部分、转换字符串为位流、计算 ECC、应用掩码、评估图像质量等步骤。
  • 流程:
    • 版本计算: 找到适合输入字符串的最小版本号。
    • 初始化: 初始化数据结构和画布。
    • 固定部分: 插入定位符、校准图案、单黑点、定时器间隙、格式信息区、定时行/列、版本块等固定部分。
    • 位流转换: 将输入字符串转换为位流,并填充到最大长度。
    • ECC 计算: 计算并附加纠错码(ECC)。
    • 打包位流: 将位流打包到帧中,避免掩码区域。
    • 掩码与评估: 应用掩码并评估图像质量,选择最佳掩码。
    • 添加元数据: 添加最终的掩码和 ECC 级别字节。
  • 关键点:
    • 复杂度高: 二维码生成涉及多个步骤,每个步骤都有详细的逻辑。
    • 错误纠正: 计算并附加 ECC 确保二维码的鲁棒性。
    • 优化选择: 通过评估图像质量选择最佳掩码,提升二维码的识别率。
  1. getFileName (downloader.js)
function getFileName(url) {
  if (util.isDataUrl(url)) { 
    const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(url) || [];
    const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
    return fileName;
  } else {
    return url;
  }
}
  • **功能: **根据 URL 获取文件名,支持 Base64 编码的图片。
  • 流程:
    • 检查 URL 是否为 Base64 编码的图片。
    • 如果是 Base64 编码,提取图片格式和内容,使用 SHA-1 生成唯一文件名。
    • 如果不是 Base64 编码,直接返回原始 URL。
  • 关键点:
    • 正则表达式: 使用正则表达式匹配 Base64 编码的图片格式和内容。
    • SHA-1 哈希: 使用 SHA-1 生成唯一文件名,确保文件名的唯一性和安全性。

  1. utf16to8 (qrcode.js)
utf16to8: function (str) {
  var out, i, len, c;

  out = "";
  len = str.length;
  for (i = 0; i < len; i++) {
    c = str.charCodeAt(i);
    if ((c >= 0x0001) && (c <= 0x007F)) {
      out += str.charAt(i);
    } else if (c > 0x07FF) {
      out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
      out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
      out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
    } else {
      out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
      out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
    }
  }
  return out;
}
  • **功能: **将 UTF-16 编码的字符串转换为 UTF-8 编码的字符串,支持中文字符。
  • 流程:
    • 遍历字符串中的每个字符,根据字符编码范围进行转换。
    • 对于 ASCII 字符(0x0001 到 0x007F),直接保留原字符。
    • 对于多字节字符(大于 0x07FF),使用 UTF-8 编码规则进行转换。
  • 关键点:
    • 字符编码转换: 精确处理不同范围的字符编码,确保转换正确。
    • UTF-8 规则: 遵循 UTF-8 编码规则,确保中文字符的正确表示。

4. 生成不同类型的海报

4.1 背景纯色的海报

handleStartDrawImg() {
  wx.showLoading({ title: '生成中' });
  this.setData({
    imgDraw: {
      width: '750rpx',
      height: '1334rpx',
      background: '#FFFFFF',
      views: [
        {
          type: 'text',
          text: '欢迎使用小程序',
          css: {
            top: '50rpx',
            left: '50rpx',
            fontSize: '32rpx',
            color: '#333333'
          }
        },
        {
          type: 'rect',
          css: {
            top: '100rpx',
            left: '50rpx',
            width: '650rpx',
            height: '200rpx',
            backgroundColor: '#F0F0F0',
            borderRadius: '16rpx'
          }
        }
      ]
    }
  });
}

4.2 带有二维码的海报

handleStartDrawImg() {
  wx.showLoading({ title: '生成中' });
  this.setData({
    imgDraw: {
      width: '750rpx',
      height: '1334rpx',
      background: '#FFFFFF',
      views: [
        {
          type: 'text',
          text: '欢迎使用小程序',
          css: {
            top: '50rpx',
            left: '50rpx',
            fontSize: '32rpx',
            color: '#333333'
          }
        },
        {
          type: 'qrcode',
          content: 'https://example.com',
          css: {
            top: '100rpx',
            left: '50rpx',
            width: '200rpx',
            height: '200rpx',
            color: '#000000',
            background: '#FFFFFF'
          }
        }
      ]
    }
  });
}

4.3 背景是图片的海报

handleStartDrawImg() {
  wx.showLoading({ title: '生成中' });
  this.setData({
    imgDraw: {
      width: '750rpx',
      height: '1334rpx',
      background: 'https://example.com/poster-bg.png',
      views: [
        {
          type: 'text',
          text: '欢迎使用小程序',
          css: {
            top: '50rpx',
            left: '50rpx',
            fontSize: '32rpx',
            color: '#FFFFFF'
          }
        },
        {
          type: 'image',
          url: 'https://example.com/logo.png',
          css: {
            top: '100rpx',
            left: '50rpx',
            width: '100rpx',
            height: '100rpx'
          }
        }
      ]
    }
  });
}

5. 实战代码及效果

5.1 代码

业务页面代码:

  • index.json
{
  "usingComponents": {
    "share-box": "/components/shareBox/index"
  }
}
  • index.wxml
<share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" imageBase64="{{imageBase64}}" posterImgBase64="{{posterImgBase64}}" bind:initData="handleClose" />
  • index.js
// index.js
// 获取应用实例
const app = getApp()

Page({
  data: {
    motto: 'Hello World',
    userInfo: {},
    poster: app.imgUrl+'poster/poster.png',
    isCanDraw: false,//海报标志

  },
  //海报开始
  handleClose() {
    this.setData({
      isCanDraw: !this.data.isCanDraw
    })
  },
  //简单的调用,没有什么参数传递
  getUserInfoAndPosterSimply(e) {
    wx.getUserProfile({
      desc: "获取您的头像昵称信息",
      success: res => {
        const { userInfo = {} } = res
        this.setData({
          userInfo,
          imageBase64:'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAIAAAAP3aGbAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAIyElEQVR4nO3dS3LcShIAQfWY7n9lzQ0EPkvVJ9Due4LVaDCsFkjW58+fP78ACv53egEAPyVYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVk/B7+/Ofz+SfruNnjOR1/vwkbjvl4/BaGa/iGb3m14VP0DvO/BTssIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CAjOlozqMNgylD85GI+wdfVg8Pza8wnC6aDycdfwyOX39uw5NshwVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpCxfDTn0erX+Y8PNNw/NTK//vwzDr+m49/yr8LhSUM33GQ7LCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgIzzozkvMJzJmM+1PDo+tLF6PmnD1MgNgynYYQEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARnedP8Hjr8DPXyPfMMxGfNbtPocDRLssIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjLOj+Ycn2tZbT41smF0ZumP/+QKqz/C3Ibxo6HjC9jADgvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBj+WjO8YmKDf7+GTccOfOCU3Ne/xHmczPf8Kf0yA4LyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgYzqa8w0HdTy6f2rkBY4f/LPaN3yJc3ZYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGZ/jAwGrj0uZW31cyvHZnRvGVlY/hxtu8tCGv8Tj5/rMf4UdFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkDE9hGLDS9LH3zA+voANVxhe//jL+o82vEc+/BUbXhNffVrKBnZYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGdNDKOZjK8cX8Oj+2Z3jC3i0euzjhtmg1Uc8PBo+BhseM4dQAF9EsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgIzlozmr3/e/YeJheBjJo9WfccN40/1Wz+7c/xg8uuEj2GEBGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVk5E/NeXT/3MmGM2lWOz4gdcO5Pse/haHjoz8/YYcFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQ8fv0AqY2zAbNhzaG159bfaDLC8wHU+o3+fhz+BN2WECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQMb0TfcN/5l/ePrA4wLufwX5BccfzKcFVo8TfMNRIKv/lDawwwIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8j4HJ94GF7h/pGLDRMPq3/F8Xt4g9WDL49WPycOoQD4lwQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvImI7m3G/D4MtwAcdPK1k9PrXhCsfv4eMajj+Hj47fop+wwwIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8g4P5pz/ECXF1h98tDc8a9pw+lKQ/ef/zRnNAf4IoIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVk/B7+/IZ5gvphIfdPVGxYocmYufkKhxNaNxxNZIcFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQMR3NeYHjIxf3u39o4/h5MI/uv4fzBWz4U7LDAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMj7Dl1Pnb98eX8Dc8WMyVr8kfcMwwN8/QuIt7eOG9/AGdlhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZ00MoNvzn/OMzGcOPMF/A/TMTxwdfNkxoHR8Cm9/D42Nw849ghwVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAxHc05PpMxd/wjHJ/5mJvfotXfwuP1V38L8wmw48/Jhjm8R3ZYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGZ/jcyfH3/dfPVZyw9jK6hU+Oj558+gFz+FqTs0B+A8EC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyDh/as7wCvNxgdUDBzdMjaxe4aPjcyer7+FPrrD0x39i9bew4Vu2wwIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjKWH0LxDepHPNz/Lv4Nhnfp/sdgfv1HDqEAvohgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGcsPoXiBx3mC40c88Oj+2aANK3zBo2iHBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkDEdzXl0fOLh0XxeYXhqzg2H1qy2eqxkOD51gw0rXP0rNjxmdlhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZy0dzHq1+nf/4TMaGeYXhr7jhOJbhGjYcOTOcfzo+HfW4hvmA14a/NTssIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CAjPOjOS9Qn/65f2rk19NNfvzx+VjJ8fmnR6+fcvtlhwWECBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWR40/0fWP0O9Op//r/hCIkXvIS9+hCK4/dwwwrnX5MdFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQMb50Zwb/rP90PB8hOH15+6fa9ngBQtY/T1umLx5ZIcFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQsXw05/jEwwusPs7khtNQhu5f4QarT2+aL8CpOcAXESwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCDj8w0jC8A72GEBGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZ/wdF9P1JnOlqawAAAABJRU5ErkJggg==',
          posterImgBase64: 'https://gitee.com/anarkhao/anarkh-we-chat-mini-program-project/raw/master/images/poster/head_icon.png',
          isCanDraw: true // 开始绘制海报图
        })
      },
      fail: err => {
        console.log(err)
      }
    })
  },
  //较为复杂的调用,从后台获取微信小程序头像图片imageBase64,posterImgBase64为海报的head图片(这里只是说明可以用图片进行传递)
  getUserInfoAndPosterComplex(e) {
    var that = this;
    wx.getUserProfile({
        desc: "获取您的头像昵称信息",
        success: res => {
            wx.request({
                url: 'https://example.com/wechat/getwxacodeunlimit?azb010='+that.data.azb010,
                data: {},
                header:app.getHeader(),
                method: "POST",
                success: function (resbak) {
                  if(resbak.data.flag=='1'){
                    const { userInfo = {} } = res
                    that.setData({
                        userInfo,
                        isCanDraw: true, // 开始绘制海报图
                        imageBase64: 'data:image/jpeg;base64,'+resbak.data.imageBase64,
                        posterImgBase64: that.data.posterImgBase64
                    })
                  }else{
                    wx.hideLoading();
                    wx.showToast({
                      title: '生成海报失败',
                      icon: 'none',
                      duration: 2000
                    });
                  }
                }
              }) 
        },
        fail: err => {
          console.log(err)
        }
      })
    
  },
  //海报结束
  // 事件处理函数
  bindViewTap() {
    wx.navigateTo({
      url: '../logs/logs'
    })
  },
  onLoad() {
    if (wx.getUserProfile) {
      this.setData({
        canIUseGetUserProfile: true
      })
    }
  }
})

组件shareBox:

  • index.js
Component({
  properties: {
    // 是否开始绘图
    isCanDraw: {
      type: Boolean,
      value: false,
      observer(newVal) {
        newVal && this.handleStartDrawImg()
      }
    },
    // 用户头像昵称信息
    userInfo: {
      type: Object,
      value: {
        avatarUrl: '',
        nickName: ''
      }
    },
    imageBase64: String,
    posterImgBase64: String
  },
  data: {
    imgDraw: {}, // 绘制图片的大对象
    sharePath: '' // 生成的分享图
  },
  methods: {
    handleStartDrawImg() {
      wx.showLoading({
        title: '生成中'
      })
      this.setData({
        imgDraw: {
          width: '750rpx',
          height: '1334rpx',
          background: '#B2E2F2',//也可以放图片
          views: [
            {
              type: 'image',
              url: this.data.posterImgBase64 || 'https://example.com/default.png',
              css: {
                top: '32rpx',
                left: '30rpx',
                right: '32rpx',
                width: '688rpx',
                height: '420rpx',
                borderRadius: '16rpx'
              },
            },
            {
              type: 'image',
              url:  'https://gitee.com/anarkhao/anarkh-we-chat-mini-program-project/raw/master/images/headphoto/head_photo.png',
              css: {
                top: '404rpx',
                left: '328rpx',
                width: '96rpx',
                height: '96rpx',
                borderWidth: '6rpx',
                borderColor: '#FFF',
                borderRadius: '96rpx'
              }
            },
            {
              type: 'text',
            //   text: this.data.userInfo.nickName || 'Anarkh_Lee',
              text: 'Anarkh_Lee',
              css: {
                top: '532rpx',
                fontSize: '28rpx',
                left: '375rpx',
                align: 'center',
                color: '#3c3c3c'
              }
            },
            {
              type: 'text',
              text: `邀请您观看我的个人博客`,
              css: {
                top: '576rpx',
                left: '375rpx',
                align: 'center',
                fontSize: '28rpx',
                color: '#3c3c3c'
              }
            },
            {
              type: 'text',
              text: `欢迎来到Anarkh-Lee的个人博客系统`,
              css: {
                top: '644rpx',
                left: '375rpx',
                maxLines: 1,
                align: 'center',
                fontWeight: 'bold',
                fontSize: '40rpx',
                color: '#3c3c3c'
              }
            },
            {
              type: 'image',
              url: this.data.imageBase64,//这是一个二维码
              css: {
                top: '834rpx',
                left: '470rpx',
                width: '200rpx',
                height: '200rpx'
              }
            },
            {
              type: 'text',
              text: `长按二维码观看博客`,
              css: {
                top: '915rpx',
                left: '250rpx',
                align: 'center',
                fontSize: '28rpx',
                color: '#3c3c3c'
              }
            }
          ]
        }
      })
    },
    onImgErr(e) {
      wx.hideLoading()
      wx.showToast({
        title: '生成分享图失败,请刷新页面重试'
      })
      //通知外部绘制完成,重置isCanDraw为false
      this.triggerEvent('initData') 
    },
    onImgOK(e) {
      wx.hideLoading()
      // 展示分享图
      wx.showShareImageMenu({
        path:  e.detail.path,
        fail: err => {
          console.log(err)
        }
      })
      //通知外部绘制完成,重置isCanDraw为false
      this.triggerEvent('initData') 
    }
  }
})

  • 获取微信头像的后端代码
@PostMapping(value="getwxacodeunlimit",produces = MediaType.APPLICATION_JSON_VALUE)
public JSONObject getwxacodeunlimit(@RequestParam(value="azb010") String azb010) throws Exception {
    JSONObject result=new JSONObject();
    RestTemplate restTemplate=new RestTemplate();

    restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
    JSONObject object = new JSONObject();

    object.put("scene", azb010);
    object.put("page", "pages/xxx/xxx/xxx/xxxx");
    object.put("auto_color", true);
    object.put("env_version","release");
    object.put("check_path",false);
    String str=object.toString();
    ResponseEntity<byte[]> responseEntity = restTemplate.postForEntity( "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token="+miniCommonUtil.getAccessTokenTest(), str,byte[].class);

    String type = responseEntity.getHeaders().getContentType().getType();
    System.out.println("类型:"+type);
    if("image".equals(type)){
        System.out.println("图片");
        //返回地是图片
        byte[] fileData = responseEntity.getBody();
        String appletCode = Base64.encodeBase64String(fileData);
        System.out.println(appletCode);
        result.put("flag", "1");
        result.put("imageBase64",appletCode);
    }else{
        System.out.println("json");
        System.out.println(new String(responseEntity.getBody()));
        result.put("flag", "0");
    }

    return result;
}

5.2 步骤

  1. 初始化和配置:
  • handleStartDrawImg 方法被调用,显示加载提示。
  • 使用 this.setData 更新 imgDraw 数据,包含画布尺寸、背景渐变色和多个视图元素的配置。
  1. 下载图片资源:
  • painter.js 中的 startPaint 方法被调用。
  • downloadImages 方法下载所有需要的图片资源,使用 downloader.js 进行 LRU 缓存管理。
  1. 设置画布:
  • 根据配置设置画布的实际宽度和高度。
  • 创建 photoContext 用于绘图。
  1. 绘制背景:
  • Pen 类中的 _background 方法被调用。
  • 使用 gradient.js 中的 doGradient 方法生成渐变色背景。
  1. 绘制视图元素:
  • 遍历 views 数组,调用 _drawAbsolute 方法绘制每个元素。
  • 图片绘制:
    • 使用 _drawAbsImage 方法绘制图片。
    • 如果图片是 Base64 编码,使用 downloader.js 中的 download 方法进行处理。
  • 文本绘制:
    • 使用 _fillAbsText 方法绘制文本。
    • 如果设置了背景颜色,使用 _doBackground 方法绘制文本背景。
  • 二维码绘制:
    • 使用 _drawQRCode 方法绘制二维码。
    • 使用 qrcode.js 中的 draw 方法生成二维码。
  1. 完成绘图:
  • 调用 this.ctx.draw 方法完成绘图。
  • 绘图完成后调用 saveImgToLocal 方法将图片保存到本地。
  • 隐藏加载提示,触发 onImgOK 回调函数,通知外部绘图完成。

5.3 详细说明

  1. 背景设置:
  • 使用背景色#B2E2F2。
  1. 图片元素:
  • Logo 图片:
    • type: ‘image’
    • url: 图片的 URL。
    • css: 设置图片的位置、大小和圆角。
  • 用户头像:
    • type: ‘image’
    • url: 用户头像的 Base64 编码或 URL。
    • css: 设置图片的位置、大小和圆角。
  1. 文本元素:
  • 用户昵称:
    • type: ‘text’
    • text: 用户昵称或默认文本。
    • css: 设置文本的位置、字体大小、对齐方式和颜色。
  • 邀请文本:
    • type: ‘text’
    • text: 邀请文本。
    • css: 设置文本的位置、字体大小、对齐方式和颜色。
  • 平台名称:
    • type: ‘text’
    • text: 平台名称。
    • css: 设置文本的位置、字体大小、对齐方式、字体粗细和颜色。
  • 二维码文本:
    • type: ‘text’
    • text: 二维码提示文本。
    • css: 设置文本的位置、字体大小、对齐方式和颜色。
  1. 二维码元素:
  • 二维码:
    • type: ‘qrcode’
    • content: 二维码内容(如直播链接)。
    • css: 设置二维码的位置、大小、背景颜色和二维码颜色。

5.4 关键函数在海报中的作用

  1. handleStartDrawImg (index.js):
  • 初始化并设置 imgDraw 数据,包含画布尺寸、背景渐变色和多个视图元素的配置。
  • 更新视图,触发 painter 组件的绘制。
  1. startPaint (painter.js):
  • 初始化屏幕比例。
  • 下载所有需要的图片资源,使用 downloader.js 进行 LRU 缓存管理。
  • 设置画布宽度和高度,并创建 photoContext。
  • 创建 Pen 实例并调用 paint 方法开始绘图。
  • 绘图完成后调用 saveImgToLocal 将图片保存到本地。
  1. download (downloader.js):
  • 下载图片资源,支持 LRU 缓存策略,确保图片资源的高效加载。
  • 处理 Base64 编码的图片,生成唯一文件名。
  1. doGradient (gradient.js):
  • 根据背景配置生成渐变色效果,支持线性渐变。
  • 使用 sha1.js 生成唯一文件名,确保文件名的唯一性和安全性。
  1. draw (pen.js):
  • 负责具体的绘图操作,包括背景绘制、元素绘制等。
  • 根据配置调用 _background 方法绘制背景。
  • 遍历 views 数组,调用 _drawAbsolute 方法绘制每个元素。
  • 完成绘图后调用 this.ctx.draw 并执行回调函数。
  1. genframe (qrcode.js):
  • 生成二维码,包括计算版本、插入固定部分、转换字符串为位流、计算 ECC、应用掩码、评估图像质量等步骤。
  • 使用 sha1.js 生成唯一文件名,确保文件名的唯一性和安全性。
  1. _drawAbsImage (pen.js):
  • 绘制图片元素。
  • 如果图片是 Base64 编码,使用 downloader.js 中的 download 方法进行处理。
  • 根据配置设置图片的位置、大小和圆角。
  1. _fillAbsText (pen.js):
  • 绘制文本元素。
  • 如果设置了背景颜色,使用 _doBackground 方法绘制文本背景。
  • 根据配置设置文本的位置、字体大小、对齐方式和颜色。
  1. _drawQRCode (pen.js):
  • 绘制二维码元素。
  • 使用 qrcode.js 中的 draw 方法生成二维码。
  • 根据配置设置二维码的位置、大小、背景颜色和二维码颜色。

5.5 效果

没有美工设计,太丑了哈哈哈

6. 代码地址

https://gitee.com/anarkhao/anarkh-we-chat-mini-program-project

7. 写在最后

通过上述配置和流程解析,我们可以看到:

  • 模块化设计: 各个方法专注于特定功能,减少了代码耦合,提高了可维护性。
  • 异步处理: 大量使用 Promise 和异步操作,确保流程顺畅,提升了用户体验。
  • 性能优化: 通过 LRU 缓存、渐变色生成、二维码生成等技术手段,优化了绘图性能。
  • 用户交互: 提供了丰富的用户交互功能,如点击、拖动等,增强了用户友好性。

这些方法共同协作,实现了从用户交互到绘图生成再到结果展示的完整流程,确保了海报生成分享功能的高效稳定运行。通过不同的配置,可以生成各种复杂的海报,包括单纯的海报、带有二维码的海报、背景是图片的海报、背景是渐变色的海报等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值