本文记录把 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 的 _createClass helper 都没有;
  • 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
dependencieslodash(宿主已有)
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 是 function
  • const X = ({…}) => … ⇒ typeof X === 'function' 且 X.prototype 是 undefined
  • X = 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. 步骤号从 1 开始(stepNum = i + 1)⇒ StepIcon 里渲染成 {stepNum} 的 Text
  2. 标签来自 children[i].props.label ⇒ 我传的 label 会出现在布局里
  3. 只渲染 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 本次验证的边界

  1. ★★★ 未验证按钮的点击交互(onNext / onPrevious / onSubmit / setActiveStep 的联动);
  2. ★★★ 按钮文本(PGS 下一步 / PGS 提交)与内容区文本未在布局 dump 里定位到(按钮是 position:'absolute');只确认了标签文本(x=858 y=1387);
  3. ★★★ 未验证圆圈/连线的动画(Animated.Value 的连线动画);
  4. ★★ 未验证"已完成"状态的对勾图标渲染(Check 桩是 ✓ 字符);
  5. ★★ 未验证 errors / removeBtnRow / scrollable 三个开关的实际效果;
  6. 未验证 renderNextButton / renderPreviousButton 自定义渲染;
  7. 未验证 isComplete 整体完成态;
  8. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  9. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动(新增的是 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 是 function
  • const X = ({…}) => … ⇒ typeof 是 function 且 X.prototype 是 undefined
  • X = 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 —— 是这几轮里质量最高的一个。

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

  1. ★★★★★ 契约 8/8 零 FAIL,因为我这次按"真实形态"断言。
    先看 export default 那一行:4 个组件全是 const X = ({…}) => …(函数组件),
    所以断言写成 typeof === 'function' + prototype.render === undefined ⇒ 一次全过 ✓
    ⇒ 我在这个点上曾经错过两次(把 forwardRef 当函数、把函数组件当 class)——
    "先抄形态、再写断言"这条现在真的固化了。

  2. ★★★★★ 预检的干净程度本身就说明"适配成本低"。
    老写法九项全空、createClass 精确命中 0、Platform.OS 零命中、useNativeDriver 都是 false
    ⇒ 没有已移除 API、没有平台分支、没有动画警告 ⇒ 这类库基本"装上就能用" ✓

  3. ★★★★★ 桩可以做得比想象中更小:先读源码看它到底用了几个图标。
    只用了 Check 一个 ⇒ 桩就 20 行(用 ✓ 字符代替 SVG 对勾),
    而且把图标渲成可见字符还顺带让它在布局 dump 里能被看到 ✓

  4. ★★★★ 精确默认值全从"函数参数解构"读(这个库没有 defaultProps):
    buttonNextText='Next' / buttonPreviousText='Previous' / buttonFinishText='Submit' /
    topOffset=60 / marginBottom=30 / CIRCLE_SIZE=40 / labelFontSize=14 ✓
    ⇒ 没有 defaultProps 不等于"没有默认值" —— 要去读解构参数。

  5. ★★★ 渲染结构决定了判据怎么取:
    times(stepCount, i => <StepIcon stepNum={i+1} label={children[i].props.label} />) ⇒ 步骤号从 1;
    React.cloneElement(children[activeStep], …) ⇒ 只渲染当前步骤。
    ⇒ 标签文本因此能命中(「PGS 第二步」x=858 y=1387)✓

  6. ★★★ 按钮文本没命中要如实说 —— 它的按钮是 position:'absolute'(left:60 / bottom:40),
    在布局 dump 里不容易按文本命中 ⇒ 只写"标签命中、按钮文本未观察到" ✓

  7. ★★ 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
适配 TAG2.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.preparenpm 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 Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.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 = 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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐