Skip to content

fix(rn): 修复响应式样式尺寸更新并支持配置计算基准 - #2418

Open
mackwang112 wants to merge 20 commits into
masterfrom
fix-resize
Open

mackwang112 wants to merge 20 commits into
masterfrom
fix-resize

Conversation

@mackwang112

@mackwang112 mackwang112 commented Jan 29, 2026

Copy link
Copy Markdown
Collaborator

背景

React Native 端此前基于 Dimensions.get('screen') 计算 rpxvwvh 和媒体查询。在分屏、折叠屏、横竖屏切换等窗口尺寸发生变化但物理屏幕尺寸不变的场景中,响应式样式可能不会按当前页面窗口更新;后台页面重新显示时也可能保留旧窗口下的样式。

直接切换为 Window 可能改变存量项目的布局效果,因此本次新增 rnConfig.dimensionsBase 配置:默认使用 'window',需要保持旧行为时可显式设置为 'screen'

mpx.config.rnConfig.dimensionsBase = 'screen'

本次更新

  • rpxvwvh 和媒体查询默认使用 Window 作为计算基准:
    • rpx = value * window.width / 750
    • vw = value * window.width / 100
    • vh = value * window.height / 100
  • 新增 rnConfig.dimensionsBase: 'window' | 'screen',默认值为 'window';响应式单位、媒体查询、onResizedeviceOrientation 使用相同的尺寸基准。
  • 合并 PR feat: RN 新增 notifyDimensionsChange 配置,支持手动触发 dimensions 更新 #2485notifyDimensionsChange(dimensions?) 全局方法:支持宿主主动通知尺寸变化;不传参数时重新读取 React Native Dimensions,并重新执行 customDimensions
  • 新增全局 getStyleDimensions(),统一返回经过 customDimensions 处理、并由 dimensionsBase 选中的当前尺寸副本。
  • 所选尺寸发生变化时,以经过 rnConfig.customDimensions 处理后的有效尺寸判断是否需要刷新。
  • 补全动态 formatValue 路径的尺寸依赖标记,确保使用响应式单位的组件能够订阅并刷新。
  • 修复纯媒体查询声明泄漏到基础样式的问题。
  • 修复纯媒体查询首次计算前未执行 customDimensions 的问题。
  • 修复组合媒体查询的匹配逻辑;@media (min-width: 600px) and (max-width: 900px) 在宽度处于 [600, 900] 时生效。
  • 所选尺寸发生变化时清理 class 样式缓存,并只触发依赖尺寸的组件重新渲染。
  • 页面重新显示时同步页面尺寸版本,使后台页面在宽屏切换为窄屏后返回时能立即使用当前尺寸对应的样式;该同步不依赖页面是否声明 onShowonHideonResize
  • 补充相关类型、文档及 Mpx2RN Skill 说明。

问题场景示例

以下示例均使用默认配置 dimensionsBase: 'window';设置为 'screen' 时,响应式单位、媒体查询、onResizedeviceOrientation 均改用 Screen。

1. 动态 rpx/vw/vh 未随 Window 更新

<view wx:style="{{rpxStyle}}"></view>
<view wx:style="{{vwStyle}}"></view>
<view wx:style="{{pxStyle}}"></view>
createComponent({
  data: {
    rpxStyle: { width: '600rpx' },
    vwStyle: { width: '50vw' },
    pxStyle: { width: '240px' },
    vhStyle: { height: '100vh' }
  }
})

修复前: 分屏、折叠或旋转导致 Window 变化但 Screen 不变时,不会触发尺寸版本更新;同时动态 formatValue 路径没有标记尺寸依赖,组件可能继续显示旧的 rpx/vw/vh 尺寸。

修复后: 默认配置下,600rpx50vw100vh 立即按新 Window 尺寸重新计算,240px 保持不变;配置为 'screen' 时继续按 Screen 尺寸计算。

2. 仅使用媒体查询的组件未刷新

.media-box {
  width: 280px;
  background-color: red;
}

@media (min-width: 600px) {
  .media-box {
    width: 520px;
    background-color: green;
  }
}

修复前: media 声明会被同时写入基础样式,导致宽度小于 600px 时也可能直接显示绿色 520px;并且仅使用媒体查询不会建立尺寸依赖,跨过断点时组件可能不刷新。

修复后: 所选尺寸跨过 600px 时立即在红色 280px 和绿色 520px 之间切换,无需依赖其他 rpx/vw/vh 样式触发刷新。

3. min-width 与 max-width 组合查询范围错误

.range-box {
  width: 280px;
  background-color: red;
}

@media (min-width: 600px) and (max-width: 900px) {
  .range-box {
    width: 520px;
    background-color: green;
  }
}

修复前: 两个条件未作为交集处理。宽度大于 900px 时会被 min-width 分支命中,宽度小于 600px 时会被 max-width 分支命中,因此该示例在区间外也会错误显示绿色 520px

修复后: 仅当 600px <= 所选尺寸.width <= 900px 时使用绿色 520px,区间外使用红色 280px,两个边界均包含。

4. 后台页面返回后仍使用旧尺寸

<view class="rpx-bar"></view>
<view class="vw-bar"></view>
<view class="px-bar"></view>
// 页面刻意不声明 onShow / onHide / onResize
createPage({})
.rpx-bar { width: 600rpx; }
.vw-bar { width: 80vw; }
.px-bar { width: 240px; }

复现步骤:宽屏进入页面后跳转到下一页,在下一页切换为窄屏再返回。

修复前: 没有声明页面生命周期的后台页面不会监听 show,页面尺寸版本未追平全局版本,返回后 600rpx80vw 仍可能保持宽屏下的旧宽度。

修复后: 返回页面时会先同步尺寸版本,600rpx80vw 立即按当前计算基准重新计算,240px 保持不变。600rpx80vw 按公式本来就相等。

5. 纯媒体查询首次计算未应用 customDimensions

mpx.config.rnConfig.customDimensions = (dimensions) => {
  dimensions.window.width /= 2
  return dimensions
}
@media (min-width: 600px) {
  .media-box { width: 520px; }
}

修复前: 如果组件没有 rpxvwvh,首次媒体查询计算可能直接使用未经 customDimensions 处理的原始尺寸,结果还会受到其他组件渲染顺序影响。

修复后: 媒体查询读取尺寸前会确保 customDimensions 已执行,首次渲染即可使用正确的有效尺寸。

vh 示例只验证所选尺寸的高度计算;非 custom navigation 下 100vh 是否扣除原生导航栏不在本次修改范围内。

notifyDimensionsChange 与 #2485 review 修复

// 重新读取当前 React Native Dimensions
notifyDimensionsChange()

// 或显式传入尺寸
notifyDimensionsChange({
  window: nextWindow,
  screen: nextScreen
})
  • 手动通知与原生 Dimensions change 事件复用同一更新流程,并按 dimensionsBase 选择 Window 或 Screen 作为刷新判断基准。
  • onResizedeviceOrientation 使用相同基准:Window 模式只响应 Window 尺寸变化,Screen 模式只响应 Screen 尺寸变化。
  • 调用入口会复制原始 Dimensions,避免 customDimensions 原地修改 React Native 返回的对象。
  • 所有尺寸应用入口都会统一维护初始化状态,修复“首次样式计算前已收到通知,随后重复执行 customDimensions”的问题。

getStyleDimensions

const dimensions = getStyleDimensions()
console.log(dimensions.width, dimensions.height)
  • 首次调用会确保尺寸完成初始化,避免其他调用方读到未经 customDimensions 处理的原始尺寸。
  • 如果在首次读取后才配置或替换 customDimensions,下次读取会基于最近一次原始 Dimensions 重新处理。
  • 返回的是尺寸副本,调用方修改返回值不会污染响应式样式使用的内部缓存。

其他已有修复

修复组件销毁后导致 rpx/vw/vh 响应式单位失效

现状:页面上的组件销毁后,尺寸变化可能不再触发使用响应式单位的组件刷新。

原因:del(global.__mpxPageSizeCountMap, pageId) 应只在页面销毁时触发,此前组件销毁时也会触发。

尺寸变化时仅刷新依赖响应式单位的组件

由刷新所有组件改为仅刷新依赖 rpxvwvh 或媒体查询的组件。

修复 RN externalClasses 变化时未触发组件 render

  • externalClasses 配置保留到运行时并加入 validProps,使其与普通 props 一样具有响应性。
  • __getStylethis.__mpxProxy.props[className] 获取 externalClasses,使样式计算与响应式数据建立关联。

验证

  • ./node_modules/.bin/jest packages/core/__tests__/common/dimensionsHelper.spec.js packages/core/__tests__/common/styleHelperMixin.ios.spec.js packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js --runInBand --watchman=false:3 suites、92 tests passed。
  • 相关文件 ESLint 检查通过。
  • npm run build:tsc 通过。
  • 手动覆盖动态 rpx/vw/vh、固定 px 对照、纯媒体查询、范围媒体查询,以及后台页面宽屏切换窄屏后返回的场景。

@mackwang112
mackwang112 marked this pull request as draft January 29, 2026 13:27
@mackwang112
mackwang112 marked this pull request as ready for review March 10, 2026 07:53
@mackwang112 mackwang112 changed the title fix: 修复rpx在屏幕尺寸变化时未跟随变化 fix(rn): 修复响应式样式尺寸更新并支持配置计算基准 Sep 10, 2026
| `setAppShow()` | 手动触发应用「进入前台」逻辑,驱动已注册的 `onShow`。 |
| `setAppHide()` | 手动触发应用「进入后台」逻辑,驱动已注册的 `onHide`。 |
| `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `dimensionsBase` 指定的尺寸判断是否刷新。 |
| `getStyleDimensions()` | 返回经过 `customDimensions` 处理后、由 `dimensionsBase` 选中的当前只读 `ScaledSize` 副本;首次调用及更换 `customDimensions` 后会确保重新处理原始尺寸。 |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个不是对外接口,不需要在skill中暴露

RN 不支持 `rem` 和 `em` 单位。需将其转换为 `rpx` 以实现响应式布局。

**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(规定屏幕宽为 750rpx)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如:
**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(RN 默认以 Window 宽度为 750rpx;配置 `dimensionsBase: "screen"` 时以 Screen 宽度为准)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不需要改描述

| --- | --- | --- |
| `px` | 绝对像素 | 直接转换为 RN 的无单位数值 |
| `rpx` | 响应式像素 | `rpx值 × 屏幕宽度 / 750` |
| `rpx` | 响应式像素 | 默认按 `rpx值 × window.width / 750` 转换 |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

屏幕宽度 改为 视口基准宽度

// TODO: 存在部分安卓折叠屏机型在折叠/展开切换时,Dimensions 监听到的 width/height 尺寸错误,并触发多次问题
function rpx (value) {
const screenInfo = global.__mpxAppDimensionsInfo.screen
const dimensionsInfo = getStyleDimensions()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

无意义的变量声明

Comment thread packages/webpack-plugin/lib/loader.js Outdated
}
const { resourcePath, queryObj } = parseRequest(this.resource)

const externalClasses = mpx.externalClasses || []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

内部从mpx中获取吧,别层层传参了

classMap[key] = val

// Media declarations only take effect when their query matches.
if (!isMedia) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

和下面isMedia的逻辑合并一下吧

// set css defalut value
const val = classMap[key] || {}
classMap[key] = Object.assign(val, classMapValue)
classMap[key] = val

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

classMap[key] = classMap[key] || {}
不需要val变量了

const validProps = Object.assign({}, rawOptions.props, rawOptions.properties)
if (global.__externalClasses && global.__externalClasses.length > 0) {
global.__externalClasses.forEach((name) => {
validProps[name] = null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为啥赋值为null,不都应该是String么

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

以及加入到validProps中会产生一些隐式的冲突,比如和data以及和methods的冲突,可以单独比对一下,如果externalClasses发生变化时,变更一个固定响应数据驱动视图更新

@hiyuki hiyuki left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充两个通过最小用例复现的问题。当前提交原有相关 92 项测试通过,但以下场景尚未覆盖。

classMap[key] = val

// Media declarations only take effect when their query matches.
if (!isMedia) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 媒体查询中的 !important 声明会失效

这里将 media 声明移出基础样式后,重要声明只保存在 _media[].value._inlineLayer.important 中,但运行时 createLayer().mergeToLayer() 仅对基础 style 调用 checkInlineLayer,没有处理命中的 mediaStyle._inlineLayer;最后 genResult() 又删除了 _inlineLayer,导致这些声明完全丢失。

最小复现:

.box { width: 100px; }
@media (min-width: 300px) {
  .box { width: 200px !important; }
}

在 window.width = 360 时,通过实际编译产物调用 __getStyle,预期 width 为 200,实际为 100。建议让命中的媒体样式也进入 _inlineLayer 优先级合并流程,并补充这个用例。


export function getStyleDimensions (dimensionsBase = getDimensionsBase()) {
if (!dimensionsInfoInitialized || appliedCustomDimensions !== Mpx.config.rnConfig?.customDimensions) {
applyDimensionsInfo(rawDimensionsInfo)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 更换 customDimensions 后读取尺寸,会让后续通知漏掉缓存刷新

这里会在 getter 中直接更新 __mpxAppDimensionsInfo,但没有同步清理 class 缓存、更新尺寸版本。随后 notifyDimensionsChange() 读取到的旧尺寸已经是处理后的新值,新旧尺寸相等会直接 return,因此即使主动通知也无法刷新之前缓存的样式。

已复现的调用顺序:

  1. window.width = 360,先渲染并缓存 .box { width: 750rpx },结果为 360。
  2. 更换 customDimensions,令 dimensions.window.width /= 2
  3. 调用 getStyleDimensions(),返回 width = 180。
  4. 调用 notifyDimensionsChange(),再计算同一个 class,结果仍为 360,预期为 180。

文档明确支持更换 customDimensions 后重新读取尺寸,因此建议统一有效尺寸变化与缓存失效、版本更新的处理,避免 getter 提前覆盖用于变化判断的旧快照,并补充上述顺序的测试。

if (localStyle = this.__getClassStyle?.(className)) {
mergeToLayer(localStyle._layer || 'normal', localStyle, getMediaStyle(localStyle._media))
// class style 计算可能触发缓存,需要单独在结果中记录是否依赖窗口尺寸,不能直接使用全局变量。
this.__dependentWindowSize = this.__dependentWindowSize || localStyle._dependentWindowSize

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dependentWindowSize = dependentWindowSize || localStyle._dependentWindowSize

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants