本文将详细介绍如何从小程序生成海报的 Wxml2Canvas 库迁移到更强大的 Painter 库,涵盖迁移背景、具体步骤、技术实现、性能优化及最佳实践。

1 迁移背景:为何放弃 Wxml2Canvas

1.1 Wxml2Canvas 的痛点

在小程序开发中,生成分享海报是一个常见需求。Wxml2Canvas 库虽然能够将 Wxml 结构转换为 Canvas 并最终生成图片,但在长期使用中暴露了一系列问题:

  • 稳定性极差:经常出现各种奇怪的 BUG,只能勉强使用。

  • 维护不足:代码库更新不及时,社区支持有限。

  • 兼容性问题:在不同机型和小程序版本上表现不一致。

这些问题严重影响了「糖纸」等项目生成分享海报的体验,促使团队寻找更可靠的替代方案。

1.2 Painter 的优势

Painter 是社区中出现的优秀替代方案,具有以下优势:

  • 专门为小程序海报生成设计:更贴合小程序环境。

  • 性能更优:生成时间比 Wxml2Canvas 缩短近 50%。

  • 更好的维护性:活跃的社区支持和持续更新。

  • 灵活性:通过 JSON 配置实现高度自定义。

2 迁移的核心挑战

2.1 使用方式的根本差异

Wxml2Canvas 和 Painter 在使用方式上存在根本区别:

Wxml2Canvas 使用 WXML 和 WXSS

html

<!-- Wxml2Canvas 方式 -->
<view class="share-card">
  <image src="{{avatar}}" class="avatar"></image>
  <text class="name">{{name}}</text>
</view>

Painter 使用 JSON 配置

javascript

// Painter 方式
{
  "width": "375px",
  "height": "200px",
  "background": "#ffffff",
  "views": [
    {
      "type": "image",
      "url": "{{avatar}}",
      "css": {
        "width": "40px",
        "height": "40px",
        "top": "10px",
        "left": "10px"
      }
    },
    {
      "type": "text",
      "text": "{{name}}",
      "css": {
        "color": "#333333",
        "fontSize": "16px",
        "top": "60px",
        "left": "10px"
      }
    }
  ]
}

这种根本性的差异使得直接迁移变得困难,特别是对于有大量现有 Wxml2Canvas 实现的项目。

3 自动化迁移方案:Wxml2Json 转换器

3.1 整体思路

为了降低迁移成本,我们可以创建一个 Wxml2Json 转换器,将现有的 WXML 结构自动转换为 Painter 所需的 JSON 配置。整体流程如下:

  1. 获取 WXML 容器的尺寸和样式

  2. 提取所有需要绘制的节点和计算样式

  3. 根据节点类型转换为对应的 Painter JSON 配置

  4. 将配置传递给 Painter 进行绘制

3.2 核心实现方法

3.2.1 获取 WXML 节点信息

转换器的核心方法是 getWxml,它负责获取容器和所有需要绘制的节点信息:

javascript

getWxml({container, className} = {}) {
  const query = wx.createSelectorQuery()
  
  const getNodes = new Promise(resolve => {
    query
      .selectAll(className)
      .fields(
        {
          id: true,
          dataset: true,
          size: true,
          rect: true,
          computedStyle: COMPOUTED_ELEMENT_STYLE,
        },
        res => {
          resolve(this.formatNodes(res))
        },
      )
      .exec()
  })

  const getContainer = new Promise(resolve => {
    query
      .select(container)
      .fields(
        {
          dataset: true,
          size: true,
          rect: true,
        },
        res => {
          resolve(res)
        },
      )
      .exec()
  })

  return Promise.all([getContainer, getNodes])
}
3.2.2 格式化节点信息

formatNodes 方法负责将获取的节点信息转换为 Painter 可识别的格式:

javascript

formatNodes(nodes) {
  return nodes
    .map(node => {
      const {dataset = {}} = node

      // 合并 dataset 到节点
      node = {...node, ...dataset}

      // 提取关键属性
      const n = _.pick(node, ['type', 'text', 'url'])

      // 根据类型设置 CSS
      n.css = this.getCssByType(node)

      return n
    })
    .filter(s => s && s.type) // 过滤有效节点
}
3.2.3 根据节点类型处理样式

getCssByType 方法根据不同的节点类型处理对应的样式:

javascript

getCssByType(node) {
  const {type, width, height, top, left} = node
  
  const baseCss = {
    width: `${width}px`,
    height: height ? `${height}px` : 'auto',
    top: `${top}px`,
    left: `${left}px`,
  }
  
  // 根据不同类型添加特定样式
  switch(type) {
    case 'image':
      return {
        ...baseCss,
        borderRadius: node.borderRadius || '0px',
      }
    case 'text':
      return {
        ...baseCss,
        color: node.color || '#000000',
        fontSize: `${node.fontSize || 14}px`,
        fontWeight: node.fontWeight || 'normal',
        lineHeight: node.lineHeight ? `${node.lineHeight}px` : 'normal',
        textAlign: node.textAlign || 'left',
      }
    // 处理其他类型...
    default:
      return baseCss
  }
}

3.3 在项目中的使用

有了转换器后,迁移工作变得非常简单:

javascript

// 迁移前
const wxml2canvas = new Wxml2Canvas()
wxml2canvas.draw(config).then(() => {
  const tempFilePath = wxml2canvas.canvasToTempFilePath()
})

// 迁移后
const wxml2json = new Wxml2Json()
wxml2json.getWxml({
  container: '#share-card',
  className: '.draw-element'
}).then(([container, nodes]) => {
  this.setData({
    painterData: {
      width: `${container.width}px`,
      height: `${container.height}px`,
      views: nodes
    }
  })
})

4 解决迁移过程中的问题

4.1 性能问题:画布尺寸与内存占用

4.1.1 问题分析

在迁移过程中,发现某些机型(特别是鸿蒙和 iPhone 12)会出现微信闪退的问题。经过分析,发现问题出现在画布尺寸设置上:

Wxml2Canvas 的做法

  • 画布宽度 = 容器宽度

  • 画布高度 = 容器高度

  • 输出图片时使用 destWidth = width × dprdestHeight = height × dpr

Painter 的做法

  • 画布宽度 = 容器宽度 × dpr

  • 画布高度 = 容器高度 × dpr

  • 输出图片时使用 destWidth = canvas宽度destHeight = canvas高度

4.1.2 问题根源

对于尺寸为 1170 × 17259 的海报,在 dpr = 3 的设备上:

  • Wxml2Canvas 创建 1170 × 17259 的画布,输出 3510 × 51777 的图片

  • Painter 创建 3510 × 51777 的画布,输出 3510 × 51777 的图片

虽然最终输出的图片尺寸相同,但 Painter 创建的画布尺寸是 Wxml2Canvas 的 3 倍,这会导致内存占用大幅增加,从而引起闪退。

4.1.3 解决方案

调整 Painter 的画布尺寸设置,采用与 Wxml2Canvas 相同的策略:

javascript

// 解决方案
const dpr = wx.getSystemInfoSync().pixelRatio

// 使用与 Wxml2Canvas 相同的画布尺寸
const canvasWidth = container.width
const canvasHeight = container.height

// 但在输出时使用 dpr 放大
wx.canvasToTempFilePath({
  x: 0,
  y: 0,
  width: canvasWidth,
  height: canvasHeight,
  destWidth: canvasWidth * dpr,
  destHeight: canvasHeight * dpr,
  canvasId: 'painter',
  success: (res) => {
    // 生成成功
  }
})

4.2 内存泄漏问题

4.2.1 问题表现

在生成海报后,滑动页面时会出现明显卡顿。通过性能分析发现:

  • 生成海报前:CPU 占用率 2%,内存占用 872 MB

  • 生成海报时:CPU 占用率升至 22%,内存占用 895 MB

  • 生成海报后:内存占用没有立即下降,直到离开页面才恢复

4.2.2 解决方案

使用 wx:if 控制分享卡片的生命周期,在生成完成后立即销毁:

html

<share-card
  wx:if="{{showShareCard}}"
  id="share-card"
/>

javascript

// 生成海报
generatePoster() {
  // 显示分享卡片
  this.setData({
    showShareCard: true
  })
  
  // 等待下一轮渲染循环
  setTimeout(() => {
    // 获取 WXML 并转换为 JSON
    this.getWxmlData().then(data => {
      // 设置 Painter 数据
      this.setData({
        painterData: data
      })
    })
  }, 100)
},

// 图片生成完成
onImgOK(e) {
  // 获取生成的图片
  this.setData({
    posterImage: e.detail.path
  })
  
  // 隐藏并销毁分享卡片
  this.setData({
    showShareCard: false
  })
  
  wx.hideLoading()
}

5 Painter 的基本使用方法

5.1 安装与引入

5.1.1 下载 Painter 组件

从 Painter GitHub 仓库 下载组件代码,放入小程序项目的组件目录中。

5.1.2 引入 Painter 组件

在页面的 JSON 配置文件中引入 Painter:

json

{
  "usingComponents": {
    "painter": "/components/painter/painter"
  }
}

5.2 基础用法

5.2.1 WXML 结构

html

<!-- 生成的海报图片预览 -->
<image src="{{src}}" style="height: 980rpx" mode="aspectFit" class="canvas-img"></image>

<!-- Painter 组件 -->
<painter palette="{{wxml}}" style="position: absolute; top: -9999rpx;" bind:imgOK="onImgOK" />

<!-- 生成海报按钮 -->
<button bindtap="painterBtn">生成海报</button>
5.2.2 JS 逻辑

javascript

// 引入 canvas 配置
import canvas from './canvas'

Page({
  data: {
    src: '',
    wxml: null
  },
  
  // 生成海报点击事件
  painterBtn() {
    this.setData({
      wxml: canvas()
    })
    wx.showLoading({
      title: '正在生成中...',
    })
  },
  
  // Painter 图片生成完成
  onImgOK(e) {
    console.log(e)
    this.setData({
      src: e.detail.path
    })
    wx.hideLoading()
  }
})

5.3 动态数据传递

5.3.1 传递参数给海报

javascript

// paintertest.js
import canvas from './canvas'

Page({
  painterBtn() {
    let name = "我是传递的参数"
    this.setData({
      wxml: canvas(name)  // 传递参数
    })
    wx.showLoading({
      title: '正在生成中...',
    })
  }
})
5.3.2 接收参数的 canvas 配置

javascript

// canvas.js
module.exports = data => {
  return {
    "width": "620px",
    "height": "980px",
    "background": "#ffffff",
    "views": [
      {
        "type": "text",
        "text": data.name, // 使用传入的参数
        "css": {
          "color": "#191846",
          "background": "rgba(0,0,0,0)",
          "width": "536px",
          "top": "486px",
          "left": "41px",
          "rotate": "0",
          "borderRadius": "",
          "borderWidth": "",
          "borderColor": "#000000",
          "shadow": "",
          "fontSize": "30px",
          "lineHeight": "43px",
          "fontWeight": "normal",
          "textStyle": "fill",
          "textDecoration": "none",
          "fontFamily": "",
          "textAlign": "left"
        }
      }
    ]
  }
}

5.4 可视化工具的使用

Painter 提供了可视化生成工具,可以大大简化 JSON 配置的编写:

  1. 访问 Painter 自定义海报工具

  2. 通过 UI 界面设计海报样式

  3. 自动生成对应的 JSON 配置

  4. 将配置复制到小程序的 canvas.js 文件中

6 高级技巧与最佳实践

6.1 性能优化

6.1.1 图片压缩与缓存

对于 Painter 中使用的图片,应该进行适当的压缩和缓存:

javascript

{
  "type": "image",
  "url": "https://example.com/image.jpg",
  "css": {
    "width": "200px",
    "height": "200px"
  }
}

最佳实践

  • 使用 CDN 图片并添加宽度/高度参数

  • 对本地图片进行适当压缩

  • 实现图片缓存机制,避免重复下载

6.1.2 视图层级优化

尽量减少 Painter 中视图的层级数量,合并可以合并的元素:

javascript

// 不推荐:层级过深
{
  "views": [
    {
      "type": "view",
      "css": {"background": "#f0f0f0"},
      "views": [
        {
          "type": "image",
          "url": "https://example.com/avatar.jpg"
        },
        {
          "type": "view",
          "views": [
            {
              "type": "text",
              "text": "用户名"
            }
          ]
        }
      ]
    }
  ]
}

// 推荐:扁平化结构
{
  "views": [
    {
      "type": "rect",
      "css": {"background": "#f0f0f0", "width": "100%", "height": "100%"}
    },
    {
      "type": "image",
      "url": "https://example.com/avatar.jpg",
      "css": {"width": "80px", "height": "80px", "top": "10px", "left": "10px"}
    },
    {
      "type": "text",
      "text": "用户名",
      "css": {"top": "100px", "left": "10px", "fontSize": "16px"}
    }
  ]
}

6.2 兼容性处理

6.2.1 字体回退方案

在不同机型上,字体渲染可能存在差异,提供回退方案:

javascript

{
  "type": "text",
  "text": "重要标题",
  "css": {
    "fontSize": "32px",
    "fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
    "color": "#333333"
  }
}
6.2.2 颜色兼容性

确保颜色值在不同平台上显示一致:

javascript

// 使用 6 位 HEX 颜色值,避免使用 3 位缩写
"color": "#ff0000", // 推荐
"color": "#f00", // 避免

// 透明度使用 rgba
"background": "rgba(255, 0, 0, 0.5)" // 推荐

6.3 错误处理与降级方案

6.3.1 完善的错误处理

javascript

// Painter 图片生成失败
onImgErr(e) {
  console.error('图片生成失败:', e.detail)
  wx.hideLoading()
  wx.showToast({
    title: '图片生成失败',
    icon: 'none'
  })
  
  // 降级方案:使用预生成图片或简单图片
  this.setData({
    src: '/images/fallback-poster.jpg'
  })
}
6.3.2 生成状态管理

javascript

Page({
  data: {
    isGenerating: false,
    generateCount: 0
  },
  
  painterBtn() {
    if (this.data.isGenerating) {
      return
    }
    
    // 防止重复生成
    if (this.data.generateCount > 3) {
      wx.showToast({
        title: '生成次数过多,请稍后再试',
        icon: 'none'
      })
      return
    }
    
    this.setData({
      isGenerating: true,
      generateCount: this.data.generateCount + 1
    })
    
    // 开始生成...
  },
  
  onImgOK(e) {
    this.setData({
      isGenerating: false
    })
    // 处理成功...
  },
  
  onImgErr(e) {
    this.setData({
      isGenerating: false
    })
    // 处理错误...
  }
})

7 迁移策略与实施计划

7.1 渐进式迁移策略

对于大型项目,建议采用渐进式迁移策略:

7.1.1 第一阶段:并行运行

让 Wxml2Canvas 和 Painter 并行运行,逐步验证 Painter 的稳定性:

javascript

// 创建统一的海报生成器
class PosterGenerator {
  constructor() {
    this.usePainter = false // 特性开关
  }
  
  generate(container, className) {
    if (this.usePainter) {
      return this.generateWithPainter(container, className)
    } else {
      return this.generateWithWxml2Canvas(container, className)
    }
  }
  
  generateWithPainter(container, className) {
    // 使用 Painter 生成
  }
  
  generateWithWxml2Canvas(container, className) {
    // 使用 Wxml2Canvas 生成
  }
  
  // 切换生成方式
  switchToPainter() {
    this.usePainter = true
  }
}
7.1.2 第二阶段:逐步替换

按照页面优先级,逐步将各个页面的 Wxml2Canvas 替换为 Painter:

  1. 从低优先级页面开始

  2. 验证稳定性和性能

  3. 逐步向高优先级页面推进

7.1.3 第三阶段:全面切换

当所有页面都迁移完毕并验证通过后,完全移除 Wxml2Canvas 依赖。

7.2 测试验证方案

7.2.1 自动化对比测试

创建自动化测试,对比 Wxml2Canvas 和 Painter 的输出结果:

javascript

// 对比测试
async runComparisonTest() {
  // 使用相同数据生成图片
  const wxml2canvasResult = await generateWithWxml2Canvas(testData)
  const painterResult = await generateWithPainter(testData)
  
  // 比较图片尺寸
  assert.equal(wxml2canvasResult.width, painterResult.width)
  assert.equal(wxml2canvasResult.height, painterResult.height)
  
  // 记录性能数据
  console.log('Wxml2Canvas 耗时:', wxml2canvasResult.duration)
  console.log('Painter 耗时:', painterResult.duration)
}
7.2.2 多机型测试清单
测试机型 操作系统版本 小程序基础库 测试结果
iPhone 12 iOS 15+ 2.20.0+
华为 P40 HarmonyOS 2.0 2.20.0+
小米 10 Android 10 2.20.0+
红米 Note 8 Android 9 2.20.0+

8 总结

从 Wxml2Canvas 迁移到 Painter 是一个值得投入的技术改进。通过本文介绍的 Wxml2Json 转换器性能优化技巧 和 渐进式迁移策略,团队可以以最小的成本完成迁移,并获得显著的性能提升和稳定性改善。

关键收获:

  • 迁移后生成时间缩短近 50%

  • 解决了 Wxml2Canvas 的不稳定性问题

  • 通过自动化转换器大幅降低迁移成本

  • 避免了画布过大导致的内存问题

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐