Better

Ethan的博客,欢迎访问交流

记录 mpvue 构建小程序的踩坑与成长

mpvue 是美团开源的,用来开发小程序的工具,通过在 vue 中添加小程序的 runtime 来实现 vue 语法快速构建小程序的目的,也许由于工具还不够完善,踩了不少坑,在这里记录下,持续更新

canvas导致秒退

这一点在 doc 中也有介绍,但是害我找了好久哇,文档是这么说的:

bug: 避免设置过大的宽高,在安卓下会有crash的问题

但是微信坑就坑在,他也不提供一个安全值,经不准确查阅,有博客说,安全值在 1200 左右,因此在项目中设置为 1200 了。

dialog组件名

不知道是踩着啥雷了,我自定义组件取名为 dialog 编译就报错,实在找不到原因了,就尝试着将名字改成了 layer。竟然可以了,我只能说怪哉怪哉。

v-else

倒不是说它控制显示不生效,而是如果在使用了 v-else 的元素上绑定事件,就会出现超级无敌的 bug,貌似事件变成了捕获型一般,但是这样说并不准确,因为并没有父子关系,直接的结果是导致页面多处事件进行了触发。

key

这就需要谈到 vue 的列表渲染优化了。文档是这么说的

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出

由于在项目中,有一个状态是由子组件管理的,删除一个元素后,他的内部状态被其他组件复用了。因此在 vue 中,我们是这么做的。

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有的唯一 id。

vue 的建议也是

建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

在项目中,我绑定了 key 属性为元素唯一 id。且经过实践,在 vue 中表现正确。但是在 mpvue 中貌似依旧采用了就地复用策略一般。重点来了,如果我删除了 key 属性或者绑定为数组遍历下表 index,却表现正常了。对的,你没有看错的。在 mpvue 中,绑定了 key 属性反而不对,去除 key 属性反正正常了,的确是相反的,我表示很痛苦。在 github 上查找 issue,并没有找到类似的。

保存海报

小程序有个保存为图片,用于用户分享的功能。在H5中是可以直接使用html2canvas和canvas2image快捷实现。html2canvas 是一个操作 dom 的插件,但是小程序中并没有 dom 概念,所以并不能如此简单的生成海报。

小程序中有一个 wx.canvasToTempFilePath 这个方法,可以用来将 canvas 保存成图片,但是对于 html2canvas 这一步骤,貌似就只能手动写了。

在具体操作这一块时,碰到三个不大不小的坑。

第一就是由于我的海报需要绘制用户的头像,在使用getImageInfo api获取远程图片的临时路径时,在真机上竟然会报错,打开调试模式准备查看原因,却又神奇的好了,具体原因需要配置服务器域名,即使的微信的服务也需要哦,头像的域名是:wx.qlogo.cn

第二个就是drawImage在模拟器上支持base64编码进行绘制,你需要知道的事,这在真机上是无效的哦。

第三个不知道是不是 mpvue 进行了处理,会自动将 css 中 px 很智能的给你转成 rpx,导致在 android 上具体效果有差异,由于在 canvas 上的绘制都是使用 px 单位的,因此如果不需要进行转换,直接在标签上写style是一个可行的办法哦。

undo操作

项目中,对图片进行涂抹之后,要支持单步撤销操作,之前想的办法是,每次涂抹完毕,就将当前 canvas 转成临时图片存储起来,后来发现完全没必要如此,不过原理也差不多,不过需要进行 canvas2image步骤,直接只用栈存储每次操作的 ImageData 即可。每次撤销就进行弹栈操作,然后将使用 put 方式将数据更新到画布上。

具体代码就不演示了,主要是一堆 api 的使用,api 如下:

  1. wx.canvasPutImageData
  2. wx.canvasGetImageData

后续,在小程序上,Get重新Put页面会糊掉,同时考虑掉PutImageData这个api比较低效,而且这种方式处理实在是太粗暴,因为每次都给整个canvas保存一个快照,内存损耗会是个大问题,因此优化为记录每次动作的方式,针对undo操作,直接pop掉最近的一次,然后整个动作重新执行一次即可。

在这里有个小坑,如果对于 undo 操作点击太快,会导致页面卡死,寻找一番之后,原因就是 draw 操作是个异步操作,在上次还没 draw 完成,又开启一个新的 draw 就会导致这个问题,因此做一个点击限制即可。

在摸索过程中,有两对 api 引起我的误会或是兴趣,在这里理解一下

save & restore

  • save:通过将当前状态放入栈中,保存 canvas 全部状态的方法。
  • restore:通过在绘图状态栈中弹出顶端的状态,将 canvas 恢复到最近的保存状态的方法。 如果没有保存状态,此方法不做任何改变。

对于这对 api,理解绘图状态很重要,保存到栈中的绘制状态包含以下几个部分:

  • 当前的变换矩阵
  • 当前的剪切区域
  • 当前的虚线列表

大致就是一下的状态值:strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled

beginPath & closePath

关于beginPath,这里需要知道的是 canvas 中的绘制方法(如stroke,fill),都会以上一次 beginPath 之后的所有路径为基础进行绘制

  • 不管你用moveTo把画笔移动到哪里,只要不beginPath,那你一直都是在画一条路径。
  • fillRect 与 strokeRect 这种直接画出独立区域的函数,也不会打断当前的 path.

理解了这个时候,看小程序的介绍就更加轻松了,表示开始创建一个路径。需要调用 fill 或者 stroke 才会使用路径进行填充或描边

  • 在最开始的时候相当于调用了一次 beginPath。
  • 同一个路径内的多次 setFillStyle、setStrokeStyle、setLineWidth等设置,以最后一次设置为准。

说完 beginPath,再来聊聊 closePath,这两者紧密联系么,答案是几乎没有,closePath 的意思不是结束路径,而是关闭路径,它会试图从当前路径的终点连一条路径到起点,让整个路径闭合起来。但是,这并不意味着它之后的路径就是新路径了

橡皮擦

项目中还有个橡皮擦功能,这应该是最不好实现的功能了,这里有个小坑,因为我之前是用的 stroke 进行涂抹功能开发,但是在 CanvasContext 的 globalCompositeOperation 属性有使用限制。

在绘制新形状时应用的合成操作的类型。目前安卓版本只适用于 fill 填充块的合成,用于 stroke 线段的合成效果都是 source-over(默认)。

因此需要将 stroke 替换成 fill,从而间距涂抹和橡皮擦功能。

纵观 canvas api,具体操作无非如下几种

  • 画图
  • 划线
  • 画图形
  • 写字
  • 动画

其中通过 stroke 控制线条样式,fill 负责填充样式,这样一来,对于 canvas 相关操作就基本了然于心了

关于合成

globalCompositeOperation 属性:设置一个源(新的)图像绘制到目标(已有)的图像上。

  • 新图在上
    • source-atop:显示原图区域
    • source-in:显示重叠区域
    • source-out:除去原图区域
    • source-over:原图与新图区域(默认)
  • 新图在下
    • destination-stop:显示原图区域
    • destination-in:显示重叠区域
    • destination-out:显示新图区域
    • destination-over:原图和新图重叠区域
  • 其他
    • lighter:重叠区域颜色混合
    • copy:保留新图
    • xor:重叠区域透明

最终方案

使用 globalCompositeOperation 属性会导致最终界面是不完整的,突然有一天想到使用 clip 的方式来实现,只用在擦除时绘制出合适的图形。调用 clip 函数限制绘图区域,然后重新绘制一遍原图片即可。

本以为万事大吉了,但是不知道是不是在小程序中 canvas 独有的问题还是 canvas 自身的问题,绘制多次之后会导致整个 canvas 很卡,一时想不出优化的办法,突然有天做梦想到,可以在用户完成一次操作后,保存当前 canvas 图像,然后清除画布,直接绘制最终状态,这样一来卡的问题就会好很多。建议结合函数防抖使用。

卷帘效果

讲道理这个是比较简单的一个功能了,但也小小的采坑了。

第一:一般的元素不能遮盖住 canvas 组件,因此只能使用 cover-view 组件,使用 button 作为其子元素用来触发 touch 事件,这里有个坑就是,cover-view 下 button 如果使用背景图片,会导致不可见,使用文本按钮才有效果,故换成 cover-image 元素,可是 cover-image 元素却不能响应 touch 事件,因此只能 button 结合 cover-image 使用了,最后一个坑就是 cover-image 不能是 base64 图片

第二个坑是在 iOS 上才表现出来,iOS 默认开启滚动,导致 touchmove 不触发,因此不需要在配置文件中设置disableScroll: true

事件与数据缓存

vue 有自己的事件,小程序也有自己的事件,且小程序的原生事件在 mpvue 中是可以直接使用的。很可能就导致事件选择困难症了。这里先看看常见事件的触发顺序

最先看看 created 事件,由于小程序的特殊性,在加载时,页面级别的组件都是一次全部加载完毕的,表现如下

  • 页面级别:应用启动成功全部触发,即使使用 reLunch 跳转,也不会导致 created 触发了
  • 普通组件:表现一致,用时触发

在看看其他事件的触发顺序

  • 页面进入有:onLoad -> onReady -> mounted
  • 进入下一层页面:onHide
  • 返回该页面:onShow
  • 退出该页面:onUnload

注:vue 事件 destroy 不执行,当前 mpvue 版本 1.0.11

由于 created 事件的特殊性,这里有个大坑,就是数据缓存的问题,这里在官方 issue 140,也是讨论一大堆,提出了各种各样解决办法,有单独开 repo 解决这个问题的,比如 mpvue-page-factory。甚至有受 mpvue 启发,再写一个轮子的,比如 megalo。目前项目解决办法为在 unUnload 事件中重置数据,为避免重复代码,采用 mixins 使用

export const resetPageData = {
    onUnload() {
        console.log('rest data worked')
        Object.assign(this, this.$options.data())
    }
}

但这种办法有个场景无法解决,比如A详情页跳转B详情页,由于没触发 onUnload 事件,势必导致数据缓存问题,应该可以采用如下方式解决,就没有测试了

const dataStack = []
export default {
    onLoad() {
        dataStack.push({...this.$data})
    },
    onUnload() {
        Object.assign(this.$data, dataStack.pop())
    }
}

drawImage质量下降

首先熟悉下canvas drawImage api

context.drawImage(img, dx, dy)
context.drawImage(img, dx, dy, dWidth, dHeight)
context.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

重点理解下,参数缺省的情况下是如何处理的。

  • dx, dy, dWidth, dHeight:表示在 canvas 上规划一片区域用来放置图片, dx, dy 为 canvas 区域左上角坐标,dWidth, dHeight 指 canvas 元素上用来显示图片的区域大小。如果没有指定sx, sy, sWidth, sHeight这四个参数,则图片会被拉伸或缩放到这片区域内
  • sx, sy, sWidth, sHeight:这四个坐标是针对元素图片的,表示图片在 canvas 画布下显示的大小和位置。sx, sy 表示图片上sx,sy这个坐标作为左上角,然后往右下角的 sWidth,sHeight尺寸范围作为最终在 canvas 上显示的图片内容

drawImage 压缩原理:把一张大的图片,直接画在一张小小的画布上,此时大图片就天然变成了小图片,自然也就压缩了。也就是说,如果图片比 canvas 大,则使用 drawImage 画到一个较小的画布上时图片就被压缩到了canvas的大小,就会造成截出来图片适量下降。

除了通过 canvas 减少图片大小之外,保存为 jpg 天然可以降低大小,同时 jpg 支持 quality 控制质量。

解决办法:

  • 方案一:准备两个 canvas,一个绘制压缩图,一个绘制原图大小,截图的时候从原图canvas截取。
  • 方案二:将 canvas 大小设为原图大小,使用 css 将 canvas 缩放到适应屏幕大小。小程序中 canvas 暂时不支持 width 和 height 属性,因此无法使用

后续备注:由于 canvas 总是避免不了质量下降的问题,导致模糊问题总是不好处理,因此转换思路,并不在原图上进行涂抹,而是在一个空的 canvas 上进行涂抹,然后保存为 png 格式,将 canvas 盖在 image 标签上。

感悟:当你觉得接下来越来越难时,也许是时候转换一种思路,加法走不通了,要学会做减法。

cover-view渲染

在项目中需要实现一个卷帘效果,需要根据手势操作同步更改元素的高度,却一直感觉有些闪烁,实在找不到原因,找了好久发现,使用 div 的话,就不存在这个问题,所以可见问题出在 cover-view 上,查找资料后发现如下回复

cover-view 跟随手指移动的支持性目前还不是很完善,我们会后续优化,之后会推出同层渲染方案,就不用 cover-view,敬请期待。

看完之后我内心真是……

选型 mpvue 思考

我想之所以选择 mpvue 主要原因如下

  1. 基于 vue 技术栈,学习成本低
  2. 大厂出品,经历过了实战和大量开发者的采坑,有 bug 响应应该比小团队来的快

这次一个项目实践下来,发现并不如预期理想,mpvue 开源后就很少更新了,对于 bug 响应也不是很快诶,所以才会大大小小踩了不少坑。

关于小程序这一块,京东的 taro 也是最近比较火的选型,基于 react 技术选型,宣传介绍看上去倒是很美好,但是否有坑,还是要去踩一踩才知道咯。有机会踩踩吧。

资料



留言