ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

本文记录把react-native-card-view(卡片布局组件)适配到 HarmonyOS 的完整过程。

这个库极小:一个 130 行的文件、6 个 class 组件、零依赖(连prop-types都没有)—— 是这批库里最干净的一个。它最后一次提交是2016 年 10 月(React 15 时代),所以"老代码在新 React 上跑不跑得起来"本身也是一个看点(结论:能跑)。

但本轮真正的发现是一个会毁掉页面布局的默认样式:

★★CardAction的separator会把卡片撑成几千 vp 高

// react-native-card-view.js L116-120separator:{flex:1,// ← 问题在这height:1,backgroundColor:'#E9E9E9'}

Separator在CardAction里被渲染成<Separator />,而它的样式是flex: 1。
在ScrollView(纵向高度无界)里,flex: 1的子视图会被撑开⇒ 卡片高度失控:

实例配置容器实测高度
C1<CardAction separator>2080 vp✗
C2同结构、不给separator171 vp✓
C3同结构(改色)140 vp✓

⇒12 倍的差距,而且C1 的卡片内容在y=1868就结束了,卡片却继续延伸数千像素(布局 dump 里:文字节点到 1868 为止,而Card节点被屏底裁断;另有一个"窄而极高"的节点就是那个Separator)。

下面的两张截图就是证据——它们都是同一张卡片内部,往下滚了两屏多,整屏空白:

一个小细节:图3/图4 的 JPEG 只有 63 KB,而图2 有 104 KB—— 因为"空白"压缩率高。文件大小自己就说明了问题。

设备侧断言30 / 30 全部通过。


一、先说结论

项结果
上游最新版0.0.3;交付包标MIT;上游 star106;最后一次提交 2016 年 10 月
库类型纯 JS 组件库:6 个 class 组件(Card/CardTitle/CardAction/CardContent/CardImage/Separator)
是否需要原生适配不需要(无harmony/、无NativeModules)
运行时依赖✅零(dependencies为空;连prop-types都没有)
prepare/postinstall✅都没有(npm 侧scripts为空)
mainreact-native-card-view.js(单文件源码直发,2498 B / 130 行)
导出形态ES 具名导出 6 个 class,没有 default 导出
需要 HAR / 权限 / ohpm❌ 都不需要
★★ 最重要的坑<CardAction separator>会把卡片撑成 2080 vp(Separator是flex: 1,在ScrollView里失控)
★ 相关的坑Card的样式里含styles.container(也有flex: 1)⇒<Card>默认会尝试撑满容器;本轮 C1/C2/C3/C6 都用styles={{card:{flex:0}}}把它压住了
★propTypes/defaultProps全部undefined(2016 年的老代码)⇒ 任何 prop 都不校验、不警告
★ React 19 兼容✅ 能跑(React.version = 19.2.3,无老 API 依赖)
设备侧断言✅30 / 30 通过(A 19 + B 8 + C 3)

二、判定与接入

步做法结果
①package.json里有没有harmony.autolinking没有
②仓库里有没有harmony/没有
③代码里有没有NativeModules/requireNativeComponent全无
④import 图体检只有react+react-native⇒ 零依赖 ✓✓
⑤scripts扫prepare家族✅ 没有
npminstall"git+https://atomgit.com/oh-react-native/react-native-card-view.git#0.0.3-ohos-1.0.0"--ignore-scripts
// metro.config.jswatchFolders:[path.resolve(__dirname,'../react-native-card-view')],
importCardViewTestAppfrom'./CardViewTestApp';AppRegistry.registerComponent('CardViewTestApp',()=>CardViewTestApp);
hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey CardViewTestApp

不需要HAR、权限、ohpm、link-harmony,也不需要补装任何依赖✓


三、读源码取到的真值(react-native-card-view.js,130 行)

3.1 六个组件都只是"包一层 View"

classCardextendsComponent{render(){constnewStyles=this.props.styles||{};// ★ 自定义样式 prop 名是 styles(复数)return<View style={[styles.container,styles.card,newStyles.card]}>{this.props.children}</View>;}}classCardImageextendsComponent{render(){…<View style={[styles.cardImage,newStyles.cardImage]}>…}}classCardTitleextendsComponent{render(){…<View style={[styles.cardTitle,newStyles.cardTitle]}>…}}classCardContentextendsComponent{render(){…<View style={[styles.cardContent,newStyles.cardContent]}>…}}classCardActionextendsComponent{render(){constnewStyles=this.props.styles||{};return(<View>{this.props.separator?<Separator/>:null}// ★ separator 是条件渲染<View style={[styles.cardAction,newStyles.cardAction]}>{this.props.children}</View></View>);}}classSeparatorextendsComponent{render(){return<View style={styles.separator}/>;}}export{Card,CardTitle,CardAction,CardContent,CardImage,Separator};

要点:

  1. stylesprop(复数)是按子组件分键的对象(styles={{card: {...}, cardTitle: {...}}}),且排在样式数组最后⇒ 可覆盖默认样式;
  2. Card把styles.container也套上了—— 见 3.2 第 2 条;
  3. Separator不接受任何styles覆盖(它只用自己的styles.separator)⇒那条flex:1无法通过 props 关掉(见 3.3)。

3.2 ★★ 默认样式(两条flex: 1)

conststyles=StyleSheet.create({container:{flex:1,alignItems:'center',backgroundColor:'#F5FCFF',margin:5},// ★ flex:1(第 1 条)card:{backgroundColor:'#fff',borderRadius:2,shadowColor:'#000000',shadowOpacity:0.3,shadowRadius:1,shadowOffset:{height:1,width:0.3}},cardImage:{flex:1},cardTitle:{flex:1,flexDirection:'row',padding:16},// ★ flex:1cardContent:{paddingRight:16,paddingLeft:16,paddingTop:16,paddingBottom:16},cardAction:{margin:8,flexDirection:'row',alignItems:'center'},separator:{flex:1,height:1,backgroundColor:'#E9E9E9'}// ★★ flex:1(第 2 条,本轮的坑)});

3.3 ★★Separator的flex: 1在ScrollView里失控

Separator的样式是{flex: 1, height: 1}。在纵向无界的ScrollView里,flex: 1会让这个子视图尽可能占满可用空间⇒ 卡片高度被撑开。

实测(本轮):

实例配置容器实测高度
C1<CardAction separator>2080 vp
C2不给separator171 vp
C3不给separator(改色)140 vp

布局 dump 的旁证(C1 区):

y=1587..2232+(被屏底裁断) w=960 ← Card 节点,远高于其内容 y=1635 "卡片标题 C1" y=1784 "正文内容 C1:验证 CardContent 的 16vp 内边距与文本渲染。" ← 内容到此结束 y=1916..2232+(窄而极高) ← 那个 Separator(flex:1 被撑开)

⇒内容在 1784 就结束了,卡片却继续延伸数千像素✓

规避方式(任选):

  1. 不要用separator(自己画一条<View style={{height:1, backgroundColor:'#E9E9E9'}} />放进CardAction的 children 里);
  2. 给卡片一个有界高度的容器(例如外层View设定height);
  3. ⚠️不能靠stylesprop 关掉 ——Separator不读props.styles。

3.4 老库的痕迹

  • 没有propTypes、没有defaultProps(六个组件实测全是undefined)⇒ 传错 prop不报警告;
  • 用了 ES class +export {},没有createClass/React.PropTypes等已移除 API ⇒在 React 19.2.3 上能正常运行✓

四、验证设计

判据内容
① 几何对照(本轮主判据)同结构的卡片,有separator/ 无separator⇒ 量容器高度
② 文本读回dump 里读卡片标题/正文,确认卡片内容渲染正常
③ 结构6 个导出都是 class、无 default 导出、propTypes/defaultProps都undefined

说明:本轮没有用"颜色像素"作主判据—— 因为Separator的颜色#E9E9E9与我自己测试页里用的#e8e8e8只差 1,像素法在这里会混淆。改用几何判据(2080 vs 171),结论更硬。


五、实测结果

5.1 断言 30 / 30

组内容结果
A 契约6 个具名导出、无 default 导出、6 个都是 class 且prototype.render存在、propTypes/defaultProps全undefined、React 版本、未使用老 API19/19
B 渲染与几何6 个实例渲染未抛错、标题不与状态栏重合、5 个容器几何、判据说明8/8
C 边界separator条件渲染、stylesprop 语义、无 propTypes3/3

关键观测值:

命名空间导出 = Card, CardAction, CardContent, CardImage, CardTitle, Separator 6 个组件的 typeof = 全 function ★ propTypes = Card:undefined CardTitle:undefined CardAction:undefined CardContent:undefined CardImage:undefined Separator:undefined ★ defaultProps = 同样全 undefined React 版本 = 19.2.3 各实例容器尺寸(vp) = {"c1":"416x2080","c2":"416x171","c3":"416x140","c4":"416x200","c5":"416x200"} UI 安全区:TITLE measureInWindow y=420 > 117 ✓

5.2 ★★ 几何对照(核心证据)

C1(有 separator) = 2080 vp ← 异常 C2(无 separator) = 171 vp C3(无 separator) = 140 vp

⇒唯一差别是separator,高度差 12 倍✓
⇒ 而且C4/C5 我给的是固定 200 vp 的容器,两者都是 200 vp(说明"有界容器"确实能压住高度)—— 这也是一个可用的规避手段 ✓

5.3 文本读回

y=1635 "卡片标题 C1" y=1784 "正文内容 C1:验证 CardContent 的 16vp 内边距与文本渲染。"

⇒CardTitle(16vp padding)与CardContent(四边 16vp padding)都正常渲染 ✓
⇒ 卡片内容正常、只是容器被撑开✓(这点很重要:不是渲染坏了,而是布局失控)

5.4 四张截图(递进叙事)

图内容文件大小
1页面顶部 + 汇总 30/30173 KB
2C1 卡片顶部(标题与正文正常)104 KB
3同一卡片内部再下滚 —— 整屏空白63 KB
4继续下滚 —— 仍然空白63 KB

⇒图3/图4 只有 63 KB(空白压缩率高),文件大小本身就印证了"整屏空白"✓


六、一个给"测试页设计"的教训

这个库的坑把我的测试页本身搞得很难操作:

  • C1 那张卡片高2080 vp(≈6240 px),而屏幕只有2232 px⇒要滚过它需要 ~3 屏;
  • 而我的测试页把 C1~C6 六个实例串在同一个 ScrollView 里⇒C2/C3/C4/C5/C6 全部被推到几千像素之外;
  • 我为此反复尝试"滚到 C3 截图",浪费了好几轮(而且devecocli ui layout在这种超长页面下还间歇性返回空)。

教训与改法:

  1. 把一个"可能失控"的实例单独隔离(例如给它一个固定高度的容器 —— 本轮 C4/C5 就是这么做的,效果很好);
  2. 或者每个实例一页(本项目的测试页本来就支持rnAppKey切页),避免一个坏实例把其它实例推走;
  3. 不要用"无限滚动 + 目标标记落在某区间"的循环去导航一个被撑坏的页面—— 步长很难选,容易跳过或永远到不了;
  4. 再次得到同一条结论:异常结论要先用 dump 复核(本轮正是 dump 里"内容到 1784 结束、Card 节点被裁断"把原因坐实的)。

七、已知限制与本次验证边界

7.1 用这个库要知道的

事项说明
<CardAction separator>会撑高布局Separator是flex:1(在ScrollView里失控)⇒ 实测 2080vp;建议不用separator或改用有界高度的容器
Separator不可用styles覆盖它不读props.styles
Card默认含flex:1因为它套用了styles.container;本轮用styles={{card:{flex:0}}}压住
CardImage/CardTitle也是flex:1放在无界容器里同样会撑开,需注意
自定义样式用styles(复数)按子组件分键:styles={{card:{…}, cardTitle:{…}}};传style(单数)无效
没有propTypes传错 prop 不警告
零依赖不需要补装任何包

7.2 本次验证的边界

  1. styles覆盖的视觉效果(颜色)未做像素核对—— 原因:Separator的#E9E9E9与测试页自用色#e8e8e8只差 1,像素法会混淆;我原本准备的"红卡/绿标题"实例(C3)因为导航被 C1 的巨型卡片阻断,没有取到截图;
  2. shadowOpacity/shadowRadius/shadowOffset的实际阴影效果未验证(鸿蒙侧阴影表现);
  3. CardImage的flex:1图像区未做真实图片验证(只放了一个定高色块);
  4. C4/C5只量了外层容器(都是 200vp),没量内部 Card 节点的高度⇒"flex:0让卡片自适应"这条只做了源码级推断,未直接量到卡片高度;
  5. 未验证margin: 5(container)与margin: 8(cardAction)的实际边距;
  6. 未做跨平台对照(iOS/Android 上的flex:1行为可能不同);
  7. 只在一台模拟器上验证(Pura X View,density 3,1320×2232),没有真机。

未改动库代码。


八、常见问题

Q1:我用了<CardAction separator>,卡片变得特别高、下面一大片空白?

这是库的默认样式导致的:Separator的样式是

separator:{flex:1,height:1,backgroundColor:'#E9E9E9'}

flex: 1在纵向无界的父容器(比如ScrollView)里会被撑开。实测:有separator的卡片容器 2080 vp,没有的 171 vp。

规避(任选其一):

// ① 不用 separator,自己画一条线(推荐) <CardAction> <Text>动作</Text> </CardAction> <View style={{height: 1, backgroundColor: '#E9E9E9'}} /> // ② 给卡片一个有界高度的容器 <View style={{height: 220}}> <Card><CardAction separator>…</CardAction></Card> </View> // ③ 注意:Separator 不读 props.styles,所以无法通过样式覆盖关掉它

Q2:<Card>一放进去就把整块空间占满了?

因为Card的样式数组是[styles.container, styles.card, newStyles.card],而container里有flex: 1:

<Card styles={{card: {flex: 0}}}>…</Card> // ✅ 压住 flex

Q3:怎么自定义样式?

用styles(复数),按子组件分键:

<Card styles={{card: {backgroundColor: '#fff', borderRadius: 8, width: 320, flex: 0}}}> <CardTitle styles={{cardTitle: {backgroundColor: '#eef2f7'}}}>标题</CardTitle> <CardContent styles={{cardContent: {padding: 20}}}>内容</CardContent> <CardAction styles={{cardAction: {justifyContent: 'flex-end'}}}>动作</CardAction> </Card>

传style(单数)无效(库不读它)。

Q4:传错 prop 为什么没有任何警告?

因为整库没有propTypes(2016 年的代码,实测六个组件全是undefined)。

Q5:这个库有依赖吗?需要补装什么?

零依赖(dependencies为空,连prop-types都没有)⇒不需要补装任何包,也不会有"目录接入缺依赖"的问题。

Q6:2016 年的库在 React 19 上能跑吗?

能。本轮在 React19.2.3上 30/30 断言通过、6 个组件都正常渲染;它没有用createClass、React.PropTypes等已移除的 API。

Q7:这个库需要鸿蒙侧做什么特殊处理吗?

不需要。纯 JS、零依赖、无原生模块、不需要 HAR / 权限 /ohpm。唯一要注意的就是上面那个flex:1的布局坑。


小结

react-native-card-view是一个 130 行文件、6 个 class 组件、零依赖的极简卡片库(2016 年的代码,在 React 19.2.3 上能跑)。设备侧30 / 30断言通过。

最值得记下来的是这几条:

  1. ★★ 一个"默认样式"就能毁掉布局。Separator写成{flex: 1, height: 1}⇒ 在ScrollView里把卡片撑到2080 vp(对照实例只有171 vp,差 12 倍),而且内容早就结束了、剩下的全是空白(布局 dump 与两张"整屏空白"的截图都能证明)。
    ⇒接一个库时,"默认样式里的flex"值得专门看一眼(尤其是会被放进ScrollView的组件)。

  2. ★ 同样的坑在Card自己身上也有一半。Card套用了styles.container(含flex: 1)⇒ 默认会尝试撑满容器;本轮我用styles={{card:{flex:0}}}压住了它 ——但请注意:这个覆盖压不住Separator(Separator不读props.styles)⇒同一张卡片上出现了"一个能覆盖、一个不能覆盖"的两种flex:1,这也是 C1 依然失控的原因。

  3. ★ 测试页要防"一个坏实例拖垮整页"。C1 高 6240 px,把同页的 C2~C6 全推到几千像素之外,导致我无法顺利滚到后续实例去截图。⇒把可能失控的实例放进固定高度的容器,或者干脆一个实例一页。

  4. ★ 判据要挑"不被干扰"的那种。本轮我放弃用颜色像素判定separator—— 因为库的#E9E9E9和测试页自用色#e8e8e8只差 1,会混淆;改用几何高度对照(2080 vs 171)后结论非常干净。⇒先看判据会不会被环境噪声吃掉,再决定用它。

  5. 零依赖的库也有它的价值:连续三轮踩"目录接入要手动补依赖"之后,这个库不需要补装任何东西,预检阶段就能确定(import 图只有react/react-native)。


本篇用到的库

项内容
三方库react-native-card-view(上游0.0.3的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-card-view
适配 TAG0.0.3-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS,零依赖)
库类型纯 JS 组件库:卡片布局(6 个 class 组件、单文件 130 行)
运行时依赖✅零(不需要补装任何包)
prepare/postinstall✅ 都没有
许可证交付包标MIT(上游仓库未标注)
上游仓库https://github.com/jacklam718/react-native-card-view(★106)
宿主工程RNOH084Demo(测试页rnAppKey = CardViewTestApp)
# 从适配仓库安装(零依赖,无需补装)npminstall"git+https://atomgit.com/oh-react-native/react-native-card-view.git#0.0.3-ohos-1.0.0"--ignore-scripts# 本地目录接入npminstall../react-native-card-view --ignore-scripts
// metro.config.jswatchFolders:[path.resolve(__dirname,'../react-native-card-view')],
import {Card, CardTitle, CardContent, CardAction, CardImage, Separator} from 'react-native-card-view'; // ① 推荐写法:不用 separator,自己画线;并用 flex:0 压住 Card 自带的 flex:1 <Card styles={{card: {width: 320, flex: 0}}}> <CardTitle><Text style={{fontSize: 16, fontWeight: '700'}}>卡片标题</Text></CardTitle> <CardContent><Text>正文内容</Text></CardContent> <View style={{height: 1, backgroundColor: '#E9E9E9'}} /> <CardAction styles={{cardAction: {justifyContent: 'flex-end'}}}> <Text style={{color: '#007aff'}}>动作 1</Text> <Text style={{color: '#007aff'}}>动作 2</Text> </CardAction> </Card> // ② 若要用内置 separator:请给外层一个有界高度,避免 flex:1 撑开 <View style={{height: 220}}> <Card styles={{card: {flex: 0}}}> <CardContent><Text>内容</Text></CardContent> <CardAction separator><Text>动作</Text></CardAction> </Card> </View> // ③ 自定义样式统一走 styles(复数),按子组件分键 // styles={{card:{…}, cardTitle:{…}, cardContent:{…}, cardAction:{…}, cardImage:{…}}} // ⚠️ 传 style(单数)无效;⚠️ Separator 不接受覆盖
# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey CardViewTestApp

判据的复现方式

# ★ 主判据是【几何对照】,不是颜色:# 同结构两张卡片,一张 CardAction 带 separator、一张不带,量外层容器高度# devecocli ui layout 里看 onLayout 上报的尺寸# 实测:带 separator = 2080vp、不带 = 171vp(差 12 倍)# ⚠️ 不要用颜色判 Separator:库的 #E9E9E9 与常见页面底色 #e8e8e8 只差 1,像素法会混淆

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.84.3
Node.js / npmv24.14.0/11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 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
本次构建assembleHap3 分 41 秒(一轮就过,无重编)
验证规模设备侧30 / 30断言;几何对照 3 例(2080 / 171 / 140 vp)+ 固定容器 2 例(均 200vp);文本读回 2 处;结构判据 6 组件 × (class + 无 propTypes + 无 defaultProps);UI 安全区 1 项

相关社区与项目地址

项目地址
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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进