React Native 在鸿蒙上跑起来之后,第一件事不是写业务,而是把错误处理想清楚。我之前那篇聊过 RN 鸿蒙版的环境搭建和启动流程,今天这篇专门说说 Redux 中间件层怎么做错误处理,以及在这过程中踩到的一些鸿蒙特有的坑。
先说结论:在 React Native 鸿蒙版项目里,Redux 中间件不只是分发 action 的管道,更是整个 App 错误信息的汇聚点和恢复策略的执行层。把错误处理收敛到中间件里做,比在每个页面 try-catch 要干净得多,也比在 reducer 里处理副作用要安全得多。
1. 为什么需要在中间件层做错误处理
1.1 React Native 鸿蒙版的特殊背景
React Native 鸿蒙版跑的是 OpenHarmony 的 JS/Native 混合环境,它的异常传播链路和 iOS/Android 上不太一样。客户端代码经过 ArkTS 运行时和 RN 的 C++ 层交互,中间隔着一层桥接,出了问题往往不只是 JS 层的报错,还可能伴随 native 层的异常状态。
在这样的环境下,错误处理如果散落在各个业务页面里,会出现几个问题:
- 错误信息碎片化,难以统一聚合上报
- UI 层和错误恢复逻辑耦合,页面代码变得臃肿
- 遇到 native 层的异常时,JS 层拿到的错误信息已经丢失了上下文
把错误处理放到 Redux 中间件层,相当于给所有 action 加了一个统一的“安检口”,既能拦截异常,又能在这个位置做聚合并触发恢复逻辑。
1.2 中间件方案的选型考量
Redux 中间件里做错误处理,主流方案有三个:自己写一个 middleware、用 redux-logger 扩展、或者集成 redux-saga 的错误通道。我选了自研中间件的方式,原因很简单——鸿蒙版的 RN 生态还比较新,第三方中间件对鸿蒙的兼容性参差不齐,而且我们需要针对鸿蒙特有的场景做定制处理,自研可控性最强。
自研中间件最核心的价值在于:它可以把“错误捕获、错误分类、状态重置、用户通知”整合成一条流水线,而且这条流水线对业务代码完全透明。业务侧只需要像平常一样 dispatch action,完全不用关心错误逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 中间件错误处理的核心设计
2.1 整体架构:三层拦截模型
我设计的中间件错误处理架构分成三层:捕获层、分类层、恢复层。
捕获层的职责是拦截 action 流里的异常。dispatch 一个 action 后,如果 reducer 执行时抛错,或者异步 action 里有未捕获的 rejection,都会在这里被截获。
分类层根据错误特征决定处理策略。比如网络超时、渲染异常、设备能力缺失,这三类错误后续的处理方式完全不同。网络超时可能需要提示用户重试,渲染异常可能需要重置整个页面状态,设备能力缺失则需要引导用户升级版本或者走兼容逻辑。
恢复层执行具体的恢复动作,比如 dispatch 一个 reset action、跳转错误页、弹 toast 提示,并同步把错误信息上报到归集服务。
2.2 中间件的代码骨架
javascript复制import { Middleware, MiddlewareAPI, Dispatch, AnyAction } from 'redux';
import { ErrorClassifier, ErrorRecoveryStrategy } from './errorTools';
interface ErrorHandlingOptions {
onError?: (error: Error, action: AnyAction) => void;
recoveryStrategies?: Record<string, ErrorRecoveryStrategy>;
enableLogging?: boolean;
}
export function createErrorHandlingMiddleware(options: ErrorHandlingOptions = {}) {
const { onError, recoveryStrategies = {}, enableLogging = true } = options;
const classifier = new ErrorClassifier();
return (store: MiddlewareAPI) => (next: Dispatch) => (action: AnyAction) => {
try {
const result = next(action);
if (result instanceof Promise) {
return result.catch((error) => {
handleError(store, error, action, classifier, recoveryStrategies, onError, enableLogging);
return Promise.reject(error);
});
}
return result;
} catch (error) {
handleError(store, error as Error, action, classifier, recoveryStrategies, onError, enableLogging);
throw error;
}
};
}
function handleError(
store: MiddlewareAPI,
error: Error,
action: AnyAction,
classifier: ErrorClassifier,
recoveryStrategies: Record<string, ErrorRecoveryStrategy>,
onError?: (error: Error, action: AnyAction) => void,
enableLogging?: boolean
) {
const errorType = classifier.classify(error);
if (enableLogging) {
console.error(`[REDUX_ERROR] type=${errorType} action=${action.type}`, error);
}
if (recoveryStrategies[errorType]) {
recoveryStrategies[errorType](store, error, action);
}
onError?.(error, action);
}
这里有个细节值得说明:next(action) 返回的如果是一个 Promise(对应异步 action),需要在 Promise 链上追加 catch,而不是只在同步代码里包 try-catch。因为异步 action 的错误是异步抛出的,同步的 try-catch 根本接不住。
2.3 链路上 catch 还是 dispatch 里 catch
很多项目把错误处理放在 dispatch 之后:
javascript复制dispatch(fetchData()).catch(err => handleError(err));
这种方式的问题在于,每个页面都要重复写 catch 逻辑,而且业务代码里混入了错误处理逻辑。中间件的方式把 catch 收敛到了一个地方,但要注意一个关键点:中间件里 catch 之后,并不表示原始错误被吞掉了。如果业务侧也需要感知错误,中间件应该把错误继续抛出去,或者通过 action 把错误状态更新到 store 里,让 UI 层通过 useSelector 去读取错误状态。
推荐的做法是双通道:中间件捕获错误后,dispatch 一个 error action 更新 store 里的错误状态,同时业务侧如果关心错误,仍可在自己的 async action 里 catch。中间件不拦截错误传播,只做聚合和恢复。
3. 鸿蒙适配中的实操过程与关键问题
3.1 ArkTS 与 JS 的异常信息差异处理
鸿蒙环境下的一个实际问题是:来自 ArkTS native 层的异常,抛到 JS 层时,错误信息往往不是标准的 Error 对象,message 字段里可能带有 native 层的日志碎片。我第一次遇到时,错误信息长这样:
code复制Error: { code: 16000011, message: "The specified module name is invalid." }
这里 16000011 是 ArkTS 的模块错误码,不带任何业务上下文。如果我们直接把这种原始错误抛给业务侧,业务侧完全无法判断该怎么处理。
解决思路是增加一个错误上下文标注环节。中间件捕获到错误后,先判断错误来源是否带有 native 特征码,如果有,就额外把当时 action 的类型、发起页面、跳转参数等上下文信息拼接进去,再交给分类层处理。
javascript复制function enrichNativeErrorContext(error: Error, action: AnyAction): Error {
if (error.message && /code: \d+/.test(error.message)) {
const enriched = new Error(`[Native]${error.message} | context: action=${action.type}, ts=${Date.now()}`);
enriched.stack = error.stack;
return enriched;
}
return error;
}
这样处理之后,后续排查问题时能拿到更完整的现场信息,而不是面对一行光秃秃的 native 报错。
3.2 错误分类器的设计与实现
错误分类器是整个中间件的核心决策模块。我做了一个基于错误特征矩阵的分类器,而不是简单地根据 error.name 来做判断,原因是鸿蒙环境里错误来源太多样了。
javascript复制export class ErrorClassifier {
private rules: Array<{ type: ErrorType; match: (err: Error) => boolean }>;
constructor() {
this.rules = [
{ type: 'network_timeout', match: (err) => err.name === 'TimeoutError' || /timeout|timed out/i.test(err.message) },
{ type: 'network_offline', match: (err) => /Network request failed|no network|offline/i.test(err.message) },
{ type: 'render_interrupted', match: (err) => /Unable to find node on an old renderer|invariant violation/i.test(err.message) },
{ type: 'native_module_missing', match: (err) => /code: 16000011|module name is invalid/i.test(err.message) },
{ type: 'api_capability', match: (err) => /not support|unsupported capability/i.test(err.message) },
{ type: 'unknown', match: () => true },
];
}
classify(error: Error): ErrorType {
for (const rule of this.rules) {
if (rule.match(error)) {
return rule.type;
}
}
return 'unknown';
}
}
分类规则里有几条需要根据实际业务持续补充。比如鸿蒙特有的 native_module_missing,这个类型在 Android/iOS 上很少见,但在鸿蒙端因为部分原生模块尚未完全适配 RN,非常容易出现。
3.3 恢复策略:针对鸿蒙场景定制
恢复策略是错误处理动作的执行主体。我针对不同的错误类型配置了不同的恢复动作:
javascript复制const recoveryStrategies = {
network_timeout: (store, error) => {
store.dispatch({ type: 'UI_SHOW_TOAST', payload: { message: '网络请求超时,请稍后重试', duration: 2000 } });
},
network_offline: (store, error) => {
store.dispatch({ type: 'NETWORK_STATUS_UPDATE', payload: { offline: true } });
},
render_interrupted: (store, error) => {
store.dispatch({ type: 'APP_RESET_TO_HOME' });
},
native_module_missing: (store, error) => {
const fallbackModule = getFallbackForError(error);
store.dispatch({ type: 'MODULE_FALLBACK_ACTIVATE', payload: { fallback: fallbackModule } });
},
api_capability: (store, error) => {
store.dispatch({ type: 'CAPABILITY_CHECK_REQUIRED' });
},
unknown: (store, error) => {
store.dispatch({ type: 'ERROR_DIALOG_OPEN', payload: { error } });
},
};
特别说明一下 render_interrupted 类型的策略。RN 在鸿蒙上启动阶段偶发页面渲染中断,如果直接跳转错误页或者杀进程,体验很不好。我的做法是 dispatch 一个 APP_RESET_TO_HOME,让整个 App 回到首页但不冷启动。实测下来,这种恢复方式对用户更友好,不会出现黑屏等待时间。
3.4 启动白屏问题与中间件的关联
热搜词里反复出现“react native 启动白屏”,这个和中间件错误处理有什么关系?其实是有的。
RN 鸿蒙版启动白屏有两个常见原因:一个是首屏 JS bundle 加载失败或解析异常,另一个是 root component 渲染前就有未捕获的异常导致 UI 层直接中断。前者通常靠加载控制解决,后者恰好可以在中间件层面拦截。
我在中间件里加了一个特殊处理:如果应用尚处于启动阶段,也就是 root component 还没有标记渲染完成,此时捕获到 render 相关异常,先不执行页面级恢复,而是走全局兜底逻辑,尝试重新回到启动阶段的初始路由。
javascript复制function isAppBootstrapping(store: MiddlewareAPI): boolean {
const state = store.getState();
return state.app.status === 'bootstrapping';
}
// 在 render_interrupted 策略里增加判断
render_interrupted: (store, error) => {
if (isAppBootstrapping(store)) {
store.dispatch({ type: 'BOOTSTRAP_RETRY_WITH_CLEAN_CACHE' });
} else {
store.dispatch({ type: 'APP_RESET_TO_HOME' });
}
}
3.5 中间件的注册与加载顺序
Redux 中间件的注册顺序是有讲究的,不同顺序会影响错误处理的实际行为。我的注册顺序是:
javascript复制import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { createErrorHandlingMiddleware } from './middleware/errorHandling';
import { logger } from './middleware/logger';
const errorHandling = createErrorHandlingMiddleware({
recoveryStrategies,
onError: reportErrorToServer,
enableLogging: true,
});
const middleware = [thunk, errorHandling, logger];
const store = createStore(rootReducer, compose(applyMiddleware(...middleware)));
这里把 errorHandling 放在 thunk 之后、logger 之前是有意的。放在 thunk 之后,是因为 thunk 会把异步 action 变成函数调用,错误处理中间件需要拦截的是执行完业务逻辑之后的 action 流,包括异步完成后的 action。如果放在 thunk 前面,thunk 本身返回的 Promise 错误就接不到了。
放 logger 之前,是为了让日志中间件也能记录到错误处理中间件 dispatch 出来的恢复 action。如果 logger 在前面,catch 到的错误在日志里展示的顺序会和实际发生顺序不一致,排查问题时容易混淆。
4. 常见问题与排查技巧实录
4.1 表:鸿蒙下 Redux 中间件错误处理的典型问题
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| 错误被中间件吞掉,业务侧 all 状态不一致 | 中间件 catch 后未重新抛出,也没有通过 action 更新 store | 中间件 catch 后无论如何要 dispatch error action,业务侧通过状态感知错误 |
| native 报错信息缺失上下文 | ArkTS 层异常只带错误码 | 错误进入中间件后先做 enrich 处理,拼接 action 信息和时间戳 |
| 启动阶段 render 异常导致白屏 | root component 渲染中断无兜底 | 中间件识别 bootstrap 阶段,执行清理缓存重试逻辑 |
| 异步 action 的 rejection 未进入 catch | 中间件只做了同步 try-catch | 对 next(action) 返回的 Promise 追加 catch 处理 |
| 页面级重复上报错误 | 每个页面各自上报错误,中间件也上报 | 统一由中间件上报,业务侧不再单独上报,日志量减半 |
4.2 错误上报的幂等处理
中间件里做错误上报,要非常注意幂等。同一类错误可能在短时间内反复触发,比如网络抖动导致连续超时,如果每次超时都上报一条完整日志,服务端日志量会爆炸。
我在上报环节做了节流处理:
javascript复制const reportQueue = new Map();
const REPORT_THROTTLE_MS = 30000;
function reportErrorToServer(error, action) {
const key = `${error.name}:${action.type}`;
const lastReportTime = reportQueue.get(key) || 0;
const now = Date.now();
if (now - lastReportTime > REPORT_THROTTLE_MS) {
// 实际发送到日志归集服务,这里用 console 示意
console.log(`[REPORT] ${key} at ${now}`);
reportQueue.set(key, now);
}
}
用 action.type 加 error.name 做聚合键,30 秒内同类型错误只上报一次,这个阈值可以根据线上流量调整。流量大的话可以缩到 10 秒,追求高保真日志的话可以拉到 60 秒。
4.3 真机调试中的错误排查顺序
在鸿蒙真机上调试中间件错误处理时,我总结了一个排查顺序,比一头扎进代码里查要高效很多:
先看 HiLog 里有没有 native 层的异常,再看中间件的 console.error 日志里有没有对应的 action 类型,最后看 store 的 state 变化是否如预期。这个顺序的逻辑是:自底向上排查,从最底层最容易定位问题的 native 日志开始,逐步向上层推进。
有几次我为了找一个启动阶段的渲染崩溃,直接在 JS 层调试了半天,最后发现原因是某个原生模块在鸿蒙上根本没注册,是 native 层的模块缺失问题,在 HiLog 里一眼就能看到。所以遇到启动崩溃、白屏这类问题,先开 HiLog 过滤 ReactNative 标签,把 native 层的线索收集完再回 JS 层排查。
4.4 避免在 reducer 里做错误处理
这个是我特别想强调的一点。有些同学会在 reducer 里直接尝试修复错误状态,比如在 reducer 里 catch 错误并修改其他字段。这违反了 reducer 的纯函数原则,容易导致 state 更新不可预测。
错误处理中间件解决的问题之一,正是把这种副作用从 reducer 里剥离出来。reducer 只负责根据 action 生成新的 state,错误本身是一种事件,不应该在 reducer 里被捕获处理。中间件层专门做这件事,既保证了 reducer 的纯净,也让错误处理逻辑可以统一维护。
5. 这个方案还能怎么扩展
5.1 接入鸿蒙的分布式能力做跨设备错误同步
鸿蒙的一个特性是分布式设备协同。中间件捕获到错误后,可以做多设备的状态同步。比如手机和平板协同工作时,手机上发生网络错误导致状态异常,中间件捕获后可以通过鸿蒙的分布式数据管理能力,把重置指令同步到平板端,让两端状态保持一致。
这块我已经在做初步的原型验证,中间件层面加一个分布式同步的策略回调即可,代码改动量不大,但价值比较明显。
5.2 从错误处理升级到健康度监控
中间件不仅能处理错误,还能承担一部分应用健康度监控的职责。我在中间件里额外统计了每个 action 的执行耗时和失败率,定时把统计数据上报。这样不仅在出错时能止损,还在出错前就能看到趋势变化。比如某个 action 的失败率在几分钟内从 1% 涨到 10%,说明灰度发布可能有回归问题,就能提前介入。
javascript复制const actionMetrics = new Map();
function trackActionMetrics(action, duration, isError) {
const metrics = actionMetrics.get(action.type) || { count: 0, errorCount: 0, totalDuration: 0 };
metrics.count += 1;
metrics.totalDuration += duration;
if (isError) metrics.errorCount += 1;
actionMetrics.set(action.type, metrics);
}
统计数据定时上报后,还能在后台做一个看板,实时展示各个 action 的健康状态。整条链路做下来,相当于给 App 加了一个专属的监控大脑。
我个人在实际操作中有一个体会特别深:中间件错误处理的方案一定要在项目早期就接入,而不是等业务跑起来之后再加。早期接入的话,所有异步 action 和业务 dispatch 都会自然地走中间件通道,错误处理从一开始就是体系化的。等到业务代码积累到一定量级再补,就需要排查很多历史代码,改造成本会高出几倍。如果项目已经有一些业务代码了,也不用太担心,先接入中间件,把新的业务 action 全部纳入,老的页面边改造边迁移,几个迭代之后就能统一了。
最后再分享一个小技巧:中间件里的错误处理逻辑写完,一定要用单元测试把每个错误类型都覆盖一遍。鸿蒙的 RN 环境相对特殊,很多问题分析起来费时费力,单测能在开发阶段就提前暴露大部分异常,上线之后踩坑的概率会小很多。我习惯用真实的 action 调用链在测试环境跑一遍各个错误分支,确保恢复策略都能按预期执行。
