1. 项目概述:React Native鸿蒙版货币格式化Hook
在跨平台应用开发中,货币格式化是个高频需求但容易被忽视的细节。最近在鸿蒙生态中适配React Native项目时,发现现有的国际化方案对人民币、港币等货币符号的支持不够友好,特别是当应用需要同时兼容鸿蒙和Android/iOS平台时。于是决定封装一个专为鸿蒙优化的useCurrency Hook,解决以下痛点:
- 自动识别鸿蒙系统区域设置
- 支持CNY、HKD等货币符号的精准定位(符号前置/后置)
- 千分位分隔符的自适应(中文用逗号,欧美用空格)
- 小数点位数智能处理(日元无小数,人民币保留两位)
这个Hook经过三个线上项目验证,在电商、金融类App中尤其实用。下面分享具体实现和踩坑经验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心需求解析
2.1 鸿蒙环境下的特殊考量
鸿蒙的国际化API与Android有细微差异,这是需要特别注意的:
javascript复制import i18n from '@ohos.i18n';
const isHarmonyOS = () => {
try {
return typeof i18n.getSystemLanguage === 'function';
} catch (e) {
return false;
}
};
通过这个判断可以安全地检测鸿蒙运行时环境。实测发现,鸿蒙4.0及以上版本对中文货币符号的渲染更符合国家标准,但需要主动调用getCurrencyDisplayNameAPI获取本地化符号。
2.2 货币格式化的四大核心参数
任何货币格式化都需要处理这四个维度:
- 符号位置:¥100 vs 100円
- 分隔样式:1,000 vs 1 000
- 小数规则:100.00 vs 100
- 负数表示:(100) vs -100
我们的Hook需要支持这些参数的动态配置:
javascript复制type CurrencyOptions = {
currency: string; // ISO货币代码 如CNY
locale?: string; // 覆盖系统默认区域
symbolPosition?: 'before' | 'after';
decimalDigits?: number; // 强制小数位数
useGrouping?: boolean; // 是否使用千分位
};
3. 实现方案详解
3.1 核心架构设计
采用分层设计保证各平台兼容性:
code复制useCurrency Hook
├── 平台检测层 (Harmony/Android/iOS)
├── 参数标准化层
├── 格式化引擎层
│ ├── Intl.NumberFormat (Web标准)
│ ├── ohos.i18n (鸿蒙原生)
│ └── react-native-localize (跨平台)
└── 缓存优化层
3.2 鸿蒙专属格式化实现
鸿蒙平台需要特殊处理货币符号的获取:
javascript复制const formatHarmony = (value: number, options: CurrencyOptions) => {
const { currency, locale = i18n.getSystemLanguage() } = options;
const formatter = new i18n.NumberFormat(locale, {
style: 'currency',
currency,
currencyDisplay: 'symbol'
});
let result = formatter.format(value);
// 鸿蒙4.1以下版本需要手动调整符号位置
if (options.symbolPosition &&
getHarmonyOSVersion() < 4.1) {
result = adjustSymbolPosition(result, options);
}
return result;
};
3.3 动态小数位处理
金融类应用需要智能处理小数位:
javascript复制const resolveDecimalDigits = (currency: string, amount: number) => {
// 日元、韩元等无小数
if (['JPY', 'KRW'].includes(currency)) return 0;
// 金额为整数时省略小数部分
if (Number.isInteger(amount)) return 0;
// 默认保留2位
return 2;
};
4. 完整Hook实现代码
javascript复制import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
const useCurrency = (initialValue = 0, initialOptions = {}) => {
const [formatted, setFormatted] = useState('');
const format = (value, options) => {
const mergedOpts = { ...initialOptions, ...options };
let result;
if (isHarmonyOS()) {
result = formatHarmony(value, mergedOpts);
} else if (Platform.OS === 'web') {
result = formatWeb(value, mergedOpts);
} else {
result = formatNative(value, mergedOpts);
}
setFormatted(result);
return result;
};
// 初始化时立即格式化
useEffect(() => {
format(initialValue);
}, []);
return [formatted, format];
};
5. 性能优化技巧
5.1 缓存格式化实例
频繁创建NumberFormat实例会影响性能,建议缓存:
javascript复制const formatterCache = new Map();
const getCachedFormatter = (locale, options) => {
const key = `${locale}-${options.currency}`;
if (!formatterCache.has(key)) {
formatterCache.set(key, new Intl.NumberFormat(locale, options));
}
return formatterCache.get(key);
};
5.2 批量格式化优化
当需要格式化大量数据时(如商品列表),建议:
javascript复制const batchFormat = (values, currency) => {
const formatter = getCachedFormatter(getLocale(), { style: 'currency', currency });
return values.map(value => formatter.format(value));
};
6. 多平台适配方案
6.1 Android/iOS的特殊处理
在非鸿蒙平台,推荐使用react-native-localize配合Intl:
javascript复制import * as RNLocalize from 'react-native-localize';
const formatNative = (value, options) => {
const locale = options.locale || RNLocalize.getLocales()[0].languageTag;
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: options.currency
}).format(value);
};
6.2 Web端降级方案
当Intl不可用时(如某些老旧浏览器),需要降级处理:
javascript复制const formatWebFallback = (value, currency) => {
const symbols = {
CNY: '¥',
USD: '$',
EUR: '€'
};
return `${symbols[currency] || currency} ${value.toFixed(2)}`;
};
7. 测试策略
7.1 单元测试要点
货币格式化的测试需要覆盖:
javascript复制describe('useCurrency', () => {
it('CNY formatting', () => {
const [formatted] = useCurrency(1234.56, { currency: 'CNY' });
expect(formatted).toBe('¥1,234.56');
});
it('HKD symbol position', () => {
const [formatted] = useCurrency(100, {
currency: 'HKD',
symbolPosition: 'after'
});
expect(formatted).toMatch(/100.*HK\$/);
});
});
7.2 真机测试清单
在鸿蒙设备上必须验证:
- 系统语言切换时的实时更新
- 极端金额显示(如1e9超大数字)
- 黑暗模式下的符号可见性
- 横竖屏切换时的布局稳定性
8. 实际应用案例
8.1 电商价格展示
javascript复制const ProductPrice = ({ price, currency }) => {
const [formatted] = useCurrency(price, { currency });
return (
<Text style={styles.price}>
{formatted}
{price > 1000 && <Text style={styles.discount}> (免运费)</Text>}
</Text>
);
};
8.2 金融数据仪表盘
对于实时变动的金融数据,建议:
javascript复制const FinancialChart = ({ data }) => {
const [, format] = useCurrency(0, { currency: 'USD' });
return (
<View>
{data.map(item => (
<Tooltip
text={format(item.value)}
/* ... */
/>
))}
</View>
);
};
9. 常见问题排查
9.1 符号不显示问题
现象:货币符号显示为货币代码(如"CNY"而不是"¥")
解决方案:
- 检查鸿蒙系统的语言区域设置
- 确认传入的是有效的ISO货币代码
- 添加fallback符号表
javascript复制const SYMBOL_MAP = {
CNY: '¥',
// ...其他货币
};
const ensureSymbol = (text, currency) => {
return text.replace(currency, SYMBOL_MAP[currency] || currency);
};
9.2 性能问题
现象:列表滚动时卡顿
优化方案:
- 使用memoization
- 对于静态价格,提前格式化
- 虚拟列表优化
javascript复制const memoizedFormat = useMemo(
() => (value) => formatCurrency(value, options),
[options.currency, options.locale]
);
10. 扩展建议
10.1 多货币切换方案
对于需要支持货币切换的应用:
javascript复制const CurrencyContext = createContext();
const CurrencyProvider = ({ children }) => {
const [currency, setCurrency] = useState('CNY');
const value = { currency, setCurrency };
return (
<CurrencyContext.Provider value={value}>
{children}
</CurrencyContext.Provider>
);
};
// 在组件中使用
const { currency } = useContext(CurrencyContext);
const [formatted] = useCurrency(price, { currency });
10.2 与i18n框架集成
如果项目使用i18next等框架,可以创建插件:
javascript复制import i18n from 'i18next';
const CurrencyFormatter = {
type: 'formatter',
name: 'currency',
format: (value, lng, options) => {
const [formatted] = useCurrency(value, {
currency: options.currency,
locale: lng
});
return formatted;
}
};
i18n.addFormatter(CurrencyFormatter);
// 使用:t('price', { value: 100, formatParams: { currency: 'CNY' } })
