小程序的交互动画这两年卷得厉害,从最简单的按钮反馈,到弹窗里的加载小人、营销页的引导动效、会员开通的礼花效果,产品经理张口就是"要那种丝滑的"。真到落地环节,用 WXSS 的 animation 写几个节点位移还行,一旦遇到设计师在 AE 里做出来的几十上百个图层、带缓动曲线和遮罩的动画,纯手写基本就是自虐。这时候 lottie 就派上用场了:设计师用 AE 加 Bodymovin 插件导出 JSON,前端拿 lottie-miniprogram 在小程序里渲染,一套流程下来动画还原度能做到很高,也不需要前端去逐帧抠参数。这篇内容聊的就是微信小程序使用 lottie 动画的完整链路,从前置条件、依赖安装、核心 API、实操代码,到性能调优和踩坑记录,面向的是有点小程序基础、但没怎么碰过 Lottie 的开发者,也照顾到已经在用但遇到帧率或者内存问题的朋友。
1. 为什么要用 Lottie 而不是原生动画:方案选型拆解
1.1 小程序自带动画能力的边界在哪
先说清楚小程序原生能做哪些动画。WXSS 层面有transition和@keyframes,配合transform、opacity这类合成属性,做按钮缩放、淡入淡出、位移动画是完全够用的,而且走的是渲染层合成,性能开销小。问题出在两个地方:一是复杂路径动画,比如一个带描边的图标沿着曲线生长,用 WXSS 基本写不出来,得靠 SVG path 配合stroke-dasharray硬凑,改一次动效就要重算一次长度;二是多元素时间轴编排,设计师给的时间轴是"第 0 帧圆点弹出、第 8 帧线条展开、第 20 帧文字逐字出现、第 45 帧整体上移",用 CSS animation-delay 去对齐几十个元素的时间点,维护成本高得离谱,改一帧全盘重算。
再看wx.createAnimation和 WXS 响应事件,这两个更适合做手势跟随类的即时反馈,比如拖动卡片、下拉回弹,它们解决的是"交互驱动"而不是"时间轴驱动"的问题。所以结论很清楚:简单交互动效用原生,复杂的、设计师主导的、帧数密集的动效,交给 Lottie。
还有一个容易被忽略的点是动画的一致性。同一个动效在 iOS 和 Android 上,因为渲染引擎差异,WXSS 动画偶尔会出现细微的时序偏差,尤其在低端安卓机上掉帧明显。Lottie 因为是按帧数据渲染到 canvas,节奏由 JS 主动驱动,反而更容易做到两端表现接近——当然代价是 JS 线程的压力变大,这点后面会细说。
1.2 lottie-miniprogram 到底解决了什么问题
lottie-web 是业界通用的 Lottie 渲染库,支持 SVG、Canvas、HTML 三种渲染器。小程序里没有 DOM,也没有标准的 SVG 元素,直接引入 lottie-web 是跑不起来的。lottie-miniprogram做的事情,本质上是把 lottie-web 内部依赖浏览器环境的几个关键模块做了小程序适配:一是把 XMLHttpRequest 换成了wx.request,这样 JSON 资源能通过网络加载;二是把document.createElement('canvas')这类操作替换成小程序提供的 canvas 2d 节点;三是把requestAnimationFrame桥接到 canvas 节点自带的帧回调上。做完这三件事之后,lottie-web 的 Canvas 渲染器就能在小程序里正常工作了。
要注意,lottie-miniprogram是腾讯官方团队维护的适配层,底层还是 lottie-web 的 canvas 渲染逻辑,所以 lottie-web 支持的 AE 特性它基本都支持,不支持的它也照样不支持。这个认知很重要,很多人第一次用的时候会以为"小程序的 Lottie 是阉割版",其实阉割的是渲染器(只有 canvas,没有 svg),特性支持度取决于 lottie-web 版本。
对比一下其他方案:手写 Canvas 逐帧绘制,等于自己实现一个动画播放器,工作量巨大;用 GIF,体积大、色带明显、没法动态改颜色;用 APNG 或 WebP 动图,小程序支持有限且同样无法交互控制。综合下来,Lottie 在小程序里目前就是"复杂 JSON 动画"的最优解,没有之一。
1.3 什么场景该用,什么场景不该用
不能无脑上 Lottie,我列几个实际判断标准。适合用的场景:营销活动的引导页动效、空状态插画动画、加载 Loading(尤其是品牌化的定制 Loading)、会员/成就解锁的庆祝动画、新手引导的手势提示动画。这些场景的共同点是动画复杂、复用度高、由设计主导。
不太适合的场景:列表项里的循环动画。比如聊天列表每个头像旁边都有一个小动画,一屏十几个实例,canvas 数量一多内存直接爆掉,这种情况建议用静态图加 CSS 的轻微缩放代替。再比如纯颜色变化的过渡动画,用 Lottie 就是杀鸡用牛刀,反而增加体积。
另外一个硬指标是包体积。小程序主包限制 2MB,一个中等复杂度的 Lottie JSON 大概 30KB 到 200KB,图片资源多的能到 1MB 以上。如果主包已经很紧张,JSON 就别放本地了,走 CDN 或者放分包,这点在第三部分会展开讲。
2. 环境准备与依赖安装:从零把库跑起来
2.1 基础库版本与 canvas 2d 的前置条件
lottie-miniprogram依赖小程序的 canvas 2d 接口,这个接口是基础库 2.9.0 才开始提供的。所以第一件事是确认你项目的最低基础库版本,在开发者工具的"详情 - 本地设置 - 调试基础库"里能看到当前调试版本,在app.json里也可以通过"lazyCodeLoading"等相关配置间接影响。更稳妥的做法是在project.config.json里设置"libVersion",把它固定在一个 2.9.0 以上的版本。
还有一个容易踩的坑:app.json里如果没有开启"lazyCodeLoading": "requiredComponents",虽然不影响 Lottie 运行,但会影响整体启动性能。这个配置和 Lottie 没有直接关系,不过很多新项目模板默认开启了,如果你从老项目迁移过来发现没开,顺手补上没坏处。
最关键的其实是不要用旧版 canvas。小程序历史上有一套老的<canvas canvas-id="xxx">,用的是wx.createCanvasContext,这套 API 已经废弃,lottie-miniprogram完全不支持。你必须用<canvas type="2d" id="xxx">这种新写法,通过SelectorQuery.node()拿到真实的 canvas 节点对象。这一点新手特别容易搞混,因为网上很多老教程还在讲createCanvasContext。
2.2 npm 安装与构建流程
lottie-miniprogram是通过 npm 分发的,所以项目得先支持 npm。流程如下:先在项目根目录执行npm init -y生成package.json,然后安装依赖。
npm install lottie-miniprogram --save装完之后,你会发现根目录多了node_modules,但小程序运行时读的是miniprogram_npm目录,所以还需要在微信开发者工具里点"工具 - 构建 npm"。构建成功后,miniprogram_npm/lottie-miniprogram就会出现,这时候才能import。
如果你用的是 TypeScript 项目,类型声明可能没有内置,可以在项目里加一个简单的declare module 'lottie-miniprogram'声明文件,或者直接require引入然后自己断言类型,不影响运行。
注意:
npm init -y生成的 package.json 里如果 name 包含中文或大写字母,构建 npm 时可能报错,记得改成合法的小写包名。
构建 npm 之后,还有一步经常被漏掉:在开发者工具里勾选"使用 npm 模块"(新版工具已经默认支持,但老版本需要在详情里手动开)。如果import lottie from 'lottie-miniprogram'报"找不到模块",八成就是构建没做或者没勾选。
2.3 目录结构与最小可运行骨架
一个干净的最小项目结构大概是这样的:
project/ ├── miniprogram/ │ ├── pages/ │ │ └── index/ │ │ ├── index.js │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── index.json │ ├── static/ │ │ ── lottie/ │ │ └── loading.json │ ├── app.js │ ├── app.json │ └── app.wxss ├── miniprogram_npm/ │ └── lottie-miniprogram/ ├── node_modules/ ├── package.json └── project.config.json把 Lottie 的 JSON 放static/lottie目录下,通过相对路径require进来是最省事的方式,因为不涉及网络请求,也不用配域名。缺点是占包体积,好处是首屏无延迟、离线可用。放本地还是放 CDN,后面会专门讲取舍逻辑。
app.json里不需要为 Lottie 做任何特殊配置,因为它不是自定义组件,只是一个 JS 库。真正需要在页面里配的只有页面本身的 WXML 结构。
3. 核心 API 与渲染原理拆解
3.1 lottie.setup 与 loadAnimation 各参数的含义
lottie-miniprogram暴露的 API 非常少,核心就两个:setup和loadAnimation。
setup(canvas)的作用是把 canvas 节点上挂载的requestAnimationFrame和cancelAnimationFrame注册到全局,这样 lottie-web 内部的动画循环才能跑起来。这一步必须在loadAnimation之前调用,而且每个页面只需要调用一次。它接收的就是通过SelectorQuery.node()拿到的 canvas 节点对象。
loadAnimation(options)是真正的加载入口,常用参数如下:
| 参数 | 类型 | 作用 | 备注 |
|---|---|---|---|
path | string | JSON 网络地址 | 需要配置 request 合法域名 |
animationData | object | 直接传入 JSON 对象 | 配合本地 require 使用 |
loop | boolean/number | 是否循环/循环次数 | true 为无限循环 |
autoplay | boolean | 是否自动播放 | 默认 true |
rendererSettings.context | CanvasRenderingContext2D | canvas 上下文 | 必填,否则渲染不出来 |
rendererSettings.clearCanvas | boolean | 每帧是否清空画布 | 默认 true,正常不要改 |
name | string | 动画实例名 | 用于lottie.getAnimationByName |
initialSegment | [number, number] | 初始播放区间 | 用于分段播放 |
path和animationData二选一。用path要注意小程序的网络请求域名白名单,因为 lottie-miniprogram 内部是用wx.request拉 JSON 的,如果你没在"开发设置 - 服务器域名"里配置 request 合法域名,真机上会直接失败,而开发者工具里勾了"不校验合法域名"却又能跑,这是个经典的"工具能跑真机白屏"问题。
用animationData就简单多了,本地require一个 JSON,直接传对象进去,没有网络请求,也就没有域名问题。缺点是打包体积。我的建议是:动画 JSON 小于 150KB 放本地,超过就上 CDN 配置域名。
3.2 canvas 2d 与 DPR 缩放的正确姿势
这是整个流程里最容易出问题的一环。小程序的 canvas 2d 节点,canvas.width和canvas.height是物理像素,而它在页面上占的位置由 WXSS 里的宽高决定,这两个是分离的。同时,lottie 内部渲染用的坐标系是 CSS 像素。
正确做法是把 canvas 的物理尺寸设成 CSS 尺寸乘以设备的pixelRatio,然后对 context 做一次scale(dpr, dpr),这样 lottie 按 CSS 尺寸渲染出来的内容才不会糊,也不会被裁切。
const dpr = wx.getWindowInfo().pixelRatio const cssWidth = 200 const cssHeight = 200 canvas.width = cssWidth * dpr canvas.height = cssHeight * dpr const context = canvas.getContext('2d') context.scale(dpr, dpr)如果忘了乘 dpr,在高分屏(大多 iPhone 是 3 倍,部分安卓是 2.75 倍)上动画会明显发虚,边缘毛刺严重。如果乘了 dpr 但忘了scale,动画内容只会占左下角四分之一,因为渲染坐标系还是 CSS 尺寸,而画布已经放大了一倍多。
wx.getWindowInfo()是较新的 API,如果你的基础库版本偏低,用wx.getSystemInfoSync().pixelRatio也完全没问题,只是前者官方推荐用在新项目里。
注意:
pixelRatio在 iPad 上可能是 2,在某些折叠屏上会是 3.5 甚至更高。写死 dpr 是典型的埋雷操作,一定要动态读取。
还有一点,canvas 的 WXSS 宽高和上面 JS 里的cssWidth/cssHeight必须一致,否则会出现内容被拉伸的问题。最稳妥的做法是把尺寸抽成常量,两边都引用同一个值,或者用SelectorQuery的boundingClientRect读实际尺寸。
3.3 JSON 资源加载:path、animationData 与 CDN 取舍
前面提了两种加载方式,这里讲清楚背后的取舍逻辑。
path方式的优点是包体积为零,JSON 更新不用发版,改完 CDN 上的文件用户下次打开就是新版。缺点是:首屏有网络延迟,弱网下动画会明显晚出现;需要配置 request 合法域名;CDN 假设挂了动画就废了。所以适合那些体积大、更新频繁、允许延迟出现的动画,比如节日活动的氛围动效。
animationData方式的优点是零延迟、可离线、无域名依赖。缺点是占包体积,更新必须重新提审发版。适合小尺寸的品牌 Loading、空状态插画这类"基础设施级"的动画。
还有一种混合方案,是很多团队在用的:把核心动画放本地保证首屏,把可选的、体积大的动画放 CDN 按需加载。加载的时候用wx.request先拿到 JSON 文本,再JSON.parse之后作为animationData传给 lottie,这样既不用配域名(如果你的请求走的是自己后端且配好了域名),又能控制加载时机。
CDN 侧还有一个实践要点:给 JSON 开 gzip 或者 br 压缩。Lottie 的 JSON 是纯文本、重复度极高,压缩率通常能到 70% 以上,一个 200KB 的 JSON 压完可能就 50KB 出头,首屏速度提升非常明显。同时记得给 CDN 配上合适的缓存策略,max-age设长一点,文件名带 hash 做版本控制。
4. 实操落地:一个完整的动画页面
4.1 WXML 与 WXSS 写法
WXML 部分非常简洁,一个 canvas 节点就够了,关键属性是type="2d"和id。
<view class="wrap"> <canvas type="2d" id="lottie-canvas" class="lottie-canvas" ></canvas> <view class="btn-group"> <button size="mini" bindtap="handlePlay">播放</button> <button size="mini" bindtap="handlePause">暂停</button> <button size="mini" bindtap="handleDestroy">销毁</button> </view> </view>WXSS 里给 canvas 一个明确的宽高,单位用 rpx,但要注意和 JS 里的 CSS 像素换算。小程序的 rpx 是按 750 设计稿宽度等比缩放的,在 JS 里拿到的是 px,所以简单起见,canvas 的宽高直接用 px 写死更稳妥,或者在 JS 里通过boundingClientRect读实际渲染尺寸。
.wrap { display: flex; flex-direction: column; align-items: center; padding: 40rpx 0; } .lottie-canvas { width: 200px; height: 200px; } .btn-group { margin-top: 40rpx; display: flex; gap: 20rpx; }这里我特意用 px 而不是 rpx 来定义 canvas 尺寸,原因是 Lottie 的动画本身有个设计稿尺寸(JSON 里的w和h字段),如果 canvas 的宽高比和动画不一致,会出现拉伸或者留白。用 px 固定尺寸,配合 lottie 的等比缩放,表现最可控。
4.2 JS 逻辑:初始化、播放、暂停、销毁
完整的页面逻辑如下:
import lottie from 'lottie-miniprogram' const ANIMATION_DATA = require('../../static/lottie/loading.json') Page({ data: {}, ani: null, canvasNode: null, onReady() { this.initLottie() }, initLottie() { const query = wx.createSelectorQuery().in(this) query .select('#lottie-canvas') .fields({ node: true, size: true }) .exec((res) => { if (!res || !res[0] || !res[0].node) { console.error('canvas 节点获取失败') return } const canvas = res[0].node const cssWidth = res[0].width const cssHeight = res[0].height const dpr = wx.getWindowInfo().pixelRatio canvas.width = cssWidth * dpr canvas.height = cssHeight * dpr const context = canvas.getContext('2d') context.scale(dpr, dpr) lottie.setup(canvas) this.canvasNode = canvas this.ani = lottie.loadAnimation({ animationData: ANIMATION_DATA, loop: true, autoplay: true, rendererSettings: { context, }, }) this.ani.addEventListener('loopComplete', () => { // 每一轮循环结束的回调 }) }) }, handlePlay() { this.ani && this.ani.play() }, handlePause() { this.ani && this.ani.pause() }, handleDestroy() { if (this.ani) { this.ani.destroy() this.ani = null } }, onHide() { this.ani && this.ani.pause() }, onUnload() { this.handleDestroy() }, })几点说明。fields({ node: true, size: true })比node()更实用,因为它一次性把节点和渲染尺寸都拿到,省了再查一次boundingClientRect。in(this)是给自定义组件场景准备的,如果这个页面本身是组件,不加in(this)会查不到节点。
onHide里暂停是必须做的。小程序退到后台时,canvas 的帧回调会停,但 lottie 内部的计时状态可能残留,回到前台时容易出现动画"跳帧"或者一次性快进很多帧的诡异现象。主动pause()再在onShow里play(),表现会稳定得多。
onUnload里destroy()也是必须的。destroy()会清掉 lottie 内部持有的动画实例、事件监听和帧定时器,不做这步的话,来回进出页面几次内存就会明显上涨,尤其在安卓低端机上容易触发卡顿。
4.3 交互进阶:进度控制、分段播放、事件回调
基础的播放暂停之外,lottie-miniprogram还支持不少实用能力。
进度控制用的是goToAndStop(value, isFrame)和goToAndPlay(value, isFrame)。value是帧号或者进度百分比(0 到 1),isFrame为 true 时按帧号解释。这个能力在做"手势拖拽控制动画进度"的时候特别有用,比如用户滑动屏幕,动画跟着手指走,松手后再play()继续。实现思路是监听touchmove,拿到滑动的百分比,转成动画进度传给goToAndStop。
分段播放靠的是initialSegment参数或者playSegments(segments, forceFlag)。比如一个按钮动画分"进入"和"循环"两段,第 0 到 30 帧是进入,第 31 到 60 帧是循环,就可以用playSegments([[0, 30]], true)先播进入,在complete事件里再playSegments([[31, 60]], true)接循环。这套东西做复杂状态机动画非常顺手。
事件回调方面,常用的有这几个:data_ready是 JSON 加载完成,DOMLoaded在 canvas 渲染器下也会触发,complete是动画播完一轮(非循环模式),loopComplete是每轮循环结束。注意complete和loopComplete的区别,循环模式下调complete是永远不会触发的,这点很多人在群里问过。
setSpeed(speed)可以动态改速度,setDirection(dir)可以反向播放(dir 传 -1),配合setSpeed负值也能实现倒放效果,用来做"展开再收回"的动画很省事,不用让设计师单独做一份反向动画。
还有一个冷门但好用的 API 是getDuration(),返回动画总时长(秒),做进度条或者超时兜底的时候能派上用场。
5. 性能优化与常见问题排查实录
5.1 内存与帧率优化清单
Lottie 在小程序里的性能瓶颈主要在 JS 线程和 canvas 绘制。下面这几条是我踩过坑之后固定下来的优化习惯。
第一,控制实例数量。一个页面最多同时跑 1 到 2 个 Lottie,超过 3 个就要警惕。如果确实需要多个动画,考虑做成一张 JSON 里包含多个图层统一调度,这样只占一个 canvas 和一个实例,开销反而更小。
第二,善用onHide暂停和onUnload销毁,前面已经强调过。另外在onShow恢复时,如果不是当前活动页面,宁可延迟一拍再play(),避免页面切换瞬间堆叠多个动画同时启动。
第三,控制动画的复杂度。这条其实要跟设计师沟通。Bodymovin 导出的图层数、蒙版数、路径点数直接决定每帧的绘制开销。一个 300 个图层的 JSON,在低端安卓上掉帧是必然的。实践下来,单个动画控制在 50 到 80 个图层以内,帧率一般能稳住。另外,尽量避免在 Lottie 里用大面积的模糊和阴影效果,这类是纯粹的算力黑洞。
第四,注意 canvas 尺寸不要开太大。有人图省事,canvas 开个 750px 乘 750px,再乘 3 倍 dpr,画布实际是 2250 的物理像素,每帧的绘制像素量巨大。按实际展示尺寸开,能用小尺寸就不用大尺寸。
第五,考虑降级策略。低端设备上可以通过wx.getDeviceInfo()判断机型或者内存等级,命中低端规则时直接不加载 Lottie,展示静态图兜底。这个策略看起来粗暴,但在真实的大促场景里能救不少机型。
| 优化项 | 建议值 | 说明 |
|---|---|---|
| 单页实例数 | 1 到 2 个 | 超过 3 个必掉帧 |
| 单动画图层数 | 50 到 80 层以内 | 300 层以上低端机必卡 |
| canvas 物理像素 | 不超过 1200 像素宽 | 再大收益递减,开销剧增 |
| JSON 体积 | 本地不超过 150KB | 超过走 CDN |
| 循环动画帧率 | 30fps 可接受 | 60fps 在低端机偏重 |
5.2 常见报错与速查表
下面这几个问题是我和团队实际遇到频率最高的,整理成表方便对照排查。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 动画完全不显示 | canvas 2d 未正确取到节点 | 检查type="2d"、id是否匹配、in(this) |
| 动画显示在左下角一小块 | 未做 dpr 缩放处理 | 检查canvas.width和context.scale |
| 动画发虚、边缘毛刺 | canvas.width未乘 dpr | 补上pixelRatio乘法 |
| 工具能跑真机白屏 | JSON 走网络但域名未配 | 检查 request 合法域名设置 |
| JSON 加载报错 | 路径错误或 JSON 格式非法 | 用JSON.parse验证文件 |
| iOS 上颜色偏暗 | canvas 色彩空间差异 | 检查 JSON 里的颜色模式 |
| 页面返回后动画重影 | 未 onUnload 销毁 | 补上destroy() |
| 后台切回后动画跳帧 | 未 onHide 暂停 | 补上pause/play |
| 多个动画只有一个能动 | 全局 rAF 被覆盖 | 每个 canvas 单独setup |
| 部分效果丢失 | AE 表达式或效果不支持 | 导出前转换表达式为关键帧 |
其中"多个动画只有一个能动"这个坑特别隐蔽。因为setup()会把最后一个 canvas 的requestAnimationFrame覆盖到全局,如果你在同一个页面里 setup 了两个 canvas,第二个会把第一个的帧回调顶掉,表现就是只有第二个动画在跑。解决办法是不用setup的全局覆盖,或者干脆一个页面只用一个 canvas,把多个动画合成到一张 JSON 里。
另外"部分效果丢失"这个问题,根源在 AE 端。lottie-web 不支持 AE 的表达式(Expressions),设计师如果用了表达式驱动的动画,导出后这部分会变成静止的。解决办法是在 AE 里把表达式烘焙成关键帧再导出。同理,一些旧的混合模式和部分内置效果(比如特定的位移模糊)导出后也会异常,导出前需要用 Bodymovin 的预览功能确认一遍。
5.3 导出环节的坑:Bodymovin 设置
前端和设计师的配合里,导出规范不对是返工率最高的环节。这里给出几条我在项目里固定下来的规范。
第一,合成尺寸尽量用偶数,常见是 750x750 或者 375x375。奇数尺寸在小尺寸位图上容易出现半像素偏移,渲染出来边缘会有细微抖动。
第二,导出时在 Bodymovin 设置里勾选"Glyphs"(文字转字形),这样文字会被转成矢量路径,不依赖系统字体。否则用户设备上没装设计师用的字体时,文字会变成默认字体,位置和样式全乱。
第三,图片资源建议转成 base64 内嵌,或者统一用 CDN 地址。用相对路径引用本地图片,在小程序里是找不到的,因为小程序没有文件系统概念。base64 内嵌会让 JSON 体积膨胀,所以图片多的情况建议走 CDN。
第四,不要用时间重映射(Time Remapping)。这个特性在 lottie-web 上支持不完整,路径跟随类的效果也建议手动拆成关键帧。
第五,导出后一定要用 Lottie 官方预览工具或者在线编辑器先看一遍,确认帧率、颜色、层级都对,再交给前端接入。这个环节花五分钟,能省掉后面两小时的联调。
提示:如果设计师不愿意配合规范,退而求其次的方案是前端拿到 JSON 后用
animationData动态改写里面的部分字段,比如替换颜色值实现主题换肤,但这属于补丁式操作,层级和解构成本高,还是提前约定规范更划算。
5.4 换肤与动态改色的实操思路
最后补一个很实用的技巧:Lottie 动画支持动态改色,这在多主题的小程序里很常见。做法是遍历animationData.layers,找到类型为形状图层的元素,修改里面的c.k颜色值。颜色值是归一化后的 RGB 数组,比如纯红是[1, 0, 0, 1],需要把十六进制颜色转成这个格式。
function hexToLottieColor(hex) { const r = parseInt(hex.slice(1, 3), 16) / 255 const g = parseInt(hex.slice(3, 5), 16) / 255 const b = parseInt(hex.slice(5, 7), 16) / 255 return [r, g, b, 1] } function applyTheme(animationData, colorArr) { const cloned = JSON.parse(JSON.stringify(animationData)) cloned.layers.forEach((layer) => { if (layer.ty === 4 && layer.shapes) { layer.shapes.forEach((shape) => { if (shape.it) { shape.it.forEach((item) => { if (item.ty === 'fl' || item.ty === 'st') { item.c.k = colorArr } }) } }) } }) return cloned }这段逻辑要注意几个点:一是必须深拷贝原始 JSON 再改,直接改原对象会污染后续复用;二是形状图层(ty === 4)里的结构可能嵌套多层,it下面还可能有it,实际项目里建议写个递归函数;三是填充(fl)和描边(st)都要处理,只改填充的话描边颜色还是旧的,视觉上会很怪。
这套方案在做暗色模式和品牌色切换的时候特别省事,同一个 JSON 一次加载,切换主题时destroy旧实例,用改色后的animationData重新loadAnimation一次即可,动画资源复用了,视觉却完全不同。
我个人在实际项目里的体会是,Lottie 在小程序里的落地难点从来不在代码本身,API 就那么几个,半天就能摸熟。真正决定成败的是两件事:一是和设计师约定好导出规范,别让带表达式的 AE 工程流到前端;二是把生命周期管住,该暂停暂停、该销毁销毁,别让一个看起来很轻的动画在用户手机上悄悄吃掉内存。这两件事做扎实了,剩下的都是水到渠成。