React Native for OpenHarmony 实战:三方库 react-native-size-matters 的鸿蒙化适配指南
本文记录把 react-native-size-matters 适配到 HarmonyOS 的完整过程。
这是一个尺寸缩放工具库:按屏幕的短边 / 长边把设计稿上的数值换算成当前设备上的 dp,并提供 ScaledSheet 让你直接在样式里写 '100@s' 这种注解。
它的核心只有 18 行代码:
const { width, height } = Dimensions.get('window');
const [shortDimension, longDimension] = width < height ? [width, height] : [height, width];
const guidelineBaseWidth = 350; // 设计稿基准(约 5 吋屏)
const guidelineBaseHeight = 680;
scale = size => shortDimension / 350 * size
verticalScale = size => longDimension / 680 * size
moderateScale = (size, factor = 0.5) => size + (scale(size) - size) * factor
本轮把公式逐项核对(用页面自己从 Dimensions 算出的期望值,不写死数字),并把换算结果放到屏幕上量像素:
| 表达式 | 期望(vp) | 实测像素 | 期望像素 | 判定 |
|---|---|---|---|---|
scale(100) | 125.714 | 377 px | 377.14 | ✓ |
moderateScale(100) | 112.857 | 339 px | 338.57 | ✓ |
scale(50) | 62.857 | 189 px | 188.57 | ✓ |
设备侧断言 31 / 31 全部通过,一次构建就过。
另外还实测到一个会让整个 bundle 打挂的坑:react-native-size-matters/extend 这个入口依赖 @env(react-native-dotenv 的虚拟模块),没配置时直接打包失败。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 0.4.2;license: MIT;上游 star 2529 |
| 库类型 | 纯 JS 工具库:scale / verticalScale / moderateScale / moderateVerticalScale + ScaledSheet |
| 是否需要原生适配 | 不需要(无 harmony/、无 NativeModules) |
| 运行时依赖 | 零(dependencies 未声明);peerDependencies 只有 react-native: * |
prepare / prepublish | 都没有(scripts 只有 test) |
main | index.js(源码,298 字节) |
| 导出形态 | 只有具名导出(ScaledSheet + 9 个函数/别名),没有 default 导出 |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| 设备侧断言 | ✅ 31 / 31 全部通过(A 13 + B 9 + C 9) |
| ★ 换算复核 | ✅ 三条实测条 377 / 339 / 189 px,与公式算出的 377.14 / 338.57 / 188.57 逐像素吻合 |
| ★ 本机基准 | Dimensions.get('window') = 440 × 744 vp(scale 3)⇒ short=440、long=744 |
| ★★ 一个会让打包失败的坑 | react-native-size-matters/extend 依赖 @env(react-native-dotenv 虚拟模块)⇒ 未配置时 Metro 直接 Unable to resolve module @env,整个 bundle 出不来 |
| ★ 一个静默失效的边界 | ScaledSheet 的注解正则只允许最多 3 位小数 ⇒ '100.1234@s' 不匹配 ⇒ 原样返回字符串(不报错) |
| ★ 我自己的失误 | 导入的别名 s 与文件底部自己的样式表 const s = StyleSheet.create(...) 重名 ⇒ Babel 报 Duplicate declaration "s"、bundle 失败 |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules / requireNativeComponent | 全无 |
npm install "git+https://atomgit.com/oh-react-native/react-native-size-matters.git#0.4.2-ohos-1.0.0" --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-size-matters') ],
import SizeMattersTestApp from './SizeMattersTestApp';
AppRegistry.registerComponent('SizeMattersTestApp', () => SizeMattersTestApp);
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey SizeMattersTestApp
不需要 HAR、权限、ohpm、link-harmony,也不需要补装任何依赖。
三、读源码取到的真值
3.1 四个换算函数(lib/scaling-utils.js)
import { Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window'); // ★ 模块加载时取一次
const [shortDimension, longDimension] = width < height ? [width, height] : [height, width];
const guidelineBaseWidth = 350; // 设计稿基准宽(约 5 吋屏)
const guidelineBaseHeight = 680; // 设计稿基准高
export const scale = size => shortDimension / guidelineBaseWidth * size;
export const verticalScale = size => longDimension / guidelineBaseHeight * size;
export const moderateScale = (size, factor = 0.5) => size + (scale(size) - size) * factor;
export const moderateVerticalScale = (size, factor = 0.5) => size + (verticalScale(size) - size) * factor;
export const s = scale; export const vs = verticalScale; export const ms = moderateScale; export const mvs = moderateVerticalScale;
要点:
- 取的是"短边 / 长边"(
min/max),不是固定的宽 / 高 ⇒ 横竖屏都会用同一套基准; shortDimension/longDimension在模块加载时取一次 ⇒ 旋转后不会自动更新;- 不做任何取整 ——
scale直接返回浮点(这与某些"会roundToNearestPixel"的换算库不同,用的时候要自己留意); moderateScale的factor默认 0.5 ⇒ 只在"原始值"与"完全缩放值"之间走一半(缓解大屏上缩放过度);s/vs/ms/mvs是同一引用(s === scale为真)。
3.2 ScaledSheet:把注解字符串换成数值(lib/ScaledSheet.js)
const validScaleSheetRegex = /^(\-?\d+(?:\.\d{1,3})?)@(mv?s(\d+(?:\.\d{1,2})?)?|s|vs)r?$/;
// └── 数字:最多 3 位小数 ──┘ └ 函数 ─┘ └─ factor,最多 2 位 ─┘ └ r = 四舍五入
'100@s'⇒scale(100)'50@vs'⇒verticalScale(50)'100@ms0.3'⇒moderateScale(100, '0.3')'100@sr'⇒Math.round(scale(100))- ⚠️ 不匹配的字符串原样返回(
return value)⇒ 静默失效,不会报错; create用deepMap递归处理对象与数组 ⇒ 嵌套的transform: [{translateX: '10@s'}]也会被转换。
3.3 ★★ extend 入口依赖 @env(本轮实测会让打包失败)
lib/extend/scaling-utils.extend.js 第 2 行:
import { SIZE_MATTERS_BASE_WIDTH, SIZE_MATTERS_BASE_HEIGHT } from '@env';
@env 不是真包,而是 react-native-dotenv 提供的虚拟模块。本宿主里:
node_modules/@env不存在;babel.config.js里没有 dotenv 相关插件。
⇒ 实测后果(Metro 打包直接失败):
error Unable to resolve module @env from E:\rnoh-work\react-native-size-matters\lib\extend\scaling-utils.extend.js
> 2 | import { SIZE_MATTERS_BASE_WIDTH, SIZE_MATTERS_BASE_HEIGHT } from '@env';
注意这不是运行时错误,而是【打包阶段】就失败 ⇒ 只要你的代码里出现 import 'react-native-size-matters/extend'(或任何指向 extend.js 的引用),整个 bundle 出不来。
要用
extend入口的前提:先配好react-native-dotenv(或等价的、能把@env映射出来的 babel 插件)。本轮没有配置它,所以extend入口未做功能验证(见"验证边界")。
四、验证设计:公式逐项核 + 屏幕量像素
因为这是纯换算库,判据分两层:
- 公式层:期望值由页面自己按源码公式算(读
Dimensions.get('window')现算),不写死数字 ⇒ 换设备也成立; - 几何层:把
scale(100)等直接当width渲染出来,再从布局 dump 量真实像素 ⇒ 证明"算出来的值真的被用上了"。
五、实测结果
5.1 断言 31 / 31
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约 | 9 个导出都是函数、ScaledSheet.create 存在、4 个别名与主函数同引用、无 default 导出 | 13/13 |
| B 换算 | 4 个公式逐项对源码核、scale(0)=0、负数、不做取整、几何实测 | 9/9 |
| C ScaledSheet 与边界 | 4 种注解 + 嵌套数组 + 正则小数位边界 + extend 结论 | 9/9 |
| 合计 | 31/31 |
运行环境实测:Dimensions.get('window') = 440 × 744 vp(scale 3)⇒ short = 440、long = 744。
5.2 ★ 换算逐像素复核
| 表达式 | 公式值(vp) | 实测像素 | 公式值 × 3 | 判定 |
|---|---|---|---|---|
scale(100) = 440/350×100 | 125.714 | 377 px | 377.14 | ✓ |
moderateScale(100) = 100+(125.714−100)×0.5 | 112.857 | 339 px | 338.57 | ✓ |
scale(50) = 440/350×50 | 62.857 | 189 px | 188.57 | ✓ |
⇒ 三条全部吻合(±1px 属布局取整)✓

5.3 ★ ScaledSheet 的正则边界(静默失效)
| 注解 | 结果 | 说明 |
|---|---|---|
'100@s' | scale(100) ✓ | 正常 |
'50@vs' | verticalScale(50) ✓ | 正常 |
'100@ms0.3' | moderateScale(100, '0.3') ✓ | factor 是字符串,靠隐式转换参与乘法 |
'100@sr' | Math.round(scale(100)) ✓ | r 后缀四舍五入 |
'100.1234@s'(4 位小数) | 原样返回 '100.1234@s' ⚠️ | 正则不匹配 ⇒ 静默失效 |
'100.123@s'(3 位) | scale(100.123) ✓ | 3 位是允许的上限 |
'abc@s' | 原样返回 ✓ | 非数字不匹配 |
嵌套 transform: [{translateX: '10@s'}] | scale(10) ✓ | deepMap 递归生效 |
⇒ '100.1234@s' 这种写法不会报错,但样式里会留下一个无效字符串 —— 排查时很容易懵。

5.4 UI:标题不与状态栏重合
TITLE measureInWindow y=420 (状态栏高 117) ⇒ 420 > 117,不重合 ✓

六、我自己的一个失误
导入的别名 s 与我自己文件底部的样式表重名了:
import { …, s, vs, ms, mvs } from 'react-native-size-matters'; // ← 库的别名 s
…
const s = StyleSheet.create({ … }); // ← 我自己的样式表也叫 s
⇒ Babel 直接报错、bundle 失败(exit 1):
error SizeMattersTestApp.tsx: Duplicate declaration "s"
纠正:把导入的别名改成 s as sScale(3 处),自己的样式表保持 s。
⚠️ 教训:这类库的别名非常短(
s/vs/ms/mvs),极易和业务代码里的局部变量撞名。用别名时要么加前缀导入,要么干脆只用全名scale/verticalScale。另外这也再次印证了一条:
npm run dev失败时绝不能接着assembleHap—— 那样装进 HAP 的是上一次的 bundle(见下)。
★ 顺带复查了"打包失败会静默装旧 bundle"这条
本轮第一次 bundle 失败时(Duplicate declaration "s"),我没有继续构建,而是先修好再打 ⇒ 避免"装旧 bundle 后误判结果"。
判据:npm run dev 的退出码必须单独看;它是非 0 时,后面的 assembleHap 报成功也毫无意义。
七、已知限制与本次验证边界
7.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
| 基准在模块加载时取一次 | Dimensions.get('window') 只在 import 时读 ⇒ 旋转 / 分屏后不会自动更新 |
| 不做取整 | scale 返回浮点;要整数得自己 Math.round,或用 @sr 注解 |
| 短边 / 长边区分 | scale 用短边、verticalScale 用长边 ⇒ 横屏时两者会互换语义(但缓存不更新) |
| 注解小数位上限 3 位 | 超过就静默失效(原样返回字符串) |
extend 入口依赖 @env | 未配 react-native-dotenv 时打包直接失败 |
| 别名很短 | s / vs / ms / mvs 极易与业务变量撞名 |
| 没有 default 导出 | 必须具名导入 |
7.2 本次验证的边界
extend入口未做功能验证 —— 它需要@env(react-native-dotenv),本宿主未安装未配置 ⇒ 只能验证到"打包会失败"这一层;配好之后的自定义基准(SIZE_MATTERS_BASE_WIDTH/HEIGHT)未验证;- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;window恰好是 440×744vp,公式中的基准(350 / 680)未做更换验证; - 横竖屏 / 分屏未验证(源码是加载时取一次,预期不会更新,但没有实测旋转);
ScaledSheet的@msfactor 边界未穷举(只测了0.3;正则上限是 2 位小数);deepMap的其它嵌套形态未测(只测了transform: [{translateX}]一层数组 + 对象;深层嵌套 / 数组套数组未测);null/undefined/ 数字 / 布尔等非字符串值在ScaledSheet里的行为未测(源码if (!validScaleSheetRegex.test(value)) return value;—— 对数字会走String()隐式转换,未实测);- 没有跨平台对照(未在 iOS/Android 上跑同一套用例)。
未改动库代码。
八、常见问题
Q1:scale(100) 到底是多少?为什么不是整数?
scale(size) = min(屏宽, 屏高) / 350 * size
本机 min = 440vp ⇒ scale(100) = 440/350×100 = 125.714...。这个库不做取整,所以拿到的是浮点;需要整数就用 Math.round(scale(100)) 或注解里的 r 后缀('100@sr')。
Q2:s / vs / ms / mvs 和全名有什么区别?
没有区别,是同一个函数的别名(实测 s === scale 为真)。⚠️ 但它们很短、极易与业务变量撞名(我就因为 const s = StyleSheet.create(...) 撞名导致 bundle 失败)。
Q3:旋转屏幕后尺寸没变?
因为 shortDimension / longDimension 是模块加载时取一次的模块级常量。⇒ 旋转后不会自动更新(这与它的设计定位有关:按"基准设计稿"等比缩放,通常不随旋转变)。
Q4:ScaledSheet.create 里写了注解但没生效?
先看小数位:正则 只允许最多 3 位小数(
/^(\-?\d+(?:\.\d{1,3})?)@(mv?s(\d+(?:\.\d{1,2})?)?|s|vs)r?$/
)⇒ '12.3456@s' 不匹配 ⇒ 原样返回字符串,样式里就是个无效值,而且不报错。
Q5:import 'react-native-size-matters/extend' 报 Unable to resolve module @env?
因为 extend 入口里有:
import { SIZE_MATTERS_BASE_WIDTH, SIZE_MATTERS_BASE_HEIGHT } from '@env';
@env 是 react-native-dotenv 的虚拟模块。没装 / 没在 babel 里配置它时,Metro 在打包阶段就失败(不是运行时)。⇒ 要么配好 react-native-dotenv,要么别用 extend 入口(用默认入口 + 需要时自己改基准)。
Q6:设计稿基准不是 350 / 680 怎么办?
默认入口把 350 / 680 写死在源码里 ⇒ 默认入口无法改。官方给的做法就是用 extend 入口(通过 @env 里的 SIZE_MATTERS_BASE_WIDTH / SIZE_MATTERS_BASE_HEIGHT 覆盖)。⚠️ 但那条入口需要先配 @env(见 Q5)。
Q7:moderateScale 的 factor 是什么?
在"原始值"和"完全缩放后的值"之间插值:
moderateScale(size, factor) = size + (scale(size) − size) × factor
factor = 0.5(默认)表示"只走一半",用于缓解大屏上缩放过度;factor = 1 就等价于 scale,factor = 0 等于不缩放。
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 纯 JS、零依赖、无原生模块、不需要 HAR / 权限 / ohpm。唯一要注意的是本文说的基准缓存、不取整、注解小数位上限和 extend 入口的 @env 依赖。
小结
react-native-size-matters 是纯 JS 尺寸换算工具库(核心只有 18 行),零依赖、无原生接线。设备侧 31 / 31 断言通过,且一次构建就过(先把源码读透了)。三条换算在屏幕上逐像素复核吻合(377 / 339 / 189 px)。
值得记下来的是这几条:
-
★★
extend入口会让整个 bundle 打挂。 它import … from '@env',而@env是react-native-dotenv的虚拟模块 ⇒ 未配置时 Metro 在打包阶段就Unable to resolve module @env,不是运行时错误。⇒ 引用一个"可选入口/子路径"前,先确认它的依赖在本工程里能解析;这类失败一出现就是整个 bundle 没了,表现上像是"这个库不能用",实际只是缺一个 babel 插件。 -
★ "不匹配就原样返回"是静默失效的高发形态。
ScaledSheet的正则只接受最多 3 位小数,超了就把字符串原样留下 —— 不报错、不警告,样式里静静地放着一个无效值。⇒ 用注解式 API 时,边界(位数、格式、非法值)必须实测。 -
换算是"不做取整"的。
scale()返回浮点,与某些会roundToNearestPixel的换算库行为不同;要整数得自己Math.round或用@sr。⇒ 用之前先确认它是取整还是浮点,否则布局会有 1px 级别的差异。 -
这类库的别名特别短(
s/vs/ms/mvs),极易撞名。 我因为const s = StyleSheet.create(...)与导入的s重名,直接Duplicate declaration打包失败。⇒ 短别名要么加前缀导入,要么只用全名。 -
打包失败绝不接着构建。 本轮第一次 bundle 失败时我停下来先修,而不是继续
assembleHap—— 否则装进 HAP 的会是上一次的 bundle,结果看起来"改了没生效"(否则"改了没生效"的假象会让你往错误方向排查)。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-size-matters(上游 0.4.2 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-size-matters |
| 适配 TAG | 0.4.2-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS,零依赖) |
| 库类型 | 纯 JS 工具库:按屏幕短边/长边缩放尺寸 + ScaledSheet 注解式样式 |
| 运行时依赖 | 零;peerDependencies 只有 react-native: * |
prepare / prepublish | 都没有 |
| 许可证 | MIT |
| 上游仓库 | https://github.com/nirsky/react-native-size-matters(★2529) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = SizeMattersTestApp) |
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-size-matters.git#0.4.2-ohos-1.0.0" --ignore-scripts
# 本地目录接入(开发常用)
npm install ../react-native-size-matters --ignore-scripts
// metro.config.js
watchFolders: [ path.resolve(__dirname, '../react-native-size-matters') ],
// 用法:按设计稿(基准 350×680)等比缩放
import {scale, verticalScale, moderateScale, moderateVerticalScale, ScaledSheet} from 'react-native-size-matters';
// ① 函数式
const styles = StyleSheet.create({
box: {width: scale(100), height: verticalScale(50)},
text: {fontSize: moderateScale(16)}, // 只缩放一半,缓解大屏过度放大
});
// ② 注解式(ScaledSheet)
const sheet = ScaledSheet.create({
box: {width: '100@s', height: '50@vs'},
pad: {paddingTop: '20@ms0.3'}, // moderateScale + factor 0.3
round: {width: '100@sr'}, // r 后缀 ⇒ Math.round
nested: {transform: [{translateX: '10@s'}]}, // 嵌套数组也会被转换
});
// ⚠️ 注解的小数位最多 3 位:'12.3456@s' 会静默失效(原样返回字符串)
// ③ ⚠️ 不要用 extend 入口(除非已配好 react-native-dotenv)
// import {scale} from 'react-native-size-matters/extend';
// ⇒ Metro: Unable to resolve module @env(整个 bundle 打不出来)
# 换页启动测试页(force-stop 不能省,换页参数只在冷启动生效)
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey SizeMattersTestApp
换算复核的复现方式
# ① 页面里按源码公式现算期望值(不写死数字)
# short = min(window.width, window.height); expect = short / 350 * 100
# ② 把 scale(100) 当 width 渲染出来,再从布局 dump 量真实像素
devecocli ui layout | Select-String 'MARK-S100'
# 期望:125.714vp × 3 = 377.14px ⇒ 实测 377px ✓
验证环境
| 项 | 版本 |
|---|---|
| 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 |
本机 Dimensions.get('window') | 440 × 744 vp(scale 3) |
| 本机状态栏高度 | 117px(页面留白取 48vp) |
| 宿主 HAP 产物 | entry-default-signed.hap |
| 本次构建 | assembleHap 4 分 28 秒(一轮就过,无重编) |
| 验证规模 | 设备侧 31 / 31 断言;换算几何复核 3 条(377 / 339 / 189 px vs 377.14 / 338.57 / 188.57);ScaledSheet 注解 7 种(含正则小数位边界);extend 入口打包失败实测复现 1 次 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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)