本文记录把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];constguidelineBaseWidth=350;// 设计稿基准(约 5 吋屏)constguidelineBaseHeight=680;scale=size=>shortDimension/350*sizeverticalScale=size=>longDimension/680*sizemoderateScale=(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;上游 star2529 |
| 库类型 | 纯 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 | 全无 |
npminstall"git+https://atomgit.com/oh-react-native/react-native-size-matters.git#0.4.2-ohos-1.0.0"--ignore-scripts// metro.config.jswatchFolders:[path.resolve(__dirname,'../react-native-size-matters')],importSizeMattersTestAppfrom'./SizeMattersTestApp';AppRegistry.registerComponent('SizeMattersTestApp',()=>SizeMattersTestApp);hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey 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];constguidelineBaseWidth=350;// 设计稿基准宽(约 5 吋屏)constguidelineBaseHeight=680;// 设计稿基准高exportconstscale=size=>shortDimension/guidelineBaseWidth*size;exportconstverticalScale=size=>longDimension/guidelineBaseHeight*size;exportconstmoderateScale=(size,factor=0.5)=>size+(scale(size)-size)*factor;exportconstmoderateVerticalScale=(size,factor=0.5)=>size+(verticalScale(size)-size)*factor;exportconsts=scale;exportconstvs=verticalScale;exportconstms=moderateScale;exportconstmvs=moderateVerticalScale;要点:
- 取的是"短边 / 长边"(
min/max),不是固定的宽 / 高 ⇒横竖屏都会用同一套基准; shortDimension/longDimension在模块加载时取一次⇒旋转后不会自动更新;- 不做任何取整——
scale直接返回浮点(这与某些"会roundToNearestPixel"的换算库不同,用的时候要自己留意); moderateScale的factor默认 0.5⇒ 只在"原始值"与"完全缩放值"之间走一半(缓解大屏上缩放过度);s/vs/ms/mvs是同一引用(s === scale为真)。
3.2ScaledSheet:把注解字符串换成数值(lib/ScaledSheet.js)
constvalidScaleSheetRegex=/^(\-?\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) × factorfactor = 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) |
# 从适配仓库安装npminstall"git+https://atomgit.com/oh-react-native/react-native-size-matters.git#0.4.2-ohos-1.0.0"--ignore-scripts# 本地目录接入(开发常用)npminstall../react-native-size-matters --ignore-scripts// metro.config.jswatchFolders:[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-bcom.rnoh084.demo-aEntryAbility--psrnAppKey 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-openharmony0.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 |
| 本次构建 | assembleHap4 分 28 秒(一轮就过,无重编) |
| 验证规模 | 设备侧31 / 31断言;换算几何复核 3 条(377 / 339 / 189 pxvs 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