资讯动态

React Native for OpenHarmony 实战:三方库 react-native-animated-spinkit 的鸿蒙化适配指南

发布时间:2026/10/24 2:13:36 来源:尧图企业网站定制
本文记录把react-native-animated-spinkit12 种 CSS 风格加载动画适配到 HarmonyOS 的完整过程。这个库的特殊之处在于它没有任何返回值可以断言。它是纯视觉效果——12 个会转、会跳、会缩放的加载指示器。所以本轮的核心问题是动画在跑这件事怎么验答案是把屏幕当被测对象连拍两帧逐像素比对。一、先说结论项结果上游最新版1.5.2npmgitHeadaa452f78…是否需要原生适配不需要——纯 JS12 个组件全靠Animated驱动无原生模块、无dependencies与 npm 上游的差异98 / 99 个文件去掉 CR 后逐字符一致唯一差异是package.json自动链接信号linked 10 libraries, skipped 1 libraries计数未增加编译assembleHap6 分 55 秒HAP 81,516,680 →81,614,985 字节98,305 ≈96 KB设备侧断言✅35 / 35 全部通过原生动画驱动✅useNativeDriver: true真的生效RNOH 提供NativeAnimatedTurboModule无回退告警帧率requestAnimationFrame实测63.7 fps像素级证据动画中两帧6.17%像素显著变化复测 6.16%停下后0.00%一个像素都不变隐藏后对照区整片纯白交付包缺陷⚠️4 处其中npm install与npm pack退出码都是 1一个重要事实这个库没有 default 导出——只有 12 个具名导出一句话结论功能完全可用12 个动画在鸿蒙上都能正常渲染并驱动走的是真正的原生动画驱动满帧率。但交付包在工程配置与文档上有 4 处问题其中npm install失败会直接影响 CI。二、判定过程这个库不需要原生适配判断一个 RN 库要不要做原生适配我固定走三步步做法这个库的结果①npm view 包名 harmony --json看有没有harmony.autolinking没有②仓库里有没有harmony/以及android/、ios/都没有③代码里有没有NativeModules/Platform.OS/requireNativeComponent有Platform.OS但只用来决定useNativeDriver的取值第 ③ 步命中的那处Platform.OS值得看一下因为它恰好是这个库在鸿蒙上最大的运行时风险点// src/utils.tsconsttimingAnimated.timing(value,{duration,easing:createKeyframeEasingFunction(keyframes,easing),toValue,useNativeDriver:Platform.OS!web,// ← 鸿蒙上是 true})stagger()里更直接非 web 分支写死了useNativeDriver: true。为什么这是风险点useNativeDriver: true要求平台提供原生动画模块。如果平台没有React Native 会打印回退告警并改用 JS 驱动——动画照跑但驱动方式、性能和语义都变了。所以它能不能跑这个问题必须换成它的原生驱动到底有没有生效。这一点留到第五节和第七节解决。公开 API12 个组件全部具名导出Plane Bounce Pulse Swing Chase Wave Flow Circle CircleFade Grid Fold Wander⚠️这个库没有 default 导出。三处入口文件src/index.ts、lib/commonjs/index.js、lib/module/index.js都只有具名导出我在设备上专门断言了typeof default undefined——通过。组件属性SpinnerProps属性默认值说明size48单位是 vpcolor#000填充色animatingtrue是否播放hidesWhenStoppedtrueanimatingfalse时是否整体隐藏opacity 0三、这个交付包长什么样与 npm 上游逐字节一致核对方法与react-native-animated-spinkit1.5.2的 npm tarball 逐文件比对先去 CR 再逐字符比较。结果98 / 99 个文件完全一致包含src/下 15 个源文件与lib/下全部构建产物。唯一不同的是package.json字段变化description追加 for React Native for OpenHarmonyscripts新增test其余含prepare: bob build原样保留repository.url改为 AtomGit 地址homepage改为 AtomGit 地址files删除了上游的白名单上游为[src,lib,!**/__tests__,...]peerDependencies收窄为react: 19/react-native: 0.84 1上游是*实现文件零改动所以后面发现的问题都属于工程配置与文档层面与动画实现无关。四、接入宿主与构建运行纯 JS 库的接法只有三处// package.jsonreact-native-animated-spinkit:file:../react-native-animated-spinkit// metro.config.js —— file: 装进来是 junction必须让 Metro 找得到源码watchFolders:[path.resolve(__dirname,../react-native-animated-spinkit)],// index.jsAppRegistry.registerComponent(SpinnerTestApp,()SpinnerTestApp);不要动harmony/entry/oh-package.json5不需要 HAR不要动module.json5不需要权限。信号是自动链接计数不增加info updated 4 file(s), linked 10 libraries, skipped 1 libraries项数值首次assembleHap6 分 55 秒二次重编改了两条断言6 分 28 秒HAP 变化81,516,680 →81,614,985 字节98,305 ≈ 96 KBbundle 产物bundle.harmony.js6,454,621 字节⚠️ 这里有个陷阱npm install实际上是失败的退出码 1但因为编译不检查 npm 退出码本地一切正常。详见第八节。五、验证设计纯视觉效果怎么验才可信这类库没有返回值、没有系统状态可读。唯一能验的对象是屏幕上的真实产物。5.1 手段连拍两帧 逐像素比对设备侧连拍两帧间隔 260 ms把图拉回 PC用System.Drawing逐像素比较hdc shellsnapshot_display -f /data/local/tmp/tag-a.jpeg# 等 260 mshdc shellsnapshot_display -f /data/local/tmp/tag-b.jpeghdcfilerecv /data/local/tmp/tag-a.jpeg E:\rnoh-work\frames\tag-a.jpeg hdcfilerecv /data/local/tmp/tag-b.jpeg E:\rnoh-work\frames\tag-b.jpeg判据用每通道最大差 24 的像素占比。选 24 是因为两张完全相同的 JPEG 截图之间也会有几个灰阶的压缩噪声而 24 能干净地把噪声和真的变了分开。5.2 页面要提供一个只有一个会动的东西的区域测试页里放了一个固定的240×240 vp 白色对照区里面只放一个 spinner12 个组件的展示网格可以一键隐藏。这样整个页面只剩一个动画裁剪区的像素变化就只可能来自它。裁剪区不能写死坐标——必须从每一帧里自动定位那个白色方块。我用的办法是找包含长连续白色游程的行白框约 720 物理像素宽而页面上的白字游程远短于此取这些行的外接矩形。5.3 三个其实是四个场景构成闭环只证明画面在变是不够的变的原因可能是噪声、可能是别的东西在动。所以必须有对照组场景预期作用animatingtrue两帧明显不同动画在跑animatingfalsehidesWhenStoppedfalse两帧完全相同且区域仍有非白像素动画真的停了且被测对象仍可见animatingfalsehidesWhenStoppedtrue两帧相同且区域整片纯白隐藏是有效的恢复animatingtrue两帧再次明显不同可重复第二个场景里区域仍有非白像素这一条不能省。否则第三个场景的整片纯白有可能只是因为根本没渲染出来——那就不是隐藏生效而是东西不在。六、实测发现一动画真的在跑停也真的停设备Pura X View模拟器1320×2232density 3。对照区实测为720×720 物理像素即 240 vp 见方裁剪时向内缩 14 像素取692×692。场景状态显著变化(24)任意差异(0)非白像素区域平均色wave_onanimatingtrue6.17%9.22%——wave_frozenanimatingfalse, hidesfalse0.00%0.00%14.66%(224,235,252)wave_hiddenanimatingfalse, hidestrue0.00%0.00%0.00%(255,255,255)wave_on2恢复 animatingtrue6.16%9.21%——四条结论逐条都有关键意义动画在跑260 ms 间隔的 6.17% 像素变化而且复测得到 6.16%——可重复停下是真的停animatingfalse后两帧之间任意差异 0.00%——连一个像素都没变。这比变化变小了强得多是动画被真正停止的直接证据hidesWhenStopped生效整片纯白平均色精确为 (255,255,255)静止时 spinner 确实还画在屏幕上14.66% 非白像素⇒ 证明第 3 条的全白是真的被隐藏而不是对照区空了。6.1 换 spinner 复验spinner结构显著变化(24)Wave5 条竖条 scaleY6.17% / 6.16%Circle12 个圆点环形分布 translate3.64%Grid9 宫格 缩放/淡入淡出23.26%三种完全不同的结构都在动说明不是某个组件的偶然行为。6.2 一个测出来的上游设计细节12 个组件的根视图尺寸一起量了一遍onLayoutPlane48x48 Bounce48x48 Pulse48x48 Swing48x48 Chase48x48 Wave48x48 Flow48x12 Circle48x48 CircleFade48x48 Grid48x48 Fold48x48 Wander48x4811 个是size × size的正方形只有Flow是size × size*0.25。这是上游的设计Flow 是 3 个横排圆点容器只需 1/4 高度不是缺陷——但如果有人写所有 spinner 都是正方形的通用布局代码会在 Flow 上踩到。我第一版断言就写死了size × size于是在 Flow 上失败了一次——这是我自己按想象写断言不是库的问题。七、实测发现二useNativeDriver: true在鸿蒙上真的生效这是第二节留下的风险点。判断方法不能靠猜要靠权威证据。7.1 权威证据一原生动画 TurboModule 存在#RNOH_CPP: TurboModuleFactory.cpp:54 Creating Turbo Module: NativeAnimatedTurboModule #RNOH_ARK: TM created: NativeAnimatedTurboModuleRNOH提供了NativeAnimatedTurboModule。页面内用TurboModuleRegistry.get(NativeAnimatedTurboModule)也能拿到非空对象。7.2 权威证据二日志里没有回退告警如果原生动画模块缺失React Native 会打印useNativeDriver is not supported because the native animated module is missing之类的告警并回退。整段运行日志里没有这类告警。⇒useNativeDriver: true真的生效没有回退。7.3 ⚠️ 一个我自己用错的判据值得记下来我最初在测试页里是这么判断驱动方式的「原生驱动下JS 侧的Animated.Value.addListener不会被调用如果 listener 有更新说明回退到了 JS 驱动。」实测把这个判据推翻了JS 驱动时 listener 更新1次原生驱动时 listener 更新18次。按我的判据会得出回退到 JS 驱动这个完全相反的错误结论。真实原因是React Native 的原生驱动本身会通过onAnimatedValueUpdate事件把动画值回传给 JS 监听所以两种驱动下 listener 都可能被调用。教训结论要建立在权威证据模块是否存在、日志有没有告警上不能建立在自己推演出来的启发式上。启发式看起来很像证据但它只是猜测。7.4 帧率页面内用requestAnimationFrame数帧requestAnimationFrame 计帧 64 帧 / 1005 ms ≈ 63.7 fps满帧率。配合原生驱动生效的结论说明动画在鸿蒙上是由原生侧逐帧驱动的不是靠 JS 定时器凑出来的。八、实测发现三交付包的 4 处问题8.1npm install与npm pack的退出码都是 1最严重npm error code 1 npm error command C:\windows\system32\cmd.exe /d /s /c bob build npm error bob is not recognized as an internal or external command根因交付包的scripts与上游只差一个test——也就是原样保留了prepare: bob build。而react-native-builder-bob是devDependencies在消费者侧和纯净检出里都不可得。prepare这个生命周期钩子在本地路径依赖file:与 git 依赖安装时都会执行所以操作实测结果宿主npm installfile:依赖exit 1npm packexit 1无产物为什么本地没炸npm 在跑prepare之前就已经建好了 junction所以node_modules/react-native-animated-spinkit存在、Metro 能解析、HAP 照样编译成功。“能编译过掩盖了安装已经失败”。实际风险有两条任何以npm install退出码作为门禁的 CI/CD 会在这一步中断——这是最现实的影响npm pack打不出 tgz⇒ README 里推荐的npm install /path/to/react-native-animated-spinkit-1.5.2.tgz这条路径无法从该仓库自行产出。修复方向交付包把prepare删掉或换成无害脚本即可bob只在需要重新构建lib/时才有意义而见下一条RN 侧根本不加载lib/。说明README 里还有一条 git 安装写法npm install https://atomgit.com/...#1.5.2-ohos-1.0.0。我实测这条路径时先败在归档下载npm error code TAR_BAD_ARCHIVE属环境问题不作为证据。按 npm 的规则git 依赖同样会执行prepare。8.2 README 说 Metro 用lib/commonjs入口实测是srcREADME 原文「Metro 使用lib/commonjs/index.js入口。」在构建产物里数引用react-native-animated-spinkit/src 出现 16 次 react-native-animated-spinkit/lib/commonjs 出现 0 次根因package.json里main指向lib/commonjs/index.js但还有一个react-native字段指向src/index.ts。React Native 的 Metro 默认resolverMainFields [react-native, browser, main]——react-native字段优先所以加载的是TypeScript 源码由 Metro 现场转译。影响lib/commonjs module typescript 三份产物约 150 KB对 RN 消费者而言是死代码而 README 的入口说明与实际正好相反会误导做入口排查的人。8.3 README 的用法示例用了 default import而库没有 default 导出README 给的示例是importLibraryfromreact-native-animated-spinkit;constLibraryrequire(react-native-animated-spinkit);实测三个入口文件都没有 default 导出设备侧断言typeof default undefined通过。⇒ 照 README 抄会拿到undefined。正确写法import{Plane,Wave,Circle}fromreact-native-animated-spinkit;8.4 契约测试不测库__tests__/harmony-contract.test.cjs全文只断言四件事assert.equal(pkg.name,react-native-animated-spinkit);assert.equal(pkg.version,1.5.2);assert.ok(require.resolve(path.resolve(__dirname,..,pkg.main)));assert.ok(fs.existsSync(path.resolve(__dirname,..,README.OpenHarmony_CN.md)));没有渲染任何组件、没有触发任何动画。它验证的是包文件存在不是库能用。这个测试没有虚假命名——README 就称它为包级 smoke test所以这是覆盖面不足不是欺骗。但注意require.resolve的目标是pkg.main而实际被 Metro 加载的是src/index.ts——也就是说这个测试连真正的入口文件是否存在都没覆盖到。8.5spec.json的可追溯性偏弱upstream:{type:git,url:githttps://github.com/tienphaw/react-native-animated-spinkit.git},native:false,validation:passupstream是对象同类交付包一般用字符串形态不一致没有upstreamCommit——上游gitHeadaa452f78…我从 npm 查到了但规格文件里没记validation是裸字符串同类交付包一般是含场景数、设备、HAP 哈希的对象。三个字段都偏弱导致可追溯性只能靠人去读 README无法脚本化核对。8.6 另外两处小问题README 的「能力一览」只有 2 行、表述空泛“组件或工具 API 支持”没有列出 12 个 spinner 的名字——而这个库的全部价值恰恰就是有哪 12 个动画上游package.json的files白名单被删掉了发布内容不再受限。九、已知限制9.1 库本身的没有 default 导出必须用具名导入。照错误文档写会拿到undefined。Flow的根视图是size × size*0.25其余 11 个是size × size。写通用布局代码时要留意。animatingfalse时动画被reset()、值归 0也就是相位会丢失——重新设回true是从起点重新开始不是从暂停处继续。hidesWhenStopped只改 opacity不影响布局所以隐藏后元素仍占位。所有 spinner 都依赖useNativeDriver: true非 web。如果某个平台的 RN 实现不支持原生动画驱动会静默回退到 JS 驱动——动画仍可跑但驱动方式与性能会变。在鸿蒙上这一点已确认没问题第七节。9.2 本次验证的边界只在一台模拟器上验证Pura X View1320×2232density 3没有真机。12 个 spinner 只逐像素验证了 3 个Wave、Circle、Grid。其余 9 个只验证了渲染成功 根尺寸正确没有逐个确认动画观感与原版一致。没有校验动画周期源码里各 spinner 的duration从 1200 ms 到 2500 ms 不等我只确认在动没有测周期是否精确。没有断言相位animating中途切换的行为是否从 0 重新开始在协议里发生过但没有做相位断言。没有跨平台视觉对照没在 Android/iOS 上跑同一份用例比对观感。帧率只测了空闲状态63.7 fps高负载/低端设备未测。组合场景未测12 个动画同时播放时的帧率、以及页面滚动时的表现。交付包的 git 安装路径未验证本次败在归档下载属环境问题。未改动库代码。发现的 4 处问题全部属于工程配置与文档层面。十、常见问题Q1这个库需要 HAR、权限或 ohpm 接线吗都不需要。它是纯 JS12 个组件全靠Animated驱动没有原生模块。判断信号是link-harmony的计数不增加本次输出linked 10 libraries, skipped 1 libraries。Q2怎么引用具名导入——这个库没有 default 导出。// ✅ 正确import{Plane,Wave,Grid}fromreact-native-animated-spinkit;// ❌ 拿到 undefinedimportSpinkitfromreact-native-animated-spinkit;Q3有哪些 spinnerimport{Plane,Bounce,Pulse,Swing,Chase,Wave,Flow,Circle,CircleFade,Grid,Fold,Wander,}fromreact-native-animated-spinkit;共 12 个。组件属性是size默认 48、color默认#000、animating默认true、hidesWhenStopped默认true其余ViewProps会透传到根视图。Q4为什么要在意useNativeDriver因为这个库在非 web 平台强制使用原生动画驱动useNativeDriver: true。如果平台不支持React Native 会静默回退到 JS 驱动——动画照样动但驱动方式、性能和语义都变了。在鸿蒙上可以确认没问题hdc shellhilog -x | grep -i NativeAnimatedTurboModule看到Creating Turbo Module: NativeAnimatedTurboModule且日志里没有useNativeDriver is not supported这类告警就说明原生驱动生效了。Q5npm install报了bob is not recognized但编译却过了要紧吗要紧。这是交付包保留了上游的prepare: bob build脚本导致的bobreact-native-builder-bob只是库的devDependencies消费者侧没有。它之所以没耽误编译是因为 npm 在跑prepare之前就已经建好了软链接Metro 照样能解析。但npm install的退出码是 1⇒ 任何以它做门禁的 CI 会中断npm pack也会失败打不出 tgz。临时绕过用 tgz 安装prepare不会执行或者忽略该步骤的退出码并把prepare从依赖的package.json里去掉。根修建议还是交付方删掉prepare。Q6怎么自己确认动画真的在跑连拍两帧做像素差hdc shellsnapshot_display -f /data/local/tmp/a.jpeg# 等 ~260 mshdc shellsnapshot_display -f /data/local/tmp/b.jpeghdcfilerecv /data/local/tmp/a.jpeg E:\tmp\a.jpeg hdcfilerecv /data/local/tmp/b.jpeg E:\tmp\b.jpeg然后在 PC 上逐像素比。记住要做一个对照把animating设成false再拍两帧这两帧应当完全相同。只有动的时候有差异、停的时候没差异差异才能归因到动画。Q7怎么判断隐藏是真的隐藏了看对照区里的非白像素占比hidesWhenStoppedtrue→ 应该0%平均色是纯白 (255,255,255)hidesWhenStoppedfalse且已停止→ 应该仍有一片非白像素本次实测 14.66%。第二条不能省。没有它“0% 非白有可能只是因为那个区域压根没渲染出来——那就不是隐藏生效”。Q8animating从 false 切回 true会从原地继续吗不会。animatingfalse时库调用的是animation.reset()并把值设为 0所以再切回true是从起点重新开始。小结react-native-animated-spinkit是个纯视觉库12 个加载动画没有原生模块、没有返回值、没有系统状态可读。这轮真正要解决的问题是怎么验一个只有视觉的东西。做法和体会效果类输出只能靠真实产物 对照。连拍两帧做像素差一次能回答三件事在不在动差异 0、停没停差异 0、该隐藏时是否真的不可见整片纯白。本次实测三档分别是 6.17% / 0.00% / 0.00%。静止时仍然可见这条对照不能省。有它才能把隐藏生效和区域根本没渲染区分开——前者是功能正常后者是测量失败但两者的数字长得一模一样。裁剪区要在每个场景里重新定位不能复用固定坐标。我早期拿到过一个假的 0.00%看着像动画确实停了实际是页面滚动后裁剪区落在了深色背景上。一个漂亮的结果先确认被测对象还在画面里。结论要建立在权威证据上不要建立在自己推演的启发式上。我一开始用原生驱动时 JS 侧 listener 不更新来判断驱动方式实测把它推翻了——RN 的原生驱动本身会通过onAnimatedValueUpdate回调 JS 监听。真正的判据是TurboModule 是否存在 日志里有没有回退告警。启发式看起来很像证据但它只是猜测。“能编译过可能掩盖安装已经失败”。这个交付包的npm install退出码是 1但因为软链接先建好了、编译又不看退出码本地一切正常。把npm install的退出码当独立检查项并顺手试一次npm pack。文档里的入口和用法都要实测一遍。这份 README 说 Metro 用lib/commonjs入口实测是src用法示例用 default import实测没有 default 导出。两条都会让照文档接入的人踩空。关于交付包实现文件与上游逐字节一致动画表现也完全正常问题集中在工程配置与文档——npm install/npm pack失败、入口说法错误、用法示例错误、契约测试不测库。其中第一条影响 CI建议优先修后三条改文档即可。本篇用到的库项内容三方库react-native-animated-spinkit上游1.5.2的鸿蒙适配版适配仓库https://atomgit.com/oh-react-native/react-native-animated-spinkit适配 TAG1.5.2-ohos-1.0.0需要 HAR / 权限 / ohpm都不需要纯 JS上游仓库https://github.com/tienphaw/react-native-animated-spinkit基线 commitaa452f7898719a2acfa466dea4b215b21eddbb21MIT宿主工程RNOH084Demo测试页rnAppKey SpinnerTestApp接入方式纯 JS零原生接线// package.jsonreact-native-animated-spinkit:file:../react-native-animated-spinkit// metro.config.js —— file: 装进来是 junction必须让 Metro 找得到源码watchFolders:[path.resolve(__dirname,../react-native-animated-spinkit)],// 必须具名导入 —— 这个库没有 default 导出 import { Plane, Bounce, Pulse, Swing, Chase, Wave, Flow, Circle, CircleFade, Grid, Fold, Wander, } from react-native-animated-spinkit; // 基本用法 Wave size{48} color#1f6feb / // 停止并隐藏默认 hidesWhenStopped true Wave size{48} color#1f6feb animating{false} / // 停止但保留占位 Wave size{48} color#1f6feb animating{false} hidesWhenStopped{false} / // 其余 ViewProps 会透传到根视图 Circle size{64} color#333 style{{marginTop: 12}} /# 换页启动测试页force-stop 不能省换页参数只在冷启动生效hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey SpinnerTestApp环境搭建本手册的验证环境是React Native for OpenHarmonyRNOH。从零准备开发环境DevEco Studio、HarmonyOS SDK API 26、Node.js、ohpm、hvigorw、模拟器与hdc调试请按 CPF-RN 开发者指南的「环境初始化」操作CPF-RN 开发者指南 · 02-搭建准备 · 环境初始化https://atomgit.com/CPF-RN/docs/blob/main/开发者指南/02-搭建准备/环境初始化.md环境就绪后本次验证使用的具体版本组合见文末「验证环境」一节。验证环境项版本React Native0.84.1React19.2.3RNOHnpm / ohpmreact-native-oh/react-native-harmony/rnoh/react-native-openharmony0.84.3Node.jsv24.14.0DevEco Studio26.0.0.621HarmonyOS SDKAPI 2626.0.0.32设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X Viewohos-x641320×2232density 3动画驱动原生驱动NativeAnimatedTurboModule可用无回退告警帧率requestAnimationFrame≈ 63.7 fps宿主 HAP 产物entry-default-signed.hap81.61 MB本次增量构建assembleHap6 分 55 秒HAP 98,305 字节验证规模设备侧35 / 35 断言全部通过像素比对逐个场景动画中6.17% / 6.16% / 3.64% / 23.26%停止后0.00%隐藏后对照区非白像素 0.00%欢迎加入 CPF-RN 鸿蒙社区https://atomgit.com/CPF-RNReact Native for OpenHarmony 组织https://atomgit.com/oh-react-nativeRN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑