本文记录把 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.714377 px377.14✓
moderateScale(100)112.857339 px338.57✓
scale(50)62.857189 px188.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)
mainindex.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;

要点:

  1. 取的是"短边 / 长边"(min / max),不是固定的宽 / 高 ⇒ 横竖屏都会用同一套基准;
  2. shortDimension / longDimension 在模块加载时取一次 ⇒ 旋转后不会自动更新;
  3. 不做任何取整 —— scale 直接返回浮点(这与某些"会 roundToNearestPixel"的换算库不同,用的时候要自己留意);
  4. moderateScale 的 factor 默认 0.5 ⇒ 只在"原始值"与"完全缩放值"之间走一半(缓解大屏上缩放过度);
  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 入口未做功能验证(见"验证边界")。


四、验证设计:公式逐项核 + 屏幕量像素

因为这是纯换算库,判据分两层:

  1. 公式层:期望值由页面自己按源码公式算(读 Dimensions.get('window') 现算),不写死数字 ⇒ 换设备也成立;
  2. 几何层:把 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×100125.714377 px377.14✓
moderateScale(100) = 100+(125.714−100)×0.5112.857339 px338.57✓
scale(50) = 440/350×5062.857189 px188.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 本次验证的边界

  1. extend 入口未做功能验证 —— 它需要 @env(react-native-dotenv),本宿主未安装未配置 ⇒ 只能验证到"打包会失败"这一层;配好之后的自定义基准(SIZE_MATTERS_BASE_WIDTH/HEIGHT)未验证;
  2. 只在一台模拟器上验证(Pura X View,density 3,1320×2232),没有真机;window 恰好是 440×744vp,公式中的基准(350 / 680)未做更换验证;
  3. 横竖屏 / 分屏未验证(源码是加载时取一次,预期不会更新,但没有实测旋转);
  4. ScaledSheet 的 @ms factor 边界未穷举(只测了 0.3;正则上限是 2 位小数);
  5. deepMap 的其它嵌套形态未测(只测了 transform: [{translateX}] 一层数组 + 对象;深层嵌套 / 数组套数组未测);
  6. null / undefined / 数字 / 布尔等非字符串值在 ScaledSheet 里的行为未测(源码 if (!validScaleSheetRegex.test(value)) return value; —— 对数字会走 String() 隐式转换,未实测);
  7. 没有跨平台对照(未在 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)。

值得记下来的是这几条:

  1. ★★ extend 入口会让整个 bundle 打挂。 它 import … from '@env',而 @env 是 react-native-dotenv 的虚拟模块 ⇒ 未配置时 Metro 在打包阶段就 Unable to resolve module @env,不是运行时错误。⇒ 引用一个"可选入口/子路径"前,先确认它的依赖在本工程里能解析;这类失败一出现就是整个 bundle 没了,表现上像是"这个库不能用",实际只是缺一个 babel 插件。

  2. ★ "不匹配就原样返回"是静默失效的高发形态。 ScaledSheet 的正则只接受最多 3 位小数,超了就把字符串原样留下 —— 不报错、不警告,样式里静静地放着一个无效值。⇒ 用注解式 API 时,边界(位数、格式、非法值)必须实测。

  3. 换算是"不做取整"的。 scale() 返回浮点,与某些会 roundToNearestPixel 的换算库行为不同;要整数得自己 Math.round 或用 @sr。⇒ 用之前先确认它是取整还是浮点,否则布局会有 1px 级别的差异。

  4. 这类库的别名特别短(s/vs/ms/mvs),极易撞名。 我因为 const s = StyleSheet.create(...) 与导入的 s 重名,直接 Duplicate declaration 打包失败。⇒ 短别名要么加前缀导入,要么只用全名。

  5. 打包失败绝不接着构建。 本轮第一次 bundle 失败时我停下来先修,而不是继续 assembleHap —— 否则装进 HAP 的会是上一次的 bundle,结果看起来"改了没生效"(否则"改了没生效"的假象会让你往错误方向排查)。


本篇用到的库

项内容
三方库react-native-size-matters(上游 0.4.2 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-size-matters
适配 TAG0.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 Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐