From 37a76e0ab72dffc0782784259af408b3f27b538c Mon Sep 17 00:00:00 2001 From: mackwang112 Date: Tue, 10 Mar 2026 16:37:50 +0800 Subject: [PATCH 01/17] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E9=94=80=E6=AF=81=E5=90=8E=E5=AF=BC=E8=87=B4rpx/vw/vh?= =?UTF-8?q?=E7=9B=B8=E5=BA=94=E5=BC=8F=E5=8D=95=E4=BD=8D=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/platform/patch/getDefaultOptions.ios.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index 62770ce107..55fac7ea0c 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -398,7 +398,7 @@ const triggerResizeEvent = (mpxProxy, sizeRef) => { } } -function usePageEffect (mpxProxy, pageId) { +function usePageEffect (mpxProxy, pageId, type) { const sizeRef = useRef(getSystemInfo()) useEffect(() => { @@ -415,7 +415,7 @@ function usePageEffect (mpxProxy, pageId) { triggerResizeEvent(mpxProxy, sizeRef) // 如果当前全局size与pagesize不一致,在show之后触发一次resize事件 - if (newVal === 'show' && global.__mpxPageSizeCountMap[pageId] !== global.__mpxSizeCount) { + if (type === 'page' && newVal === 'show' && global.__mpxPageSizeCountMap[pageId] !== global.__mpxSizeCount) { // 刷新__mpxPageSizeCountMap, 每个页面仅会执行一次,直接驱动render刷新 global.__mpxPageSizeCountMap[pageId] = global.__mpxSizeCount } @@ -427,7 +427,9 @@ function usePageEffect (mpxProxy, pageId) { } return () => { unWatch && unWatch() - del(global.__mpxPageSizeCountMap, pageId) + if (type === 'page') { + del(global.__mpxPageSizeCountMap, pageId) + } } }, []) } @@ -697,7 +699,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { } }) - usePageEffect(proxy, pageId) + usePageEffect(proxy, pageId, type) useEffect(() => { proxy.mounted() return () => { From 69ea9c180943ff690606fb430d1bb973782c7872 Mon Sep 17 00:00:00 2001 From: mackwang112 Date: Tue, 10 Mar 2026 16:39:05 +0800 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=E8=BE=93=E5=87=BARN=20=E5=B1=8F?= =?UTF-8?q?=E5=B9=95=E5=B0=BA=E5=AF=B8=E5=8F=98=E5=8C=96=E6=97=B6=E4=BB=85?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E4=BE=9D=E8=B5=96rpx/vw/vh=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BC=8F=E5=8D=95=E4=BD=8D=E7=9A=84=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtInMixins/styleHelperMixin.ios.js | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 8113fb97cb..6c87870e63 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -12,8 +12,22 @@ global.__mpxPageSizeCountMap = reactive({}) global.__GCC = function (className, classMap, classMapValueCache) { if (!classMapValueCache.has(className)) { - const styleObj = classMap[className]?.(global.__formatValue) - styleObj && classMapValueCache.set(className, styleObj) + const originalDependentScreenSize = dependentScreenSize + dependentScreenSize = false + + let styleObj = classMap[className]?.(formatValue) + if (!styleObj) return + if (!styleObj._media?.length) { + styleObj = { + _default: styleObj + } + } + + // 记录是否依赖屏幕尺寸,在屏幕尺寸变化时决定是否重新渲染对应组件 + styleObj._dependentScreenSize = dependentScreenSize + dependentScreenSize = dependentScreenSize || originalDependentScreenSize + + classMapValueCache.set(className, styleObj) } return classMapValueCache.get(className) } @@ -79,12 +93,15 @@ const unit = { const empty = {} +// 记录style是否依赖屏幕尺寸 +let dependentScreenSize = false function formatValue (value, unitType) { if (!dimensionsInfoInitialized) useDimensionsInfo(global.__mpxAppDimensionsInfo) if (unitType === 'hairlineWidth') { return StyleSheet.hairlineWidth } if (unitType && typeof unit[unitType] === 'function') { + dependentScreenSize = true return unit[unitType](+value) } const matched = unitRegExp.exec(value) @@ -254,11 +271,11 @@ export default function styleHelperMixin () { return concat(staticClass, stringifyDynamicClass(dynamicClass)) }, __getStyle (staticClass, dynamicClass, staticStyle, dynamicStyle, hide) { + // 重置依赖标记 + dependentScreenSize = false const isNativeStaticStyle = staticStyle && isNativeStyle(staticStyle) let result = isNativeStaticStyle ? [] : {} const mergeResult = isNativeStaticStyle ? (...args) => result.push(...args) : (...args) => Object.assign(result, ...args) - // 使用一下 __getSizeCount 触发其 get - this.__getSizeCount() if (staticClass || dynamicClass) { // todo 当前为了复用小程序unocss产物,暂时进行mpEscape,等后续正式支持unocss后可不进行mpEscape @@ -267,17 +284,12 @@ export default function styleHelperMixin () { classString.split(/\s+/).forEach((className) => { let localStyle, appStyle if (localStyle = this.__getClassStyle?.(className)) { - if (localStyle._media?.length) { - mergeResult(localStyle._default, getMediaStyle(localStyle._media)) - } else { - mergeResult(localStyle) - } + mergeResult(localStyle._default, getMediaStyle(localStyle._media)) + // class style 计算可能触发缓存,需要单独在结果中记录是否依赖屏幕尺寸,不能直接使用全局变量。 + this.__dependentScreenSize = this.__dependentScreenSize || localStyle._dependentScreenSize } else if (appStyle = global.__getAppClassStyle?.(className)) { - if (appStyle._media?.length) { - mergeResult(appStyle._default, getMediaStyle(appStyle._media)) - } else { - mergeResult(appStyle) - } + mergeResult(appStyle._default, getMediaStyle(appStyle._media)) + this.__dependentScreenSize = this.__dependentScreenSize || appStyle._dependentScreenSize } else if (isObject(this.__props[className])) { // externalClasses必定以对象形式传递下来 mergeResult(this.__props[className]) @@ -319,6 +331,12 @@ export default function styleHelperMixin () { }) } const isEmpty = isNativeStaticStyle ? !result.length : isEmptyObject(result) + + // 仅在依赖屏幕尺寸时才触发__getSizeCount进行相应式关联,避免屏幕尺寸变化时不必要的性能损耗 + this.__dependentScreenSize = this.__dependentScreenSize || dependentScreenSize + if (this.__dependentScreenSize) { + this.__getSizeCount() + } return isEmpty ? empty : result } } From 97b32fc3fe02a4cd0c7700e406a8af16b28bfa82 Mon Sep 17 00:00:00 2001 From: mackwang112 Date: Tue, 10 Mar 2026 16:40:34 +0800 Subject: [PATCH 03/17] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=BE=93?= =?UTF-8?q?=E5=87=BARN=20externalClasses=20=E5=8F=98=E5=8C=96=E6=97=B6?= =?UTF-8?q?=E6=9C=AA=E8=A7=A6=E5=8F=91=E7=BB=84=E4=BB=B6render?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/platform/builtInMixins/styleHelperMixin.ios.js | 4 ++-- packages/core/src/platform/patch/getDefaultOptions.ios.js | 5 +++++ packages/webpack-plugin/lib/loader.js | 3 ++- packages/webpack-plugin/lib/react/index.js | 4 +++- packages/webpack-plugin/lib/react/processScript.js | 5 +++-- packages/webpack-plugin/lib/react/script-helper.js | 4 +++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 6c87870e63..fea47aa337 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -290,9 +290,9 @@ export default function styleHelperMixin () { } else if (appStyle = global.__getAppClassStyle?.(className)) { mergeResult(appStyle._default, getMediaStyle(appStyle._media)) this.__dependentScreenSize = this.__dependentScreenSize || appStyle._dependentScreenSize - } else if (isObject(this.__props[className])) { + } else if (isObject(this.__mpxProxy.props[className])) { // externalClasses必定以对象形式传递下来 - mergeResult(this.__props[className]) + mergeResult(this.__mpxProxy.props[className]) } }) } diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index 55fac7ea0c..b1a2240cfe 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -639,6 +639,11 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { }) } const validProps = Object.assign({}, rawOptions.props, rawOptions.properties) + if (global.__externalClasses && global.__externalClasses.length > 0) { + global.__externalClasses.forEach((name) => { + validProps[name] = null + }) + } const { hasDescendantRelation, hasAncestorRelation } = checkRelation(rawOptions) if (rawOptions.methods) rawOptions.methods = wrapMethodsWithErrorHandling(rawOptions.methods) const defaultOptions = memo(forwardRef((props, ref) => { diff --git a/packages/webpack-plugin/lib/loader.js b/packages/webpack-plugin/lib/loader.js index cfb6f030a5..89c7960e20 100644 --- a/packages/webpack-plugin/lib/loader.js +++ b/packages/webpack-plugin/lib/loader.js @@ -38,7 +38,7 @@ module.exports = function (content) { return content } const { resourcePath, queryObj } = parseRequest(this.resource) - + const externalClasses = mpx.externalClasses || [] const packageRoot = queryObj.packageRoot || mpx.currentPackageRoot const packageName = packageRoot || 'main' const independent = queryObj.independent @@ -178,6 +178,7 @@ module.exports = function (content) { originalUsingComponents, componentGenerics, autoScope, + externalClasses, callback }) } diff --git a/packages/webpack-plugin/lib/react/index.js b/packages/webpack-plugin/lib/react/index.js index 4673d270f0..9969a34dd4 100644 --- a/packages/webpack-plugin/lib/react/index.js +++ b/packages/webpack-plugin/lib/react/index.js @@ -24,6 +24,7 @@ module.exports = function ({ originalUsingComponents, componentGenerics, autoScope, + externalClasses, callback }) { if (ctorType === 'app' && !queryObj.isApp) { @@ -95,7 +96,8 @@ module.exports = function ({ wxsModuleMap: templateRes.wxsModuleMap, localComponentsMap: jsonRes.localComponentsMap, localPagesMap: jsonRes.localPagesMap, - rnConfig + rnConfig, + externalClasses }, callback) } ], (err, scriptRes) => { diff --git a/packages/webpack-plugin/lib/react/processScript.js b/packages/webpack-plugin/lib/react/processScript.js index 9b88cb9b3a..d0d640b6fc 100644 --- a/packages/webpack-plugin/lib/react/processScript.js +++ b/packages/webpack-plugin/lib/react/processScript.js @@ -15,7 +15,8 @@ module.exports = function (script, { localPagesMap, rnConfig, componentGenerics, - genericsInfo + genericsInfo, + externalClasses }, callback) { const { appInfo, i18n } = loaderContext.getMpx() @@ -49,7 +50,7 @@ import { getComponent, getAsyncSuspense } from ${stringifyRequest(loaderContext, jsonConfig, rnConfig }) - output += buildGlobalParams({ moduleId, scriptSrcMode, loaderContext, isProduction, ctorType, jsonConfig, componentsMap, pagesMap, firstPage, hasApp }) + output += buildGlobalParams({ moduleId, scriptSrcMode, loaderContext, isProduction, ctorType, jsonConfig, componentsMap, pagesMap, firstPage, hasApp, externalClasses }) output += getRequireScript({ ctorType, script, loaderContext }) output += `export default global.__mpxOptionsMap[${JSON.stringify(moduleId)}]\n` } else { diff --git a/packages/webpack-plugin/lib/react/script-helper.js b/packages/webpack-plugin/lib/react/script-helper.js index a86e3ad72d..8347251dd2 100644 --- a/packages/webpack-plugin/lib/react/script-helper.js +++ b/packages/webpack-plugin/lib/react/script-helper.js @@ -150,7 +150,8 @@ function buildGlobalParams ({ firstPage, outputPath, genericsInfo, - hasApp + hasApp, + externalClasses }) { let content = '' if (ctorType === 'app') { @@ -165,6 +166,7 @@ global.__style = ${JSON.stringify(jsonConfig.style || 'v1')} global.__mpxPageConfig = ${JSON.stringify(jsonConfig.window)} global.__appComponentsMap = ${shallowStringify(componentsMap)} global.__preloadRule = ${JSON.stringify(jsonConfig.preloadRule)} +global.__externalClasses = ${JSON.stringify(externalClasses || [])} global.currentInject.pagesMap = ${shallowStringify(pagesMap)} global.currentInject.firstPage = ${JSON.stringify(firstPage)}\n` } else { From 098ec295e1203c8c1afc2e768981c09b7e0f8d80 Mon Sep 17 00:00:00 2001 From: mackwang Date: Fri, 31 Jul 2026 18:57:50 +0800 Subject: [PATCH 04/17] fix(core): update RN external class styles reactively --- .../core/src/platform/builtInMixins/styleHelperMixin.ios.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index cf403bb6fb..fc171d5989 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -374,9 +374,9 @@ export default function styleHelperMixin () { } else if (appStyle = global.__getAppClassStyle?.(className)) { mergeToLayer(appStyle._layer || 'app', appStyle, getMediaStyle(appStyle._media)) this.__dependentScreenSize = this.__dependentScreenSize || appStyle._dependentScreenSize - } else if (isObject(this.__props[className])) { + } else if (isObject(this.__mpxProxy.props[className])) { // externalClasses必定以对象形式传递下来 - mergeToLayer('normal', this.__props[className]) + mergeToLayer('normal', this.__mpxProxy.props[className]) } }) From fa43f6d1d336235a941d6c133712d7af026efcbb Mon Sep 17 00:00:00 2001 From: mackwang Date: Fri, 4 Sep 2026 13:45:39 +0800 Subject: [PATCH 05/17] fix(rn): update responsive styles with window dimensions --- .agents/skills/mpx2rn/SKILL.md | 2 +- .../mpx2rn/references/rn-style-practice.md | 2 +- .../mpx2rn/references/rn-style-reference.md | 17 +- docs-vitepress/guide/rn/application-api.md | 6 +- docs-vitepress/guide/rn/style.md | 17 +- packages/core/@types/index.d.ts | 2 +- .../common/styleHelperMixin.ios.spec.js | 149 ++++++++++++++++++ .../builtInMixins/styleHelperMixin.ios.js | 84 +++++----- .../platform/patch/getDefaultOptions.ios.js | 15 +- .../webpack-plugin/lib/react/style-helper.js | 8 +- .../test/platform/wx/style/style-rn.spec.js | 35 ++++ 11 files changed, 260 insertions(+), 77 deletions(-) create mode 100644 packages/core/__tests__/common/styleHelperMixin.ios.spec.js diff --git a/.agents/skills/mpx2rn/SKILL.md b/.agents/skills/mpx2rn/SKILL.md index 6f9d3f6392..bf9b97ba57 100644 --- a/.agents/skills/mpx2rn/SKILL.md +++ b/.agents/skills/mpx2rn/SKILL.md @@ -2,7 +2,7 @@ name: mpx2rn description: Mpx 跨端输出 RN(简称 Mpx2RN 或 Mpx2DRN)的开发适配指南,覆盖模板、脚本、样式、JSON 配置四大维度。当用户进行 Mpx2RN 相关任务时强制调用,包括但不限于:技术方案设计、页面 / 组件的开发迭代、旧项目跨端适配改造、编译和运行时报错排查、Code Review 等。当用户问题不涉及 Mpx2RN 时不应调用,如 Mpx 小程序开发问题,RN 原生开发问题、Mpx2Web 相关问题等。 metadata: - version: "2.12.7" + version: "2.12.8" author: donghongping --- diff --git a/.agents/skills/mpx2rn/references/rn-style-practice.md b/.agents/skills/mpx2rn/references/rn-style-practice.md index 52dfffe611..1abaa77989 100644 --- a/.agents/skills/mpx2rn/references/rn-style-practice.md +++ b/.agents/skills/mpx2rn/references/rn-style-practice.md @@ -692,7 +692,7 @@ export default { RN 不支持 `rem` 和 `em` 单位。需将其转换为 `rpx` 以实现响应式布局。 -**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(规定屏幕宽为 750rpx)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如: +**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(RN 中规定 Window 宽度为 750rpx)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如: - 若设定 `1rem = 100px` (基于 750px 设计稿),则 `1rem = 100rpx`。 - 若基于浏览器默认字号 (`16px`),则 `1rem = 32rpx` (1px = 2rpx)。 diff --git a/.agents/skills/mpx2rn/references/rn-style-reference.md b/.agents/skills/mpx2rn/references/rn-style-reference.md index 6e02481b89..1b9b98becb 100644 --- a/.agents/skills/mpx2rn/references/rn-style-reference.md +++ b/.agents/skills/mpx2rn/references/rn-style-reference.md @@ -168,15 +168,15 @@ Mpx 在 RN 平台支持多种 CSS 单位,并在运行时进行转换。 | 单位 | 说明 | 转换规则 | | --- | --- | --- | | `px` | 绝对像素 | 直接转换为 RN 的无单位数值 | -| `rpx` | 响应式像素 | `rpx值 × 屏幕宽度 / 750` | +| `rpx` | 响应式像素 | `rpx值 × window.width / 750` | | `%` | 百分比 | 转换为字符串形式(如 `'50%'`),由 RN 原生支持或框架处理 | -| `vw` | 视口宽度百分比 | `vw值 × 屏幕宽度 / 100` | -| `vh` | 视口高度百分比 | `vh值 × 屏幕高度 / 100` | +| `vw` | 视口宽度百分比 | `vw值 × window.width / 100` | +| `vh` | 视口高度百分比 | `vh值 × window.height / 100` | | `hairlineWidth` | RN 特有极细线 | `StyleSheet.hairlineWidth` | #### 样式计算基准与自定义 -`rpx`、`vw`、`vh` 的计算默认基于运行时的 `screen.width` 和 `screen.height`。 +`rpx`、`vw`、`vh` 与媒体查询的计算默认基于运行时的 `window.width` 和 `window.height`。 同时支持通过运行时配置 `Mpx.config.rnConfig.customDimensions` 自定义样式计算基准: @@ -188,18 +188,15 @@ mpx.config.rnConfig = Object.assign({}, mpx.config.rnConfig, { const nextWindow = Object.assign({}, dimensions.window, { height: dimensions.window.height - 44 }) - const nextScreen = Object.assign({}, dimensions.screen, { - height: dimensions.screen.height - 44 - }) return { window: nextWindow, - screen: nextScreen + screen: dimensions.screen } } }) ``` -配置生效后,`rpx`、`vw`、`vh` 会按自定义后的 `screen` 宽高进行计算。 +配置生效后,`rpx`、`vw`、`vh` 与媒体查询会按自定义后的 `window` 宽高进行计算。 #### 百分比计算规则 @@ -408,7 +405,7 @@ Mpx 在 RN 平台支持 `@media` 规则,但能力受限。 **限制:** -- 媒体查询中的宽度条件仅支持 `px` 单位,并基于运行时 `screen.width` 判断。 +- 媒体查询中的宽度条件仅支持 `px` 单位,并基于运行时 `window.width` 判断。 - 不支持 `width` 精确匹配、`height`、`orientation`、`all` 等其他特性。 ### 动画支持 diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index c16e0e29ca..c63a2e9929 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -474,11 +474,11 @@ mpx.config.rnConfig.disablePageTransition = true (dimensions: { window: ScaledSize; screen: ScaledSize }) => { window: ScaledSize; screen: ScaledSize } | void ``` -在某些情况下,我们可能不希望当前应用全屏展示,Mpx 内部基于 ScreenWidth 与 ScreenHeight 作为 rpx、vh、vw、媒体查询、onResize等特性的依赖内容,此时可在 `mpx.config.rnConfig.customDimensions` 中自定义 screen 尺寸信息来得到想要的渲染效果。 +在某些情况下,我们可能不希望当前应用全屏展示。Mpx 内部基于 `window.width` 与 `window.height` 计算 `rpx` / `vh` / `vw` 与媒体查询,并在 Window 尺寸变化时触发相关的响应式更新与 `onResize`。此时可在 `mpx.config.rnConfig.customDimensions` 中自定义 window 尺寸信息,得到想要的渲染效果。 -可在此方法中返回修改后的 dimensions,如果无返回或返回 undefined,则以入参作为返回值 +可在此方法中返回修改后的 dimensions,如果无返回或返回 `undefined`,则使用原始入参。 -例如: 在折叠屏中我们期望只在其中一半屏上展示,可在 customDimensions 中判断当前是否为折叠屏展开状态,如果是则将 ScreenWidth 设置为原来的一半。 +例如,折叠屏展开时如果期望应用只在一半窗口中展示,可在 `customDimensions` 中将 `window.width` 设为原来的一半。 ### 前后台切换 {#app-state-change} diff --git a/docs-vitepress/guide/rn/style.md b/docs-vitepress/guide/rn/style.md index bb71d6b346..be4cf2dfe2 100644 --- a/docs-vitepress/guide/rn/style.md +++ b/docs-vitepress/guide/rn/style.md @@ -103,19 +103,14 @@ Mpx 转 RN 支持以下单位,部分单位在特定情况下存在使用限制 |------|---------|----------| | `%` | ✅ 支持 | 百分比单位参考 [百分比单位说明](#percentage-unit-explanation) | | `px` | ✅ 支持 | 绝对像素单位 | -| `rpx` | ✅ 支持 | 响应式像素,根据屏幕宽度动态计算 | -| `vh` | ✅ 支持 | 相对于视口的高度 | -| `vw` | ✅ 支持 | 相对视口的宽度 | +| `rpx` | ✅ 支持 | 响应式像素,根据 Window 宽度动态计算 | +| `vh` | ✅ 支持 | Window 高度的百分比 | +| `vw` | ✅ 支持 | Window 宽度的百分比 | -> [!tip] vh 单位使用注意 +> [!tip] Window 尺寸说明 > -> - **问题**:使用系统默认导航栏时,`vh` 的计算基准可能会发生变化 -> - 页面首次加载:`100vh = 屏幕总高度` -> - 状态更新后:`100vh = 屏幕高度 - 导航栏高度` -> -> - **影响**:可能导致布局在运行时突然变化 -> -> - **建议**:如需使用 `vh` 单位,推荐配合自定义导航栏使用,以确保计算基准始终一致 +> `rpx` / `vw` / `vh` 和媒体查询均使用 React Native `Dimensions.get('window')` 的尺寸。Window 尺寸变化时,依赖这些能力的组件会重新计算样式。 + ### 百分比单位说明 {#percentage-unit-explanation} RN 原生较多属性不支持百分比,或对百分比的支持存在 bug(如 `font-size`、`translate` 等),但这些属性在编写 Web/小程序代码时使用较多,所以框架进行了抹平支持。 diff --git a/packages/core/@types/index.d.ts b/packages/core/@types/index.d.ts index 01d4b387c6..786a4fe324 100644 --- a/packages/core/@types/index.d.ts +++ b/packages/core/@types/index.d.ts @@ -476,7 +476,7 @@ export interface RnConfig { }) => void /** - * 自定义屏幕尺寸信息,用于 mpx style 渲染等依赖尺寸的功能。 + * 自定义窗口和屏幕尺寸信息,用于 mpx style 渲染等依赖尺寸的功能。 * * @param dimensions 包含 window 和 screen 的尺寸信息 * @returns 返回修改后的尺寸对象,或 void 表示不修改 diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js new file mode 100644 index 0000000000..5b8feb2da9 --- /dev/null +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -0,0 +1,149 @@ +const mockDimensions = { + window: { width: 360, height: 640 }, + screen: { width: 720, height: 1280 } +} +let mockDimensionsChangeHandler + +jest.mock('react-native', () => ({ + StyleSheet: { + hairlineWidth: 0.5 + }, + Dimensions: { + get: jest.fn(type => type === 'window' + ? { width: 360, height: 640 } + : { width: 720, height: 1280 }), + addEventListener: jest.fn((event, handler) => { + mockDimensionsChangeHandler = handler + }) + } +}), { virtual: true }) + +jest.mock('@mpxjs/utils', () => ({ + isObject: value => value !== null && typeof value === 'object', + isArray: Array.isArray, + dash2hump: value => value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), + cached: fn => fn, + isEmptyObject: value => Object.keys(value).length === 0, + hasOwn: (value, key) => Object.prototype.hasOwnProperty.call(value, key), + getFocusedNavigation: jest.fn() +})) + +jest.mock('../../src/observer/reactive', () => ({ + reactive: value => value +})) + +jest.mock('@mpxjs/perf', () => ({ + scopeStart: jest.fn(), + scopeEnd: jest.fn() +})) + +jest.mock('../../src/index', () => ({ + __esModule: true, + default: { + config: {} + } +})) + +// eslint-disable-next-line import/first +import styleHelperMixin from '../../src/platform/builtInMixins/styleHelperMixin.ios' + +describe('RN styleHelperMixin dimensions', () => { + beforeEach(() => { + global.__mpx_perf_framework__ = false + global.__mpxAppDimensionsInfo.window = mockDimensions.window + global.__mpxAppDimensionsInfo.screen = mockDimensions.screen + global.__mpxSizeCount = 0 + global.__classCaches = new Set() + }) + + it('converts responsive units with window dimensions', () => { + expect(global.__formatValue('750rpx')).toBe(360) + expect(global.__formatValue('100vw')).toBe(360) + expect(global.__formatValue('100vh')).toBe(640) + }) + + it('tracks window dependency for dynamic responsive unit styles', () => { + const context = { + __pageId: 'page', + __mpxProxy: { props: {} }, + __getSizeCount: jest.fn() + } + + const result = styleHelperMixin().methods.__getStyle.call(context, '', '', '', { + width: '750rpx' + }) + + expect(result.width).toBe(360) + expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + }) + + it('updates responsive styles when window dimensions change', () => { + const cache = { clear: jest.fn() } + global.__classCaches.add(cache) + + mockDimensionsChangeHandler({ + window: { width: 400, height: 700 }, + screen: mockDimensions.screen + }) + + expect(cache.clear).toHaveBeenCalledTimes(1) + expect(global.__mpxSizeCount).toBe(1) + expect(global.__formatValue('750rpx')).toBe(400) + + mockDimensionsChangeHandler({ + window: { width: 400, height: 700 }, + screen: { width: 800, height: 1400 } + }) + + expect(cache.clear).toHaveBeenCalledTimes(1) + expect(global.__mpxSizeCount).toBe(1) + }) + + it('matches media queries with window width', () => { + const style = { + color: 'red', + _media: [{ + options: { minWidth: 500 }, + value: { opacity: 1 } + }] + } + const context = { + __pageId: 'page', + __mpxProxy: { props: {} }, + __getClassStyle: jest.fn(() => style), + __getSizeCount: jest.fn() + } + + const result = styleHelperMixin().methods.__getStyle.call(context, 'responsive') + + expect(result.color).toBe('red') + expect(result.opacity).toBeUndefined() + expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + }) + + it('matches min/max media queries only within the inclusive range', () => { + const style = { + color: 'red', + _media: [{ + options: { minWidth: 600, maxWidth: 900 }, + value: { color: 'green' } + }] + } + const context = { + __pageId: 'page', + __mpxProxy: { props: {} }, + __getClassStyle: jest.fn(() => style), + __getSizeCount: jest.fn() + } + const getColorAtWidth = width => { + global.__mpxAppDimensionsInfo.window = { width, height: 640 } + return styleHelperMixin().methods.__getStyle.call(context, 'responsive').color + } + + expect(getColorAtWidth(599)).toBe('red') + expect(getColorAtWidth(600)).toBe('green') + expect(getColorAtWidth(750)).toBe('green') + expect(getColorAtWidth(900)).toBe('green') + expect(getColorAtWidth(901)).toBe('red') + }) +}) diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index e8ce66d5c6..ea62568a1b 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -13,20 +13,20 @@ global.__mpxPageSizeCountMap = reactive({}) global.__GCC = function (className, classMap, classMapValueCache) { if (!classMapValueCache.has(className)) { - const originalDependentScreenSize = dependentScreenSize - dependentScreenSize = false + const originalDependentWindowSize = dependentWindowSize + dependentWindowSize = false const styleObj = classMap[className]?.(formatValue) if (!styleObj) { - dependentScreenSize = originalDependentScreenSize + dependentWindowSize = originalDependentWindowSize return } - // 使用不可枚举属性记录屏幕尺寸依赖,避免该内部标记被合并到 RN 样式中 - Object.defineProperty(styleObj, '_dependentScreenSize', { - value: dependentScreenSize + // 使用不可枚举属性记录窗口尺寸依赖,避免该内部标记被合并到 RN 样式中 + Object.defineProperty(styleObj, '_dependentWindowSize', { + value: dependentWindowSize }) - dependentScreenSize = dependentScreenSize || originalDependentScreenSize + dependentWindowSize = dependentWindowSize || originalDependentWindowSize classMapValueCache.set(className, styleObj) } @@ -43,16 +43,16 @@ function useDimensionsInfo (dimensions) { global.__mpxAppDimensionsInfo.screen = dimensions.screen } -function getPageSize (window = global.__mpxAppDimensionsInfo.screen) { +function getWindowSize (window = global.__mpxAppDimensionsInfo.window) { return window.width + 'x' + window.height } Dimensions.addEventListener('change', ({ window, screen }) => { - const oldScreen = getPageSize(global.__mpxAppDimensionsInfo.screen) + const oldWindowSize = getWindowSize() useDimensionsInfo({ window, screen }) - // 对比 screen 高宽是否存在变化 - if (getPageSize(screen) === oldScreen) return + // 对比自定义处理后的 window 高宽是否存在变化 + if (getWindowSize() === oldWindowSize) return global.__classCaches?.forEach(cache => cache?.clear()) @@ -69,21 +69,20 @@ Dimensions.addEventListener('change', ({ window, screen }) => { } }) -// TODO: 1 目前测试鸿蒙下折叠屏screen固定为展开状态下屏幕尺寸,仅window会变化,且window包含状态栏高度 -// TODO: 2 存在部分安卓折叠屏机型在折叠/展开切换时,Dimensions监听到的width/height尺寸错误,并触发多次问题 +// TODO: 存在部分安卓折叠屏机型在折叠/展开切换时,Dimensions 监听到的 width/height 尺寸错误,并触发多次问题 function rpx (value) { - const screenInfo = global.__mpxAppDimensionsInfo.screen + const windowInfo = global.__mpxAppDimensionsInfo.window // rn 单位 dp = 1(css)px = 1 物理像素 * pixelRatio(像素比) - // px = rpx * (750 / 屏幕宽度) - return value * screenInfo.width / 750 + // px = rpx * (窗口宽度 / 750) + return value * windowInfo.width / 750 } function vw (value) { - const screenInfo = global.__mpxAppDimensionsInfo.screen - return value * screenInfo.width / 100 + const windowInfo = global.__mpxAppDimensionsInfo.window + return value * windowInfo.width / 100 } function vh (value) { - const screenInfo = global.__mpxAppDimensionsInfo.screen - return value * screenInfo.height / 100 + const windowInfo = global.__mpxAppDimensionsInfo.window + return value * windowInfo.height / 100 } const unit = { @@ -94,13 +93,13 @@ const unit = { const empty = {} -// 记录style是否依赖屏幕尺寸 -let dependentScreenSize = false +// 记录 style 是否依赖窗口尺寸 +let dependentWindowSize = false const isNum = (v) => !isNaN(+v) function formatValue (value, unitType) { if (!dimensionsInfoInitialized) useDimensionsInfo(global.__mpxAppDimensionsInfo) if (unitType && typeof unit[unitType] === 'function') { - dependentScreenSize = true + dependentWindowSize = true return unit[unitType](+value) } if (value === 'hairlineWidth') { @@ -111,6 +110,7 @@ function formatValue (value, unitType) { if (!matched[2] || matched[2] === 'px') { return +matched[1] } else { + dependentWindowSize = true return unit[matched[2]](+matched[1]) } } @@ -244,17 +244,19 @@ function isNativeStyle (style) { function getMediaStyle (media) { if (!media || !media.length) return {} - const { width } = global.__mpxAppDimensionsInfo.screen + dependentWindowSize = true + const { width } = global.__mpxAppDimensionsInfo.window return media.reduce((styleObj, item) => { const { options = {}, value = {} } = item const { minWidth, maxWidth } = options - if (!isNaN(minWidth) && !isNaN(maxWidth) && width >= minWidth && width <= maxWidth) { - Object.assign(styleObj, value) - } else if (!isNaN(minWidth) && width >= minWidth) { - Object.assign(styleObj, value) - } else if (!isNaN(maxWidth) && width <= maxWidth) { - Object.assign(styleObj, value) - } + const hasMinWidth = !isNaN(minWidth) + const hasMaxWidth = !isNaN(maxWidth) + const matched = hasMinWidth && hasMaxWidth + ? width >= minWidth && width <= maxWidth + : hasMinWidth + ? width >= minWidth + : hasMaxWidth && width <= maxWidth + if (matched) Object.assign(styleObj, value) return styleObj }, {}) } @@ -346,7 +348,7 @@ export default function styleHelperMixin () { if (__mpx_perf_framework__) idTotal = perf.scopeStart('instance:render:getStyle') // 重置依赖标记 - dependentScreenSize = false + dependentWindowSize = false const isNativeStaticStyle = staticStyle && isNativeStyle(staticStyle) const { mergeToLayer, mergeToLayerWithStyles, genResult } = createLayer(isNativeStaticStyle) @@ -362,18 +364,18 @@ export default function styleHelperMixin () { let localStyle, appStyle, unoStyle, unoVarStyle if (localStyle = this.__getClassStyle?.(className)) { mergeToLayer(localStyle._layer || 'normal', localStyle, getMediaStyle(localStyle._media)) - // class style 计算可能触发缓存,需要单独在结果中记录是否依赖屏幕尺寸,不能直接使用全局变量。 - this.__dependentScreenSize = this.__dependentScreenSize || localStyle._dependentScreenSize + // class style 计算可能触发缓存,需要单独在结果中记录是否依赖窗口尺寸,不能直接使用全局变量。 + this.__dependentWindowSize = this.__dependentWindowSize || localStyle._dependentWindowSize } else if (unoStyle = global.__getUnoStyle?.(className)) { mergeToLayer(unoStyle._layer || 'uno', unoStyle, getMediaStyle(unoStyle._media)) - this.__dependentScreenSize = this.__dependentScreenSize || unoStyle._dependentScreenSize + this.__dependentWindowSize = this.__dependentWindowSize || unoStyle._dependentWindowSize if (unoStyle.transform || unoStyle.filter) needAddUnoPreflight = true } else if (unoVarStyle = global.__getUnoVarStyle?.(className)) { mergeToLayer('important', unoVarStyle) - this.__dependentScreenSize = this.__dependentScreenSize || unoVarStyle._dependentScreenSize + this.__dependentWindowSize = this.__dependentWindowSize || unoVarStyle._dependentWindowSize } else if (appStyle = global.__getAppClassStyle?.(className)) { mergeToLayer(appStyle._layer || 'app', appStyle, getMediaStyle(appStyle._media)) - this.__dependentScreenSize = this.__dependentScreenSize || appStyle._dependentScreenSize + this.__dependentWindowSize = this.__dependentWindowSize || appStyle._dependentWindowSize } else if (isObject(this.__mpxProxy.props[className])) { // externalClasses必定以对象形式传递下来 mergeToLayer('normal', this.__mpxProxy.props[className]) @@ -383,7 +385,7 @@ export default function styleHelperMixin () { if (needAddUnoPreflight) { const unoPreflightStyle = global.__getAppClassStyle?.('__uno_preflight') mergeToLayer('preflight', unoPreflightStyle) - this.__dependentScreenSize = this.__dependentScreenSize || unoPreflightStyle._dependentScreenSize + this.__dependentWindowSize = this.__dependentWindowSize || unoPreflightStyle._dependentWindowSize } if (__mpx_perf_framework__) perf.scopeEnd(idClass) @@ -416,9 +418,9 @@ export default function styleHelperMixin () { const isEmpty = isNativeStaticStyle ? !result.length : isEmptyObject(result) - // 仅在依赖屏幕尺寸时才触发__getSizeCount进行相应式关联,避免屏幕尺寸变化时不必要的性能损耗 - this.__dependentScreenSize = this.__dependentScreenSize || dependentScreenSize - if (this.__dependentScreenSize) { + // 仅在依赖窗口尺寸时才触发 __getSizeCount 进行响应式关联,避免窗口尺寸变化时不必要的性能损耗 + this.__dependentWindowSize = this.__dependentWindowSize || dependentWindowSize + if (this.__dependentWindowSize) { this.__getSizeCount() } if (__mpx_perf_framework__) perf.scopeEnd(idTotal) diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index c6083426dc..3910fc57d6 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -415,19 +415,20 @@ function usePageEffect (mpxProxy, pageId, type) { const hasShowHook = hasPageHook(mpxProxy, [ONSHOW, 'show']) const hasHideHook = hasPageHook(mpxProxy, [ONHIDE, 'hide']) const hasResizeHook = hasPageHook(mpxProxy, [ONRESIZE, 'resize']) - if (hasShowHook || hasHideHook || hasResizeHook) { + // Page 即使没有注册页面生命周期,也需要监听 show 来追平后台期间错过的尺寸版本。 + // Component 仍仅在声明了对应生命周期时监听,避免无意义的 watcher。 + if (type === 'page' || hasShowHook || hasHideHook || hasResizeHook) { if (hasOwn(pageStatusMap, pageId)) { unWatch = watch(() => pageStatusMap[pageId], (newVal) => { if (newVal === 'show' || newVal === 'hide') { - triggerPageStatusHook(mpxProxy, newVal) - // 仅在尺寸确实变化时才触发resize事件 - triggerResizeEvent(mpxProxy, sizeRef) - - // 如果当前全局size与pagesize不一致,在show之后触发一次resize事件 + // 后台页面重新显示时先追平尺寸版本,驱动依赖 rpx/vw/vh 和媒体查询的组件刷新。 if (type === 'page' && newVal === 'show' && global.__mpxPageSizeCountMap[pageId] !== global.__mpxSizeCount) { - // 刷新__mpxPageSizeCountMap, 每个页面仅会执行一次,直接驱动render刷新 global.__mpxPageSizeCountMap[pageId] = global.__mpxSizeCount } + + triggerPageStatusHook(mpxProxy, newVal) + // 仅在尺寸确实变化时才触发resize事件 + triggerResizeEvent(mpxProxy, sizeRef) } else if (/^resize/.test(newVal)) { triggerResizeEvent(mpxProxy, sizeRef) } diff --git a/packages/webpack-plugin/lib/react/style-helper.js b/packages/webpack-plugin/lib/react/style-helper.js index 3f614927a2..b970d170c2 100644 --- a/packages/webpack-plugin/lib/react/style-helper.js +++ b/packages/webpack-plugin/lib/react/style-helper.js @@ -180,9 +180,13 @@ function getClassMap ({ styles, filename, inputFileSystem, mode, srcMode, ctorTy if (classMapKeys.length) { classMapKeys.forEach((key) => { if (Object.keys(classMapValue).length) { - // set css defalut value const val = classMap[key] || {} - classMap[key] = Object.assign(val, classMapValue) + classMap[key] = val + + // Media declarations only take effect when their query matches. + if (!isMedia) { + Object.assign(val, classMapValue) + } if (layer) { classMap[key]._layer = layer diff --git a/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js b/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js index 575fc31094..b62f1ec815 100644 --- a/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js +++ b/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js @@ -90,6 +90,41 @@ describe('React Native style validation for CSS variables', () => { }) }) + describe('Media queries', () => { + test('keeps media declarations out of the default class style', () => { + const config = createConfig() + const result = getClassMap({ + content: ` + .media-box { + width: 280px; + background-color: red; + } + @media (min-width: 600px) { + .media-box { + width: 520px; + background-color: green; + } + } + `, + filename: 'test.css', + ...config + }) + + expect(result['media-box']).toEqual({ + width: '280', + backgroundColor: '"red"', + _media: [{ + options: { minWidth: 600 }, + value: { + width: '520', + backgroundColor: '"green"' + } + }] + }) + expect(config.error).not.toHaveBeenCalled() + }) + }) + describe('CSS variable fallback validation', () => { test('should filter out letter-spacing with invalid "normal" fallback', () => { const css = '.text { letter-spacing: var(--x, normal); }' From 718e8e6c8071430ebd77bfe0f2befb53fe87af06 Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 10 Sep 2026 15:30:17 +0800 Subject: [PATCH 06/17] feat(rn): allow configuring responsive style dimensions --- .agents/skills/mpx2rn/SKILL.md | 2 +- .../mpx2rn/references/rn-script-reference.md | 2 + .../mpx2rn/references/rn-style-reference.md | 16 +++-- docs-vitepress/guide/rn/application-api.md | 20 +++++- docs-vitepress/guide/rn/style.md | 10 +-- packages/core/@types/index.d.ts | 6 ++ .../common/styleHelperMixin.ios.spec.js | 68 ++++++++++++++++++- packages/core/src/index.js | 3 +- .../builtInMixins/styleHelperMixin.ios.js | 29 ++++---- 9 files changed, 127 insertions(+), 29 deletions(-) diff --git a/.agents/skills/mpx2rn/SKILL.md b/.agents/skills/mpx2rn/SKILL.md index bf9b97ba57..917ab6cc8b 100644 --- a/.agents/skills/mpx2rn/SKILL.md +++ b/.agents/skills/mpx2rn/SKILL.md @@ -2,7 +2,7 @@ name: mpx2rn description: Mpx 跨端输出 RN(简称 Mpx2RN 或 Mpx2DRN)的开发适配指南,覆盖模板、脚本、样式、JSON 配置四大维度。当用户进行 Mpx2RN 相关任务时强制调用,包括但不限于:技术方案设计、页面 / 组件的开发迭代、旧项目跨端适配改造、编译和运行时报错排查、Code Review 等。当用户问题不涉及 Mpx2RN 时不应调用,如 Mpx 小程序开发问题,RN 原生开发问题、Mpx2Web 相关问题等。 metadata: - version: "2.12.8" + version: "2.12.9" author: donghongping --- diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 7c301c9379..1b6ce834a7 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -693,6 +693,7 @@ import Mpx from "@mpxjs/core" // 须在 createApp 与页面脚本执行前完成赋值 Mpx.config.rnConfig = { + styleDimensionsBase: "window", parseAppProps(props) { return { initialRouteName: "pages/index", @@ -721,6 +722,7 @@ Mpx.config.rnConfig = { | `onStateChange` | 导航 state 变化时回调。 | | `disablePageTransition` | 为 `true` 时禁用 RN 页面转场动画,框架内部映射为 `animation: "none"`。 | | `disableAppStateListener` | 为 `true` 时不注册 `AppState` 监听(避免与宿主 App 重复)。 | +| `styleDimensionsBase` | `rpx` / `vw` / `vh` 与媒体查询使用的尺寸基准,可选 `"window"` 或 `"screen"`,默认为 `"window"`。 | | `openTypeHandler` | 对象,注册 `button` 组件在 RN 上 `open-type` 的容器侧实现,未注册对应键时点击会告警。 | | `openTypeHandler.onShareAppMessage` | 对应模板中 `open-type="share"`:框架会先取当前页 `onShareAppMessage` 的返回(含与默认 `title` / `path` 的合并及可选 `promise` 异步结果),再调用本回调,入参为 `{ title, path, imageUrl? }`,由宿主调起系统分享等能力。 | | `openTypeHandler.onUserInfo` | 对应模板中 `open-type="getUserInfo"`:由宿主实现获取用户信息的逻辑,结果需满足按钮侧对 `bindgetuserinfo` 的约定(以 `@mpxjs/webpack-plugin` 中 `mpx-button` 运行时为准)。 | diff --git a/.agents/skills/mpx2rn/references/rn-style-reference.md b/.agents/skills/mpx2rn/references/rn-style-reference.md index 1b9b98becb..a4400066a9 100644 --- a/.agents/skills/mpx2rn/references/rn-style-reference.md +++ b/.agents/skills/mpx2rn/references/rn-style-reference.md @@ -168,15 +168,21 @@ Mpx 在 RN 平台支持多种 CSS 单位,并在运行时进行转换。 | 单位 | 说明 | 转换规则 | | --- | --- | --- | | `px` | 绝对像素 | 直接转换为 RN 的无单位数值 | -| `rpx` | 响应式像素 | `rpx值 × window.width / 750` | +| `rpx` | 响应式像素 | 默认按 `rpx值 × window.width / 750` 转换 | | `%` | 百分比 | 转换为字符串形式(如 `'50%'`),由 RN 原生支持或框架处理 | -| `vw` | 视口宽度百分比 | `vw值 × window.width / 100` | -| `vh` | 视口高度百分比 | `vh值 × window.height / 100` | +| `vw` | 视口宽度百分比 | 默认按 `vw值 × window.width / 100` 转换 | +| `vh` | 视口高度百分比 | 默认按 `vh值 × window.height / 100` 转换 | | `hairlineWidth` | RN 特有极细线 | `StyleSheet.hairlineWidth` | #### 样式计算基准与自定义 -`rpx`、`vw`、`vh` 与媒体查询的计算默认基于运行时的 `window.width` 和 `window.height`。 +`rpx`、`vw`、`vh` 与媒体查询的计算默认基于运行时的 `window.width` 和 `window.height`。如需保持旧版本基于 Screen 尺寸计算的效果,可将 `Mpx.config.rnConfig.styleDimensionsBase` 设置为 `"screen"`: + +```javascript +Mpx.config.rnConfig.styleDimensionsBase = "screen" +``` + +该配置支持 `"window"` 和 `"screen"`,默认值为 `"window"`。所选尺寸发生变化时,依赖响应式单位或媒体查询的组件会重新计算样式。 同时支持通过运行时配置 `Mpx.config.rnConfig.customDimensions` 自定义样式计算基准: @@ -196,7 +202,7 @@ mpx.config.rnConfig = Object.assign({}, mpx.config.rnConfig, { }) ``` -配置生效后,`rpx`、`vw`、`vh` 与媒体查询会按自定义后的 `window` 宽高进行计算。 +配置生效后,`rpx`、`vw`、`vh` 与媒体查询会按自定义后的 `styleDimensionsBase` 对应尺寸进行计算。 #### 百分比计算规则 diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index c63a2e9929..64e18393f3 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -468,17 +468,33 @@ mpx.config.rnConfig.disablePageTransition = true ### 折叠屏适配 {#foldable-screen-adaption} +#### mpx.config.rnConfig.styleDimensionsBase + +```ts +'window' | 'screen' +``` + +控制 `rpx` / `vw` / `vh` 与媒体查询使用的尺寸基准,默认值为 `'window'`。 + +如果需要保持旧版本基于屏幕尺寸计算的效果,可配置为 `'screen'`: + +```js +mpx.config.rnConfig.styleDimensionsBase = 'screen' +``` + +配置后,响应式单位和媒体查询将使用 `Dimensions.get('screen')` 的宽高,并在 Screen 尺寸变化时重新计算相关样式。 + #### mpx.config.rnConfig.customDimensions ```ts (dimensions: { window: ScaledSize; screen: ScaledSize }) => { window: ScaledSize; screen: ScaledSize } | void ``` -在某些情况下,我们可能不希望当前应用全屏展示。Mpx 内部基于 `window.width` 与 `window.height` 计算 `rpx` / `vh` / `vw` 与媒体查询,并在 Window 尺寸变化时触发相关的响应式更新与 `onResize`。此时可在 `mpx.config.rnConfig.customDimensions` 中自定义 window 尺寸信息,得到想要的渲染效果。 +在某些情况下,我们可能不希望当前应用全屏展示。此时可在 `mpx.config.rnConfig.customDimensions` 中自定义 window 或 screen 尺寸信息,`rpx` / `vh` / `vw` 与媒体查询会使用 `styleDimensionsBase` 指定的尺寸进行计算,并在该尺寸变化时触发相关的响应式更新与 `onResize`。 可在此方法中返回修改后的 dimensions,如果无返回或返回 `undefined`,则使用原始入参。 -例如,折叠屏展开时如果期望应用只在一半窗口中展示,可在 `customDimensions` 中将 `window.width` 设为原来的一半。 +例如,使用默认的 `styleDimensionsBase: 'window'` 时,折叠屏展开后如果期望应用只在一半窗口中展示,可在 `customDimensions` 中将 `window.width` 设为原来的一半。 ### 前后台切换 {#app-state-change} diff --git a/docs-vitepress/guide/rn/style.md b/docs-vitepress/guide/rn/style.md index be4cf2dfe2..41b360b6a4 100644 --- a/docs-vitepress/guide/rn/style.md +++ b/docs-vitepress/guide/rn/style.md @@ -103,13 +103,13 @@ Mpx 转 RN 支持以下单位,部分单位在特定情况下存在使用限制 |------|---------|----------| | `%` | ✅ 支持 | 百分比单位参考 [百分比单位说明](#percentage-unit-explanation) | | `px` | ✅ 支持 | 绝对像素单位 | -| `rpx` | ✅ 支持 | 响应式像素,根据 Window 宽度动态计算 | -| `vh` | ✅ 支持 | Window 高度的百分比 | -| `vw` | ✅ 支持 | Window 宽度的百分比 | +| `rpx` | ✅ 支持 | 响应式像素,默认根据 Window 宽度动态计算 | +| `vh` | ✅ 支持 | 默认使用 Window 高度计算 | +| `vw` | ✅ 支持 | 默认使用 Window 宽度计算 | -> [!tip] Window 尺寸说明 +> [!tip] 尺寸基准说明 > -> `rpx` / `vw` / `vh` 和媒体查询均使用 React Native `Dimensions.get('window')` 的尺寸。Window 尺寸变化时,依赖这些能力的组件会重新计算样式。 +> `rpx` / `vw` / `vh` 和媒体查询默认使用 React Native `Dimensions.get('window')` 的尺寸。可将 `mpx.config.rnConfig.styleDimensionsBase` 设置为 `'screen'`,恢复基于 Screen 尺寸计算的旧版本效果。所选尺寸变化时,依赖这些能力的组件会重新计算样式。 ### 百分比单位说明 {#percentage-unit-explanation} diff --git a/packages/core/@types/index.d.ts b/packages/core/@types/index.d.ts index 786a4fe324..ccc99b0143 100644 --- a/packages/core/@types/index.d.ts +++ b/packages/core/@types/index.d.ts @@ -485,6 +485,12 @@ export interface RnConfig { dimensions: T ) => T | void + /** + * rpx、vw、vh 与媒体查询使用的尺寸基准。 + * @default 'window' + */ + styleDimensionsBase?: 'window' | 'screen' + /** * 加载并执行异步分包的方法。 * diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index 5b8feb2da9..d4b544a626 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -40,10 +40,16 @@ jest.mock('@mpxjs/perf', () => ({ jest.mock('../../src/index', () => ({ __esModule: true, default: { - config: {} + config: { + rnConfig: { + styleDimensionsBase: 'window' + } + } } })) +// eslint-disable-next-line import/first +import Mpx from '../../src/index' // eslint-disable-next-line import/first import styleHelperMixin from '../../src/platform/builtInMixins/styleHelperMixin.ios' @@ -54,15 +60,28 @@ describe('RN styleHelperMixin dimensions', () => { global.__mpxAppDimensionsInfo.screen = mockDimensions.screen global.__mpxSizeCount = 0 global.__classCaches = new Set() + Mpx.config.rnConfig = { + styleDimensionsBase: 'window' + } }) it('converts responsive units with window dimensions', () => { + Mpx.config.rnConfig = {} + expect(global.__formatValue('750rpx')).toBe(360) expect(global.__formatValue('100vw')).toBe(360) expect(global.__formatValue('100vh')).toBe(640) }) - it('tracks window dependency for dynamic responsive unit styles', () => { + it('converts responsive units with screen dimensions when configured', () => { + Mpx.config.rnConfig.styleDimensionsBase = 'screen' + + expect(global.__formatValue('750rpx')).toBe(720) + expect(global.__formatValue('100vw')).toBe(720) + expect(global.__formatValue('100vh')).toBe(1280) + }) + + it('tracks dimensions dependency for dynamic responsive unit styles', () => { const context = { __pageId: 'page', __mpxProxy: { props: {} }, @@ -99,6 +118,29 @@ describe('RN styleHelperMixin dimensions', () => { expect(global.__mpxSizeCount).toBe(1) }) + it('updates responsive styles only when screen dimensions change in screen mode', () => { + Mpx.config.rnConfig.styleDimensionsBase = 'screen' + const cache = { clear: jest.fn() } + global.__classCaches.add(cache) + + mockDimensionsChangeHandler({ + window: { width: 400, height: 700 }, + screen: mockDimensions.screen + }) + + expect(cache.clear).not.toHaveBeenCalled() + expect(global.__mpxSizeCount).toBe(0) + + mockDimensionsChangeHandler({ + window: { width: 400, height: 700 }, + screen: { width: 800, height: 1400 } + }) + + expect(cache.clear).toHaveBeenCalledTimes(1) + expect(global.__mpxSizeCount).toBe(1) + expect(global.__formatValue('750rpx')).toBe(800) + }) + it('matches media queries with window width', () => { const style = { color: 'red', @@ -121,6 +163,28 @@ describe('RN styleHelperMixin dimensions', () => { expect(context.__getSizeCount).toHaveBeenCalledTimes(1) }) + it('matches media queries with screen width when configured', () => { + Mpx.config.rnConfig.styleDimensionsBase = 'screen' + const style = { + color: 'red', + _media: [{ + options: { minWidth: 600 }, + value: { color: 'green' } + }] + } + const context = { + __pageId: 'page', + __mpxProxy: { props: {} }, + __getClassStyle: jest.fn(() => style), + __getSizeCount: jest.fn() + } + + const result = styleHelperMixin().methods.__getStyle.call(context, 'responsive') + + expect(result.color).toBe('green') + expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + }) + it('matches min/max media queries only within the inclusive range', () => { const style = { color: 'red', diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 830dd60e0a..6291aaf1f8 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -150,7 +150,8 @@ Mpx.config = { */ rnConfig: { defaultBoxSizing: 'content-box', - disablePageTransition: false + disablePageTransition: false, + styleDimensionsBase: 'window' } } diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index ea62568a1b..be16a1b899 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -43,16 +43,21 @@ function useDimensionsInfo (dimensions) { global.__mpxAppDimensionsInfo.screen = dimensions.screen } -function getWindowSize (window = global.__mpxAppDimensionsInfo.window) { - return window.width + 'x' + window.height +function getStyleDimensions () { + const dimensionsType = Mpx.config.rnConfig?.styleDimensionsBase === 'screen' ? 'screen' : 'window' + return global.__mpxAppDimensionsInfo[dimensionsType] +} + +function getStyleDimensionsSize (dimensions = getStyleDimensions()) { + return dimensions.width + 'x' + dimensions.height } Dimensions.addEventListener('change', ({ window, screen }) => { - const oldWindowSize = getWindowSize() + const oldStyleDimensionsSize = getStyleDimensionsSize() useDimensionsInfo({ window, screen }) - // 对比自定义处理后的 window 高宽是否存在变化 - if (getWindowSize() === oldWindowSize) return + // 对比自定义处理后的样式计算尺寸高宽是否存在变化 + if (getStyleDimensionsSize() === oldStyleDimensionsSize) return global.__classCaches?.forEach(cache => cache?.clear()) @@ -71,18 +76,16 @@ Dimensions.addEventListener('change', ({ window, screen }) => { // TODO: 存在部分安卓折叠屏机型在折叠/展开切换时,Dimensions 监听到的 width/height 尺寸错误,并触发多次问题 function rpx (value) { - const windowInfo = global.__mpxAppDimensionsInfo.window + const dimensionsInfo = getStyleDimensions() // rn 单位 dp = 1(css)px = 1 物理像素 * pixelRatio(像素比) - // px = rpx * (窗口宽度 / 750) - return value * windowInfo.width / 750 + // px = rpx * (样式计算宽度 / 750) + return value * dimensionsInfo.width / 750 } function vw (value) { - const windowInfo = global.__mpxAppDimensionsInfo.window - return value * windowInfo.width / 100 + return value * getStyleDimensions().width / 100 } function vh (value) { - const windowInfo = global.__mpxAppDimensionsInfo.window - return value * windowInfo.height / 100 + return value * getStyleDimensions().height / 100 } const unit = { @@ -245,7 +248,7 @@ function isNativeStyle (style) { function getMediaStyle (media) { if (!media || !media.length) return {} dependentWindowSize = true - const { width } = global.__mpxAppDimensionsInfo.window + const { width } = getStyleDimensions() return media.reduce((styleObj, item) => { const { options = {}, value = {} } = item const { minWidth, maxWidth } = options From cd4bfd4d6c6bc17626e52dce8e1a4f217ab793b8 Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 10 Sep 2026 16:40:39 +0800 Subject: [PATCH 07/17] feat(rn): add manual dimensions change notification Integrate PR #2485 and address its review feedback. --- .agents/skills/mpx2rn/SKILL.md | 2 +- .../mpx2rn/references/rn-script-reference.md | 5 +++ .../mpx2rn/references/rn-style-reference.md | 2 + docs-vitepress/guide/rn/application-api.md | 21 ++++++++++ packages/core/@types/global.d.ts | 5 +++ packages/core/@types/index.d.ts | 6 +-- .../common/styleHelperMixin.ios.spec.js | 42 ++++++++++++++++++- .../builtInMixins/styleHelperMixin.ios.js | 27 ++++++++---- 8 files changed, 97 insertions(+), 13 deletions(-) diff --git a/.agents/skills/mpx2rn/SKILL.md b/.agents/skills/mpx2rn/SKILL.md index 917ab6cc8b..8458476c28 100644 --- a/.agents/skills/mpx2rn/SKILL.md +++ b/.agents/skills/mpx2rn/SKILL.md @@ -2,7 +2,7 @@ name: mpx2rn description: Mpx 跨端输出 RN(简称 Mpx2RN 或 Mpx2DRN)的开发适配指南,覆盖模板、脚本、样式、JSON 配置四大维度。当用户进行 Mpx2RN 相关任务时强制调用,包括但不限于:技术方案设计、页面 / 组件的开发迭代、旧项目跨端适配改造、编译和运行时报错排查、Code Review 等。当用户问题不涉及 Mpx2RN 时不应调用,如 Mpx 小程序开发问题,RN 原生开发问题、Mpx2Web 相关问题等。 metadata: - version: "2.12.9" + version: "2.12.10" author: donghongping --- diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 1b6ce834a7..ee0d80cca8 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -753,6 +753,9 @@ console.log(stack.length) // 宿主或测试场景下可手动触发应用前后台逻辑 setAppShow() setAppHide() + +// 宿主容器尺寸变化后,手动通知响应式样式重新计算 +notifyDimensionsChange() ``` | API | 说明 | @@ -761,11 +764,13 @@ setAppHide() | `getCurrentPages()` | 返回当前导航栈中已映射的页面实例列表(顺序与路由 state 相关)。 | | `setAppShow()` | 手动触发应用「进入前台」逻辑,驱动已注册的 `onShow`。 | | `setAppHide()` | 手动触发应用「进入后台」逻辑,驱动已注册的 `onHide`。 | +| `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `styleDimensionsBase` 指定的尺寸判断是否刷新。 | #### 注意事项 - 勿在 App 构造函数执行完成前依赖 `getApp()` 内业务字段已赋值完毕;与路由相关的初始化宜放在 `onLaunch` / `onShow`。 - `getCurrentPages()` 依赖 React Navigation 焦点与 `__mpxPagesMap`,与原生小程序栈细节不完全相同。 +- `notifyDimensionsChange` 在 RN 样式运行时模块加载后注入,应在应用开始渲染后调用。 --- diff --git a/.agents/skills/mpx2rn/references/rn-style-reference.md b/.agents/skills/mpx2rn/references/rn-style-reference.md index a4400066a9..5b3cec2544 100644 --- a/.agents/skills/mpx2rn/references/rn-style-reference.md +++ b/.agents/skills/mpx2rn/references/rn-style-reference.md @@ -204,6 +204,8 @@ mpx.config.rnConfig = Object.assign({}, mpx.config.rnConfig, { 配置生效后,`rpx`、`vw`、`vh` 与媒体查询会按自定义后的 `styleDimensionsBase` 对应尺寸进行计算。 +宿主容器尺寸变化但 React Native 未派发 Dimensions change 事件,或需要基于外部状态重新执行 `customDimensions` 时,可调用全局方法 `notifyDimensionsChange(dimensions?)` 主动通知框架。省略参数时会通过 `Dimensions.get` 重新读取当前原始尺寸。 + #### 百分比计算规则 跨端输出 RN 时, 部分百分比单位原生支持,而部分百分比单位则由框架进行计算抹平实现,详情如下: diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 64e18393f3..1cf49faca1 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -496,6 +496,27 @@ mpx.config.rnConfig.styleDimensionsBase = 'screen' 例如,使用默认的 `styleDimensionsBase: 'window'` 时,折叠屏展开后如果期望应用只在一半窗口中展示,可在 `customDimensions` 中将 `window.width` 设为原来的一半。 +#### notifyDimensionsChange + +```ts +(dimensions?: { window: ScaledSize; screen: ScaledSize }) => void +``` + +主动通知框架 Dimensions 发生变化,使 `rpx` / `vw` / `vh`、媒体查询和 `onResize` 等依赖尺寸的能力重新计算。传入 `dimensions` 时使用传入值;不传参数时通过 `Dimensions.get('window')` 与 `Dimensions.get('screen')` 获取当前原始尺寸。两种方式都会重新执行 `rnConfig.customDimensions`,并根据 `styleDimensionsBase` 指定的尺寸判断是否触发刷新。 + +```js +// 宿主容器尺寸发生变化后,重新读取当前 Dimensions +notifyDimensionsChange() + +// 也可以显式传入尺寸 +notifyDimensionsChange({ + window: nextWindow, + screen: nextScreen +}) +``` + +该全局方法在 RN 样式运行时模块加载后注入,应在应用开始渲染后调用。框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 + ### 前后台切换 {#app-state-change} diff --git a/packages/core/@types/global.d.ts b/packages/core/@types/global.d.ts index 51dbff4263..dce272e145 100644 --- a/packages/core/@types/global.d.ts +++ b/packages/core/@types/global.d.ts @@ -14,3 +14,8 @@ declare module '*?resolve' { declare let setAppShow: () => void declare let setAppHide: () => void + +declare let notifyDimensionsChange: (dimensions?: { + window: import('react-native').ScaledSize + screen: import('react-native').ScaledSize +}) => void diff --git a/packages/core/@types/index.d.ts b/packages/core/@types/index.d.ts index ccc99b0143..ec691140b8 100644 --- a/packages/core/@types/index.d.ts +++ b/packages/core/@types/index.d.ts @@ -481,9 +481,9 @@ export interface RnConfig { * @param dimensions 包含 window 和 screen 的尺寸信息 * @returns 返回修改后的尺寸对象,或 void 表示不修改 */ - customDimensions?: ( - dimensions: T - ) => T | void + customDimensions?: ( + dimensions: { window: ScaledSize; screen: ScaledSize } + ) => { window: ScaledSize; screen: ScaledSize } | void /** * rpx、vw、vh 与媒体查询使用的尺寸基准。 diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index d4b544a626..a47720754a 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -65,6 +65,25 @@ describe('RN styleHelperMixin dimensions', () => { } }) + it('does not apply customDimensions twice when notified before the first style calculation', () => { + const customDimensions = jest.fn((dimensions) => { + dimensions.window.width /= 2 + return dimensions + }) + Mpx.config.rnConfig.customDimensions = customDimensions + const dimensions = { + window: { width: 800, height: 640 }, + screen: mockDimensions.screen + } + + global.notifyDimensionsChange(dimensions) + + expect(dimensions.window.width).toBe(800) + expect(global.__mpxAppDimensionsInfo.window.width).toBe(400) + expect(global.__formatValue('750rpx')).toBe(400) + expect(customDimensions).toHaveBeenCalledTimes(1) + }) + it('converts responsive units with window dimensions', () => { Mpx.config.rnConfig = {} @@ -123,7 +142,7 @@ describe('RN styleHelperMixin dimensions', () => { const cache = { clear: jest.fn() } global.__classCaches.add(cache) - mockDimensionsChangeHandler({ + global.notifyDimensionsChange({ window: { width: 400, height: 700 }, screen: mockDimensions.screen }) @@ -131,7 +150,7 @@ describe('RN styleHelperMixin dimensions', () => { expect(cache.clear).not.toHaveBeenCalled() expect(global.__mpxSizeCount).toBe(0) - mockDimensionsChangeHandler({ + global.notifyDimensionsChange({ window: { width: 400, height: 700 }, screen: { width: 800, height: 1400 } }) @@ -141,6 +160,25 @@ describe('RN styleHelperMixin dimensions', () => { expect(global.__formatValue('750rpx')).toBe(800) }) + it('reads current Dimensions and reapplies customDimensions when notified without arguments', () => { + let widthOffset = 10 + const customDimensions = jest.fn((dimensions) => { + dimensions.window.width -= widthOffset + return dimensions + }) + Mpx.config.rnConfig.customDimensions = customDimensions + + global.notifyDimensionsChange() + + expect(global.__mpxAppDimensionsInfo.window.width).toBe(350) + + widthOffset = 20 + global.notifyDimensionsChange() + + expect(global.__mpxAppDimensionsInfo.window.width).toBe(340) + expect(customDimensions).toHaveBeenCalledTimes(2) + }) + it('matches media queries with window width', () => { const style = { color: 'red', diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index be16a1b899..0a89982971 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -5,8 +5,8 @@ import { reactive } from '../../observer/reactive' import Mpx from '../../index' global.__mpxAppDimensionsInfo = { - window: Dimensions.get('window'), - screen: Dimensions.get('screen') + window: Object.assign({}, Dimensions.get('window')), + screen: Object.assign({}, Dimensions.get('screen')) } global.__mpxSizeCount = 0 global.__mpxPageSizeCountMap = reactive({}) @@ -34,7 +34,7 @@ global.__GCC = function (className, classMap, classMapValueCache) { } let dimensionsInfoInitialized = false -function useDimensionsInfo (dimensions) { +function applyDimensionsInfo (dimensions) { dimensionsInfoInitialized = true if (typeof Mpx.config.rnConfig?.customDimensions === 'function') { dimensions = Mpx.config.rnConfig.customDimensions(dimensions) || dimensions @@ -52,9 +52,18 @@ function getStyleDimensionsSize (dimensions = getStyleDimensions()) { return dimensions.width + 'x' + dimensions.height } -Dimensions.addEventListener('change', ({ window, screen }) => { +function onDimensionsChange (dimensions) { const oldStyleDimensionsSize = getStyleDimensionsSize() - useDimensionsInfo({ window, screen }) + if (!dimensions) { + dimensions = { + window: Dimensions.get('window'), + screen: Dimensions.get('screen') + } + } + applyDimensionsInfo({ + window: Object.assign({}, dimensions.window), + screen: Object.assign({}, dimensions.screen) + }) // 对比自定义处理后的样式计算尺寸高宽是否存在变化 if (getStyleDimensionsSize() === oldStyleDimensionsSize) return @@ -72,7 +81,11 @@ Dimensions.addEventListener('change', ({ window, screen }) => { global.__mpxPageStatusMap[navigation.pageId] = `resize${global.__mpxSizeCount}` } } -}) +} + +global.notifyDimensionsChange = onDimensionsChange + +Dimensions.addEventListener('change', onDimensionsChange) // TODO: 存在部分安卓折叠屏机型在折叠/展开切换时,Dimensions 监听到的 width/height 尺寸错误,并触发多次问题 function rpx (value) { @@ -100,7 +113,7 @@ const empty = {} let dependentWindowSize = false const isNum = (v) => !isNaN(+v) function formatValue (value, unitType) { - if (!dimensionsInfoInitialized) useDimensionsInfo(global.__mpxAppDimensionsInfo) + if (!dimensionsInfoInitialized) applyDimensionsInfo(global.__mpxAppDimensionsInfo) if (unitType && typeof unit[unitType] === 'function') { dependentWindowSize = true return unit[unitType](+value) From 6b1ab5583f286ea2158fe9c1a204ca4edf9979ba Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 10 Sep 2026 16:51:57 +0800 Subject: [PATCH 08/17] docs(rn): clarify dimensions notification timing --- .agents/skills/mpx2rn/references/rn-script-reference.md | 2 +- docs-vitepress/guide/rn/application-api.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index ee0d80cca8..2a1d7999f3 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -770,7 +770,7 @@ notifyDimensionsChange() - 勿在 App 构造函数执行完成前依赖 `getApp()` 内业务字段已赋值完毕;与路由相关的初始化宜放在 `onLaunch` / `onShow`。 - `getCurrentPages()` 依赖 React Navigation 焦点与 `__mpxPagesMap`,与原生小程序栈细节不完全相同。 -- `notifyDimensionsChange` 在 RN 样式运行时模块加载后注入,应在应用开始渲染后调用。 +- `notifyDimensionsChange` 在 RN 样式运行时模块加载时注入;`@mpxjs/core` 的 RN 运行时完成加载后即可调用,不需要等待 `createApp`、页面挂载或首次样式计算。 --- diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 1cf49faca1..cb365083d8 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -515,7 +515,7 @@ notifyDimensionsChange({ }) ``` -该全局方法在 RN 样式运行时模块加载后注入,应在应用开始渲染后调用。框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 +该全局方法在 RN 样式运行时模块加载时注入。只要 `@mpxjs/core` 的 RN 运行时已完成加载即可调用,不需要等待 `createApp`、页面挂载或首次样式计算。框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 ### 前后台切换 {#app-state-change} From 64fe2f3331460188e2f33e6e9e8d1c774af34c82 Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 10 Sep 2026 16:54:00 +0800 Subject: [PATCH 09/17] docs(rn): clarify dimensions API availability --- .agents/skills/mpx2rn/references/rn-script-reference.md | 2 +- docs-vitepress/guide/rn/application-api.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 2a1d7999f3..40f1936286 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -770,7 +770,7 @@ notifyDimensionsChange() - 勿在 App 构造函数执行完成前依赖 `getApp()` 内业务字段已赋值完毕;与路由相关的初始化宜放在 `onLaunch` / `onShow`。 - `getCurrentPages()` 依赖 React Navigation 焦点与 `__mpxPagesMap`,与原生小程序栈细节不完全相同。 -- `notifyDimensionsChange` 在 RN 样式运行时模块加载时注入;`@mpxjs/core` 的 RN 运行时完成加载后即可调用,不需要等待 `createApp`、页面挂载或首次样式计算。 +- RN 的 JS Bundle 启动后,执行到 `app.mpx` 业务脚本时 `notifyDimensionsChange` 已经可用,不需要等待页面或组件渲染;如果由原生宿主触发,应等待 RN JS Bundle 加载完成。 --- diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index cb365083d8..50025995a8 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -515,7 +515,7 @@ notifyDimensionsChange({ }) ``` -该全局方法在 RN 样式运行时模块加载时注入。只要 `@mpxjs/core` 的 RN 运行时已完成加载即可调用,不需要等待 `createApp`、页面挂载或首次样式计算。框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 +可以简单理解为:RN 的 JS Bundle 启动后,执行到 `app.mpx` 业务脚本时,该全局方法已经可用,不需要等待页面或组件渲染。如果由原生宿主触发,应等待 RN JS Bundle 加载完成。框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 ### 前后台切换 {#app-state-change} From b20f42c0534fa4c13ddb1b156474b61b8727d247 Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 10 Sep 2026 16:56:19 +0800 Subject: [PATCH 10/17] docs(rn): remove dimensions API timing note --- .agents/skills/mpx2rn/references/rn-script-reference.md | 1 - docs-vitepress/guide/rn/application-api.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 40f1936286..678e09dc4e 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -770,7 +770,6 @@ notifyDimensionsChange() - 勿在 App 构造函数执行完成前依赖 `getApp()` 内业务字段已赋值完毕;与路由相关的初始化宜放在 `onLaunch` / `onShow`。 - `getCurrentPages()` 依赖 React Navigation 焦点与 `__mpxPagesMap`,与原生小程序栈细节不完全相同。 -- RN 的 JS Bundle 启动后,执行到 `app.mpx` 业务脚本时 `notifyDimensionsChange` 已经可用,不需要等待页面或组件渲染;如果由原生宿主触发,应等待 RN JS Bundle 加载完成。 --- diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 50025995a8..6659fd93a5 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -515,7 +515,7 @@ notifyDimensionsChange({ }) ``` -可以简单理解为:RN 的 JS Bundle 启动后,执行到 `app.mpx` 业务脚本时,该全局方法已经可用,不需要等待页面或组件渲染。如果由原生宿主触发,应等待 RN JS Bundle 加载完成。框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 +框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 ### 前后台切换 {#app-state-change} From d8352e39b4d13c43625dfa605c18ad55b5f02bf5 Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 10 Sep 2026 22:01:25 +0800 Subject: [PATCH 11/17] fix(rn): align dimensions base behavior --- .../mpx2rn/references/rn-script-reference.md | 6 +- .../mpx2rn/references/rn-style-practice.md | 2 +- .../mpx2rn/references/rn-style-reference.md | 8 +- docs-vitepress/guide/rn/application-api.md | 14 ++-- docs-vitepress/guide/rn/style.md | 2 +- packages/core/@types/index.d.ts | 4 +- .../__tests__/common/dimensionsHelper.spec.js | 83 +++++++++++++++++++ .../common/styleHelperMixin.ios.spec.js | 67 +++++++++++++-- packages/core/src/index.js | 2 +- .../builtInMixins/styleHelperMixin.ios.js | 13 +-- .../core/src/platform/dimensionsHelper.js | 46 ++++++++++ .../platform/patch/getDefaultOptions.ios.js | 37 +-------- 12 files changed, 218 insertions(+), 66 deletions(-) create mode 100644 packages/core/__tests__/common/dimensionsHelper.spec.js create mode 100644 packages/core/src/platform/dimensionsHelper.js diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 678e09dc4e..27f91d5ae3 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -693,7 +693,7 @@ import Mpx from "@mpxjs/core" // 须在 createApp 与页面脚本执行前完成赋值 Mpx.config.rnConfig = { - styleDimensionsBase: "window", + dimensionsBase: "window", parseAppProps(props) { return { initialRouteName: "pages/index", @@ -722,7 +722,7 @@ Mpx.config.rnConfig = { | `onStateChange` | 导航 state 变化时回调。 | | `disablePageTransition` | 为 `true` 时禁用 RN 页面转场动画,框架内部映射为 `animation: "none"`。 | | `disableAppStateListener` | 为 `true` 时不注册 `AppState` 监听(避免与宿主 App 重复)。 | -| `styleDimensionsBase` | `rpx` / `vw` / `vh` 与媒体查询使用的尺寸基准,可选 `"window"` 或 `"screen"`,默认为 `"window"`。 | +| `dimensionsBase` | `rpx` / `vw` / `vh`、媒体查询与 `onResize` 使用的尺寸基准,可选 `"window"` 或 `"screen"`,默认为 `"window"`。 | | `openTypeHandler` | 对象,注册 `button` 组件在 RN 上 `open-type` 的容器侧实现,未注册对应键时点击会告警。 | | `openTypeHandler.onShareAppMessage` | 对应模板中 `open-type="share"`:框架会先取当前页 `onShareAppMessage` 的返回(含与默认 `title` / `path` 的合并及可选 `promise` 异步结果),再调用本回调,入参为 `{ title, path, imageUrl? }`,由宿主调起系统分享等能力。 | | `openTypeHandler.onUserInfo` | 对应模板中 `open-type="getUserInfo"`:由宿主实现获取用户信息的逻辑,结果需满足按钮侧对 `bindgetuserinfo` 的约定(以 `@mpxjs/webpack-plugin` 中 `mpx-button` 运行时为准)。 | @@ -764,7 +764,7 @@ notifyDimensionsChange() | `getCurrentPages()` | 返回当前导航栈中已映射的页面实例列表(顺序与路由 state 相关)。 | | `setAppShow()` | 手动触发应用「进入前台」逻辑,驱动已注册的 `onShow`。 | | `setAppHide()` | 手动触发应用「进入后台」逻辑,驱动已注册的 `onHide`。 | -| `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `styleDimensionsBase` 指定的尺寸判断是否刷新。 | +| `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `dimensionsBase` 指定的尺寸判断是否刷新。 | #### 注意事项 diff --git a/.agents/skills/mpx2rn/references/rn-style-practice.md b/.agents/skills/mpx2rn/references/rn-style-practice.md index 1abaa77989..9bfee1fa37 100644 --- a/.agents/skills/mpx2rn/references/rn-style-practice.md +++ b/.agents/skills/mpx2rn/references/rn-style-practice.md @@ -692,7 +692,7 @@ export default { RN 不支持 `rem` 和 `em` 单位。需将其转换为 `rpx` 以实现响应式布局。 -**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(RN 中规定 Window 宽度为 750rpx)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如: +**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(RN 默认以 Window 宽度为 750rpx;配置 `dimensionsBase: "screen"` 时以 Screen 宽度为准)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如: - 若设定 `1rem = 100px` (基于 750px 设计稿),则 `1rem = 100rpx`。 - 若基于浏览器默认字号 (`16px`),则 `1rem = 32rpx` (1px = 2rpx)。 diff --git a/.agents/skills/mpx2rn/references/rn-style-reference.md b/.agents/skills/mpx2rn/references/rn-style-reference.md index 5b3cec2544..4358ee8bba 100644 --- a/.agents/skills/mpx2rn/references/rn-style-reference.md +++ b/.agents/skills/mpx2rn/references/rn-style-reference.md @@ -176,10 +176,10 @@ Mpx 在 RN 平台支持多种 CSS 单位,并在运行时进行转换。 #### 样式计算基准与自定义 -`rpx`、`vw`、`vh` 与媒体查询的计算默认基于运行时的 `window.width` 和 `window.height`。如需保持旧版本基于 Screen 尺寸计算的效果,可将 `Mpx.config.rnConfig.styleDimensionsBase` 设置为 `"screen"`: +`rpx`、`vw`、`vh` 与媒体查询的计算默认基于运行时的 `window.width` 和 `window.height`。如需保持旧版本基于 Screen 尺寸计算的效果,可将 `Mpx.config.rnConfig.dimensionsBase` 设置为 `"screen"`: ```javascript -Mpx.config.rnConfig.styleDimensionsBase = "screen" +Mpx.config.rnConfig.dimensionsBase = "screen" ``` 该配置支持 `"window"` 和 `"screen"`,默认值为 `"window"`。所选尺寸发生变化时,依赖响应式单位或媒体查询的组件会重新计算样式。 @@ -202,7 +202,7 @@ mpx.config.rnConfig = Object.assign({}, mpx.config.rnConfig, { }) ``` -配置生效后,`rpx`、`vw`、`vh` 与媒体查询会按自定义后的 `styleDimensionsBase` 对应尺寸进行计算。 +配置生效后,`rpx`、`vw`、`vh` 与媒体查询会按自定义后的 `dimensionsBase` 对应尺寸进行计算。 宿主容器尺寸变化但 React Native 未派发 Dimensions change 事件,或需要基于外部状态重新执行 `customDimensions` 时,可调用全局方法 `notifyDimensionsChange(dimensions?)` 主动通知框架。省略参数时会通过 `Dimensions.get` 重新读取当前原始尺寸。 @@ -413,7 +413,7 @@ Mpx 在 RN 平台支持 `@media` 规则,但能力受限。 **限制:** -- 媒体查询中的宽度条件仅支持 `px` 单位,并基于运行时 `window.width` 判断。 +- 媒体查询中的宽度条件仅支持 `px` 单位,并基于 `dimensionsBase` 选中尺寸的运行时 `width` 判断,默认使用 `window.width`。 - 不支持 `width` 精确匹配、`height`、`orientation`、`all` 等其他特性。 ### 动画支持 diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 6659fd93a5..238ff9a1b4 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -468,21 +468,21 @@ mpx.config.rnConfig.disablePageTransition = true ### 折叠屏适配 {#foldable-screen-adaption} -#### mpx.config.rnConfig.styleDimensionsBase +#### mpx.config.rnConfig.dimensionsBase ```ts 'window' | 'screen' ``` -控制 `rpx` / `vw` / `vh` 与媒体查询使用的尺寸基准,默认值为 `'window'`。 +控制 `rpx` / `vw` / `vh`、媒体查询与 `onResize` 使用的尺寸基准,默认值为 `'window'`。 如果需要保持旧版本基于屏幕尺寸计算的效果,可配置为 `'screen'`: ```js -mpx.config.rnConfig.styleDimensionsBase = 'screen' +mpx.config.rnConfig.dimensionsBase = 'screen' ``` -配置后,响应式单位和媒体查询将使用 `Dimensions.get('screen')` 的宽高,并在 Screen 尺寸变化时重新计算相关样式。 +配置后,响应式单位和媒体查询将使用 `Dimensions.get('screen')` 的宽高,并在 Screen 尺寸变化时重新计算相关样式及触发 `onResize`。 #### mpx.config.rnConfig.customDimensions @@ -490,11 +490,11 @@ mpx.config.rnConfig.styleDimensionsBase = 'screen' (dimensions: { window: ScaledSize; screen: ScaledSize }) => { window: ScaledSize; screen: ScaledSize } | void ``` -在某些情况下,我们可能不希望当前应用全屏展示。此时可在 `mpx.config.rnConfig.customDimensions` 中自定义 window 或 screen 尺寸信息,`rpx` / `vh` / `vw` 与媒体查询会使用 `styleDimensionsBase` 指定的尺寸进行计算,并在该尺寸变化时触发相关的响应式更新与 `onResize`。 +在某些情况下,我们可能不希望当前应用全屏展示。此时可在 `mpx.config.rnConfig.customDimensions` 中自定义 window 或 screen 尺寸信息,`rpx` / `vh` / `vw`、媒体查询与 `onResize` 会使用 `dimensionsBase` 指定的尺寸,并在该尺寸变化时触发相关更新。 可在此方法中返回修改后的 dimensions,如果无返回或返回 `undefined`,则使用原始入参。 -例如,使用默认的 `styleDimensionsBase: 'window'` 时,折叠屏展开后如果期望应用只在一半窗口中展示,可在 `customDimensions` 中将 `window.width` 设为原来的一半。 +例如,使用默认的 `dimensionsBase: 'window'` 时,折叠屏展开后如果期望应用只在一半窗口中展示,可在 `customDimensions` 中将 `window.width` 设为原来的一半。 #### notifyDimensionsChange @@ -502,7 +502,7 @@ mpx.config.rnConfig.styleDimensionsBase = 'screen' (dimensions?: { window: ScaledSize; screen: ScaledSize }) => void ``` -主动通知框架 Dimensions 发生变化,使 `rpx` / `vw` / `vh`、媒体查询和 `onResize` 等依赖尺寸的能力重新计算。传入 `dimensions` 时使用传入值;不传参数时通过 `Dimensions.get('window')` 与 `Dimensions.get('screen')` 获取当前原始尺寸。两种方式都会重新执行 `rnConfig.customDimensions`,并根据 `styleDimensionsBase` 指定的尺寸判断是否触发刷新。 +主动通知框架 Dimensions 发生变化,使 `rpx` / `vw` / `vh`、媒体查询和 `onResize` 等依赖尺寸的能力重新计算。传入 `dimensions` 时使用传入值;不传参数时通过 `Dimensions.get('window')` 与 `Dimensions.get('screen')` 获取当前原始尺寸。两种方式都会重新执行 `rnConfig.customDimensions`,并根据 `dimensionsBase` 指定的尺寸判断是否触发刷新。 ```js // 宿主容器尺寸发生变化后,重新读取当前 Dimensions diff --git a/docs-vitepress/guide/rn/style.md b/docs-vitepress/guide/rn/style.md index 41b360b6a4..62649e293c 100644 --- a/docs-vitepress/guide/rn/style.md +++ b/docs-vitepress/guide/rn/style.md @@ -109,7 +109,7 @@ Mpx 转 RN 支持以下单位,部分单位在特定情况下存在使用限制 > [!tip] 尺寸基准说明 > -> `rpx` / `vw` / `vh` 和媒体查询默认使用 React Native `Dimensions.get('window')` 的尺寸。可将 `mpx.config.rnConfig.styleDimensionsBase` 设置为 `'screen'`,恢复基于 Screen 尺寸计算的旧版本效果。所选尺寸变化时,依赖这些能力的组件会重新计算样式。 +> `rpx` / `vw` / `vh` 和媒体查询默认使用 React Native `Dimensions.get('window')` 的尺寸。可将 `mpx.config.rnConfig.dimensionsBase` 设置为 `'screen'`,恢复基于 Screen 尺寸计算的旧版本效果。所选尺寸变化时,依赖这些能力的组件会重新计算样式。 ### 百分比单位说明 {#percentage-unit-explanation} diff --git a/packages/core/@types/index.d.ts b/packages/core/@types/index.d.ts index ec691140b8..546b4f9e5e 100644 --- a/packages/core/@types/index.d.ts +++ b/packages/core/@types/index.d.ts @@ -486,10 +486,10 @@ export interface RnConfig { ) => { window: ScaledSize; screen: ScaledSize } | void /** - * rpx、vw、vh 与媒体查询使用的尺寸基准。 + * rpx、vw、vh、媒体查询与 onResize 使用的尺寸基准。 * @default 'window' */ - styleDimensionsBase?: 'window' | 'screen' + dimensionsBase?: 'window' | 'screen' /** * 加载并执行异步分包的方法。 diff --git a/packages/core/__tests__/common/dimensionsHelper.spec.js b/packages/core/__tests__/common/dimensionsHelper.spec.js new file mode 100644 index 0000000000..17ffd90bc7 --- /dev/null +++ b/packages/core/__tests__/common/dimensionsHelper.spec.js @@ -0,0 +1,83 @@ +jest.mock('../../src/index', () => ({ + __esModule: true, + default: { + config: { + rnConfig: { + dimensionsBase: 'window' + } + } + } +})) + +// eslint-disable-next-line import/first +import Mpx from '../../src/index' +// eslint-disable-next-line import/first +import { ONRESIZE } from '../../src/core/innerLifecycle' +// eslint-disable-next-line import/first +import { getSystemInfo, triggerResizeEvent } from '../../src/platform/dimensionsHelper' + +describe('RN dimensions helper', () => { + beforeEach(() => { + Mpx.config.rnConfig = { + dimensionsBase: 'window' + } + global.__mpxAppDimensionsInfo = { + window: { width: 360, height: 640 }, + screen: { width: 720, height: 1280 } + } + }) + + function createPageResizeContext () { + const target = { + onResize: jest.fn() + } + return { + target, + mpxProxy: { + options: { __type__: 'page' }, + target, + callHook: jest.fn() + }, + sizeRef: { + current: getSystemInfo() + } + } + } + + it('triggers onResize only when Window changes in window mode', () => { + const { target, mpxProxy, sizeRef } = createPageResizeContext() + + global.__mpxAppDimensionsInfo.screen = { width: 800, height: 1400 } + triggerResizeEvent(mpxProxy, sizeRef) + + expect(mpxProxy.callHook).not.toHaveBeenCalled() + expect(target.onResize).not.toHaveBeenCalled() + + global.__mpxAppDimensionsInfo.window = { width: 700, height: 400 } + triggerResizeEvent(mpxProxy, sizeRef) + + const systemInfo = getSystemInfo() + expect(systemInfo.deviceOrientation).toBe('landscape') + expect(mpxProxy.callHook).toHaveBeenCalledWith(ONRESIZE, [systemInfo]) + expect(target.onResize).toHaveBeenCalledWith(systemInfo) + }) + + it('triggers onResize only when Screen changes in screen mode', () => { + Mpx.config.rnConfig.dimensionsBase = 'screen' + const { target, mpxProxy, sizeRef } = createPageResizeContext() + + global.__mpxAppDimensionsInfo.window = { width: 700, height: 400 } + triggerResizeEvent(mpxProxy, sizeRef) + + expect(mpxProxy.callHook).not.toHaveBeenCalled() + expect(target.onResize).not.toHaveBeenCalled() + + global.__mpxAppDimensionsInfo.screen = { width: 1400, height: 800 } + triggerResizeEvent(mpxProxy, sizeRef) + + const systemInfo = getSystemInfo() + expect(systemInfo.deviceOrientation).toBe('landscape') + expect(mpxProxy.callHook).toHaveBeenCalledWith(ONRESIZE, [systemInfo]) + expect(target.onResize).toHaveBeenCalledWith(systemInfo) + }) +}) diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index a47720754a..4d66c14ef8 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -42,7 +42,7 @@ jest.mock('../../src/index', () => ({ default: { config: { rnConfig: { - styleDimensionsBase: 'window' + dimensionsBase: 'window' } } } @@ -61,7 +61,7 @@ describe('RN styleHelperMixin dimensions', () => { global.__mpxSizeCount = 0 global.__classCaches = new Set() Mpx.config.rnConfig = { - styleDimensionsBase: 'window' + dimensionsBase: 'window' } }) @@ -93,7 +93,7 @@ describe('RN styleHelperMixin dimensions', () => { }) it('converts responsive units with screen dimensions when configured', () => { - Mpx.config.rnConfig.styleDimensionsBase = 'screen' + Mpx.config.rnConfig.dimensionsBase = 'screen' expect(global.__formatValue('750rpx')).toBe(720) expect(global.__formatValue('100vw')).toBe(720) @@ -138,7 +138,7 @@ describe('RN styleHelperMixin dimensions', () => { }) it('updates responsive styles only when screen dimensions change in screen mode', () => { - Mpx.config.rnConfig.styleDimensionsBase = 'screen' + Mpx.config.rnConfig.dimensionsBase = 'screen' const cache = { clear: jest.fn() } global.__classCaches.add(cache) @@ -202,7 +202,7 @@ describe('RN styleHelperMixin dimensions', () => { }) it('matches media queries with screen width when configured', () => { - Mpx.config.rnConfig.styleDimensionsBase = 'screen' + Mpx.config.rnConfig.dimensionsBase = 'screen' const style = { color: 'red', _media: [{ @@ -248,4 +248,61 @@ describe('RN styleHelperMixin dimensions', () => { expect(getColorAtWidth(900)).toBe('green') expect(getColorAtWidth(901)).toBe('red') }) + + it('applies customDimensions before the first media-only style calculation', () => { + const originalGlobals = { + notifyDimensionsChange: global.notifyDimensionsChange, + GCC: global.__GCC, + formatValue: global.__formatValue, + dimensionsInfo: global.__mpxAppDimensionsInfo, + sizeCount: global.__mpxSizeCount, + pageSizeCountMap: global.__mpxPageSizeCountMap, + classCaches: global.__classCaches, + dimensionsChangeHandler: mockDimensionsChangeHandler + } + + try { + jest.isolateModules(() => { + const FreshMpx = require('../../src/index').default + const freshStyleHelperMixin = require('../../src/platform/builtInMixins/styleHelperMixin.ios').default + const customDimensions = jest.fn((dimensions) => { + dimensions.window.width /= 2 + return dimensions + }) + FreshMpx.config.rnConfig = { + dimensionsBase: 'window', + customDimensions + } + const style = { + color: 'red', + _media: [{ + options: { minWidth: 300 }, + value: { color: 'green' } + }] + } + const context = { + __pageId: 'page', + __mpxProxy: { props: {} }, + __getClassStyle: jest.fn(() => style), + __getSizeCount: jest.fn() + } + + const result = freshStyleHelperMixin().methods.__getStyle.call(context, 'responsive') + + expect(result.color).toBe('red') + expect(global.__mpxAppDimensionsInfo.window.width).toBe(180) + expect(customDimensions).toHaveBeenCalledTimes(1) + expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + }) + } finally { + global.notifyDimensionsChange = originalGlobals.notifyDimensionsChange + global.__GCC = originalGlobals.GCC + global.__formatValue = originalGlobals.formatValue + global.__mpxAppDimensionsInfo = originalGlobals.dimensionsInfo + global.__mpxSizeCount = originalGlobals.sizeCount + global.__mpxPageSizeCountMap = originalGlobals.pageSizeCountMap + global.__classCaches = originalGlobals.classCaches + mockDimensionsChangeHandler = originalGlobals.dimensionsChangeHandler + } + }) }) diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 6291aaf1f8..fdcb3d947c 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -151,7 +151,7 @@ Mpx.config = { rnConfig: { defaultBoxSizing: 'content-box', disablePageTransition: false, - styleDimensionsBase: 'window' + dimensionsBase: 'window' } } diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 0a89982971..450565cb3c 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -3,6 +3,7 @@ import * as perf from '@mpxjs/perf' import { StyleSheet, Dimensions } from 'react-native' import { reactive } from '../../observer/reactive' import Mpx from '../../index' +import { getDimensionsBase } from '../dimensionsHelper' global.__mpxAppDimensionsInfo = { window: Object.assign({}, Dimensions.get('window')), @@ -44,16 +45,15 @@ function applyDimensionsInfo (dimensions) { } function getStyleDimensions () { - const dimensionsType = Mpx.config.rnConfig?.styleDimensionsBase === 'screen' ? 'screen' : 'window' - return global.__mpxAppDimensionsInfo[dimensionsType] + return global.__mpxAppDimensionsInfo[getDimensionsBase()] } -function getStyleDimensionsSize (dimensions = getStyleDimensions()) { +function getDimensionsBaseSize (dimensions = getStyleDimensions()) { return dimensions.width + 'x' + dimensions.height } function onDimensionsChange (dimensions) { - const oldStyleDimensionsSize = getStyleDimensionsSize() + const oldDimensionsBaseSize = getDimensionsBaseSize() if (!dimensions) { dimensions = { window: Dimensions.get('window'), @@ -65,8 +65,8 @@ function onDimensionsChange (dimensions) { screen: Object.assign({}, dimensions.screen) }) - // 对比自定义处理后的样式计算尺寸高宽是否存在变化 - if (getStyleDimensionsSize() === oldStyleDimensionsSize) return + // 对比自定义处理后的基准尺寸高宽是否存在变化 + if (getDimensionsBaseSize() === oldDimensionsBaseSize) return global.__classCaches?.forEach(cache => cache?.clear()) @@ -260,6 +260,7 @@ function isNativeStyle (style) { function getMediaStyle (media) { if (!media || !media.length) return {} + if (!dimensionsInfoInitialized) applyDimensionsInfo(global.__mpxAppDimensionsInfo) dependentWindowSize = true const { width } = getStyleDimensions() return media.reduce((styleObj, item) => { diff --git a/packages/core/src/platform/dimensionsHelper.js b/packages/core/src/platform/dimensionsHelper.js new file mode 100644 index 0000000000..6aafc715e8 --- /dev/null +++ b/packages/core/src/platform/dimensionsHelper.js @@ -0,0 +1,46 @@ +import { ONRESIZE } from '../core/innerLifecycle' +import Mpx from '../index' + +export function getDimensionsBase () { + return Mpx.config.rnConfig?.dimensionsBase === 'screen' ? 'screen' : 'window' +} + +export function getSystemInfo () { + const windowDimensions = global.__mpxAppDimensionsInfo.window + const screenDimensions = global.__mpxAppDimensionsInfo.screen + const baseDimensions = global.__mpxAppDimensionsInfo[getDimensionsBase()] + return { + deviceOrientation: baseDimensions.width > baseDimensions.height ? 'landscape' : 'portrait', + size: { + screenWidth: screenDimensions.width, + screenHeight: screenDimensions.height, + windowWidth: windowDimensions.width, + windowHeight: windowDimensions.height + } + } +} + +export function triggerResizeEvent (mpxProxy, sizeRef) { + const oldSize = sizeRef.current.size + const systemInfo = getSystemInfo() + const newSize = systemInfo.size + const dimensionsBase = getDimensionsBase() + const widthKey = `${dimensionsBase}Width` + const heightKey = `${dimensionsBase}Height` + + if (oldSize && oldSize[widthKey] === newSize[widthKey] && oldSize[heightKey] === newSize[heightKey]) { + return + } + + Object.assign(sizeRef.current, systemInfo) + + const type = mpxProxy.options.__type__ + const target = mpxProxy.target + mpxProxy.callHook(ONRESIZE, [systemInfo]) + if (type === 'page') { + target.onResize && target.onResize(systemInfo) + } else { + const pageLifetimes = mpxProxy.options.pageLifetimes + pageLifetimes && typeof pageLifetimes.resize === 'function' && pageLifetimes.resize.call(target, systemInfo) + } +} diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index 3910fc57d6..c5371af538 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -19,20 +19,7 @@ import { PortalHost, useSafeAreaInsets, initialWindowMetrics } from '../env/navi import { useInnerHeaderHeight } from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-nav' import Mpx from '../../index' import * as perf from '@mpxjs/perf' - -function getSystemInfo () { - const windowDimensions = global.__mpxAppDimensionsInfo.window - const screenDimensions = global.__mpxAppDimensionsInfo.screen - return { - deviceOrientation: windowDimensions.width > windowDimensions.height ? 'landscape' : 'portrait', - size: { - screenWidth: screenDimensions.width, - screenHeight: screenDimensions.height, - windowWidth: windowDimensions.width, - windowHeight: windowDimensions.height - } - } -} +import { getSystemInfo, triggerResizeEvent } from '../dimensionsHelper' function createEffect (proxy, componentsMap) { const update = proxy.update = () => { @@ -385,28 +372,6 @@ const triggerPageStatusHook = (mpxProxy, event) => { } } -const triggerResizeEvent = (mpxProxy, sizeRef) => { - const oldSize = sizeRef.current.size - const systemInfo = getSystemInfo() - const newSize = systemInfo.size - - if (oldSize && oldSize.windowWidth === newSize.windowWidth && oldSize.windowHeight === newSize.windowHeight) { - return - } - - Object.assign(sizeRef.current, systemInfo) - - const type = mpxProxy.options.__type__ - const target = mpxProxy.target - mpxProxy.callHook(ONRESIZE, [systemInfo]) - if (type === 'page') { - target.onResize && target.onResize(systemInfo) - } else { - const pageLifetimes = mpxProxy.options.pageLifetimes - pageLifetimes && isFunction(pageLifetimes.resize) && pageLifetimes.resize.call(target, systemInfo) - } -} - function usePageEffect (mpxProxy, pageId, type) { const sizeRef = useRef(getSystemInfo()) From 370f3cdef60e873002adb0f90530e0e839f826ff Mon Sep 17 00:00:00 2001 From: mackwang Date: Fri, 11 Sep 2026 15:18:27 +0800 Subject: [PATCH 12/17] fix(core): expose processed style dimensions --- .../mpx2rn/references/rn-script-reference.md | 1 + docs-vitepress/guide/rn/application-api.md | 13 ++++++ packages/core/@types/global.d.ts | 2 + .../__tests__/common/dimensionsHelper.spec.js | 30 ++++++++++++-- .../common/styleHelperMixin.ios.spec.js | 15 ++++++- .../builtInMixins/styleHelperMixin.ios.js | 40 +++++-------------- .../core/src/platform/dimensionsHelper.js | 40 ++++++++++++++++++- 7 files changed, 106 insertions(+), 35 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 27f91d5ae3..7ab0512db2 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -765,6 +765,7 @@ notifyDimensionsChange() | `setAppShow()` | 手动触发应用「进入前台」逻辑,驱动已注册的 `onShow`。 | | `setAppHide()` | 手动触发应用「进入后台」逻辑,驱动已注册的 `onHide`。 | | `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `dimensionsBase` 指定的尺寸判断是否刷新。 | +| `getStyleDimensions()` | 返回经过 `customDimensions` 处理后、由 `dimensionsBase` 选中的当前只读 `ScaledSize` 副本;首次调用及更换 `customDimensions` 后会确保重新处理原始尺寸。 | #### 注意事项 diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 238ff9a1b4..e006fe643a 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -517,6 +517,19 @@ notifyDimensionsChange({ 框架会复制原始 Dimensions 后再交给 `customDimensions`,避免自定义逻辑直接修改 React Native 返回的原对象。 +#### getStyleDimensions + +```ts +() => ScaledSize +``` + +返回经过 `rnConfig.customDimensions` 处理后、由 `rnConfig.dimensionsBase` 选中的当前尺寸副本。修改返回值不会影响框架内部缓存。首次调用会确保完成尺寸初始化;如果后续更换 `customDimensions`,再次调用时会基于最近一次原始 Dimensions 重新处理。 + +```js +const dimensions = getStyleDimensions() +console.log(dimensions.width, dimensions.height) +``` + ### 前后台切换 {#app-state-change} diff --git a/packages/core/@types/global.d.ts b/packages/core/@types/global.d.ts index dce272e145..680daabc3b 100644 --- a/packages/core/@types/global.d.ts +++ b/packages/core/@types/global.d.ts @@ -19,3 +19,5 @@ declare let notifyDimensionsChange: (dimensions?: { window: import('react-native').ScaledSize screen: import('react-native').ScaledSize }) => void + +declare let getStyleDimensions: () => Readonly diff --git a/packages/core/__tests__/common/dimensionsHelper.spec.js b/packages/core/__tests__/common/dimensionsHelper.spec.js index 17ffd90bc7..e5e8aeade3 100644 --- a/packages/core/__tests__/common/dimensionsHelper.spec.js +++ b/packages/core/__tests__/common/dimensionsHelper.spec.js @@ -14,17 +14,41 @@ import Mpx from '../../src/index' // eslint-disable-next-line import/first import { ONRESIZE } from '../../src/core/innerLifecycle' // eslint-disable-next-line import/first -import { getSystemInfo, triggerResizeEvent } from '../../src/platform/dimensionsHelper' +import { getSystemInfo, initDimensionsInfo, triggerResizeEvent } from '../../src/platform/dimensionsHelper' describe('RN dimensions helper', () => { beforeEach(() => { Mpx.config.rnConfig = { dimensionsBase: 'window' } - global.__mpxAppDimensionsInfo = { + initDimensionsInfo({ window: { width: 360, height: 640 }, screen: { width: 720, height: 1280 } - } + }) + }) + + it('uses customDimensions for the initial onResize size snapshot', () => { + const customDimensions = jest.fn((dimensions) => { + dimensions.window.width /= 2 + return dimensions + }) + Mpx.config.rnConfig.customDimensions = customDimensions + + expect(getSystemInfo().size.windowWidth).toBe(180) + expect(customDimensions).toHaveBeenCalledTimes(1) + }) + + it('applies customDimensions configured after an earlier dimensions read', () => { + expect(getSystemInfo().size.windowWidth).toBe(360) + const customDimensions = jest.fn((dimensions) => { + dimensions.window.width /= 2 + return dimensions + }) + + Mpx.config.rnConfig.customDimensions = customDimensions + + expect(getSystemInfo().size.windowWidth).toBe(180) + expect(customDimensions).toHaveBeenCalledTimes(1) }) function createPageResizeContext () { diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index 4d66c14ef8..7246b02629 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -52,12 +52,13 @@ jest.mock('../../src/index', () => ({ import Mpx from '../../src/index' // eslint-disable-next-line import/first import styleHelperMixin from '../../src/platform/builtInMixins/styleHelperMixin.ios' +// eslint-disable-next-line import/first +import { initDimensionsInfo } from '../../src/platform/dimensionsHelper' describe('RN styleHelperMixin dimensions', () => { beforeEach(() => { global.__mpx_perf_framework__ = false - global.__mpxAppDimensionsInfo.window = mockDimensions.window - global.__mpxAppDimensionsInfo.screen = mockDimensions.screen + initDimensionsInfo(mockDimensions) global.__mpxSizeCount = 0 global.__classCaches = new Set() Mpx.config.rnConfig = { @@ -92,6 +93,14 @@ describe('RN styleHelperMixin dimensions', () => { expect(global.__formatValue('100vh')).toBe(640) }) + it('does not expose the mutable dimensions cache through the global getter', () => { + const dimensions = global.getStyleDimensions() + + dimensions.width = 1 + + expect(global.__formatValue('750rpx')).toBe(360) + }) + it('converts responsive units with screen dimensions when configured', () => { Mpx.config.rnConfig.dimensionsBase = 'screen' @@ -252,6 +261,7 @@ describe('RN styleHelperMixin dimensions', () => { it('applies customDimensions before the first media-only style calculation', () => { const originalGlobals = { notifyDimensionsChange: global.notifyDimensionsChange, + getStyleDimensions: global.getStyleDimensions, GCC: global.__GCC, formatValue: global.__formatValue, dimensionsInfo: global.__mpxAppDimensionsInfo, @@ -296,6 +306,7 @@ describe('RN styleHelperMixin dimensions', () => { }) } finally { global.notifyDimensionsChange = originalGlobals.notifyDimensionsChange + global.getStyleDimensions = originalGlobals.getStyleDimensions global.__GCC = originalGlobals.GCC global.__formatValue = originalGlobals.formatValue global.__mpxAppDimensionsInfo = originalGlobals.dimensionsInfo diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 450565cb3c..75f014268b 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -2,13 +2,12 @@ import { isObject, isArray, dash2hump, cached, isEmptyObject, hasOwn, getFocused import * as perf from '@mpxjs/perf' import { StyleSheet, Dimensions } from 'react-native' import { reactive } from '../../observer/reactive' -import Mpx from '../../index' -import { getDimensionsBase } from '../dimensionsHelper' +import { applyDimensionsInfo, getDimensionsBase, getStyleDimensions, initDimensionsInfo } from '../dimensionsHelper' -global.__mpxAppDimensionsInfo = { - window: Object.assign({}, Dimensions.get('window')), - screen: Object.assign({}, Dimensions.get('screen')) -} +initDimensionsInfo({ + window: Dimensions.get('window'), + screen: Dimensions.get('screen') +}) global.__mpxSizeCount = 0 global.__mpxPageSizeCountMap = reactive({}) @@ -34,39 +33,23 @@ global.__GCC = function (className, classMap, classMapValueCache) { return classMapValueCache.get(className) } -let dimensionsInfoInitialized = false -function applyDimensionsInfo (dimensions) { - dimensionsInfoInitialized = true - if (typeof Mpx.config.rnConfig?.customDimensions === 'function') { - dimensions = Mpx.config.rnConfig.customDimensions(dimensions) || dimensions - } - global.__mpxAppDimensionsInfo.window = dimensions.window - global.__mpxAppDimensionsInfo.screen = dimensions.screen -} - -function getStyleDimensions () { - return global.__mpxAppDimensionsInfo[getDimensionsBase()] -} - -function getDimensionsBaseSize (dimensions = getStyleDimensions()) { +function getDimensionsBaseSize (dimensions) { return dimensions.width + 'x' + dimensions.height } function onDimensionsChange (dimensions) { - const oldDimensionsBaseSize = getDimensionsBaseSize() + // 读取旧缓存时不触发初始化,避免首次通知重复执行 customDimensions。 + const oldDimensionsBaseSize = getDimensionsBaseSize(global.__mpxAppDimensionsInfo[getDimensionsBase()]) if (!dimensions) { dimensions = { window: Dimensions.get('window'), screen: Dimensions.get('screen') } } - applyDimensionsInfo({ - window: Object.assign({}, dimensions.window), - screen: Object.assign({}, dimensions.screen) - }) + applyDimensionsInfo(dimensions) // 对比自定义处理后的基准尺寸高宽是否存在变化 - if (getDimensionsBaseSize() === oldDimensionsBaseSize) return + if (getDimensionsBaseSize(getStyleDimensions()) === oldDimensionsBaseSize) return global.__classCaches?.forEach(cache => cache?.clear()) @@ -84,6 +67,7 @@ function onDimensionsChange (dimensions) { } global.notifyDimensionsChange = onDimensionsChange +global.getStyleDimensions = () => Object.assign({}, getStyleDimensions()) Dimensions.addEventListener('change', onDimensionsChange) @@ -113,7 +97,6 @@ const empty = {} let dependentWindowSize = false const isNum = (v) => !isNaN(+v) function formatValue (value, unitType) { - if (!dimensionsInfoInitialized) applyDimensionsInfo(global.__mpxAppDimensionsInfo) if (unitType && typeof unit[unitType] === 'function') { dependentWindowSize = true return unit[unitType](+value) @@ -260,7 +243,6 @@ function isNativeStyle (style) { function getMediaStyle (media) { if (!media || !media.length) return {} - if (!dimensionsInfoInitialized) applyDimensionsInfo(global.__mpxAppDimensionsInfo) dependentWindowSize = true const { width } = getStyleDimensions() return media.reduce((styleObj, item) => { diff --git a/packages/core/src/platform/dimensionsHelper.js b/packages/core/src/platform/dimensionsHelper.js index 6aafc715e8..5992123950 100644 --- a/packages/core/src/platform/dimensionsHelper.js +++ b/packages/core/src/platform/dimensionsHelper.js @@ -1,14 +1,52 @@ import { ONRESIZE } from '../core/innerLifecycle' import Mpx from '../index' +let dimensionsInfoInitialized = false +let rawDimensionsInfo +let appliedCustomDimensions + +function cloneDimensionsInfo (dimensions) { + return { + window: Object.assign({}, dimensions.window), + screen: Object.assign({}, dimensions.screen) + } +} + export function getDimensionsBase () { return Mpx.config.rnConfig?.dimensionsBase === 'screen' ? 'screen' : 'window' } +export function initDimensionsInfo (dimensions) { + dimensionsInfoInitialized = false + appliedCustomDimensions = undefined + rawDimensionsInfo = cloneDimensionsInfo(dimensions) + global.__mpxAppDimensionsInfo = cloneDimensionsInfo(rawDimensionsInfo) +} + +export function applyDimensionsInfo (dimensions) { + rawDimensionsInfo = cloneDimensionsInfo(dimensions) + const customDimensions = Mpx.config.rnConfig?.customDimensions + dimensionsInfoInitialized = true + appliedCustomDimensions = customDimensions + dimensions = cloneDimensionsInfo(rawDimensionsInfo) + if (typeof customDimensions === 'function') { + dimensions = customDimensions(dimensions) || dimensions + } + global.__mpxAppDimensionsInfo.window = dimensions.window + global.__mpxAppDimensionsInfo.screen = dimensions.screen +} + +export function getStyleDimensions () { + if (!dimensionsInfoInitialized || appliedCustomDimensions !== Mpx.config.rnConfig?.customDimensions) { + applyDimensionsInfo(rawDimensionsInfo) + } + return global.__mpxAppDimensionsInfo[getDimensionsBase()] +} + export function getSystemInfo () { + const baseDimensions = getStyleDimensions() const windowDimensions = global.__mpxAppDimensionsInfo.window const screenDimensions = global.__mpxAppDimensionsInfo.screen - const baseDimensions = global.__mpxAppDimensionsInfo[getDimensionsBase()] return { deviceOrientation: baseDimensions.width > baseDimensions.height ? 'landscape' : 'portrait', size: { From 8a98485ce1c2d562d974512f2d3f4e59108e5d5e Mon Sep 17 00:00:00 2001 From: wangxiaokou Date: Fri, 11 Sep 2026 16:37:25 +0800 Subject: [PATCH 13/17] =?UTF-8?q?=E8=A1=A5=E5=85=85api-proxy=E6=94=AF?= =?UTF-8?q?=E6=8C=81customDimensions=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/mpx2rn/references/rn-api-reference.md | 2 ++ .agents/skills/mpx2rn/references/rn-script-reference.md | 2 +- docs-vitepress/api-proxy/base/system/getWindowInfo.md | 4 +++- docs-vitepress/guide/rn/application-api.md | 5 +++-- packages/api-proxy/src/platform/api/system/rnSystem.js | 2 +- packages/core/@types/global.d.ts | 2 +- .../core/src/platform/builtInMixins/styleHelperMixin.ios.js | 2 +- packages/core/src/platform/dimensionsHelper.js | 4 ++-- 8 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-api-reference.md b/.agents/skills/mpx2rn/references/rn-api-reference.md index bd62d9b721..65b95603e8 100644 --- a/.agents/skills/mpx2rn/references/rn-api-reference.md +++ b/.agents/skills/mpx2rn/references/rn-api-reference.md @@ -263,6 +263,8 @@ mpx.use(apiProxy, { 同步获取窗口、屏幕与安全区信息,常用于布局与刘海区域避让。 +可通过 `Mpx.config.rnConfig.customDimensions` 自定义尺寸,`getWindowInfo`、`getSystemInfo` 和 `getSystemInfoSync` 会基于处理后的尺寸计算相关字段。 + #### 入参 无。 diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 7ab0512db2..008060d444 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -765,7 +765,7 @@ notifyDimensionsChange() | `setAppShow()` | 手动触发应用「进入前台」逻辑,驱动已注册的 `onShow`。 | | `setAppHide()` | 手动触发应用「进入后台」逻辑,驱动已注册的 `onHide`。 | | `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `dimensionsBase` 指定的尺寸判断是否刷新。 | -| `getStyleDimensions()` | 返回经过 `customDimensions` 处理后、由 `dimensionsBase` 选中的当前只读 `ScaledSize` 副本;首次调用及更换 `customDimensions` 后会确保重新处理原始尺寸。 | +| `getStyleDimensions(dimensionsBase?)` | 返回经过 `customDimensions` 处理后的当前只读 `ScaledSize` 副本;可传入 `"window"` 或 `"screen"`,不传时使用配置中的 `dimensionsBase`,传参不修改配置;首次调用及更换 `customDimensions` 后会确保重新处理原始尺寸。 | #### 注意事项 diff --git a/docs-vitepress/api-proxy/base/system/getWindowInfo.md b/docs-vitepress/api-proxy/base/system/getWindowInfo.md index 0031797142..6d17dcf1b5 100644 --- a/docs-vitepress/api-proxy/base/system/getWindowInfo.md +++ b/docs-vitepress/api-proxy/base/system/getWindowInfo.md @@ -4,6 +4,8 @@ 支持情况: 微信、支付宝、web、RN +> RN 下可通过 `Mpx.config.rnConfig.customDimensions` 自定义尺寸,`getWindowInfo`、`getSystemInfo` 和 `getSystemInfoSync` 会基于处理后的尺寸计算相关字段。 + [参考文档](https://developers.weixin.qq.com/miniprogram/dev/api/base/system/wx.getWindowInfo.html) ### 参数 {#parameters} @@ -60,4 +62,4 @@ console.log(deviceInfo.brand) console.log(deviceInfo.model) console.log(deviceInfo.platform) console.log(deviceInfo.system) -``` \ No newline at end of file +``` diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index e006fe643a..8ca774a9a5 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -520,14 +520,15 @@ notifyDimensionsChange({ #### getStyleDimensions ```ts -() => ScaledSize +(dimensionsBase?: 'window' | 'screen') => ScaledSize ``` -返回经过 `rnConfig.customDimensions` 处理后、由 `rnConfig.dimensionsBase` 选中的当前尺寸副本。修改返回值不会影响框架内部缓存。首次调用会确保完成尺寸初始化;如果后续更换 `customDimensions`,再次调用时会基于最近一次原始 Dimensions 重新处理。 +返回经过 `rnConfig.customDimensions` 处理后的当前尺寸副本。可传入 `'window'` 或 `'screen'` 指定本次获取的尺寸;不传时使用 `rnConfig.dimensionsBase`,传参不会修改该配置。修改返回值不会影响框架内部缓存。首次调用会确保完成尺寸初始化;如果后续更换 `customDimensions`,再次调用时会基于最近一次原始 Dimensions 重新处理。 ```js const dimensions = getStyleDimensions() console.log(dimensions.width, dimensions.height) +const screenDimensions = getStyleDimensions('screen') ``` diff --git a/packages/api-proxy/src/platform/api/system/rnSystem.js b/packages/api-proxy/src/platform/api/system/rnSystem.js index d19e21c299..51187c68da 100644 --- a/packages/api-proxy/src/platform/api/system/rnSystem.js +++ b/packages/api-proxy/src/platform/api/system/rnSystem.js @@ -3,7 +3,7 @@ import { initialWindowMetrics } from 'react-native-safe-area-context' import { getFocusedNavigation } from '../../../common/js' const getWindowInfo = function () { - const dimensionsScreen = Dimensions.get('screen') + const dimensionsScreen = global.getStyleDimensions?.('screen') || Dimensions.get('screen') const navigation = getFocusedNavigation() || {} const initialWindowMetricsInset = initialWindowMetrics?.insets || {} const navigationInsets = navigation.insets || {} diff --git a/packages/core/@types/global.d.ts b/packages/core/@types/global.d.ts index 680daabc3b..7784c9cba5 100644 --- a/packages/core/@types/global.d.ts +++ b/packages/core/@types/global.d.ts @@ -20,4 +20,4 @@ declare let notifyDimensionsChange: (dimensions?: { screen: import('react-native').ScaledSize }) => void -declare let getStyleDimensions: () => Readonly +declare let getStyleDimensions: (dimensionsBase?: 'window' | 'screen') => Readonly diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 75f014268b..3ed99e97cd 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -67,7 +67,7 @@ function onDimensionsChange (dimensions) { } global.notifyDimensionsChange = onDimensionsChange -global.getStyleDimensions = () => Object.assign({}, getStyleDimensions()) +global.getStyleDimensions = dimensionsBase => Object.assign({}, getStyleDimensions(dimensionsBase)) Dimensions.addEventListener('change', onDimensionsChange) diff --git a/packages/core/src/platform/dimensionsHelper.js b/packages/core/src/platform/dimensionsHelper.js index 5992123950..dd44369547 100644 --- a/packages/core/src/platform/dimensionsHelper.js +++ b/packages/core/src/platform/dimensionsHelper.js @@ -36,11 +36,11 @@ export function applyDimensionsInfo (dimensions) { global.__mpxAppDimensionsInfo.screen = dimensions.screen } -export function getStyleDimensions () { +export function getStyleDimensions (dimensionsBase = getDimensionsBase()) { if (!dimensionsInfoInitialized || appliedCustomDimensions !== Mpx.config.rnConfig?.customDimensions) { applyDimensionsInfo(rawDimensionsInfo) } - return global.__mpxAppDimensionsInfo[getDimensionsBase()] + return global.__mpxAppDimensionsInfo[dimensionsBase] } export function getSystemInfo () { From 77f25905504d009e224d8a318c65029800ccdfbb Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 17 Sep 2026 20:05:45 +0800 Subject: [PATCH 14/17] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=20RN=20?= =?UTF-8?q?=E5=B0=BA=E5=AF=B8=E5=8F=98=E5=8C=96=E7=9A=84=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/styleHelperMixin.ios.spec.js | 60 +++++++++++++++++++ .../builtInMixins/styleHelperMixin.ios.js | 43 +++---------- .../core/src/platform/dimensionsHelper.js | 53 +++++++++++++++- 3 files changed, 119 insertions(+), 37 deletions(-) diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index 7246b02629..060146f56c 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -124,9 +124,30 @@ describe('RN styleHelperMixin dimensions', () => { expect(context.__getSizeCount).toHaveBeenCalledTimes(1) }) + it('stops tracking dimensions after responsive styles switch to fixed values', () => { + const context = { + __pageId: 'page', + __mpxProxy: { props: {} }, + __getSizeCount: jest.fn() + } + + styleHelperMixin().methods.__getStyle.call(context, '', '', '', { + width: '750rpx' + }) + context.__getSizeCount.mockClear() + + const result = styleHelperMixin().methods.__getStyle.call(context, '', '', '', { + width: '240px' + }) + + expect(result.width).toBe(240) + expect(context.__getSizeCount).not.toHaveBeenCalled() + }) + it('updates responsive styles when window dimensions change', () => { const cache = { clear: jest.fn() } global.__classCaches.add(cache) + global.getStyleDimensions() mockDimensionsChangeHandler({ window: { width: 400, height: 700 }, @@ -188,6 +209,45 @@ describe('RN styleHelperMixin dimensions', () => { expect(customDimensions).toHaveBeenCalledTimes(2) }) + it('invalidates cached responsive class styles when customDimensions changes', () => { + const classMap = { + box: formatValue => ({ width: formatValue('750rpx') }) + } + const classMapValueCache = new Map() + global.__classCaches.add(classMapValueCache) + + expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(360) + + Mpx.config.rnConfig.customDimensions = (dimensions) => { + dimensions.window.width /= 2 + return dimensions + } + + expect(global.getStyleDimensions().width).toBe(180) + expect(global.__mpxSizeCount).toBe(1) + + global.notifyDimensionsChange() + + expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(180) + expect(global.__mpxSizeCount).toBe(1) + }) + + it('invalidates cached responsive class styles when dimensionsBase changes', () => { + const classMap = { + box: formatValue => ({ width: formatValue('750rpx') }) + } + const classMapValueCache = new Map() + global.__classCaches.add(classMapValueCache) + + expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(360) + + Mpx.config.rnConfig.dimensionsBase = 'screen' + + expect(global.getStyleDimensions().width).toBe(720) + expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(720) + expect(global.__mpxSizeCount).toBe(1) + }) + it('matches media queries with window width', () => { const style = { color: 'red', diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 3ed99e97cd..da7b4180bb 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -1,8 +1,8 @@ -import { isObject, isArray, dash2hump, cached, isEmptyObject, hasOwn, getFocusedNavigation } from '@mpxjs/utils' +import { isObject, isArray, dash2hump, cached, isEmptyObject, hasOwn } from '@mpxjs/utils' import * as perf from '@mpxjs/perf' import { StyleSheet, Dimensions } from 'react-native' import { reactive } from '../../observer/reactive' -import { applyDimensionsInfo, getDimensionsBase, getStyleDimensions, initDimensionsInfo } from '../dimensionsHelper' +import { getStyleDimensions, initDimensionsInfo, syncDimensions } from '../dimensionsHelper' initDimensionsInfo({ window: Dimensions.get('window'), @@ -33,37 +33,14 @@ global.__GCC = function (className, classMap, classMapValueCache) { return classMapValueCache.get(className) } -function getDimensionsBaseSize (dimensions) { - return dimensions.width + 'x' + dimensions.height -} - function onDimensionsChange (dimensions) { - // 读取旧缓存时不触发初始化,避免首次通知重复执行 customDimensions。 - const oldDimensionsBaseSize = getDimensionsBaseSize(global.__mpxAppDimensionsInfo[getDimensionsBase()]) if (!dimensions) { dimensions = { window: Dimensions.get('window'), screen: Dimensions.get('screen') } } - applyDimensionsInfo(dimensions) - - // 对比自定义处理后的基准尺寸高宽是否存在变化 - if (getDimensionsBaseSize(getStyleDimensions()) === oldDimensionsBaseSize) return - - global.__classCaches?.forEach(cache => cache?.clear()) - - // 更新全局和栈顶页面的标记,其他后台页面的标记在show之后更新 - global.__mpxSizeCount++ - - const navigation = getFocusedNavigation() - - if (navigation) { - global.__mpxPageSizeCountMap[navigation.pageId] = global.__mpxSizeCount - if (hasOwn(global.__mpxPageStatusMap, navigation.pageId)) { - global.__mpxPageStatusMap[navigation.pageId] = `resize${global.__mpxSizeCount}` - } - } + syncDimensions(dimensions) } global.notifyDimensionsChange = onDimensionsChange @@ -363,18 +340,17 @@ export default function styleHelperMixin () { let localStyle, appStyle, unoStyle, unoVarStyle if (localStyle = this.__getClassStyle?.(className)) { mergeToLayer(localStyle._layer || 'normal', localStyle, getMediaStyle(localStyle._media)) - // class style 计算可能触发缓存,需要单独在结果中记录是否依赖窗口尺寸,不能直接使用全局变量。 - this.__dependentWindowSize = this.__dependentWindowSize || localStyle._dependentWindowSize + dependentWindowSize = dependentWindowSize || localStyle._dependentWindowSize } else if (unoStyle = global.__getUnoStyle?.(className)) { mergeToLayer(unoStyle._layer || 'uno', unoStyle, getMediaStyle(unoStyle._media)) - this.__dependentWindowSize = this.__dependentWindowSize || unoStyle._dependentWindowSize + dependentWindowSize = dependentWindowSize || unoStyle._dependentWindowSize if (unoStyle.transform || unoStyle.filter) needAddUnoPreflight = true } else if (unoVarStyle = global.__getUnoVarStyle?.(className)) { mergeToLayer('important', unoVarStyle) - this.__dependentWindowSize = this.__dependentWindowSize || unoVarStyle._dependentWindowSize + dependentWindowSize = dependentWindowSize || unoVarStyle._dependentWindowSize } else if (appStyle = global.__getAppClassStyle?.(className)) { mergeToLayer(appStyle._layer || 'app', appStyle, getMediaStyle(appStyle._media)) - this.__dependentWindowSize = this.__dependentWindowSize || appStyle._dependentWindowSize + dependentWindowSize = dependentWindowSize || appStyle._dependentWindowSize } else if (isObject(this.__mpxProxy.props[className])) { // externalClasses必定以对象形式传递下来 mergeToLayer('normal', this.__mpxProxy.props[className]) @@ -384,7 +360,7 @@ export default function styleHelperMixin () { if (needAddUnoPreflight) { const unoPreflightStyle = global.__getAppClassStyle?.('__uno_preflight') mergeToLayer('preflight', unoPreflightStyle) - this.__dependentWindowSize = this.__dependentWindowSize || unoPreflightStyle._dependentWindowSize + dependentWindowSize = dependentWindowSize || unoPreflightStyle._dependentWindowSize } if (__mpx_perf_framework__) perf.scopeEnd(idClass) @@ -418,8 +394,7 @@ export default function styleHelperMixin () { const isEmpty = isNativeStaticStyle ? !result.length : isEmptyObject(result) // 仅在依赖窗口尺寸时才触发 __getSizeCount 进行响应式关联,避免窗口尺寸变化时不必要的性能损耗 - this.__dependentWindowSize = this.__dependentWindowSize || dependentWindowSize - if (this.__dependentWindowSize) { + if (dependentWindowSize) { this.__getSizeCount() } if (__mpx_perf_framework__) perf.scopeEnd(idTotal) diff --git a/packages/core/src/platform/dimensionsHelper.js b/packages/core/src/platform/dimensionsHelper.js index dd44369547..d67dd1591c 100644 --- a/packages/core/src/platform/dimensionsHelper.js +++ b/packages/core/src/platform/dimensionsHelper.js @@ -1,9 +1,12 @@ import { ONRESIZE } from '../core/innerLifecycle' +import { getFocusedNavigation, hasOwn } from '@mpxjs/utils' import Mpx from '../index' let dimensionsInfoInitialized = false let rawDimensionsInfo let appliedCustomDimensions +let appliedDimensionsBase +let styleDimensionsSnapshot function cloneDimensionsInfo (dimensions) { return { @@ -19,26 +22,70 @@ export function getDimensionsBase () { export function initDimensionsInfo (dimensions) { dimensionsInfoInitialized = false appliedCustomDimensions = undefined + appliedDimensionsBase = undefined + styleDimensionsSnapshot = undefined rawDimensionsInfo = cloneDimensionsInfo(dimensions) global.__mpxAppDimensionsInfo = cloneDimensionsInfo(rawDimensionsInfo) } -export function applyDimensionsInfo (dimensions) { +function getStyleDimensionsSnapshot (dimensionsBase, dimensions) { + const baseDimensions = dimensions[dimensionsBase] + return { + dimensionsBase, + width: baseDimensions.width, + height: baseDimensions.height + } +} + +function isSameStyleDimensions (a, b) { + return a.dimensionsBase === b.dimensionsBase && + a.width === b.width && + a.height === b.height +} + +function triggerStyleDimensionsChange () { + global.__classCaches?.forEach(cache => cache?.clear()) + global.__mpxSizeCount++ + + const navigation = getFocusedNavigation() + + if (navigation) { + global.__mpxPageSizeCountMap[navigation.pageId] = global.__mpxSizeCount + if (hasOwn(global.__mpxPageStatusMap, navigation.pageId)) { + global.__mpxPageStatusMap[navigation.pageId] = `resize${global.__mpxSizeCount}` + } + } +} + +export function syncDimensions (dimensions, options = {}) { + const oldStyleDimensionsSnapshot = styleDimensionsSnapshot rawDimensionsInfo = cloneDimensionsInfo(dimensions) const customDimensions = Mpx.config.rnConfig?.customDimensions + const dimensionsBase = getDimensionsBase() dimensionsInfoInitialized = true appliedCustomDimensions = customDimensions + appliedDimensionsBase = dimensionsBase dimensions = cloneDimensionsInfo(rawDimensionsInfo) if (typeof customDimensions === 'function') { dimensions = customDimensions(dimensions) || dimensions } global.__mpxAppDimensionsInfo.window = dimensions.window global.__mpxAppDimensionsInfo.screen = dimensions.screen + styleDimensionsSnapshot = getStyleDimensionsSnapshot(dimensionsBase, dimensions) + + if (!options.silent && oldStyleDimensionsSnapshot && !isSameStyleDimensions(oldStyleDimensionsSnapshot, styleDimensionsSnapshot)) { + triggerStyleDimensionsChange() + } } export function getStyleDimensions (dimensionsBase = getDimensionsBase()) { - if (!dimensionsInfoInitialized || appliedCustomDimensions !== Mpx.config.rnConfig?.customDimensions) { - applyDimensionsInfo(rawDimensionsInfo) + if (!dimensionsInfoInitialized) { + syncDimensions(rawDimensionsInfo, { silent: true }) + } else if ( + appliedDimensionsBase !== getDimensionsBase() || + appliedCustomDimensions !== Mpx.config.rnConfig?.customDimensions + ) { + syncDimensions(rawDimensionsInfo) } return global.__mpxAppDimensionsInfo[dimensionsBase] } From 04ca7118c4d987f8f6fc24b6720344320c1580fb Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 17 Sep 2026 20:11:53 +0800 Subject: [PATCH 15/17] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E6=9F=A5=E8=AF=A2=E4=B8=AD=E7=9A=84=20important=20?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/styleHelperMixin.ios.spec.js | 33 +++++++++++++++++++ .../builtInMixins/styleHelperMixin.ios.js | 20 +++++++++-- .../test/platform/wx/style/style-rn.spec.js | 31 +++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index 060146f56c..10dadc73a7 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -318,6 +318,39 @@ describe('RN styleHelperMixin dimensions', () => { expect(getColorAtWidth(901)).toBe('red') }) + it('applies important declarations from all matching media queries', () => { + const style = { + width: 100, + height: 50, + _media: [{ + options: { minWidth: 300 }, + value: { + _inlineLayer: { + important: { width: 200 } + } + } + }, { + options: { maxWidth: 500 }, + value: { + _inlineLayer: { + important: { height: 120 } + } + } + }] + } + const context = { + __pageId: 'page', + __mpxProxy: { props: {} }, + __getClassStyle: jest.fn(() => style), + __getSizeCount: jest.fn() + } + + const result = styleHelperMixin().methods.__getStyle.call(context, 'responsive') + + expect(result.width).toBe(200) + expect(result.height).toBe(120) + }) + it('applies customDimensions before the first media-only style calculation', () => { const originalGlobals = { notifyDimensionsChange: global.notifyDimensionsChange, diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index da7b4180bb..1456eee733 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -232,7 +232,20 @@ function getMediaStyle (media) { : hasMinWidth ? width >= minWidth : hasMaxWidth && width <= maxWidth - if (matched) Object.assign(styleObj, value) + if (matched) { + Object.keys(value).forEach(key => { + if (key !== '_inlineLayer') styleObj[key] = value[key] + }) + if (value._inlineLayer) { + styleObj._inlineLayer = styleObj._inlineLayer || {} + Object.keys(value._inlineLayer).forEach(layer => { + styleObj._inlineLayer[layer] = Object.assign( + styleObj._inlineLayer[layer] || {}, + value._inlineLayer[layer] + ) + }) + } + } return styleObj }, {}) } @@ -255,8 +268,11 @@ const createLayer = (isNativeStyle) => { const mergeToLayer = (name, style, mediaStyle) => { const layer = layerMap[name] || layerMap.normal layer.push(style) - if (mediaStyle) layer.push(mediaStyle) if (style._inlineLayer) checkInlineLayer(style, mergeToLayer) + if (mediaStyle) { + layer.push(mediaStyle) + if (mediaStyle._inlineLayer) checkInlineLayer(mediaStyle, mergeToLayer) + } } const mergeToLayerWithStyles = (name, styles) => { diff --git a/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js b/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js index b62f1ec815..26609e454b 100644 --- a/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js +++ b/packages/webpack-plugin/test/platform/wx/style/style-rn.spec.js @@ -123,6 +123,37 @@ describe('React Native style validation for CSS variables', () => { }) expect(config.error).not.toHaveBeenCalled() }) + + test('keeps important declarations in the media style', () => { + const config = createConfig() + const result = getClassMap({ + content: ` + .media-box { + width: 100px; + } + @media (min-width: 300px) { + .media-box { + width: 200px !important; + } + } + `, + filename: 'test.css', + ...config + }) + + expect(result['media-box']).toEqual({ + width: '100', + _media: [{ + options: { minWidth: 300 }, + value: { + _inlineLayer: { + important: { width: '200' } + } + } + }] + }) + expect(config.error).not.toHaveBeenCalled() + }) }) describe('CSS variable fallback validation', () => { From 1247d03a3cede6aa6d76b949a03b5b459a08ffef Mon Sep 17 00:00:00 2001 From: mackwang Date: Thu, 17 Sep 2026 20:37:40 +0800 Subject: [PATCH 16/17] fix: address RN dimensions review feedback --- .../mpx2rn/references/rn-script-reference.md | 3 +- .../mpx2rn/references/rn-style-practice.md | 2 +- .../mpx2rn/references/rn-style-reference.md | 8 ++- docs-vitepress/guide/rn/application-api.md | 4 +- packages/core/@types/index.d.ts | 2 + .../__tests__/common/dimensionsHelper.spec.js | 66 +++++++++++++++++-- .../common/styleHelperMixin.ios.spec.js | 16 +++-- .../builtInMixins/styleHelperMixin.ios.js | 3 +- .../core/src/platform/dimensionsHelper.js | 40 +++++------ .../platform/patch/getDefaultOptions.ios.js | 4 +- packages/webpack-plugin/lib/loader.js | 2 - packages/webpack-plugin/lib/react/index.js | 4 +- .../webpack-plugin/lib/react/processScript.js | 5 +- .../webpack-plugin/lib/react/script-helper.js | 4 +- .../webpack-plugin/lib/react/style-helper.js | 20 +++--- 15 files changed, 120 insertions(+), 63 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-script-reference.md b/.agents/skills/mpx2rn/references/rn-script-reference.md index 008060d444..29474eeaf0 100644 --- a/.agents/skills/mpx2rn/references/rn-script-reference.md +++ b/.agents/skills/mpx2rn/references/rn-script-reference.md @@ -764,8 +764,7 @@ notifyDimensionsChange() | `getCurrentPages()` | 返回当前导航栈中已映射的页面实例列表(顺序与路由 state 相关)。 | | `setAppShow()` | 手动触发应用「进入前台」逻辑,驱动已注册的 `onShow`。 | | `setAppHide()` | 手动触发应用「进入后台」逻辑,驱动已注册的 `onHide`。 | -| `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `dimensionsBase` 指定的尺寸判断是否刷新。 | -| `getStyleDimensions(dimensionsBase?)` | 返回经过 `customDimensions` 处理后的当前只读 `ScaledSize` 副本;可传入 `"window"` 或 `"screen"`,不传时使用配置中的 `dimensionsBase`,传参不修改配置;首次调用及更换 `customDimensions` 后会确保重新处理原始尺寸。 | +| `notifyDimensionsChange(dimensions?)` | 主动通知框架 Dimensions 发生变化,触发 `rpx`、`vw`、`vh`、媒体查询和 `onResize` 等能力重新计算。不传参数时通过 `Dimensions.get` 读取当前原始尺寸;两种调用方式都会重新执行 `customDimensions`,并按 `dimensionsBase` 指定的尺寸判断是否刷新。运行期间修改 `dimensionsBase` 或 `customDimensions` 后需要调用此方法使配置生效。 | #### 注意事项 diff --git a/.agents/skills/mpx2rn/references/rn-style-practice.md b/.agents/skills/mpx2rn/references/rn-style-practice.md index 9bfee1fa37..52dfffe611 100644 --- a/.agents/skills/mpx2rn/references/rn-style-practice.md +++ b/.agents/skills/mpx2rn/references/rn-style-practice.md @@ -692,7 +692,7 @@ export default { RN 不支持 `rem` 和 `em` 单位。需将其转换为 `rpx` 以实现响应式布局。 -**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(RN 默认以 Window 宽度为 750rpx;配置 `dimensionsBase: "screen"` 时以 Screen 宽度为准)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如: +**转换说明:** `rpx` 是小程序和 Mpx RN 的响应式单位(规定屏幕宽为 750rpx)。若原项目使用 `rem` 进行响应式适配,通常存在固定的换算比例。例如: - 若设定 `1rem = 100px` (基于 750px 设计稿),则 `1rem = 100rpx`。 - 若基于浏览器默认字号 (`16px`),则 `1rem = 32rpx` (1px = 2rpx)。 diff --git a/.agents/skills/mpx2rn/references/rn-style-reference.md b/.agents/skills/mpx2rn/references/rn-style-reference.md index 4358ee8bba..b16c11cc0e 100644 --- a/.agents/skills/mpx2rn/references/rn-style-reference.md +++ b/.agents/skills/mpx2rn/references/rn-style-reference.md @@ -168,16 +168,18 @@ Mpx 在 RN 平台支持多种 CSS 单位,并在运行时进行转换。 | 单位 | 说明 | 转换规则 | | --- | --- | --- | | `px` | 绝对像素 | 直接转换为 RN 的无单位数值 | -| `rpx` | 响应式像素 | 默认按 `rpx值 × window.width / 750` 转换 | +| `rpx` | 响应式像素 | 按 `rpx值 × 视口基准宽度 / 750` 转换 | | `%` | 百分比 | 转换为字符串形式(如 `'50%'`),由 RN 原生支持或框架处理 | -| `vw` | 视口宽度百分比 | 默认按 `vw值 × window.width / 100` 转换 | -| `vh` | 视口高度百分比 | 默认按 `vh值 × window.height / 100` 转换 | +| `vw` | 视口宽度百分比 | 按 `vw值 × 视口基准宽度 / 100` 转换 | +| `vh` | 视口高度百分比 | 按 `vh值 × 视口基准高度 / 100` 转换 | | `hairlineWidth` | RN 特有极细线 | `StyleSheet.hairlineWidth` | #### 样式计算基准与自定义 `rpx`、`vw`、`vh` 与媒体查询的计算默认基于运行时的 `window.width` 和 `window.height`。如需保持旧版本基于 Screen 尺寸计算的效果,可将 `Mpx.config.rnConfig.dimensionsBase` 设置为 `"screen"`: +运行期间修改 `dimensionsBase` 或 `customDimensions` 后,需要调用 `notifyDimensionsChange()` 使配置生效。 + ```javascript Mpx.config.rnConfig.dimensionsBase = "screen" ``` diff --git a/docs-vitepress/guide/rn/application-api.md b/docs-vitepress/guide/rn/application-api.md index 8ca774a9a5..222968dc99 100644 --- a/docs-vitepress/guide/rn/application-api.md +++ b/docs-vitepress/guide/rn/application-api.md @@ -496,6 +496,8 @@ mpx.config.rnConfig.dimensionsBase = 'screen' 例如,使用默认的 `dimensionsBase: 'window'` 时,折叠屏展开后如果期望应用只在一半窗口中展示,可在 `customDimensions` 中将 `window.width` 设为原来的一半。 +运行期间修改 `dimensionsBase` 或 `customDimensions` 后,需要调用 `notifyDimensionsChange()` 使配置生效。可以同时修改两项配置,再统一通知一次。 + #### notifyDimensionsChange ```ts @@ -523,7 +525,7 @@ notifyDimensionsChange({ (dimensionsBase?: 'window' | 'screen') => ScaledSize ``` -返回经过 `rnConfig.customDimensions` 处理后的当前尺寸副本。可传入 `'window'` 或 `'screen'` 指定本次获取的尺寸;不传时使用 `rnConfig.dimensionsBase`,传参不会修改该配置。修改返回值不会影响框架内部缓存。首次调用会确保完成尺寸初始化;如果后续更换 `customDimensions`,再次调用时会基于最近一次原始 Dimensions 重新处理。 +返回最近一次初始化或 `notifyDimensionsChange()` 后生效的尺寸副本。可传入 `'window'` 或 `'screen'` 指定本次获取的尺寸;不传时使用最近一次生效的 `rnConfig.dimensionsBase`,传参不会修改该配置。修改返回值不会影响框架内部缓存。该方法只读取尺寸,不会使运行期间修改的 `dimensionsBase` 或 `customDimensions` 生效。 ```js const dimensions = getStyleDimensions() diff --git a/packages/core/@types/index.d.ts b/packages/core/@types/index.d.ts index 546b4f9e5e..1312a79ba7 100644 --- a/packages/core/@types/index.d.ts +++ b/packages/core/@types/index.d.ts @@ -477,6 +477,7 @@ export interface RnConfig { /** * 自定义窗口和屏幕尺寸信息,用于 mpx style 渲染等依赖尺寸的功能。 + * 运行期间修改后需调用 notifyDimensionsChange() 使配置生效。 * * @param dimensions 包含 window 和 screen 的尺寸信息 * @returns 返回修改后的尺寸对象,或 void 表示不修改 @@ -487,6 +488,7 @@ export interface RnConfig { /** * rpx、vw、vh、媒体查询与 onResize 使用的尺寸基准。 + * 运行期间修改后需调用 notifyDimensionsChange() 使配置生效。 * @default 'window' */ dimensionsBase?: 'window' | 'screen' diff --git a/packages/core/__tests__/common/dimensionsHelper.spec.js b/packages/core/__tests__/common/dimensionsHelper.spec.js index e5e8aeade3..21213d97ae 100644 --- a/packages/core/__tests__/common/dimensionsHelper.spec.js +++ b/packages/core/__tests__/common/dimensionsHelper.spec.js @@ -9,22 +9,33 @@ jest.mock('../../src/index', () => ({ } })) +jest.mock('@mpxjs/utils', () => ({ + getFocusedNavigation: jest.fn(), + hasOwn: (value, key) => Object.prototype.hasOwnProperty.call(value, key) +})) + // eslint-disable-next-line import/first import Mpx from '../../src/index' // eslint-disable-next-line import/first import { ONRESIZE } from '../../src/core/innerLifecycle' // eslint-disable-next-line import/first -import { getSystemInfo, initDimensionsInfo, triggerResizeEvent } from '../../src/platform/dimensionsHelper' +import { getDimensionsBase, getSystemInfo, initDimensionsInfo, syncDimensions, triggerResizeEvent } from '../../src/platform/dimensionsHelper' + +const dimensions = { + window: { width: 360, height: 640 }, + screen: { width: 720, height: 1280 } +} describe('RN dimensions helper', () => { beforeEach(() => { Mpx.config.rnConfig = { dimensionsBase: 'window' } - initDimensionsInfo({ - window: { width: 360, height: 640 }, - screen: { width: 720, height: 1280 } - }) + global.__classCaches = new Set() + global.__mpxSizeCount = 0 + global.__mpxPageSizeCountMap = {} + global.__mpxPageStatusMap = {} + initDimensionsInfo(dimensions) }) it('uses customDimensions for the initial onResize size snapshot', () => { @@ -38,7 +49,7 @@ describe('RN dimensions helper', () => { expect(customDimensions).toHaveBeenCalledTimes(1) }) - it('applies customDimensions configured after an earlier dimensions read', () => { + it('applies runtime dimension config only after dimensions are synchronized', () => { expect(getSystemInfo().size.windowWidth).toBe(360) const customDimensions = jest.fn((dimensions) => { dimensions.window.width /= 2 @@ -47,10 +58,39 @@ describe('RN dimensions helper', () => { Mpx.config.rnConfig.customDimensions = customDimensions + expect(getSystemInfo().size.windowWidth).toBe(360) + expect(customDimensions).not.toHaveBeenCalled() + + syncDimensions(dimensions) + expect(getSystemInfo().size.windowWidth).toBe(180) expect(customDimensions).toHaveBeenCalledTimes(1) }) + it('does not expose the dimensions object retained by customDimensions', () => { + let retainedDimensions + Mpx.config.rnConfig.customDimensions = (dimensions) => { + retainedDimensions = dimensions + return dimensions + } + + expect(getSystemInfo().size.windowWidth).toBe(360) + + retainedDimensions.window.width = 180 + + expect(getSystemInfo().size.windowWidth).toBe(360) + }) + + it('keeps the last effective dimensions when customDimensions throws', () => { + expect(getSystemInfo().size.windowWidth).toBe(360) + Mpx.config.rnConfig.customDimensions = () => { + throw new Error('custom dimensions failed') + } + + expect(() => syncDimensions(dimensions)).toThrow('custom dimensions failed') + expect(getSystemInfo().size.windowWidth).toBe(360) + }) + function createPageResizeContext () { const target = { onResize: jest.fn() @@ -63,7 +103,7 @@ describe('RN dimensions helper', () => { callHook: jest.fn() }, sizeRef: { - current: getSystemInfo() + current: Object.assign(getSystemInfo(), { dimensionsBase: getDimensionsBase() }) } } } @@ -104,4 +144,16 @@ describe('RN dimensions helper', () => { expect(mpxProxy.callHook).toHaveBeenCalledWith(ONRESIZE, [systemInfo]) expect(target.onResize).toHaveBeenCalledWith(systemInfo) }) + + it('triggers onResize when the effective dimensions base changes', () => { + const { target, mpxProxy, sizeRef } = createPageResizeContext() + + Mpx.config.rnConfig.dimensionsBase = 'screen' + syncDimensions(dimensions) + triggerResizeEvent(mpxProxy, sizeRef) + + const systemInfo = getSystemInfo() + expect(mpxProxy.callHook).toHaveBeenCalledWith(ONRESIZE, [systemInfo]) + expect(target.onResize).toHaveBeenCalledWith(systemInfo) + }) }) diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index 10dadc73a7..f75ff56c88 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -209,7 +209,7 @@ describe('RN styleHelperMixin dimensions', () => { expect(customDimensions).toHaveBeenCalledTimes(2) }) - it('invalidates cached responsive class styles when customDimensions changes', () => { + it('invalidates cached responsive class styles after customDimensions changes are notified', () => { const classMap = { box: formatValue => ({ width: formatValue('750rpx') }) } @@ -223,16 +223,18 @@ describe('RN styleHelperMixin dimensions', () => { return dimensions } - expect(global.getStyleDimensions().width).toBe(180) - expect(global.__mpxSizeCount).toBe(1) + expect(global.getStyleDimensions().width).toBe(360) + expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(360) + expect(global.__mpxSizeCount).toBe(0) global.notifyDimensionsChange() + expect(global.getStyleDimensions().width).toBe(180) expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(180) expect(global.__mpxSizeCount).toBe(1) }) - it('invalidates cached responsive class styles when dimensionsBase changes', () => { + it('invalidates cached responsive class styles after dimensionsBase changes are notified', () => { const classMap = { box: formatValue => ({ width: formatValue('750rpx') }) } @@ -243,6 +245,12 @@ describe('RN styleHelperMixin dimensions', () => { Mpx.config.rnConfig.dimensionsBase = 'screen' + expect(global.getStyleDimensions().width).toBe(360) + expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(360) + expect(global.__mpxSizeCount).toBe(0) + + global.notifyDimensionsChange() + expect(global.getStyleDimensions().width).toBe(720) expect(global.__GCC('box', classMap, classMapValueCache).width).toBe(720) expect(global.__mpxSizeCount).toBe(1) diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 1456eee733..ca173f16f5 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -50,10 +50,9 @@ Dimensions.addEventListener('change', onDimensionsChange) // TODO: 存在部分安卓折叠屏机型在折叠/展开切换时,Dimensions 监听到的 width/height 尺寸错误,并触发多次问题 function rpx (value) { - const dimensionsInfo = getStyleDimensions() // rn 单位 dp = 1(css)px = 1 物理像素 * pixelRatio(像素比) // px = rpx * (样式计算宽度 / 750) - return value * dimensionsInfo.width / 750 + return value * getStyleDimensions().width / 750 } function vw (value) { return value * getStyleDimensions().width / 100 diff --git a/packages/core/src/platform/dimensionsHelper.js b/packages/core/src/platform/dimensionsHelper.js index d67dd1591c..43fdaef77f 100644 --- a/packages/core/src/platform/dimensionsHelper.js +++ b/packages/core/src/platform/dimensionsHelper.js @@ -4,7 +4,6 @@ import Mpx from '../index' let dimensionsInfoInitialized = false let rawDimensionsInfo -let appliedCustomDimensions let appliedDimensionsBase let styleDimensionsSnapshot @@ -15,13 +14,16 @@ function cloneDimensionsInfo (dimensions) { } } -export function getDimensionsBase () { +function getConfiguredDimensionsBase () { return Mpx.config.rnConfig?.dimensionsBase === 'screen' ? 'screen' : 'window' } +export function getDimensionsBase () { + return dimensionsInfoInitialized ? appliedDimensionsBase : getConfiguredDimensionsBase() +} + export function initDimensionsInfo (dimensions) { dimensionsInfoInitialized = false - appliedCustomDimensions = undefined appliedDimensionsBase = undefined styleDimensionsSnapshot = undefined rawDimensionsInfo = cloneDimensionsInfo(dimensions) @@ -59,35 +61,34 @@ function triggerStyleDimensionsChange () { export function syncDimensions (dimensions, options = {}) { const oldStyleDimensionsSnapshot = styleDimensionsSnapshot - rawDimensionsInfo = cloneDimensionsInfo(dimensions) + const nextRawDimensionsInfo = cloneDimensionsInfo(dimensions) const customDimensions = Mpx.config.rnConfig?.customDimensions - const dimensionsBase = getDimensionsBase() - dimensionsInfoInitialized = true - appliedCustomDimensions = customDimensions - appliedDimensionsBase = dimensionsBase - dimensions = cloneDimensionsInfo(rawDimensionsInfo) + const dimensionsBase = getConfiguredDimensionsBase() + dimensions = cloneDimensionsInfo(nextRawDimensionsInfo) if (typeof customDimensions === 'function') { dimensions = customDimensions(dimensions) || dimensions } + dimensions = cloneDimensionsInfo(dimensions) + const nextStyleDimensionsSnapshot = getStyleDimensionsSnapshot(dimensionsBase, dimensions) + + // 自定义尺寸计算成功后再统一提交,避免中途异常留下部分更新状态。 + rawDimensionsInfo = nextRawDimensionsInfo + dimensionsInfoInitialized = true + appliedDimensionsBase = dimensionsBase global.__mpxAppDimensionsInfo.window = dimensions.window global.__mpxAppDimensionsInfo.screen = dimensions.screen - styleDimensionsSnapshot = getStyleDimensionsSnapshot(dimensionsBase, dimensions) + styleDimensionsSnapshot = nextStyleDimensionsSnapshot if (!options.silent && oldStyleDimensionsSnapshot && !isSameStyleDimensions(oldStyleDimensionsSnapshot, styleDimensionsSnapshot)) { triggerStyleDimensionsChange() } } -export function getStyleDimensions (dimensionsBase = getDimensionsBase()) { +export function getStyleDimensions (dimensionsBase) { if (!dimensionsInfoInitialized) { syncDimensions(rawDimensionsInfo, { silent: true }) - } else if ( - appliedDimensionsBase !== getDimensionsBase() || - appliedCustomDimensions !== Mpx.config.rnConfig?.customDimensions - ) { - syncDimensions(rawDimensionsInfo) } - return global.__mpxAppDimensionsInfo[dimensionsBase] + return global.__mpxAppDimensionsInfo[dimensionsBase || appliedDimensionsBase] } export function getSystemInfo () { @@ -110,14 +111,15 @@ export function triggerResizeEvent (mpxProxy, sizeRef) { const systemInfo = getSystemInfo() const newSize = systemInfo.size const dimensionsBase = getDimensionsBase() + const oldDimensionsBase = sizeRef.current.dimensionsBase || dimensionsBase const widthKey = `${dimensionsBase}Width` const heightKey = `${dimensionsBase}Height` - if (oldSize && oldSize[widthKey] === newSize[widthKey] && oldSize[heightKey] === newSize[heightKey]) { + if (oldDimensionsBase === dimensionsBase && oldSize && oldSize[widthKey] === newSize[widthKey] && oldSize[heightKey] === newSize[heightKey]) { return } - Object.assign(sizeRef.current, systemInfo) + Object.assign(sizeRef.current, systemInfo, { dimensionsBase }) const type = mpxProxy.options.__type__ const target = mpxProxy.target diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index c5371af538..823dff4c0d 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -19,7 +19,7 @@ import { PortalHost, useSafeAreaInsets, initialWindowMetrics } from '../env/navi import { useInnerHeaderHeight } from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-nav' import Mpx from '../../index' import * as perf from '@mpxjs/perf' -import { getSystemInfo, triggerResizeEvent } from '../dimensionsHelper' +import { getDimensionsBase, getSystemInfo, triggerResizeEvent } from '../dimensionsHelper' function createEffect (proxy, componentsMap) { const update = proxy.update = () => { @@ -373,7 +373,7 @@ const triggerPageStatusHook = (mpxProxy, event) => { } function usePageEffect (mpxProxy, pageId, type) { - const sizeRef = useRef(getSystemInfo()) + const sizeRef = useRef(Object.assign(getSystemInfo(), { dimensionsBase: getDimensionsBase() })) useEffect(() => { let unWatch diff --git a/packages/webpack-plugin/lib/loader.js b/packages/webpack-plugin/lib/loader.js index 39fb08f9e3..d0d9964486 100644 --- a/packages/webpack-plugin/lib/loader.js +++ b/packages/webpack-plugin/lib/loader.js @@ -38,7 +38,6 @@ module.exports = function (content) { return content } const { resourcePath, queryObj } = parseRequest(this.resource) - const externalClasses = mpx.externalClasses || [] const packageRoot = queryObj.packageRoot || mpx.currentPackageRoot const packageName = packageRoot || 'main' const independent = queryObj.independent @@ -179,7 +178,6 @@ module.exports = function (content) { componentGenerics, componentPlaceholder, autoScope, - externalClasses, callback }) } diff --git a/packages/webpack-plugin/lib/react/index.js b/packages/webpack-plugin/lib/react/index.js index 28da8b1d0c..d5d7be9e2e 100644 --- a/packages/webpack-plugin/lib/react/index.js +++ b/packages/webpack-plugin/lib/react/index.js @@ -25,7 +25,6 @@ module.exports = function ({ componentGenerics, componentPlaceholder, autoScope, - externalClasses, callback }) { if (ctorType === 'app' && !queryObj.isApp) { @@ -99,8 +98,7 @@ module.exports = function ({ wxsModuleMap: templateRes.wxsModuleMap, localComponentsMap: jsonRes.localComponentsMap, localPagesMap: jsonRes.localPagesMap, - rnConfig, - externalClasses + rnConfig }, callback) } ], (err, scriptRes) => { diff --git a/packages/webpack-plugin/lib/react/processScript.js b/packages/webpack-plugin/lib/react/processScript.js index f4453a92f8..d358c2c26c 100644 --- a/packages/webpack-plugin/lib/react/processScript.js +++ b/packages/webpack-plugin/lib/react/processScript.js @@ -15,8 +15,7 @@ module.exports = function (script, { localPagesMap, rnConfig, componentGenerics, - genericsInfo, - externalClasses + genericsInfo }, callback) { const { appInfo, i18n } = loaderContext.getMpx() @@ -47,7 +46,7 @@ import { getComponent, getAsyncSuspense } from ${stringifyRequest(loaderContext, jsonConfig, rnConfig }) - output += buildGlobalParams({ moduleId, srcMode, loaderContext, isProduction, ctorType, jsonConfig, componentsMap, pagesMap, firstPage, hasApp, externalClasses }) + output += buildGlobalParams({ moduleId, srcMode, loaderContext, isProduction, ctorType, jsonConfig, componentsMap, pagesMap, firstPage, hasApp }) output += getRequireScript({ ctorType, script, loaderContext }) output += `export default global.__mpxOptionsMap[${JSON.stringify(moduleId)}]\n` } else { diff --git a/packages/webpack-plugin/lib/react/script-helper.js b/packages/webpack-plugin/lib/react/script-helper.js index f1860bac59..181f922d08 100644 --- a/packages/webpack-plugin/lib/react/script-helper.js +++ b/packages/webpack-plugin/lib/react/script-helper.js @@ -150,11 +150,11 @@ function buildGlobalParams ({ firstPage, outputPath, genericsInfo, - hasApp, - externalClasses + hasApp }) { let content = '' if (ctorType === 'app') { + const externalClasses = loaderContext.getMpx().externalClasses || [] content += ` global.getApp = function () {} global.getCurrentPages = function () { return [] } diff --git a/packages/webpack-plugin/lib/react/style-helper.js b/packages/webpack-plugin/lib/react/style-helper.js index b970d170c2..7d9f24a088 100644 --- a/packages/webpack-plugin/lib/react/style-helper.js +++ b/packages/webpack-plugin/lib/react/style-helper.js @@ -180,26 +180,22 @@ function getClassMap ({ styles, filename, inputFileSystem, mode, srcMode, ctorTy if (classMapKeys.length) { classMapKeys.forEach((key) => { if (Object.keys(classMapValue).length) { - const val = classMap[key] || {} - classMap[key] = val + classMap[key] = classMap[key] || {} // Media declarations only take effect when their query matches. - if (!isMedia) { - Object.assign(val, classMapValue) - } - - if (layer) { - classMap[key]._layer = layer - } - - // set css media if (isMedia) { - const _media = classMap[key]?._media || [] + const _media = classMap[key]._media || [] _media.push({ options, value: classMapValue }) classMap[key]._media = _media + } else { + Object.assign(classMap[key], classMapValue) + } + + if (layer) { + classMap[key]._layer = layer } } }) From 0a0e1c27c75679bac64ab4a0801b84e78a8698f3 Mon Sep 17 00:00:00 2001 From: mackwang Date: Fri, 18 Sep 2026 11:04:11 +0800 Subject: [PATCH 17/17] fix: update RN external class styles reactively --- .../common/styleHelperMixin.ios.spec.js | 75 +++++++++++++++---- .../builtInMixins/styleHelperMixin.ios.js | 20 +++-- .../platform/patch/getDefaultOptions.ios.js | 26 ++++--- 3 files changed, 92 insertions(+), 29 deletions(-) diff --git a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js index f75ff56c88..c2d9430257 100644 --- a/packages/core/__tests__/common/styleHelperMixin.ios.spec.js +++ b/packages/core/__tests__/common/styleHelperMixin.ios.spec.js @@ -61,6 +61,7 @@ describe('RN styleHelperMixin dimensions', () => { initDimensionsInfo(mockDimensions) global.__mpxSizeCount = 0 global.__classCaches = new Set() + global.__externalClasses = ['custom-class', 'i-class'] Mpx.config.rnConfig = { dimensionsBase: 'window' } @@ -113,7 +114,7 @@ describe('RN styleHelperMixin dimensions', () => { const context = { __pageId: 'page', __mpxProxy: { props: {} }, - __getSizeCount: jest.fn() + __trackPageSizeCount: jest.fn() } const result = styleHelperMixin().methods.__getStyle.call(context, '', '', '', { @@ -121,27 +122,75 @@ describe('RN styleHelperMixin dimensions', () => { }) expect(result.width).toBe(360) - expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + expect(context.__trackPageSizeCount).toHaveBeenCalledTimes(1) + }) + + it('reads external class styles from raw props and tracks the internal version', () => { + let versionReads = 0 + const externalClassesState = {} + Object.defineProperty(externalClassesState, 'version', { + get () { + versionReads++ + return 0 + } + }) + const context = { + __props: { + 'custom-class': { color: 'red' } + }, + __mpxProxy: { + externalClassesState + }, + __trackPageSizeCount: jest.fn() + } + + const result = styleHelperMixin().methods.__getStyle.call(context, 'custom-class') + + expect(result.color).toBe('red') + expect(versionReads).toBe(1) + }) + + it('tracks the internal version before an external class style is provided', () => { + let versionReads = 0 + const externalClassesState = {} + Object.defineProperty(externalClassesState, 'version', { + get () { + versionReads++ + return 0 + } + }) + const context = { + __props: {}, + __mpxProxy: { + externalClassesState + }, + __trackPageSizeCount: jest.fn() + } + + const result = styleHelperMixin().methods.__getStyle.call(context, 'custom-class') + + expect(result).toEqual({}) + expect(versionReads).toBe(1) }) it('stops tracking dimensions after responsive styles switch to fixed values', () => { const context = { __pageId: 'page', __mpxProxy: { props: {} }, - __getSizeCount: jest.fn() + __trackPageSizeCount: jest.fn() } styleHelperMixin().methods.__getStyle.call(context, '', '', '', { width: '750rpx' }) - context.__getSizeCount.mockClear() + context.__trackPageSizeCount.mockClear() const result = styleHelperMixin().methods.__getStyle.call(context, '', '', '', { width: '240px' }) expect(result.width).toBe(240) - expect(context.__getSizeCount).not.toHaveBeenCalled() + expect(context.__trackPageSizeCount).not.toHaveBeenCalled() }) it('updates responsive styles when window dimensions change', () => { @@ -268,14 +317,14 @@ describe('RN styleHelperMixin dimensions', () => { __pageId: 'page', __mpxProxy: { props: {} }, __getClassStyle: jest.fn(() => style), - __getSizeCount: jest.fn() + __trackPageSizeCount: jest.fn() } const result = styleHelperMixin().methods.__getStyle.call(context, 'responsive') expect(result.color).toBe('red') expect(result.opacity).toBeUndefined() - expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + expect(context.__trackPageSizeCount).toHaveBeenCalledTimes(1) }) it('matches media queries with screen width when configured', () => { @@ -291,13 +340,13 @@ describe('RN styleHelperMixin dimensions', () => { __pageId: 'page', __mpxProxy: { props: {} }, __getClassStyle: jest.fn(() => style), - __getSizeCount: jest.fn() + __trackPageSizeCount: jest.fn() } const result = styleHelperMixin().methods.__getStyle.call(context, 'responsive') expect(result.color).toBe('green') - expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + expect(context.__trackPageSizeCount).toHaveBeenCalledTimes(1) }) it('matches min/max media queries only within the inclusive range', () => { @@ -312,7 +361,7 @@ describe('RN styleHelperMixin dimensions', () => { __pageId: 'page', __mpxProxy: { props: {} }, __getClassStyle: jest.fn(() => style), - __getSizeCount: jest.fn() + __trackPageSizeCount: jest.fn() } const getColorAtWidth = width => { global.__mpxAppDimensionsInfo.window = { width, height: 640 } @@ -350,7 +399,7 @@ describe('RN styleHelperMixin dimensions', () => { __pageId: 'page', __mpxProxy: { props: {} }, __getClassStyle: jest.fn(() => style), - __getSizeCount: jest.fn() + __trackPageSizeCount: jest.fn() } const result = styleHelperMixin().methods.__getStyle.call(context, 'responsive') @@ -395,7 +444,7 @@ describe('RN styleHelperMixin dimensions', () => { __pageId: 'page', __mpxProxy: { props: {} }, __getClassStyle: jest.fn(() => style), - __getSizeCount: jest.fn() + __trackPageSizeCount: jest.fn() } const result = freshStyleHelperMixin().methods.__getStyle.call(context, 'responsive') @@ -403,7 +452,7 @@ describe('RN styleHelperMixin dimensions', () => { expect(result.color).toBe('red') expect(global.__mpxAppDimensionsInfo.window.width).toBe(180) expect(customDimensions).toHaveBeenCalledTimes(1) - expect(context.__getSizeCount).toHaveBeenCalledTimes(1) + expect(context.__trackPageSizeCount).toHaveBeenCalledTimes(1) }) } finally { global.notifyDimensionsChange = originalGlobals.notifyDimensionsChange diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index ca173f16f5..fc495310dc 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -69,6 +69,10 @@ const unit = { const empty = {} +function trackExternalClassesVersion (externalClassesState) { + return externalClassesState.version +} + // 记录 style 是否依赖窗口尺寸 let dependentWindowSize = false const isNum = (v) => !isNaN(+v) @@ -328,7 +332,7 @@ const HIDE_STYLE = { export default function styleHelperMixin () { return { methods: { - __getSizeCount () { + __trackPageSizeCount () { return global.__mpxPageSizeCountMap[this.__pageId] }, __getClass (staticClass, dynamicClass) { @@ -366,9 +370,13 @@ export default function styleHelperMixin () { } else if (appStyle = global.__getAppClassStyle?.(className)) { mergeToLayer(appStyle._layer || 'app', appStyle, getMediaStyle(appStyle._media)) dependentWindowSize = dependentWindowSize || appStyle._dependentWindowSize - } else if (isObject(this.__mpxProxy.props[className])) { - // externalClasses必定以对象形式传递下来 - mergeToLayer('normal', this.__mpxProxy.props[className]) + } else if (global.__externalClasses?.includes(className)) { + // 始终读取版本号,确保 externalClasses 从无到有时也能触发样式重算。 + trackExternalClassesVersion(this.__mpxProxy.externalClassesState) + const externalClassStyle = this.__props[className] + if (isObject(externalClassStyle)) { + mergeToLayer('normal', externalClassStyle) + } } }) @@ -408,9 +416,9 @@ export default function styleHelperMixin () { const isEmpty = isNativeStaticStyle ? !result.length : isEmptyObject(result) - // 仅在依赖窗口尺寸时才触发 __getSizeCount 进行响应式关联,避免窗口尺寸变化时不必要的性能损耗 + // 仅在依赖窗口尺寸时才建立响应式关联,避免窗口尺寸变化时不必要的性能损耗 if (dependentWindowSize) { - this.__getSizeCount() + this.__trackPageSizeCount() } if (__mpx_perf_framework__) perf.scopeEnd(idTotal) return isEmpty ? empty : result diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index 823dff4c0d..6fa29691b0 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -66,7 +66,7 @@ function getRootProps (props, validProps) { const rootProps = {} for (const key in props) { const altKey = dash2hump(key) - if (!hasOwn(validProps, key) && !hasOwn(validProps, altKey) && key !== 'children') { + if (!hasOwn(validProps, key) && !hasOwn(validProps, altKey) && !global.__externalClasses?.includes(key) && key !== 'children') { rootProps[key] = props[key] } } @@ -302,6 +302,7 @@ function createInstance ({ propsRef, type, rawOptions, currentInject, validProps } const proxy = instance.__mpxProxy = new MpxProxy(rawOptions, instance) + proxy.externalClassesState = reactive({ version: 0 }) proxy.created() if (type === 'page') { @@ -623,6 +624,10 @@ function updateProps (instance, props, validProps) { }) } +function isExternalClassesChanged (oldProps, newProps) { + return global.__externalClasses?.some(name => !Object.is(oldProps[name], newProps[name])) +} + export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { rawOptions = mergeOptions(rawOptions, type, false) const componentsMap = currentInject.componentsMap @@ -632,11 +637,6 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { }) } const validProps = Object.assign({}, rawOptions.props, rawOptions.properties) - if (global.__externalClasses && global.__externalClasses.length > 0) { - global.__externalClasses.forEach((name) => { - validProps[name] = null - }) - } const { hasDescendantRelation, hasAncestorRelation } = checkRelation(rawOptions) if (rawOptions.methods) rawOptions.methods = wrapMethodsWithErrorHandling(rawOptions.methods) const defaultOptions = memo(forwardRef((props, ref) => { @@ -649,6 +649,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { if (hasDescendantRelation || hasAncestorRelation) { relation = useContext(RelationsContext) } + const oldProps = propsRef.current propsRef.current = props let isFirst = false if (!instanceRef.current) { @@ -679,14 +680,19 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { } if (!isFirst) { + const externalClassesChanged = isExternalClassesChanged(oldProps, props) + const update = () => { + updateProps(instance, props, validProps) + if (externalClassesChanged) { + proxy.externalClassesState.version++ + } + } // 处理props更新 if (Mpx.config.forceFlushSync) { // 避免开启forceFlushSync时react报错:Cannot update a component while rendering a different component - Promise.resolve().then(() => { - updateProps(instance, props, validProps) - }) + Promise.resolve().then(update) } else { - updateProps(instance, props, validProps) + update() } }