React Native for OpenHarmony 实战:三方库 react-native-shimmer-placeholder 的鸿蒙化适配指南
本文记录把 react-native-shimmer-placeholder(骨架屏 / 闪光占位符)适配到 HarmonyOS 的完整过程。
这个库有一个特别容易静默踩坑的设计:它的 peerDependencies 里写着 react-native-linear-gradient(一个原生库),但代码里根本没有 import 它 —— 它用的是依赖注入 + View 兜底:
export const createShimmerPlaceholder = (LinearGradient = View) => … // 不传就降级成 View
看起来"不装也能跑"(确实不报错、也确实在渲染)。但跑起来屏幕上是完全静止的 —— 因为那条负责"扫过"的闪光带子是:
<LinearGradient colors={shimmerColors} style={{flex: 1, width: width * shimmerWidthPercent}} … />
降级成 View 之后它没有任何背景色,也就是透明的;外层容器只有一块静态灰(shimmerColors[0] = #ebebeb)。⇒ 动画在跑(translateX 一直在变),但屏幕上什么都不会动。
这一条我用三点像素对照做成了硬证据:
| 实例 | 配置 | 两帧差异像素 | 最大色差 |
|---|---|---|---|
| P1 | 默认(View 兜底) | 0 / 22000(0.00%) | 0 |
| P2 | ★ 换成有颜色的渐变替身 | 8857 / 22000(40.26%) | 490 |
| P4 | 有颜色的替身 + stopAutoRun | 0 / 22000(0.00%) | 0 |
⇒ P1 完全静止、P2 明显在动、P4 关掉动画又回到静止 ⇒ 既证明了"默认看不到动",也证明了"动画本身一直在跑,只是带子透明"。
设备侧断言 21 / 21 全部通过。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 2.0.9;license: MIT;上游 star 1088 |
| 库类型 | 纯 JS 单组件库(骨架屏):ShimmerPlaceholder(class)+ createShimmerPlaceholder(HOC) |
| 是否需要原生适配 | 本库自己不需要(无 harmony/、无原生模块引用) |
| peerDependencies | ⚠️ react-native-linear-gradient >= 2.4.0(原生库)+ prop-types |
| 它真的会 import 那个原生库吗 | ❌ 不会 —— 靠 createShimmerPlaceholder(LinearGradient) 注入,不传就用 View 兜底 |
| ★ 不传的后果 | ⚠️ 不报错、能渲染,但屏幕上看不到任何动效(闪光带子透明)见下文实测 |
| 运行时依赖 | 零(dependencies 未声明) |
prepare / prepublish | 都没有(scripts 只有 test) |
main | ./lib/ShimmerPlaceholder.js(源码) |
| 需要 HAR / 权限 / ohpm | 本库不需要;但想要真实渐变效果就得另接一个原生渐变库 |
| 设备侧断言 | ✅ 21 / 21 全部通过 |
| ★ 像素判据 | ✅ 三点对照:默认 0%、有颜色替身 40.26%、stopAutoRun 0% |
| ★ 另一个坑 | width/height 的默认值不在 defaultProps 里,而在函数的解构默认里(实测不传尺寸 = 200x15 ✓) |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules / requireNativeComponent | 全无 |
| ④ | 有没有 import react-native-linear-gradient | 没有(只在 peerDependencies 与文档里出现) |
npm install "git+https://atomgit.com/oh-react-native/react-native-shimmer-placeholder.git#2.0.9-ohos-1.0.0" --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-shimmer-placeholder') ],
import ShimmerTestApp from './ShimmerTestApp';
AppRegistry.registerComponent('ShimmerTestApp', () => ShimmerTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ShimmerTestApp
本库不需要 HAR / 权限 / ohpm / link-harmony,也不需要补装任何 dependencies。
三、读源码取到的真值(lib/ShimmerPlaceholder.js,共 162 行)
3.1 三个导出实体
class ShimmerPlaceholder extends PureComponent { … } // 持有 Animated.Value,管 start/stop
const BasedShimmerPlaceholder = (props) => { … } // 真正渲染(未导出)
export const createShimmerPlaceholder = (LinearGradient = View) =>
React.forwardRef((props, ref) => (
<ShimmerPlaceholder LinearGradient={LinearGradient} ref={ref} {...props} />
));
export default ShimmerPlaceholder;
ShimmerPlaceholder是 class(PureComponent);createShimmerPlaceholder()返回的是 HOC 对象(forwardRef的产物 ⇒typeof 'object'、没有可用的prototype);BasedShimmerPlaceholder是函数组件,但没导出。
3.2 defaultProps 只有 3 个键
ShimmerPlaceholder.defaultProps = {
delay: 0,
duration: 1000,
isInteraction: true,
};
★ 注意:width / height / shimmerColors 这些常用默认值【不在】defaultProps 里 —— 它们在 BasedShimmerPlaceholder 的参数解构默认值里:
const {
width = 200,
height = 15,
shimmerColors = ["#ebebeb", "#c5c5c5", "#ebebeb"],
location = [0.3, 0.5, 0.7],
isReversed = false,
stopAutoRun = false,
LinearGradient = View, // ← 兜底就是这里
shimmerWidthPercent = 1,
…
} = props;
⇒ 所以"<ShimmerPlaceholder /> 什么都不传"渲染出来是 200 × 15(实测 200x15 ✓),但它不是 defaultProps 提供的。
3.3 动画与渲染结构
// 动画:无限循环 + 原生驱动(非 web)
Animated.loop(Animated.timing(beginShimmerPosition, {
toValue: 1, delay, duration,
useNativeDriver: Platform.OS !== "web", // ⇒ 鸿蒙上走原生驱动,JS 侧读不到值
isInteraction,
}))
// 位移:从 -width 扫到 +width(isReversed 则反向)
const linearTranslate = beginShimmerPosition.interpolate({
inputRange: [-1, 1],
outputRange: getOutputRange(width, isReversed),
});
// 结构(visible 为假时):
<View style={{flex:1, backgroundColor: shimmerColors[0]}}> // 静态底色
<Animated.View style={{flex:1, transform:[{translateX: linearTranslate}]}}>
<LinearGradient colors={shimmerColors} style={{flex:1, width: width*shimmerWidthPercent}} … />
{/* ↑ 降级成 View 时【没有 backgroundColor】⇒ 透明 ⇒ 什么都看不到 */}
</Animated.View>
</View>
visible 的语义:为真时不渲染闪光带子、children 用 contentStyle 正常显示;为假时 children 被压成 0×0 + opacity 0。
四、验证设计:三点像素对照
因为 useNativeDriver 在鸿蒙上为真(JS 侧读不到动画值),只能用像素证。设计三个实例:
| 实例 | 配置 | 预期 |
|---|---|---|
| P1 | <ShimmerPlaceholder width={300} height={40} />(默认 View 兜底) | 看不到动 |
| P2 | ★ 正对照:自造一个有颜色的"假渐变组件"(红色 View),用 createShimmerPlaceholder(FakeRedGradient) 注入 | 看得到动 |
| P4 | ★ 反向对照:同用红色假渐变,但加 stopAutoRun | 看不到动 |
P2 的设计用意:不需要任何原生库,只用一个纯 JS 的红色 View 冒充渐变,就能把"那条带子"变得可见 ⇒ 从而证明 P1 看不到动,不是因为动画没跑,而是因为带子是透明的。
协议:对每个区域连拍两帧(间隔约 900ms),逐像素比较(判据:三通道差的绝对值之和 > 24 即记为"这个像素变了")。
五、实测结果
5.1 断言 21 / 21
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约 | 导出/实体形态(class vs HOC 对象)、defaultProps 3 键、宿主里没有 react-native-linear-gradient | 11/11 |
| B 渲染与几何 | 渲染未抛错、标题不与状态栏重合、5 个实例的实测尺寸(含不传尺寸 = 200x15) | 9/9 |
| C 行为与像素结论 | 三个对照实例就位、原生驱动说明 | 1/1 |
| 合计 | 21/21 |
5.2 ★★ 三点像素对照(核心证据)
| 区域 | 配置 | 采样点 | 差异像素 | 占比 | 最大色差 |
|---|---|---|---|---|---|
| P1 | 默认(View 兜底) | 22000 | 0 | 0.00% | 0 |
| P2 | 红色假渐变(动画开) | 22000 | 8857 | 40.26% | 490 |
| P4 | 红色假渐变 + stopAutoRun | 22000 | 0 | 0.00% | 0 |
颜色分布也印证了结论:
P1 区(默认)最常见颜色:235,235,235 ×111 ← 就是 shimmerColors[0] 的静态灰,且两帧完全相同
P2 区(对照)最常见颜色:254,0,0 ×67 ← 红色带子确实存在

两帧拼图(同一区域各取两帧):
| frame1 | frame2 | |
|---|---|---|
| P1 默认 | 全灰 | 全灰(与 frame1 完全相同) |
| P2 正对照 | 红带在右侧 | 红带在左侧 ← 明显移动了 |

5.3 ★ 另一个坑:width/height 默认值不在 defaultProps 里
用一个什么都不传的实例(P6)实测:200x15 ✓ —— 与源码里 BasedShimmerPlaceholder 的解构默认 width = 200, height = 15 一致。
而 ShimmerPlaceholder.defaultProps 里只有 delay / duration / isInteraction 三个键(实测通过)。
⇒ 想知道"不传会怎样",必须同时看 defaultProps 和函数参数的解构默认 —— 这个库两者都有,且分工不同。
5.4 UI:标题不与状态栏重合
TITLE measureInWindow y=420 (状态栏高 117) ⇒ 420 > 117,不重合 ✓

六、我自己的三个失误(都值得记)
| # | 失误 | 后果 | 纠正 |
|---|---|---|---|
| 1 | 把 onLayout 挂在外层 stage 上,想量 shimmer 的尺寸 | 量到的是 stage(全宽)⇒ "P1 尺寸 200x15"假失败 | 用它自己的 containerProps={{onLayout}} 传进去(源码把 containerProps 展开到最外层 View 上)⇒ 实测 300x40 ✓ |
| 2 | 断言与自己传的 props 矛盾:我给 P1 显式传了 width={300} height={40},却断言默认值 200x15 | 又一条假失败 | 拆成两个:P1 断言 300x40(我传的);另加一个不传尺寸的 P6 断言 200x15(真默认) |
| 3 | 用"按行替换"改代码,删掉了一个 } | 文件语法坏掉 ⇒ npm run dev 失败(exit 1) | 用行号重写修回;并且发现了下面这个更严重的问题 ⬇️ |
★★ 失误 3 牵出的真问题:打包失败时,HAP 会静默装成旧 bundle
那一次的现象是:
npm run dev → bundle exit=1 (Babel 解析报错,bundle 没生成)
hvigorw … assembleHap → BUILD SUCCESSFUL in 3 min 50 s ← 仍然"成功"!
hdc install → 装上的是【上一次】的 bundle
⇒ 于是页面上跑的还是旧断言、结果看起来"没变化",我一度以为修复没生效。
这是个很危险的现象:assembleHap 成功 ≠ 你刚写的代码进了包。
判据:把"打包"和"构建"的退出码分开看。只要 npm run dev 的 exit code != 0,后面 assembleHap 报成功也毫无意义 —— 它只是把旧的 bundle 又打了一遍。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| 不传渐变组件 ⇒ 看不到动效 | 闪光带子降级成透明 View,只剩静态底色;不报错、不警告 |
| 想要真实渐变要自己接 | 接 react-native-linear-gradient(原生库)并用 createShimmerPlaceholder(LinearGradient) 注入 |
width/height 默认值不在 defaultProps | 在函数参数的解构默认里(200 / 15) |
createShimmerPlaceholder() 返回对象 | forwardRef 产物 ⇒ typeof 'object'、没有可用 prototype |
useNativeDriver 在鸿蒙为真 | JS 侧读不到动画值 ⇒ 验证只能靠像素 |
visible 切换会重建渲染结构 | 为真时不渲染闪光带子、children 正常显示 |
7.2 本次验证的边界(按用户选择:不接原生渐变库)
- 没有接
react-native-linear-gradient(本次范围外)⇒ 真实渐变效果未验证;本轮只证明了"换成有颜色的渐变替身就会动"(用纯 JS 红色View冒充)。
鸿蒙侧确实有适配版可用:@react-native-ohos/react-native-linear-gradient@3.2.0(带harmony字段)—— 但接它要另接一个原生 HAR,本轮未做。 - 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机。 delay/duration/isInteraction未做行为验证(只用断言核对了defaultProps的取值)。shimmerColors/location/shimmerWidthPercent/shimmerStyle/contentStyle/containerProps(除onLayout外)未测。visible的像素行为未做两帧验证(只确认了 children 会显示、带子不渲染)。isReversed只渲染了实例,未做帧差对照(预期扫动方向相反)。children在visible=false时被压成 0×0 的行为未做像素核对。- 没有跨平台对照(未在 iOS/Android 上跑同一套用例)。
未改动库代码。
八、常见问题
Q1:peerDependencies 里有 react-native-linear-gradient,我不装会怎样?
不会报错,能跑,但屏幕上看不到任何动效 —— 因为移动的那条带子降级成了 View(没有背景色 ⇒ 透明),只剩一块静态灰。
实测:默认实例两帧像素差 0.00%;把它换成"有颜色的渐变替身"后两帧差 40.26%;再加 stopAutoRun 又回到 0.00%。
解法(二选一):
- 接一个渐变库并注入:
const S = createShimmerPlaceholder(LinearGradient);鸿蒙侧可用@react-native-ohos/react-native-linear-gradient; - 或者自己写一个纯 JS 的"假渐变"(有背景色的组件即可,比如
<View style={{flex:1, backgroundColor:'#c5c5c5'}} />)—— 不算真渐变,但至少能看到扫动。
Q2:<ShimmerPlaceholder /> 什么都不传,尺寸是多少?
200 × 15(实测)。但注意它不是 defaultProps 提供的 —— 它在 BasedShimmerPlaceholder 的参数解构默认值里:
const { width = 200, height = 15, … } = props;
而 ShimmerPlaceholder.defaultProps 里只有 delay: 0 / duration: 1000 / isInteraction: true。
Q3:createShimmerPlaceholder() 返回的是什么?为什么我拿不到 prototype?
它返回的是 React.forwardRef(...) 的产物(一个对象,typeof 'object'),不是 class ⇒ 没有可用的 prototype。要拿实例引用用 ref(它把 ref 透传给了内部的 ShimmerPlaceholder)。
Q4:为什么我在 JS 里读不到动画进度?
源码里 useNativeDriver: Platform.OS !== "web" ⇒ 鸿蒙上走原生驱动,动画值不在 JS 侧更新。⇒ 验证动效只能靠像素(连拍两帧比差),或者只做结构/属性层面的断言。
Q5:stopAutoRun 是干什么的?
为 true 时不自动启动循环动画(useEffect 里 if (!stopAutoRun) animatedValue.start())。本轮用它做了反向对照:同样有颜色的带子,加上它之后两帧差回到 0.00% ⇒ 证明帧差确实由动画驱动。
Q6:visible 传 true 是什么效果?
不渲染闪光带子,children 正常显示(contentStyle);传 false 时 children 被压成 0×0 + opacity: 0,只显示骨架。典型用法是"数据没回来时 visible={false},回来了 visible={true}"。
Q7:这个库需要鸿蒙侧做什么特殊处理吗?
本库不需要(纯 JS、无原生模块、无 HAR/权限/ohpm)。唯一要注意的是:它不会自动帮你接渐变库 —— 不接就只能看到静止的灰块(而且不报错)。
Q8:为什么我改了代码、重新构建,页面上还是旧行为?
⚠️ 检查打包步骤的退出码。npm run dev 失败(比如语法错)时 hvigorw assembleHap 仍可能报 BUILD SUCCESSFUL —— 因为它只是把已存在的(旧的) bundle 打进包里。只有 npm run dev 的 exit code 为 0,构建结果才有意义。
小结
react-native-shimmer-placeholder 是纯 JS 单组件库(class + 一个 forwardRef HOC),本库零依赖、不需要任何原生接线。设备侧 21 / 21 断言通过。
最值得记下来的是这三条:
-
★★
peerDependencies里有原生库,但代码不 import 它 —— 这是"静默失效"的典型温床。
它靠createShimmerPlaceholder(LinearGradient = View)依赖注入,不传就降级成透明的View⇒ 不报错、能渲染、但屏幕完全静止。
实测三点对照:默认 0.00% 帧差、换成有颜色的替身 40.26%、加stopAutoRun又回到 0.00%。
⇒ 遇到"可选原生依赖",一定要在没有它的状态下实测效果,并把"没有它会发生什么"写成结论。 而且要自己做正/反两个对照,否则"没动"既可能是"没接依赖",也可能是"动画本来就没跑"。 -
默认值可能在两个地方,缺一不可查。 这个库的
defaultProps只有 3 个键,而width/height/shimmerColors/...全在函数参数的解构默认值里。⇒ 只读defaultProps会漏掉一大半默认行为(实测"什么都不传" =200x15)。 -
★★
assembleHap成功 ≠ 你刚改的代码进了包。 我有一次npm run dev因语法错失败(exit 1),而hvigorw依然报BUILD SUCCESSFUL、照样装上 —— 装进去的是上一次的 bundle,差点据此得出"修复没生效"的错误结论。
⇒ 打包步骤的退出码必须单独看;只要npm run dev非 0,后面的构建成功没有任何意义。
(另外两条我自己的低级错误也一并记着:onLayout 要挂在组件自己的 containerProps 上而不是外层包装;断言不能和自己传的 props 矛盾。)
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-shimmer-placeholder(上游 2.0.9 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-shimmer-placeholder |
| 适配 TAG | 2.0.9-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 本库不需要 |
| 库类型 | 纯 JS 单组件库:骨架屏 / 闪光占位符 |
| 运行时依赖 | 零;但 peerDependencies 里有 react-native-linear-gradient(原生库)+ prop-types,且代码不 import 前者 |
prepare / prepublish | 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/tomzaku/react-native-shimmer-placeholder(★1088) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = ShimmerTestApp) |
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-shimmer-placeholder.git#2.0.9-ohos-1.0.0" --ignore-scripts
# 本地目录接入(开发常用)
npm install ../react-native-shimmer-placeholder --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-shimmer-placeholder') ],
import ShimmerPlaceholder, {createShimmerPlaceholder} from 'react-native-shimmer-placeholder';
// ① 直接用默认导出 —— ⚠️ 不传渐变组件 ⇒ 只有静态灰块,看不到扫动
<ShimmerPlaceholder width={300} height={40} />
// ② 想要真实渐变:接一个渐变库并注入
import LinearGradient from 'react-native-linear-gradient'; // 鸿蒙侧可用 @react-native-ohos/react-native-linear-gradient
const Shimmer = createShimmerPlaceholder(LinearGradient);
<Shimmer width={300} height={40} />
// ③ 或者用纯 JS 的"假渐变"(有背景色即可)—— 不算真渐变,但至少能看到扫动
const FakeGradient = props => <View {...props} style={[props.style, {backgroundColor: '#c5c5c5'}]} />;
const Shimmer2 = createShimmerPlaceholder(FakeGradient);
<Shimmer2 width={300} height={40} />
// ④ 数据回来后显示 children
<ShimmerPlaceholder width={300} height={40} visible={isFetched}>
<Text>真实内容</Text>
</ShimmerPlaceholder>
// ⑤ 想量它的尺寸:用 containerProps 传 onLayout(不要挂在外层包装上)
<ShimmerPlaceholder
width={300} height={40}
containerProps={{onLayout: e => console.log(e.nativeEvent.layout)}}
/>
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ShimmerTestApp
像素协议复现(本节判据的复现方式)
# 对同一区域连拍两帧(间隔约 900ms),逐像素比较(三通道差之和 > 24 记为"变了")
hdc shell "snapshot_display -f /data/local/tmp/f1.jpeg"; hdc file recv /data/local/tmp/f1.jpeg .\f1.jpeg
Start-Sleep -Milliseconds 900
hdc shell "snapshot_display -f /data/local/tmp/f2.jpeg"; hdc file recv /data/local/tmp/f2.jpeg .\f2.jpeg
Add-Type -AssemblyName System.Drawing
$a = New-Object System.Drawing.Bitmap('f1.jpeg'); $b = New-Object System.Drawing.Bitmap('f2.jpeg')
$diff = 0; $total = 0
for ($y = $y0; $y -lt $y1; $y += 3) { for ($x = 0; $x -lt $a.Width; $x += 3) {
$p1 = $a.GetPixel($x,$y); $p2 = $b.GetPixel($x,$y)
if (([Math]::Abs($p1.R-$p2.R) + [Math]::Abs($p1.G-$p2.G) + [Math]::Abs($p1.B-$p2.B)) -gt 24) { $diff++ }
$total++ } }
"差异像素 = $diff / $total"
# 默认(View 兜底)⇒ 0;有颜色的渐变替身 ⇒ 明显 > 0;再加 stopAutoRun ⇒ 回到 0
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3 |
| Node.js / npm | v24.14.0 / 11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 26(26.0.0.32) |
| 设备 | HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3 |
| 本机状态栏高度 | 117px(页面留白取 48vp) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 3 分 40 秒 / 3 分 50 秒 / 3 分 52 秒(多轮) |
| 验证规模 | 设备侧 21 / 21 断言;三点像素对照(默认 0% / 有颜色替身 40.26% / stopAutoRun 0%);实例尺寸 6 个(含不传尺寸 200x15);UI 安全区 1 项(标题窗口 y=420 > 117) |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| React Native 鸿蒙官方社区(CPF-RN) | https://atomgit.com/CPF-RN |
| RNOH 框架仓库 | https://atomgit.com/openharmony-RN/ohos_react_native |
| RNOH 三方库使用文档 | https://atomgit.com/OpenHarmony-RN/usage-docs |
| React Native for OpenHarmony 组织 | https://atomgit.com/oh-react-native |
| RN 三方库鸿蒙适配清单 | https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview |
欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN
React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview
更多推荐



所有评论(0)