通过 canvas 理解 面向对象、面向过程 (小程序 canvas 表单)

1,312 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前言

该文章主要讲解思路有两个方向:

  • 在接到一个 需求 时,如何整理思路,提前找到项目中的不确定性,以避免在开发中遇到意外。
  • 在代码开发的过程中,如何让代码更抽象,以便于代码的复用、维护难度的降低。

看完该文章,你将收获:

  • 面向对象面向过程 之间的区别
  • 意图实现 的区别
  • 微信小程序 canvas 基本使用
  • canvas 中如何进行换行操作
  • canvas 如何做到高度的自适应

项目基本介绍

我们最近在做一个 问卷 类型的小程序,项目类似于 问卷星金数据
在我们一个问卷收集完毕后,用户会看到自己已经填写的数据。

新的需求

这时用户希望导出该答卷的数据,以便于后续查看。 image.png

需求调研

实现屏幕截图在我们的思想中有这么几个方案:

  • 用户手动截图
  • 通过 wx 提供的接口查看是否提供屏幕截取功能(类似于 chrome 的元素截图)
  • 通过 canvas 将数据渲染成一张图片,再通过 canvas 提供的接口转换为 base64 位的数据导出图片

用户手动截图

由于我们的目标用户是中老年人,那么 截长屏 对于中老年人来说学习成本是难以接受的,所以该方案被首先 pass。

wx 接口截图

通过对于文档的调研,暂未找到该接口的支持,那么该方案也不可行。

canvas 导出图片

该方案在初始调研阶段分为两块:

  • 小程序 canvas 绘制
  • canvas 转换为 base64 的图片

这两个方面支持则该方案具有基本的落地能力。

DEMO 制作

具体的开发思路通过思考分为以下五块:

  1. 获取 canvasnode节点
  2. 获取 context 绘制上下文
  3. 具体绘制
  4. 导出为图片

获取 canvas 的 node节点

wxml

<canvas id='canvas' type='2d' class='canvas' />

wxss

.canvas {
  width: 100%;
  height: 500px;
}

js

const query = wx.createSelectorQuery();

query
 .select("#canvas")
 .fields({ node: true, size: true })
 .exec(res => {
   const canvasEl = res[0].node;
   canvasEl.width = res[0].width;
   canvasEl.height = res[0].height;
});

获取 context 绘制上下文

    ...
   canvasEl.height = res[0].height;
   const ctx = canvasEl.getContext("2d");
});

具体绘制

   ...
   const ctx = canvasEl.getContext("2d");
   ctx.fillText('题目1', 20, 20);
   ctx.fillText('tips提示', 20, 40);
   ctx.fillText('答案: 选项1', 20, 60);
});

导出为图片

   ...
   ctx.fillText('答案: 选项1', 20, 60);
   exportImage(canvasEl);
});

const exportImage = (canvasEl) => {
  const base64Data = canvasEl
    .toDataURL("image/jpg")
    .slice(22, canvasEl.toDataURL("image/jpg").length);

  const fs = wx.getFileSystemManager();
  const path = wx.env.USER_DATA_PATH + "/temp.jpg";
  fs.writeFile({
    filePath: path,
    data: base64Data,
    encoding: "base64",
    success(wrRes) {
      wx.saveImageToPhotosAlbum({filePath: path});
    }
  });
};

完整 Demo

到这步为止,我们的 Demo 就跑通了,前置条件都满足,下面我们便可以进行后续开发。

正式开发

正式开发前,先给大家看一下需要渲染数据的数据结构:

const questions = [
  {
    "title": { "value": "1. 单选题", "size": 20, "color": "#000" },
    "tips": { "value": "测试单选题", "size": 16, "color": "#999" },
    "value": { "value": "其他", "size": 16, "color": "#333" },
  },
  {
    "title": { "value": "2. 多选题", "size": 20, "color": "#000" },
    "tips": { "value": "测试多选题", "size": 16, "color": "#999" },
    "value": { "value": "选项1、选项2、其他", "size": 16, "color": "#333" },
  },
  {
    "title": { "value": "段落", "size": 20, "color": "#000" },
    "tips":
      {
        "value": "测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行tips测试测试段落,多行ti",
        "size": 16,
        "color": "#999",
      },
    "value": { "size": 16, "color": "#333" },
  },
  {
    "title": { "value": "3. 单行文本填空", "size": 20, "color": "#000" },
    "tips": { "value": "测试 单行文本填空", "size": 16, "color": "#999" },
    "value": { "value": "测试", "size": 16, "color": "#333" },
  },
  {
    "title": { "value": "4. 多行文本填空", "size": 20, "color": "#000" },
    "tips": { "value": "测试多行文本填空", "size": 16, "color": "#999" },
    "value": { "value": "测试", "size": 16, "color": "#333" },
  },
  {
    "title": { "value": "5. 数字填空", "size": 20, "color": "#000" },
    "tips": { "value": "测试数字", "size": 16, "color": "#999" },
    "value": { "value": "6", "size": 16, "color": "#333" },
  },
  {
    "title": { "value": "6. 日期", "size": 20, "color": "#000" },
    "tips": { "value": "请选择日期 年-月-日", "size": 16, "color": "#999" },
    "value": { "value": "2021-04-14", "size": 16, "color": "#333" },
  },
]

version1 版本代码演示

该版本完成了页面的渲染但遇到了两个问题:

  • canvas 在单行文本过长时,不会自动换行
  • canvas 在子元素超过父元素时,不会自动撑开

canvas 文本过长不换行问题解决

canvascontext 提供了 measureText 接口,可以获取到当前文字的渲染宽度。

我们可以通过该接口将数据进行裁切,代码如下:

const textList = [];
let tmp = "";
for (let i = 0; i < text.length; i++) {
  const cur = text[i];
  if (ctx.measureText(tmp + cur).width < width) {
    tmp += cur;
    if (i == text.length - 1) {
      textList.push(tmp)
    }
  } else {
    textList.push(tmp);
    tmp = cur;
  }
}

canvas 的子元素超过父元素时,不会自动撑开问题解决

解决思路也很简单,通过第一次调用绘制函数,返回绘制的最终高度,然后改变 canvas 画布高度,再进行重新绘制。

wxml

<canvas ... style={{height}} />

js

const drawQuestions = () => { 
  ...
  return drawY;
}

...
const height = drawQuestions();
canvasEl.height = height;
setData({ ...this.data, height });

drawQuestions();
...

version2 版本代码演示

到这步为止,我们的业务算是基本实现了,但是这个代码 Emmm..

看起来有点恶心?过于的 面向过程 ,导致代码几乎丧失了 扩展性意图实现 耦合在一起,如果这是我维护其他人的代码的话,一定会忍不住跑路吧?

举个栗子:
代码中的 canvas 高度获取canvas 绘制 调用了同一个函数,在不知道代码由来的人看来,完全不明白该函数是什么作用。

version3 版本最终代码实现

写在最后

写到这里,业务实现就基本完成了。

下一版本需要考虑的问题就是,我们再接收到类似的业务,如何避免重复代码的二次开发。将通用的内容再次封装将是我们下一次的开发目标。