React Native for OpenHarmony 实战:三方库 react-native-progress-steps 的鸿蒙化适配指南
本文记录把 react-native-progress-steps(分步向导 / 进度步骤条)适配到 HarmonyOS 的完整过程。
它做的事:把若干 <ProgressStep> 组成一个向导,顶部是圆圈 + 连线 + 步骤标签,底部是"上一步 / 下一步 / 提交"按钮,中间是当前步骤的内容区。
结论:可用,而且是这几轮里"预检最干净、契约零 FAIL"的一次。
- 预检的老写法九项 +
ListView+ViewPropTypes+Text.propTypes+this.refs+mixins+componentWillReceiveProps+componentWillMount+getDefaultProps+isRequired+Platform.OS+measure+onLayout+NativeModules+<Modal—— 全部为空; createClass的精确命中是 0,连 babel 的_createClasshelper 都没有;useNativeDriver只有 2 处、而且都是false(正确用法,无警告风险);- 契约 8 条全 PASS、零 FAIL —— 包括"4 个组件都是函数组件"这一条(这次按源码里的真实形态断言,没有靠经验假设"是 class");
- 渲染上,步骤标签「PGS 第二步」在布局里拿到了真实坐标(
x=858 y=1387)。
★★★★★ 一、预检:这是至今最干净的一个库
| 预检项 | 结果 |
|---|---|
老写法九项(createClass / React.PropTypes / ViewPropTypes / Text.propTypes / 字符串 ref / this.refs / mixins / componentWillReceiveProps / componentWillMount / getDefaultProps) | ✅✅ 全部为空 |
ListView.DataSource / <ListView | ✅ 空 |
measure() / onLayout / NativeModules | ✅ 全部空 |
Platform.OS | ✅ 空(连平台分支都没有) |
isRequired | ✅ 空 |
<Modal | ✅ 空(纯 View + Animated) |
createClass | ✅ 精确命中 0、裸词也 0(连 helper 都没有) |
useNativeDriver | ✅ 2 处,都是 false |
dist/index.js(148 B) | ✅ 只有 2 个具名导出:ProgressSteps / ProgressStep |
dependencies | lodash(宿主已有) |
peerDependencies | ⚠️ react / react-native / lucide-react-native >= 0.475.0(图标库) |
scripts.prepare | ⚠️ npm run build(tsc)⇒ 必须 --ignore-scripts |
lucide-react-native:桩可以做得极简
它是个基于 react-native-svg 的图标库(每个图标都是 SVG 组件)⇒ 含原生依赖 ⇒ 需要用桩隔离。
但先读源码,发现它只用到 一个图标:
// src/ProgressSteps/StepIcon.tsx L3
import { Check } from 'lucide-react-native';
⇒ 桩只需要提供具名导出 Check ✓
const makeIcon = (glyph, displayName) => {
const Icon = ({size = 24, color = '#000', style, ...rest}) => (
<View style={[{width: size, height: size, alignItems: 'center', justifyContent: 'center'}, style]} {...rest}>
<Text style={{color, fontSize: size * 0.8, fontWeight: '700'}}>{glyph}</Text>
</View>
);
Icon.displayName = `${displayName}(stub)`;
return Icon;
};
const Check = makeIcon('✓', 'Check');
module.exports = {Check, ChevronRight: makeIcon('›'), ChevronLeft: makeIcon('‹'), X: makeIcon('×')};
⇒ 用"✓ 字符"代替 SVG 对勾 ⇒ 既不崩、又能在布局 dump 里看到它 ✓
⇒ 另外顺手给了 ChevronRight / ChevronLeft / X,防止该库以后新增图标时直接崩 ✓
★★★★★ 二、契约 8/8 全 PASS(零 FAIL)—— 因为它按真实形态断言
先看源码确认形态(这几轮的教训:别假设):
src/ProgressSteps/ProgressStep.tsx L5: const ProgressStep = ({...}) => {
src/ProgressSteps/ProgressSteps.tsx L7: const ProgressSteps = ({...}) => {
src/ProgressSteps/StepIcon.tsx L11: const StepIcon = ({...}) => {
dist/ProgressSteps/ProgressButtons.js : const ProgressButtons = ({...}) => (…)
⇒ 4 个组件全是函数组件,没有一个是 class ✓
所以断言写成"function + 没有 prototype.render":
PASS | ★★★ ProgressSteps 是 function
PASS | ★★ ProgressSteps 没有 prototype.render(它是函数组件)
PASS | ★★★ ProgressStep 是 function
PASS | ★★ ProgressStep 没有 prototype.render(它是函数组件)
NOTE | 默认导出(本库无 default) = undefined
PASS | ★★★ dist/index.js 的具名导出清单 | 期望=[ProgressStep,ProgressSteps] | 实际=[ProgressStep,ProgressSteps]
PASS | ★★ ProgressSteps 非 null
PASS | ★★ ProgressStep 非 null
SUMMARY | 合计 8/8 通过,失败 0
⇒ ⇒ 8 条全 PASS、零 FAIL ✓✓
这一条为什么值得单独说:
我在"组件形态"上曾经错过两次 —— 一次把forwardRef当函数,
一次把函数组件当 class(rn-78,那个库 6 个 UI 组件里有 4 个是function)。
这一轮我先看export default那一行的写法,然后才写断言 ⇒ 一次全过 ✓
判别标准很硬:
class X extends Component⇒typeof X === 'function'且X.prototype.render是 functionconst X = ({…}) => …⇒typeof X === 'function'且X.prototype是undefinedX = React.forwardRef(…)⇒typeof X === 'object'且X.$$typeof === Symbol(react.forward_ref)
★★★★★ 三、精确默认值:全部从源码解构参数读出来
这个库没有 defaultProps,默认值全写在函数参数解构里 ⇒ 只能读源码:
ProgressStep(21 个,前 12 个):
const ProgressStep = ({
errors = false, removeBtnRow = false, scrollable = true,
activeStep = 0, stepCount = 0,
buttonNextText = 'Next', // ★★
buttonPreviousText = 'Previous', // ★★
buttonFinishText = 'Submit', // ★★
buttonNextDisabled = false, buttonPreviousDisabled = false, buttonFinishDisabled = false,
buttonBottomOffset = 20, buttonTopOffset = 12, buttonHorizontalOffset = 30,
buttonFillColor = '#2D2D2D', buttonNextTextColor = '#FFFFFF',
buttonPreviousTextColor = '#2D2D2D', buttonFinishTextColor = '#FFFFFF',
buttonBorderColor = '#2D2D2D', buttonDisabledColor = '#CDCDCD',
buttonDisabledTextColor = '#FFFFFF',
...props
}: ProgressStepProps) => { … }
ProgressSteps(4 个):
const ProgressSteps = ({
children, isComplete = false, activeStep: initialActiveStep = 0,
topOffset = 60, marginBottom = 30, ...props
}) => { … }
StepIcon(4 个常量 + 17 个默认值):
const CIRCLE_SIZE = 40; // ★ 圆圈直径
const MOBILE_BREAKPOINT = 768;
const MOBILE_LINE_POSITION = 78;
const DESKTOP_LINE_POSITION = 58;
borderWidth = 2, labelFontSize = 14, labelColor = '#D3D3D3',
progressBarColor = '#EBEBE4', completedProgressBarColor = '#2D2D2D',
activeStepIconColor = 'transparent', completedStepIconColor = '#2D2D2D',
disabledStepIconColor = '#EBEBE4', activeLabelColor = '#2D2D2D',
completedLabelColor = '#2D2D2D', activeStepNumColor = '#2D2D2D',
completedStepNumColor = '#2D2D2D', disabledStepNumColor = '#FFFFFF',
completedCheckColor = '#FFFFFF', activeStepIconBorderColor = '#2D2D2D'
⇒ ⇒ 这些值都是从源码逐行抄的,不是猜的 ✓
⇒ 它们同时是"改样式时的默认参照" —— 比如 CIRCLE_SIZE = 40、labelFontSize = 14。
★★★★ 四、渲染结构决定了文本判据怎么取
ProgressSteps 的渲染逻辑(src/ProgressSteps/ProgressSteps.tsx L31-64):
const renderStepIcons = () => (
times(stepCount, (i) => (
<View key={i} style={styles.stepContainer}>
<StepIcon
{...props}
stepNum={i + 1} // ★ 步骤号从 1 开始
label={children[i].props.label} // ★ 标签取自子元素的 props.label
isFirstStep={i === 0}
isLastStep={i === stepCount - 1}
isCompletedStep={isComplete ? true : i < activeStep}
isActiveStep={isComplete ? false : i === activeStep}
/>
</View>
))
);
return (
<View style={styles.container}>
<View style={[styles.stepsContainer, {paddingTop: topOffset, marginBottom}]}>{renderStepIcons()}</View>
<View style={styles.contentContainer}>
{React.cloneElement(children[activeStep], { // ★ 只渲染当前步骤那一个 Step
setActiveStep: handleSetActiveStep, activeStep, stepCount,
})}
</View>
</View>
);
⇒ 三条可用于判据的结构事实:
- 步骤号从 1 开始(
stepNum = i + 1)⇒StepIcon里渲染成{stepNum}的Text - 标签来自
children[i].props.label⇒ 我传的label会出现在布局里 - 只渲染
children[activeStep]⇒ 非当前步骤的内容区不存在
实测:标签真的渲染出来了 ✓
✓ 「PGS 第二步」 w=216 h=49 x=858 y=1387
⇒ x=858 恰好在屏幕右半(屏宽 1320)—— 第二步的标签排在右侧,与"步骤条横向排列"一致 ✓
⚠️ 但其他文本(按钮文本 PGS 下一步 / PGS 提交 / 内容区文本)本轮没在 dump 里定位到
⇒ 这个组件的按钮是 position: 'absolute' 定位的(ProgressButtons 里 left:60 / bottom:40),
可能因为绝对定位 + 层级关系,在布局 dump 里不容易按文本命中。
⇒ 如实记为"未观察到" ✓
★★★ 五、其余预检结论
| 预检项 | 结果 |
|---|---|
| 交付包 | dist/ 完整:4 个组件的 .js + .d.ts + .map,index.js 只有 148 B |
Animated 6 处 | StepIcon 里用 Animated.Value 做连线动画 ✓ |
lodash 的用法 | import { times } from 'lodash' |
| 组件规模 | ProgressStep.js 4111 B / ProgressSteps.js 2090 B / StepIcon.js 6055 B / ProgressButtons.js 480 B |



六、接入与预检(要点)
# ★ 必须 --ignore-scripts:它的 scripts.prepare 是 "npm run build"(tsc)
npm install "git+https://atomgit.com/oh-react-native/react-native-progress-steps.git#2.0.3-ohos-1.0.0" --ignore-scripts
# lodash 宿主已有;lucide-react-native 是图标 peer ⇒ 装官方的,或用纯 JS 桩(只需 Check 一个图标)
七、交付包质量与基线说明
| 类别 | 结果 |
|---|---|
| 交付包内容 | dist/ 完整(4 组件的 .js + .d.ts + .map);index.js 148 B ⇒ 只有 2 个具名导出 |
spec.json | ⚠️ 缺 upstreamCommit |
validation | ⚠️ "build-pass-device-pending" |
| ⚠️ 声明的基线 | reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾 |
| 备注 | 库代码零改动 |
关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
本库零平台分支、零原生代码、useNativeDriver 都是 false ⇒ 结论跨基线成立 ✓
八、已知限制与本次验证边界
8.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| ★★★★★ 4 个组件全是函数组件 | ProgressSteps / ProgressStep / StepIcon / ProgressButtons 都是 const X = ({…}) => … ⇒ 没有 prototype.render |
★★★★★ 必要 peer:lucide-react-native | 基于 react-native-svg 的图标库;源码只用 Check 一个图标 ⇒ 桩可极简 |
★★★★ dist/index.js 只有 2 个导出 | ProgressSteps / ProgressStep(StepIcon / ProgressButtons 是内部的,未从入口导出) |
| ★★★★ 默认按钮文本 | buttonNextText='Next' / buttonPreviousText='Previous' / buttonFinishText='Submit' |
| ★★★ 步骤号从 1 开始 | stepNum = i + 1 |
| ★★★ 只渲染当前步骤 | React.cloneElement(children[activeStep], …) ⇒ 非当前步骤的内容区不存在 |
★★★ topOffset 默认 60、marginBottom 默认 30 | 步骤条的上下留白 |
★★★ StepIcon 的关键常量 | CIRCLE_SIZE=40 / labelFontSize=14 / borderWidth=2 |
★★ 必须是 <ProgressSteps> 包多个 <ProgressStep> | ProgressSteps 用 React.Children.count(children) 算步骤数、用 children[i].props.label 取标签 |
★ scripts.prepare 会跑 tsc | 安装必须 --ignore-scripts |
8.2 本次验证的边界
- ★★★ 未验证按钮的点击交互(
onNext/onPrevious/onSubmit/setActiveStep的联动); - ★★★ 按钮文本(
PGS 下一步/PGS 提交)与内容区文本未在布局 dump 里定位到(按钮是position:'absolute');只确认了标签文本(x=858 y=1387); - ★★★ 未验证圆圈/连线的动画(
Animated.Value的连线动画); - ★★ 未验证"已完成"状态的对勾图标渲染(
Check桩是✓字符); - ★★ 未验证
errors/removeBtnRow/scrollable三个开关的实际效果; - 未验证
renderNextButton/renderPreviousButton自定义渲染; - 未验证
isComplete整体完成态; - 未在
RN 0.88.0/RNOH 0.84.4上复验; - 未测其它屏幕宽度;未做跨平台对照。
库代码未改动(新增的是 stubs/lucide-react-native.js)。
九、常见问题
Q1:它在 RN 0.84 上能跑吗?
能,而且很干净。 预检的老写法九项 + ListView / ViewPropTypes / Text.propTypes / measure / onLayout / NativeModules / Platform.OS 全部为空;
createClass 精确命中 0;useNativeDriver 只有 2 处且都是 false;
契约 8 条全 PASS(零 FAIL);标签文本拿到真实坐标(x=858 y=1387);LogBox 无通知。
Q2:Cannot find module 'lucide-react-native'?
它是 peerDependency(基于 react-native-svg 的图标库)。两条路:
① 装官方鸿蒙化版本;② 写个极简桩 —— 源码只用到一个图标:
// stubs/lucide-react-native.js
const Check = ({size = 24, color = '#000'}) => (
<View style={{width: size, height: size, alignItems: 'center', justifyContent: 'center'}}>
<Text style={{color, fontSize: size * 0.8, fontWeight: '700'}}>✓</Text>
</View>
);
module.exports = {Check};
Q3:怎么判断这些组件是 class 还是函数组件?
看 export default 那一行的写法,再用 prototype.render 验证:
typeof ProgressSteps; // 'function'
typeof ProgressSteps.prototype; // 'undefined' ← 函数组件
typeof ProgressSteps.prototype?.render; // 'undefined' ← 所以没有 prototype.render
判别三档:
class X extends Component⇒typeof是 function 且X.prototype.render是 functionconst X = ({…}) => …⇒typeof是 function 且X.prototype是undefinedX = React.forwardRef(…)⇒typeof是 object 且X.$$typeof是Symbol(react.forward_ref)
Q4:默认按钮文本是什么?怎么改?
默认 Next / Previous / Submit,分别对应 buttonNextText / buttonPreviousText / buttonFinishText:
<ProgressStep
label="第一步"
buttonNextText="下一步"
buttonPreviousText="上一步"
buttonFinishText="提交"
/>
Q5:为什么非当前步骤的内容不显示?
因为 ProgressSteps 用 React.cloneElement(children[activeStep], …) 只渲染当前那一个 Step。
所以想"所有步骤都挂载、只是隐藏"是做不到的 —— 它本来就是"一次一份内容"的设计。
Q6:StepIcon / ProgressButtons 能单独 import 吗?
不能从入口拿 —— dist/index.js 只有 2 个导出(ProgressSteps / ProgressStep)。
要单独用它们得走子路径:react-native-progress-steps/dist/ProgressSteps/StepIcon。
Q7:步骤号为什么从 1 开始?
源码里 stepNum={i + 1}(ProgressSteps.tsx L40)⇒ 显示 1、2、3…,不是从 0 开始。
小结
react-native-progress-steps 是分步向导组件(圆圈 + 连线 + 标签 + 上一步/下一步/提交按钮),
预检最干净、契约零 FAIL —— 是这几轮里质量最高的一个。
最值得记下来的是这几条:
-
★★★★★ 契约 8/8 零 FAIL,因为我这次按"真实形态"断言。
先看export default那一行:4 个组件全是const X = ({…}) => …(函数组件),
所以断言写成typeof === 'function'+prototype.render === undefined⇒ 一次全过 ✓
⇒ 我在这个点上曾经错过两次(把forwardRef当函数、把函数组件当 class)——
"先抄形态、再写断言"这条现在真的固化了。 -
★★★★★ 预检的干净程度本身就说明"适配成本低"。
老写法九项全空、createClass精确命中 0、Platform.OS零命中、useNativeDriver都是false
⇒ 没有已移除 API、没有平台分支、没有动画警告 ⇒ 这类库基本"装上就能用" ✓ -
★★★★★ 桩可以做得比想象中更小:先读源码看它到底用了几个图标。
只用了Check一个 ⇒ 桩就 20 行(用✓字符代替 SVG 对勾),
而且把图标渲成可见字符还顺带让它在布局 dump 里能被看到 ✓ -
★★★★ 精确默认值全从"函数参数解构"读(这个库没有
defaultProps):
buttonNextText='Next'/buttonPreviousText='Previous'/buttonFinishText='Submit'/
topOffset=60/marginBottom=30/CIRCLE_SIZE=40/labelFontSize=14✓
⇒ 没有defaultProps不等于"没有默认值" —— 要去读解构参数。 -
★★★ 渲染结构决定了判据怎么取:
times(stepCount, i => <StepIcon stepNum={i+1} label={children[i].props.label} />)⇒ 步骤号从 1;
React.cloneElement(children[activeStep], …)⇒ 只渲染当前步骤。
⇒ 标签文本因此能命中(「PGS 第二步」x=858 y=1387)✓ -
★★★ 按钮文本没命中要如实说 —— 它的按钮是
position:'absolute'(left:60 / bottom:40),
在布局 dump 里不容易按文本命中 ⇒ 只写"标签命中、按钮文本未观察到" ✓ -
★★
dist/index.js只有 2 个导出(ProgressSteps/ProgressStep)⇒
StepIcon/ProgressButtons要单独用必须走子路径 ✓
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-progress-steps(上游 2.0.3 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-progress-steps |
| 适配 TAG | 2.0.3-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS) |
| 库类型 | 分步向导(圆圈 + 连线 + 标签 + 上一步/下一步/提交) |
| 运行时依赖 | lodash(宿主已有) |
| peerDependencies | ⚠️ lucide-react-native >= 0.475.0(基于 react-native-svg 的图标库,源码只用 Check) |
| 导出形态 | 2 个具名导出:ProgressSteps / ProgressStep(无默认导出);4 个组件全是函数组件 |
⚠️ scripts.prepare | npm run build(tsc)⇒ 安装需 --ignore-scripts |
| 许可证 | MIT |
| 上游仓库 | https://github.com/colbymillerdev/react-native-progress-steps |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = ProgressStepsTestApp) |
npm install "git+https://atomgit.com/oh-react-native/react-native-progress-steps.git#2.0.3-ohos-1.0.0" --ignore-scripts
# peer:lucide-react-native(图标库)—— 装官方鸿蒙版,或用只含 Check 的极简桩
import {ProgressSteps, ProgressStep} from 'react-native-progress-steps';
// ① 基本用法:ProgressSteps 包多个 ProgressStep
<ProgressSteps activeStep={0} topOffset={60} marginBottom={30}>
<ProgressStep label="第一步" buttonNextText="下一步">
<View><Text>第 1 步内容</Text></View>
</ProgressStep>
<ProgressStep label="第二步" buttonPreviousText="上一步" buttonFinishText="提交">
<View><Text>第 2 步内容</Text></View>
</ProgressStep>
</ProgressSteps>
// ② 默认值(源码解构参数,不是 defaultProps)
// buttonNextText='Next' / buttonPreviousText='Previous' / buttonFinishText='Submit'
// topOffset=60 / marginBottom=30
// StepIcon: CIRCLE_SIZE=40 / labelFontSize=14 / borderWidth=2 / labelColor='#D3D3D3'
// ⚠️ 4 个组件全是函数组件 ⇒ 没有 prototype.render(不是 class)
// ⚠️ 步骤号从 1 开始(stepNum = i + 1)
// ⚠️ 只渲染当前 activeStep 那一个 Step(React.cloneElement(children[activeStep]))
// ⚠️ 入口只导出 ProgressSteps / ProgressStep;StepIcon / ProgressButtons 要走子路径
// ⚠️ peer lucide-react-native 基于 react-native-svg ⇒ 需装官方鸿蒙版或用桩(只需 Check)
// ⚠️ 安装必须 --ignore-scripts(prepare = npm run build)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey ProgressStepsTestApp
判据的复现方式
# ① 契约(8 条,零 FAIL)
hdc shell "hilog -x | grep pgs-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ⇒ 4 个组件都是 function 且 prototype.render 为 undefined
# ⇒ dist/index.js 的具名导出清单 = [ProgressStep, ProgressSteps]
# ② 标签文本(真实坐标)
devecocli ui layout | Select-String 'PGS 第二步'
# ⇒ w=216 h=49 x=858 y=1387
验证环境
| 项 | 版本 |
|---|---|
| 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 = 39vp |
| 宿主依赖 | ⚠️ 1 个纯 JS 桩(lucide-react-native 的 Check) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 4 分 12 秒 ⇒ 一次成功(打包与构建退出码都检查了) |
| 验证规模 | 契约 8/8 全 PASS、零 FAIL;预检九项 + createClass + Platform.OS 全空;标签文本拿到真实坐标;LogBox 无通知;4 张截图互异 |
⚠️ 基线说明:交付包
spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
本库零平台分支、零原生代码、useNativeDriver都是false⇒ 结论跨基线成立 ✓
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)