一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
前言
该文章主要讲解思路有两个方向:
- 在接到一个 需求 时,如何整理思路,提前找到项目中的不确定性,以避免在开发中遇到意外。
- 在代码开发的过程中,如何让代码更抽象,以便于代码的复用、维护难度的降低。
看完该文章,你将收获:
- 面向对象 、 面向过程 之间的区别
- 意图 、 实现 的区别
- 微信小程序
canvas基本使用 canvas中如何进行换行操作canvas如何做到高度的自适应
项目基本介绍
我们最近在做一个 问卷 类型的小程序,项目类似于 问卷星 、 金数据 。
在我们一个问卷收集完毕后,用户会看到自己已经填写的数据。
新的需求
这时用户希望导出该答卷的数据,以便于后续查看。
需求调研
实现屏幕截图在我们的思想中有这么几个方案:
- 用户手动截图
- 通过
wx提供的接口查看是否提供屏幕截取功能(类似于 chrome 的元素截图) - 通过
canvas将数据渲染成一张图片,再通过canvas提供的接口转换为base64位的数据导出图片
用户手动截图
由于我们的目标用户是中老年人,那么 截长屏 对于中老年人来说学习成本是难以接受的,所以该方案被首先 pass。
wx 接口截图
通过对于文档的调研,暂未找到该接口的支持,那么该方案也不可行。
canvas 导出图片
该方案在初始调研阶段分为两块:
- 小程序
canvas绘制 canvas转换为base64的图片
这两个方面支持则该方案具有基本的落地能力。
DEMO 制作
具体的开发思路通过思考分为以下五块:
- 获取
canvas的node节点 - 获取
context绘制上下文 - 具体绘制
- 导出为图片
获取 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 文本过长不换行问题解决
canvas 的 context 提供了 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 版本最终代码实现
写在最后
写到这里,业务实现就基本完成了。
下一版本需要考虑的问题就是,我们再接收到类似的业务,如何避免重复代码的二次开发。将通用的内容再次封装将是我们下一次的开发目标。