ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序 Lottie 实战:lottie-miniprogram 接入与调优

微信小程序 Lottie 实战:lottie-miniprogram 接入与调优 微信小程序里做动效这件事我从最早的 CSS keyframes 一路摸到 Lottie中间绕的弯路足够凑一篇长贴。项目里一旦出现设计师给的那种带缓动曲线、路径变形、多层错帧的 AE 动效用纯 CSS 或者序列帧去还原基本等于手工重画一遍改一版还得重来一次。lottie-miniprogram 这个库的价值就在这儿它让前端直接吃 AE 导出的 JSON在小程序的 canvas 上把动画跑起来资源体积小、还原度高、改稿成本低。这篇东西不讲高深理论就是把我从零接入、尺寸适配、播放控制、性能调优到上线排查踩过的坑按顺序摊开说一遍。不管你是刚接手小程序项目的新人还是被动效需求反复折磨的老手都能从里面挑到能直接抄的部分。1. 小程序动画选型为什么最后把票投给 Lottie1.1 几条常见路子的实际表现对比做小程序动画可选的路子其实就那么几条我几乎都试过一遍。CSS animation 最轻写起来也快但能力天花板很低遇到路径变形、蒙版、多图层错帧就抓瞎。GIF 兼容性没得说可它只有 256 色渐变和半透明边缘的锯齿在小程序里被放大得很难看而且体积动不动就几百 KB 起步。序列帧图片更夸张一套 30 帧的 750×750 PNG压缩完可能好几 MB主包根本塞不下只能扔 CDN 走网络请求首屏体验直接崩掉。原生 canvas 自己画图控制力最强但开发成本极高动效稍微复杂一点就是一堆三角函数和贝塞尔曲线改一版动效约等于重写一遍代码。至于 web-view 里嵌 H5 跑 lottie-web问题也不少通信靠 postMessage白屏概率高层级还容易和原生组件打架。比来比去把 AE 动效直接在小程序原生 canvas 上还原才是那条成本和效果最平衡的路。方案资源体积动效还原度开发成本主要短板CSS animation极小低低复杂路径、蒙版做不了GIF中到大中低仅 256 色边缘锯齿序列帧图片极大高中体积失控首屏慢手写 canvas极小取决于人极高改稿等于重写web-view lottie-web中高中白屏、层级、通信麻烦lottie-miniprogram小高低依赖 canvas 2d 能力1.2 lottie-miniprogram 到底解决了什么一句话概括它把 lottie-web 的 CanvasRenderer 搬到了小程序的 canvas 2d 上。设计师在 AE 里用 Bodymovin 插件导出 JSON前端把这个 JSON 喂给 lottie-miniprogram库内部解析图层、关键帧、缓动曲线逐帧绘制到 canvas 上。整个过程不依赖 DOM也不依赖 SVG所以小程序这种没有真实 DOM 的环境才能跑得动。它有明确的能力边界这点必须先说清楚。第一它只做渲染不做交互逻辑动画里的按钮点击不会自动绑定事件得自己根据动画进度去判断。第二它依赖 canvas 2d 接口基础库版本太低低于 2.9.0的机器上拿不到 canvas 节点。第三表达式的支持是残缺的AE 里用表达式驱动的动效导出后大概率会失效这个必须在和设计师对接时就提出来别等上线才发现动画「缺了一块」。实操心得选型阶段就把「动效能不能被 Lottie 完整导出」当成一个前置检查项。让设计师导出后自己先用预览工具放一遍确认和 AE 里看到的一致再交付给前端能省掉后面大量的扯皮。2. 渲染原理从 AE 工程到画布上的每一帧2.1 Lottie JSON 的内部结构长什么样理解 JSON 结构对排查问题帮助极大。一个典型的 Lottie 文件顶层有这么几个字段v是 Bodymovin 版本号fr是帧率ip和op是动画的起始帧和结束帧w和h是画布的逻辑宽高layers是图层数组assets放图片等外部资源。真正决定画面的是layers每个图层里有ks变换包含位置、缩放、旋转、锚点、透明度、shapes形状路径、t时间轴上的关键帧序列。关键帧里的k字段是核心。如果它是一个数组说明是静态值如果是一个带t和s的对象数组说明是按时间变化的关键帧每一帧还带i和o描述缓动曲线的控制点。lottie-miniprogram 干的事就是在每一帧根据当前时间在关键帧之间做插值算出每个图层的最终变换矩阵然后按顺序绘制。这也解释了为什么动效要尽量少用表达式、少用轨道遮罩这些特性在导出时会被转成私有字段运行时如果库不支持画面就会缺东西或者干脆不显示。2.2 lottie-miniprogram 在小程序里的渲染链路完整链路是这样的页面加载后通过wx.createSelectorQuery().select(#canvas).node()拿到 canvas 2d 节点调用lottie.setup(canvas)做一次全局初始化把 canvas 的requestAnimationFrame、createImage这些方法挂到库内部的适配层上然后lottie.loadAnimation()创建动画实例传入 JSON 数据和渲染上下文库内部启动一个逐帧循环每帧解析关键帧、算矩阵、调 canvas 的 2D API 绘制。这里有个容易被忽略的点lottie.setup()对同一个 canvas 只需要调一次。我见过有人在onShow里反复 setup结果动画越跑越卡因为每调一次就往适配层里塞了一份 rAF 回调。正确的做法是把 setup 放在onReady里只执行一次页面重进时复用同一个实例或者重建。2.3 为什么必须用 canvas 2d 而不是旧版 canvas旧版 canvas 通过canvas-id加wx.createCanvasContext()使用绘制指令是异步提交到原生层的没有同步的绘图上下文也拿不到 canvas 节点本身。lottie-miniprogram 需要直接操作 2D context还需要canvas.requestAnimationFrame来驱动逐帧循环这两样旧版都不给。所以现在写代码必须用canvas type2d然后通过node字段拿到真实节点。基础库低于 2.9.0 的客户端拿不到 node这时候要么提示用户升级微信要么走降级方案放一张静态图顶着。这个版本线在做兼容性评估时必须查一遍别等测试机上报「动画不显示」才回头找原因。3. 从零跑通一个最小示例3.1 依赖安装与开发者工具配置第一步装依赖在项目根目录执行npm install lottie-miniprogram --save装完之后打开微信开发者工具在菜单里找到「工具 - 构建 npm」等构建完成项目根目录会多出一个miniprogram_npm文件夹。这一步不做的话import lottie from lottie-miniprogram会直接报模块找不到。如果项目用的是自定义构建目录记得在project.config.json里把miniprogramRoot和packNpmManually配置对齐否则构建出来的包位置和实际引用路径对不上。有段时间我遇到构建成功但运行时报错的情况排查下来是开发者工具的 npm 缓存脏了删掉miniprogram_npm重新构建就好。这个操作现在已经是我的条件反射凡是 npm 包相关的诡异报错先清缓存重建。3.2 页面三件套的完整写法先看 WXML注意type2d和id都必须有view classanim-wrap canvas idanimCanvas type2d classanim-canvas/canvas /viewWXSS 里控制的是展示尺寸不要和 canvas 的物理像素混淆.anim-wrap { display: flex; justify-content: center; align-items: center; } .anim-canvas { width: 240px; height: 240px; }JS 是核心部分完整写法如下import lottie from lottie-miniprogram Page({ data: {}, onReady() { this.initLottie() }, initLottie() { const query wx.createSelectorQuery() query.select(#animCanvas) .fields({ node: true, size: true }) .exec((res) { if (!res || !res[0] || !res[0].node) { console.warn(canvas 节点未获取到检查 type2d 与基础库版本) return } const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getWindowInfo().pixelRatio canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) lottie.setup(canvas) this.animation lottie.loadAnimation({ loop: true, autoplay: true, animationData: require(../../anim/loading.js), rendererSettings: { context: ctx, }, }) }) }, onUnload() { if (this.animation) { this.animation.destroy() this.animation null } }, })几个细节值得单独拎出来说。fields({ node: true, size: true })必须同时要 node 和 sizesize 用来算 DPRanimationData这里用的是require的 JS 模块而不是 JSON 文件原因在下一节讲destroy()一定要在onUnload里调不然页面退出后动画实例还挂在内存里来回切几次页面内存就上去了。3.3 动效资源怎么放、体积怎么控小程序对 JSON 文件的require支持在不同版本的工具上表现不一致最稳的做法是把 JSON 内容转成 JS 模块。在文件开头加一行module.exports 扩展名改成.js就能被正常打包和引用。转换脚本很简单Node 里跑一下就行const fs require(fs) const src fs.readFileSync(./loading.json, utf-8) fs.writeFileSync(./loading.js, module.exports src)如果一定要走网络加载那就用wx.request把 JSON 拉下来再传animationDatawx.request({ url: https://your-domain.com/anim/loading.json, success: (res) { this.animation lottie.loadAnimation({ loop: true, autoplay: true, animationData: res.data, rendererSettings: { context: ctx }, }) }, })走网络的前提是域名已经配到小程序的合法域名列表里否则真机上直接失败开发者工具里可能因为「不校验合法域名」的开关而正常这也是典型的「工具正常真机白屏」案例。体积方面动效 JSON 上百 KB 非常常见带图片资源的甚至能到几 MB。主包有 2MB 的硬限制所以我的习惯是把动画资源统一丢进分包配合分包预下载在需要播放的页面之前把资源拉好。如果 JSON 里引用了图片优先让设计师直接内嵌成 base64虽然会让 JSON 变大但省掉了图片路径适配的麻烦如果一定要外链图片那图片的路径必须是 canvas 的createImage能识别的也就是本地绝对路径或者已配置域名的网络地址。4. 尺寸适配与播放控制把动画真正用起来4.1 DPR 适配的推导过程为什么要有 DPR 这一步我用具体数字讲一遍。假设 WXSS 里 canvas 的展示尺寸是 240×240 CSS 像素手机 DPR 是 3。如果不做处理canvas.width默认等于 240那么 240 个物理像素要铺满 720 个物理像素的屏幕区域结果就是模糊加锯齿矢量图形的边缘尤其明显。正确做法是把 canvas 的物理尺寸设成240 × 3 720然后调ctx.scale(3, 3)让后续所有绘制坐标都按 CSS 逻辑像素计算。这样 lottie 按 720×720 的物理尺寸渲染绘制指令在逻辑空间里是 240×240最终映射回 720 个物理像素画面就清晰了。这里有个坑ctx.scale()在同一个 context 上重复调用会累乘。如果页面里有多个动画共用同一个 canvas或者你在某个回调里又调了一次 scale画面会被放大到离谱。我的做法是在 setup 之前只调一次 scale并写一句注释标明「此处只执行一次」防止后来接手的人改出问题。4.2 播放控制 API 与常用事件lottie-miniprogram 暴露的控制接口和 lottie-web 基本一致常用的有这么几个play()从当前位置继续播放pause()暂停在当前帧stop()停止并将帧位重置到起始destroy()销毁实例释放内存setSpeed(n)设置倍速1 是原速负数表示反向播放goToAndStop(frame, true)跳到指定帧并停下第二个参数为 true 表示按帧号而不是按秒goToAndPlay(frame, true)跳到指定帧并继续播放playSegments([[start, end]], true)只播放指定区间常用于「播一段特定动作」事件方面complete在整个动画播完时触发loopComplete在每一轮循环结束时触发enterFrame每帧都触发。最后这个要慎用它会在每帧回调里执行你的逻辑稍微重一点的操作就会掉帧。我一般只在需要做「动画进度条」或者「按进度切文案」时用enterFrame而且回调里只做取值和setData的最小操作。this.animation.addEventListener(complete, () { // 动画播完例如进入下一步 wx.showToast({ title: 加载完成, icon: none }) })4.3 多实例与页面生命周期管理一个 canvas 只能承载一个 lottie 实例这是硬约束。如果你需要在同一页面上跑两个动画就得放两个 canvas分别 setup、分别 loadAnimation。别想着在一个 canvas 上叠两层绘制上下文会互相覆盖。生命周期这块我总结了三条固定的处理规则。onHide时调pause()避免页面在后台还白白占着 CPU 和电量onShow时调play()恢复onUnload时调destroy()并把引用置空。对于长列表里带多个动画的场景还要做可视区域判断滚出屏幕的实例先 pause滚回来再 play。注意事项destroy()之后不要再去调这个实例的任何方法会直接报错。我习惯在置空前统一加一个判空写起来啰嗦但能挡住不少线上异常。5. 性能优化别让一个动画拖垮整个页面5.1 JSON 与图层层面的瘦身动画卡不卡七成看资源本身。我优化过的项目里最常见的三个问题是图层数量过多、关键帧密度过高、图片资源过大。图层数量这块设计师习惯在 AE 里把每个图形拆成独立图层导出后 layers 数组可能有上百项每帧都要遍历计算。让设计师在导出前合并同类图层、删掉完全被遮挡的图层往往能砍掉三到五成的计算量。关键帧密度也值得说。AE 里如果开了「自动贝塞尔」并且每帧都有关键帧导出的 JSON 里就会有大量冗余数据文件体积翻倍插值计算也变多。让设计师在导出设置里开启简化关键帧对运动轨迹做稀疏化视觉上看不出区别体积能小一大截。图片资源方面能内嵌 base64 的就内嵌不能的就把尺寸压到实际展示大小的 1.5 倍以内别放原始大图。一个 500KB 的 PNG 换成压缩后的 WebP 或者小尺寸图加载时间能差出好几倍。5.2 播放策略与降级方案性能不是无限可压的得学会根据设备能力做策略调整。我在项目里做了一个简单的分级通过wx.getDeviceInfo()拿到设备信息对性能较弱的机型降低动效复杂度或者干脆只播首尾两帧做静态展示。另外一个通用做法是延迟初始化。动画不在首屏关键路径上的就等页面主要数据渲染完再初始化避免和首屏的请求、渲染抢主线程。实现上可以用wx.nextTick或者一个短延时效果立竿见影。还有一种情况是动画只在特定状态下播放比如加载中。这时候不必常驻实例用到的时候再 loadAnimation用完就 destroy。这种做法牺牲了一点初始化时间换来的是页面整体更轻。6. 常见问题排查实录6.1 白屏、不显示、只有第一帧白屏是最高频的问题我把排查路径按顺序列一下基本能覆盖九成情况。第一检查 canvas 标签有没有type2d漏了这个属性拿不到 node。第二检查fields({ node: true, size: true })是不是写全了只写 node 拿不到尺寸DPR 算不出来。第三检查lottie.setup()有没有在loadAnimation()之前调用。第四检查 canvas 的宽高是不是 0父容器如果用了display: none或者高度塌陷canvas 尺寸就是 0什么都画不出来。第五检查 JSON 是否加载成功网络请求失败时不会有明显报错只是安静地不显示。只有第一帧的情况通常是autoplay设成了 false或者逐帧循环没跑起来。后者多半是因为 canvas 节点不是真正的 2d 节点导致requestAnimationFrame不存在。还有一种可能是页面进入了后台rAF 被系统暂停了。6.2 开发者工具正常、真机异常这类问题我遇到过好几次原因集中在三处。一是合法域名没配开发者工具里勾了「不校验合法域名」所以正常真机上直接请求失败。二是图片路径在真机上的解析规则和工具不同相对路径需要换成绝对路径。三是基础库版本差异工具默认用最新的线上用户的基础库版本参差不齐canvas 2d在低版本上根本不存在。现象高频原因排查动作完全白屏未设 type2d 或未 setup检查标签属性与调用顺序画面模糊DPR 未处理设物理尺寸并 scale只有第一帧autoplay 为 false改为 true 或手动 play工具正常真机白屏域名未配置检查合法域名列表页面切回后卡顿实例未销毁onUnload 中 destroy动画缺图层JSON 含表达式或蒙版让设计师简化后重新导出内存持续上涨重复 setup 或重复创建实例保证 setup 只执行一次7. 协作与上线前的经验复盘7.1 和设计师的交接流程动效这件事前端和设计师的交接质量直接决定返工次数。我现在的固定流程是这样的设计师先在 AE 里用 Bodymovin 导出 JSON导出前必须确认三件事——不使用表达式、不依赖第三方插件生成的私有图层、图片资源要么内嵌要么单独打包。导出后先自己用预览工具过一遍确认和 AE 里看到的一致再交给前端。交付时我会要一份说明写清楚动画的帧率、时长、是否循环、画布逻辑尺寸。这几项直接影响我代码里的参数设置。有了这些信息接入基本一次就能对上不用来回问。还有一个反向的约定前端在真机上验证后把截图或录屏发回给设计师确认避免双方对「还原度」的理解有偏差。7.2 上线前必须过一遍的自查清单所有动画资源的体积加起来是否超过分包限制每个动画实例是否都有对应的 destroy 调用onHide与onShow是否处理了暂停与恢复网络加载的 JSON 域名是否已加入合法域名低版本基础库的降级方案是否准备好弱机型上是否做过实际帧率验证动画内的图片资源路径在真机上是否可加载页面切后台再切回动画是否正常恢复这个清单我贴在项目文档里每次发版前逐条勾一遍。看着琐碎但真能挡住问题尤其是「切后台回来卡死」这种偶现 bug不提前处理等用户反馈就晚了。我在实际项目里最大的体会是lottie-miniprogram 本身不难用难的是资源管线和边界处理。库只是把渲染这件事解决掉剩下的体积控制、生命周期、弱机降级、和设计师的沟通全得自己拿主意。把这些提前想明白动效才会从一个「拖后腿的需求」变成页面里最出彩的那部分。
返回列表