使用 Vite+Vue3+pinia 写一个长图拼接 PWA

237 阅读6分钟

简介

一个简单的长图拼接工具,主要应用了 Vue3 技术栈和 HTML5 的 Canvas 技术。最后包装成 PWA,可以从手机桌面直接访问。本文做一个项目回顾和总结,源码和地址附下:

线上地址: picStitch
源码: hooozen/picStitch

技术小结

面向对象开发

无论是主动还是被迫,随着业务逻辑变得复杂,开发者必然要引入面向对象技术。对于这个项目,采用面向对象技术主要有两个原因:

从主观方面来讲,我想把这个项目做成 Web 和小程序多端应用,于是把一些图片裁剪、绘制的核心代码封装成公共基础类。这样可以方便地实现复用和维护,为后面多端的开发和维护节省大量精力。

客观方面,这个项目涉及了图片预览和图片编辑两块画布。两块画布在图片绘制方面有共同的逻辑,同时又分别承担保存和编辑等不同的职责。最重要的是,在两块画布之间要做状态的复制和同步:当从预览画布到编辑画布时,需要把预览画布的图片状态复制到编辑画布。当编辑画布编辑成功时,要把编辑画布的状态同步回预览画布。为了避免性能问题,两块画布要共用图片对象,也就是两块画布仅保存图片的裁剪信息,这样可以极大地节约内存。在对业务逻辑反复梳理后,主要封装了以下几个基础的类(源码):

  • ImageClipView: 图像的裁剪视图,用来保存图片的裁剪状态,并对外提供裁剪方法
  • ImageCavnasView: 图像的绘制视图,用来保存图片在画布上的绘制状态
  • PicStitchCavnas: 项目的 Canvas 基类,封装了 HTML5 Canvas 的绘制、刷新和保存等方法
  • FlexCanvas: 弹性画布类. 因为预览图片时画布的高度以及图片的缩放比例要根据所有图片动态计算,因此封装成类.
  • JointCanva: “连接”画布类. 用于编辑图片的连接处,主要封装了对画布上图片进行裁剪的方法.
  • ImageClipViewManager: 图像管理类. 单例模式,通过管理图片对象来生成视图,以及实现视图的复制和状态同步.

以上类的封装主要遵循了单一职责原则,在我看来这是面向对象最重要的原则。单一原则使得类的扩展和使用变得十分简单清晰.

最后,写 JS 的面向对象一定要用 Typescript,这将为你节约大量宝贵时间。

Vue3 技术栈

我本来是想使用原生 Web 技术去写的,但考虑到最近要找工作面试,所以熟悉下“生产力工具”吧。尝试下来,Vite + Vue3 + pinia 确实是一套特别省力和轻量的工具,尤其是引入 TypeScript 和组合式 API 后,可以抛弃之前 Vue2 时代很多复杂的概念。例如 pinia 实现的状态管理几乎可以不使用 actiongetter 就可以实现绝大部分业务逻辑,对于小型项目来说要比 Vuex “人性化”得多。Vite 的构建速度和配置速度也要甩 webpack 好几道街。

总的来说,Vite + Vue3 是一套离“开箱即用”又近了一大步的前端技术栈,非常推荐。

PWA

记得 2017 年第一次在谷歌开发者沙龙中了解到这个技术时我非常的兴奋,年轻的我觉得这个技术将快速飞速发展,然后垄断全端的开发。没想到,这么多年过去了 PWA 仍然不温不火,反而是各类“小程序”野蛮生长,成了跨端开发的重要技术。

虽然如此,我仍然认为 PWA 是一个优秀的设想。并且,在 Vite 下只需要一个插件,就可以将 Web 应用包装成 PWA:Vite PWA

图片拖动

图片的拖动排序看似是一个简单的功能,其实里面有很多细节和难点。我来一一介绍:

首先是 HTML5 的 Drag 事件。Drag 事件最大的问题就在于拖动时,元素本身是不移动的,而只是一个半透明的元素投影跟随鼠标移动。其实这有没有什么大问题,但是如果想实现流畅和优美的动画效果,就非常难了。因此反而不如使用鼠标事件更方便。

其次是使用 Flex 定位来布局元素的初始定位,当元素排序变化时使用 Translate 来移动元素的位置。这样做的最大问题是,当移除某个元素时,元素在文档流中的初始位置会发生变化。这就意味着还要使用不同的参数去计算各个元素的 Translate,非常复杂。

直接改变 DOM 节点的真实位置,这个方案我根本没有尝试。因为在拖动过程中,元素的位置会不停的改变,虽然 Vue 的虚拟节点算法,但我仍然觉得这是对性能的浪费。更重要的是直接改变 DOM 节点的位置意味着将无法使用过渡动画!不能接受!

我最后采用了【绝对定位+鼠标事件+Translate】位移的方案,大体的思路如下:

首先通过外层元素的大小来划分格子。然后让所有的图片都 absolute 定位在外层元素的左上角,然后根据它们的顺序计算应该偏移的位置。这样做的好处是,所有的图片计算偏移量的算法都一样,也不会随着图片数量的变化而变化。同时能够兼顾使用各种过渡动画。

移动端触摸事件

移动端的触摸事件并不兼容鼠标事件!并且有很多反常的行为,例如 touchmove 事件的 target 永远指向初始点击的元素。并且像鼠标事件的 offsetX 等属性在触摸事件上也是没有的。另外移动端的触摸事件还容易触发浏览器的返回和刷新等手势。

总之,如果你之前没有进行过移动端触摸事件的适配,那么一定为它预留好足够的时间来查阅文档和 StackOverflow。

Canvas 和 SVG

项目中用到的图片包括我自己设计的 LOGO 全都使用了 SVG 技术,这是一个很优雅的图片解决方案。并且 SVG 还可以通过 base64 编码嵌入到 CSS 作为 background-url

Canvas 用来处理图片非常方便,要注意保存图片的一些方法。例如 canvas.toBlobcanvas.toDataURL 方法的对比。

其他

一定要事先进行 UI 和交互设计,否则可能写着写着发现一些功能在当前的 UI 下很难实现,或者操作逻辑非常奇怪。而避免出现这种问题的最好的方法就是参考类似功能的 APP,很明显我这款 APP 的 UI 很大部分借鉴了微信,而交互逻辑借鉴了App Store 上的“PicTailor” (apple.com),如果这款软件有安卓版我也不会开发这个小工具了。