接到一个临时需求:手上React Native的项目,下个月要上架鸿蒙应用市场,业务代码能不能直接跑?我当时第一反应是怀疑——毕竟鸿蒙的运行时、组件体系和Android/iOS差异不小,RN这种靠桥接层调原生能力的框架,到了鸿蒙上八成要伤筋动骨。但实际调研之后发现,情况比预想乐观:RN在鸿蒙上确实能跑,而且核心业务组件几乎不用改,真正要改的是工程壳、构建配置和一部分原生依赖适配。
这篇东西就围绕一个很典型的组件展开:面包屑导航。选它当入门案例有两个原因:第一,它足够小,不牵扯复杂SDK,能把"RN工程如何在鸿蒙上跑起来"这件事讲透;第二,它又足够典型,涉及路由监听、状态管理、原生返回键联动、折叠屏适配等多个跨平台开发的坑。也就是说,看完这篇,你不光能写出一个面包屑组件,还能顺带搞明白一套"RN代码搬到鸿蒙"的完整技术路径。
适合的读者有三类:一是手里有RN项目、正在评估鸿蒙适配成本的开发者;二是想了解鸿蒙开发但不想丢掉RN技术栈的跨端工程师;三是刚开始学鸿蒙、想要一个"既能练手又不至于太简单"的实战项目的人。我会从环境搭建讲起,一直到真机调试的坑和可复用组件的进阶写法,全程按我自己踩过坑之后的正确顺序来。
1. 先搞清楚一件事:RN在鸿蒙上是怎么跑起来的
1.1 不是另起炉灶,而是"换壳"
很多人一听"React Native鸿蒙版",下意识以为是鸿蒙官方另出了一套框架,或者需要用ArkTS把JS逻辑重写一遍。都不是。
RN在鸿蒙上的方案,目前主流是OpenHarmony社区维护的React Native适配层(包名通常是 @react-native-oh/react-native)。它的思路和RN在Android/iOS上的原理一模一样:JS业务代码跑在JS引擎里,通过桥接协议调用原生UI组件,原生这头再用鸿蒙的ArkUI组件把界面渲染出来。
换句话说,你写的React组件、状态管理、业务逻辑,绝大多数可以原封不动搬过来。真正要动的是三块:原生工程壳(从Android Studio换成DevEco Studio)、构建脚本、以及部分依赖鸿蒙原生能力的第三方库。
1.2 和纯ArkTS开发怎么选
鸿蒙原生开发主推的是ArkTS + ArkUI,如果你只做鸿蒙一个平台,直接学ArkTS没毛病。但跨平台团队的情况不一样:
- 团队已经沉淀了RN组件库和业务代码,用ArkTS重写一遍不管是人力还是维护成本都不低。
- 多个平台需要保持一致的用户体验,RN天然双端对齐,再加一个鸿蒙端反而是同等待遇。
- 生态差距:ArkTS虽然发展快,但第三方库生态和npm几十万个包没法比。RN上很多能力都有现成库,ArkTS经常要自己从零写。
我自己的选择标准是:核心业务越复杂、越依赖现有RN生态,越建议走RN适配路线;如果只是简单的工具类App、又特别要求鸿蒙原生体验细节,那ArkTS可能更合适。
1.3 一个面包屑组件能带来什么
选面包屑导航做入门,是因为它把跨端适配的典型问题全串起来了:
- 面包屑要显示"当前路径",意味着你得有一个全局的路由状态,这涉及状态管理设计;
- 用户点击面包屑要能跳回任意层级,涉及页面跳转的编程式导航;
- 鸿蒙的手势返回和系统返回键要让面包屑同步,涉及原生事件监听;
- App被冷启动或深链接直达时,面包屑要能还原路径,涉及初始化逻辑。
一个小组件,逼你把整个RN跨平台开发的闭环走一遍。所以我下面先讲环境,再讲组件实现,最后是调试经验,这个顺序就是我自己实际开发时的顺序。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建:从空目录到RN页面出现在鸿蒙设备上
2.1 需要准备的东西清单
这一步很重要,很多人在网上找了一堆教程但版本对不上,最后卡在编译阶段。以我当时可用的稳定组合为例,列个清单:
| 工具 | 推荐版本 | 说明 |
|---|---|---|
| Node.js | 18 或 20 LTS | 新版RN要求较新Node,太老会装不上依赖 |
| DevEco Studio | 5.0 及以上 | 鸿蒙官方的IDE,用于编译原生壳 |
| react-native | 0.72 或 0.73 | 社区适配层一般跟随这个版本线 |
| @react-native-oh/react-native | 与RN版本匹配 | OpenHarmony的RN适配包 |
| Git | 任意 | 拉取示例工程用 |
| HDC工具 | 随DevEco附带 | 鸿蒙设备调试工具,类似adb |
核心原则:RN版本和鸿蒙适配版本必须严格对应。我见过有人直接 npm install 最新版RN,结果鸿蒙侧还不支持,报一堆C++编译错。靠谱的做法是去 @react-native-oh/react-native 的发布页看它官方支持哪个RN版本,按那个来。
2.2 创建鸿蒙外壳工程
RN本身不直接支持 react-native run-harmony 这样的命令(至少在社区适配的早期阶段不行),你得手动创建鸿蒙壳工程。我当时用的最稳的路径是:
- 先用常规方式初始化一个RN工程:
bash复制npx react-native init RNHarmonyDemo --version 0.73.2
cd RNHarmonyDemo
-
把OpenHarmony的RN示例作为外壳参考,把
harmony目录(或者叫entry的鸿蒙模块目录)复制到自己工程里。这一步相当于给RN套一个"鸿蒙容器壳"。 -
在项目根目录安装鸿蒙适配包,并同步配置RN核心依赖:
bash复制npm install @react-native-oh/react-native
npm install react@18.2.0 react-native@0.73.2
需要注意,有些鸿蒙适配会提供 template 方式自动初始化,你可以直接查对应版本仓库里的 README,如果支持就用官方CLI,避免手动复制配错文件。
2.3 DevEco Studio里的关键配置
外壳弄好之后,用DevEco Studio把鸿蒙工程目录打开,需要检查几个地方:
- 模块依赖:确保
oh-package.json5里引用了@react-native-oh/react-native对应的har包。 - JS bundle路径:让鸿蒙原生入口知道去哪加载JS包。开发阶段一般指向Metro服务,比如
http://localhost:8081/index.bundle?platform=harmony;发布阶段指向本地打包好的bundle.harmony.js。 - 签名配置:真机调试必须配置签名,DevEco里登录华为账号自动签名就行,模拟器可以跳过。
这些配置项的准确位置在不同DevEco版本里会变,但核心逻辑不变:RN页面最终是被一个ArkTS组件(通常叫 RNComponent 或类似名字)加载的,你要确保这个组件的创建参数里有正确的bundle地址。
2.4 跑通最简页面的验证方法
我习惯用一个最简验证方式:在 App.tsx 里只放一个 <Text>Hello Harmony</Text>,然后:
- 终端启动Metro:
bash复制npm start
-
用HDC连接鸿蒙设备,在DevEco里点击Run按钮。
-
设备上如果能看到这个文本,说明RN和鸿蒙的桥接是通的,后面所有工作才能成立。
如果看到的是白屏或者直接崩溃,先别急着查业务代码,大概率是bundle地址配错、Metro没启动、或者HDC没连上设备。这三个问题我在后面单独开一章讲。
3. 面包屑导航的需求拆解:它比看起来复杂一点
3.1 面包屑的本质是"路径的视觉映射"
面包屑导航看起来只是一排文字加分隔符,但它背后要回答三个问题:
- 用户现在在哪:需要维护一个当前路径栈;
- 用户怎么到这里的:记录跳转历史;
- 用户能否快速回到任意一层:让每个非当前层级都能点击跳转。
这三个问题在Web开发里都很简单,因为浏览器内置了history对象。但在RN里,你既没有 window.history,也没有DOM的层级结构,尤其到了鸿蒙平台,连RN Android上常用的 BackHandler 都不一定完全一样。所以必须自己在JS层实现一套"路径栈"。
3.2 为什么照搬Web版面包屑组件会翻车
我在网上找过很多现成的面包屑组件,大部分是给React Web用的,依赖 react-router 或者 window.location。搬到RN环境时两个致命问题:
- 路由来源不同:RN里有各式导航方案,有人用
react-navigation,有人用自研的页面状态机,有人直接用Modal切换。面包屑组件如果写死了某个路由库的API,换个项目就废了。 - 没有DOM事件:Web版点击是
<a>标签跳转,RN里要自己封装Touchable并调用具体导航方法。
所以这次我决定不依赖任何路由库,而是定义一套极简路由协议,面包屑只关心"路径栈"这个抽象数据结构。页面跳转谁来实现都行,只要往里push、pop数据,面包屑自然跟着变。这样不管是 react-navigation 还是自研方案,都能接上。
3.3 设计面包屑的数据模型
路径栈的数据结构我设计成:
typescript复制export type BreadcrumbItem = {
/** 页面显示名,比如"设置" */
name: string;
/** 页面唯一路由标识,比如"settings/profile" */
path: string;
/** 附加参数,方便点击跳转时带原始参数 */
params?: Record<string, any>;
};
export type BreadcrumbState = {
/** 路径栈 */
stack: BreadcrumbItem[];
/** 当前所在层级的索引,一般就是栈顶 */
activeIndex: number;
};
用数组天然表达层级关系:栈底是首页,栈顶是当前页。面包屑组件的任务就是把 stack 渲染成一行可点击的文字。这个模型不关心你的页面是用的Fragment、Modal还是原生容器,它只记录"逻辑路径"。
边界情况也需要在设计阶段定好:
- 首页不显示面包屑:栈长度小于2时整组隐藏;
- 点击中间层级:点击第N层,应该把N之后的所有层级清空并跳转,而不是开一个平行页面;
- 层级过长:超过4层时中间部分折叠成"...",避免UI被挤爆。
4. 核心实现:一个不依赖路由库的面包屑组件
4.1 用Context + useReducer实现路由状态机
组件要保持"单一数据源",我用React的Context把路径栈做成全局状态。用 useReducer 是因为后续扩展动作多(push、pop、jump、reset),回调写法更可控。
typescript复制import React, { createContext, useReducer, useContext, ReactNode } from 'react';
import { BreadcrumbItem, BreadcrumbState } from './types';
type Action =
| { type: 'PUSH'; payload: BreadcrumbItem }
| { type: 'POP' }
| { type: 'JUMP'; payload: number }
| { type: 'RESET'; payload: BreadcrumbItem[] };
const initialState: BreadcrumbState = { stack: [], activeIndex: -1 };
function breadcrumbReducer(state: BreadcrumbState, action: Action): BreadcrumbState {
switch (action.type) {
case 'PUSH': {
// 如果目标path已存在,则视为跳回,不做重复叠加
const existingIndex = state.stack.findIndex(item => item.path === action.payload.path);
if (existingIndex >= 0) {
return { stack: state.stack.slice(0, existingIndex + 1), activeIndex: existingIndex };
}
const nextStack = [...state.stack, action.payload];
return { stack: nextStack, activeIndex: nextStack.length - 1 };
}
case 'POP': {
if (state.stack.length <= 1) return state;
const nextStack = state.stack.slice(0, -1);
return { stack: nextStack, activeIndex: nextStack.length - 1 };
}
case 'JUMP': {
const index = Math.min(action.payload, state.stack.length - 1);
return { stack: state.stack.slice(0, index + 1), activeIndex: index };
}
case 'RESET': {
return { stack: action.payload, activeIndex: action.payload.length - 1 };
}
default:
return state;
}
}
const BreadcrumbContext = createContext({ state: initialState, dispatch: () => {} });
export const BreadcrumbProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(breadcrumbReducer, initialState);
return (
<BreadcrumbContext.Provider value={{ state, dispatch }}>
{children}
</BreadcrumbContext.Provider>
);
};
export const useBreadcrumb = () => useContext(BreadcrumbContext);
这段代码里PUSH动作有个细节:如果目标路径已经存在,不是简单追加,而是跳到那个已有层级并截断。这个策略和大多数App的"首页->列表->详情"返回逻辑一致,避免用户反复进入同一个详情页导致面包屑无限膨胀。
4.2 面包屑UI组件本体
状态有了,渲染就只是单纯的UI转换。我把组件设计成纯展示组件,只有 state 没有业务逻辑,方便后面做单元测试或替换成深色主题。
tsx复制import React, { Fragment } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useBreadcrumb } from './BreadcrumbContext';
import { BreadcrumbItem } from './types';
const MAX_VISIBLE_COUNT = 4;
export function Breadcrumb() {
const { state, dispatch } = useBreadcrumb();
const { stack, activeIndex } = state;
if (stack.length < 2) return null;
// 层级过多时折叠中间部分
let visibleItems: Array<BreadcrumbItem | 'ellipsis'> = [...stack];
if (stack.length > MAX_VISIBLE_COUNT) {
const head = stack.slice(0, 1);
const tail = stack.slice(stack.length - 2);
visibleItems = [...head, 'ellipsis' as const, ...tail];
}
const jumpTo = (index: number) => {
if (index === activeIndex) return;
dispatch({ type: 'JUMP', payload: index });
// 这里由页面方监听activeIndex变化,执行实际跳转
};
return (
<View style={styles.container}>
{visibleItems.map((item, idx) => {
if (item === 'ellipsis') {
return (
<Fragment key={`ellipsis-${idx}`}>
<Text style={styles.separator}> / </Text>
<Text style={styles.ellipsis}>...</Text>
<Text style={styles.separator}> / </Text>
</Fragment>
);
}
const stackIndex = stack.indexOf(item);
const isActive = stackIndex === activeIndex;
const isFirst = idx === 0;
return (
<Fragment key={item.path}>
{!isFirst && <Text style={styles.separator}> / </Text>}
{isActive || item.path === '' ? (
<Text style={[styles.label, styles.activeLabel]}>{item.name}</Text>
) : (
<TouchableOpacity activeOpacity={0.6} onPress={() => jumpTo(stackIndex)}>
<Text style={[styles.label, styles.linkLabel]}>{item.name}</Text>
</TouchableOpacity>
)}
</Fragment>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: '#f7f7f7',
},
separator: {
color: '#999',
marginHorizontal: 4,
fontSize: 14,
},
label: {
fontSize: 14,
color: '#333',
},
linkLabel: {
color: '#3478f6',
},
activeLabel: {
fontWeight: '500',
color: '#111',
},
ellipsis: {
color: '#999',
fontSize: 14,
},
});
折叠逻辑我专门写了:保留首页和最近两层的完整文案,中间用省略号代替。这个策略参考了网站后台管理系统的成熟做法,层级太深时全展示反而失去导航意义。
4.3 页面接入和导航联动
组件本身只负责展示,真正让"点击面包屑能跳转"的动作,还需要一层监听。我的做法是:页面方订阅 activeIndex 变化,然后执行自己的导航逻辑。
举个例子,如果项目用的是 react-navigation:
tsx复制import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
function useBreadcrumbNavigation() {
const navigation = useNavigation();
const { state, dispatch } = useBreadcrumb();
useEffect(() => {
const currentItem = state.stack[state.activeIndex];
if (!currentItem) return;
// 这里根据currentItem.path跳转到对应页面
navigation.navigate(currentItem.path as never, currentItem.params as never);
}, [state.activeIndex]);
return { state, dispatch };
}
如果用的是自研路由,比如一个简单的Modal栈,同样可以在 useEffect 里做对应处理。重点只有一个:页面跳转逻辑放页面侧,面包屑组件只管数据展示。这样两个模块可以独立演进,也方便测试。
初始路径怎么塞进去?App启动时根据配置的初始页面push一次:
tsx复制useEffect(() => {
dispatch({ type: 'PUSH', payload: { name: '首页', path: 'home' } });
}, []);
深链接直达时则用RESET:
tsx复制dispatch({
type: 'RESET',
payload: [
{ name: '首页', path: 'home' },
{ name: '商品列表', path: 'product-list' },
{ name: '商品详情', path: 'product/123' },
],
});
这个接口设计得足够通用,后面接深链接、接元服务卡片,都不需要改组件内部。
4.4 处理鸿蒙系统返回键和手势返回
纯JS的面包屑在页面正常跳转时没问题,但用户按系统返回键或手势返回时,路径栈可能没有同步。RN里Android有 BackHandler,鸿蒙适配层需要确认对应的监听方式,常见做法是复用RN的 BackHandler API,或者通过原生事件通道监听。
我当时的做法是在入口组件里注册返回拦截:
tsx复制useEffect(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
const { state, dispatch } = breadcrumbRef.current;
if (state.stack.length > 1) {
dispatch({ type: 'POP' });
return true; // 拦截默认返回
}
return false; // 栈底交给系统处理
});
return () => sub.remove();
}, []);
同步的关键是:无论用户通过什么方式返回,都必须走同一个POP逻辑。否则就会出现"页面已经返回了,但面包屑还停留在深一层"的状态错乱。这类问题在开发阶段不容易发现,测试时一定要多按几次系统返回键。
5. 真机调试高频坑:启动白屏、设备连接、样式差异
5.1 启动白屏的定位思路
RN跑鸿蒙,"启动白屏"绝对是出现频率第一的问题。我当时遇到时查了一圈资料,发现原因五花八门,但整体定位思路是一致的:先确认JS bundle到底有没有被设备加载到。
排查步骤:
- 看Metro终端有没有输出bundle请求日志。如果没有任何请求,说明设备根本没连上Metro,问题在网络配置或bundle地址。
- 如果能请求到bundle但还是白屏,打开DevEco的Logcat面板,看JS侧有没有报错。最常见的是一些原生模块在鸿蒙上缺失,导致初始化抛异常。
- 白屏但没有任何日志,很可能是启动时机问题——RN页面组件在调用原生模块时,桥接还没初始化完成。
我犯过最蠢的一个错:把Metro的bundle地址写成了 10.0.2.2:8081,那是给Android模拟器用的,鸿蒙真机上应该用 localhost:8081 配合 hdc rport 反向端口映射,或者直接写电脑的局域网IP。这个配置改对之后,白屏问题立刻少了一半。
5.2 HDC设备识别和处理
鸿蒙真机调试用的是HDC工具,不是adb。常见问题包括:
hdc list targets看不到设备。先确认USB调试有没有在开发者选项中打开,以及插上设备后手机弹出的授权弹窗有没有点"允许"。这个授权弹窗有时候会一闪而过,重新插拔一次就能让它再出来。- 看到设备但状态是 offline。通常是HDC版本和设备系统版本不匹配,升级DevEco自带的HDC组件。
- 想要远程调试时,用命令建立端口转发:
bash复制hdc tapport 8081 8081
这条命令把设备的8081端口转发到电脑,Metro才能被设备访问。很多开发者卡在这一步,因为RN文档里写的是adb的反向转发,用hdc时命令名和参数都不一样。
5.3 样式渲染差异
同样的RN代码,Android和鸿蒙上渲染出的样式会有细微差异。我自己踩过的坑:
- 字体加粗:
fontWeight: 'bold'在鸿蒙上某些版本渲染效果偏细,改用fontWeight: '700'更稳定。 - 圆角溢出:
overflow: 'hidden'+borderRadius组合在鸿蒙上偶尔不生效,图片会溢出圆角。解决办法是外面再包一层同圆角的View。 - SafeArea:鸿蒙的刘海屏/挖孔屏安全区API和Android不完全一致,建议使用
react-native-safe-area-context的最新版本,它对鸿蒙做了适配。 - 阴影:
elevation在Android上有效,鸿蒙上不一定,用boxShadow或直接上背景色叠加。
面包屑这个组件虽然简单,但分隔符的间距、字体大小在不同平台上肉眼可见地不一样。我最终的做法是:样式上不追求像素级一致,只保证结构和阅读顺序一致。跨端开发里,视觉近似往往比视觉一致更现实也更好维护。
5.4 元服务与普通App的差异意识
我看到后台不少热搜词都在问"鸿蒙元服务"和"鸿蒙App开发"的区别。简单提一句:元服务是鸿蒙上的一种轻量免安装形态,如果你打算把RN页面跑在元服务里,加载方式和生命周期会有额外限制。面包屑导航这种依赖全局状态栈的组件,在元服务里要额外处理好"退出到桌面再回来"之后的状态,因为元服务可能被系统随时销毁重建。稳妥的方案是把路径栈持久化到本地存储,启动时先读取恢复。这个做法在下面一节展开。
6. 向前一步:把面包屑做成可复用的跨端组件
6.1 支持深链接和"直达路径"映射
外卖App从分享链接直接打开一个商品详情页时,用户没有经过首页和列表页,面包屑却仍然可以显示"首页 / 美食 / 店铺 / 商品",这就是路径映射的作用。
实现方式:维护一张路由映射表,把path对应到面包屑显示名和父级关系。
typescript复制// routeMap.ts
export const ROUTE_MAP = {
'home': { name: '首页', parent: null },
'product-list': { name: '商品列表', parent: 'home' },
'product': { name: '商品详情', parent: 'product-list' },
};
export function buildBreadcrumbByPath(path: string, params?: Record<string, any>) {
const items: BreadcrumbItem[] = [];
let currentPath: string | null = path;
while (currentPath && ROUTE_MAP[currentPath]) {
const route = ROUTE_MAP[currentPath];
items.unshift({ name: route.name, path: currentPath, params });
currentPath = route.parent;
}
return items;
}
有了这张映射表,深链接打开时一句 RESET 就能恢复完整面包屑,不需要用户手动重建路径。这也是我在真实项目里对面包屑组件做得最有价值的一个扩展。
6.2 路径栈持久化:让用户切后台回来不迷路
鸿蒙系统对后台App的管理比较激进,进程被杀了再回到前台,JS状态全部丢失。如果不想让用户看到"面包屑消失了"的突兀感,把路径栈存到本地存储是关键。
我用的方案是 @react-native-async-storage/async-storage,在每次PUSH/POP/JUMP之后把当前stack序列化存储:
typescript复制function persistState(state: BreadcrumbState) {
AsyncStorage.setItem('@breadcrumb_stack', JSON.stringify(state.stack));
}
// 在Provider里用useEffect监听state变化
useEffect(() => {
if (state.stack.length > 0) {
persistState(state);
}
}, [state]);
App启动时异步读出来恢复:
typescript复制AsyncStorage.getItem('@breadcrumb_stack').then(raw => {
if (raw) {
dispatch({ type: 'RESET', payload: JSON.parse(raw) });
}
});
需要注意恢复时机:要在页面导航容器准备好之后再RESET,否则页面跳转会报错。配合深链接的 buildBreadcrumbByPath,冷启动的路径栈基本都是正确的。
6.3 折叠屏和平板的适配策略
到了鸿蒙跨平台,设备形态对面包屑的影响主要体现在两个方向:
- 横向空间充足(平板、折叠屏展开态):面包屑可以完整展示所有层级,甚至可以额外显示面包屑右侧的"当前页面操作按钮"(比如"刷新""分享"),反正空间多。
- 横向空间紧凑(手机):折叠策略生效,超过4层折叠中间内容。
我实现时给组件加了一个 maxVisibleCount 属性,默认4,在平板/折叠屏上通过 useWindowDimensions 动态判断:
tsx复制const { width } = useWindowDimensions();
const maxVisibleCount = width >= 600 ? 8 : 4;
这样组件在不同设备上自适应,代码量增加不到10行,体验提升却很明显。别忘了同时调整容器的高度和字号,大屏上14号字会显得过于小气。
6.4 组件库化的最后一步:支持受控模式
如果你打算把这个面包屑组件放到多个项目里复用,最好支持受控模式:外部传入 state 和 onJump,组件内部只渲染。我加了个可选的 mode 参数:
'controlled':stack和activeIndex由外部传入,点击跳转通过onJump回调通知外部;'uncontrolled':内部使用Provider管理状态,适合快速接入。
其实多加这个模式的意义在于:某些项目已经有自己的全局状态库(比如Redux/Zustand),再套一层Context反而多此一举。受控模式下,组件纯粹是"路径栈的UI渲染器",和业务彻底解耦。
这一点也对应了整篇的核心思路——跨端开发里,组件越不依赖具体平台能力,迁移成本就越低。面包屑组件从设计一开始就没碰任何鸿蒙专属API,所以从Android到鸿蒙,只改了外壳配置,组件代码一行没动。
最后再分享一个实用小技巧:调试面包屑组件时,可以在DevTools里手动调dispatch,模拟各种边界情况。比如直接从4层跳回第1层,或者连续快速push三个页面,看折叠逻辑和返回键同步是否正确。自动化测试可以针对reducer写纯函数用例,因为Reducer里没有依赖任何平台API,你甚至可以在Node环境里直接跑jest验证跳转逻辑。这类"纯JS可测"的组件,在跨端工程里价值会越来越高,因为不同平台的差异都留在了壳层,核心逻辑反而比以往更好维护了。
