diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 753b8bb043..8113fb97cb 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -1,4 +1,4 @@ -import { isObject, isArray, dash2hump, cached, isEmptyObject, hasOwn, getFocusedNavigation, noop } from '@mpxjs/utils' +import { isObject, isArray, dash2hump, cached, isEmptyObject, hasOwn, getFocusedNavigation } from '@mpxjs/utils' import { StyleSheet, Dimensions } from 'react-native' import { reactive } from '../../observer/reactive' import Mpx from '../../index' @@ -12,7 +12,7 @@ global.__mpxPageSizeCountMap = reactive({}) global.__GCC = function (className, classMap, classMapValueCache) { if (!classMapValueCache.has(className)) { - const styleObj = classMap[className]?.() + const styleObj = classMap[className]?.(global.__formatValue) styleObj && classMapValueCache.set(className, styleObj) } return classMapValueCache.get(className) @@ -266,18 +266,17 @@ export default function styleHelperMixin () { classString.split(/\s+/).forEach((className) => { let localStyle, appStyle - const getAppClassStyle = global.__getAppClassStyle || noop - if (localStyle = this.__getClassStyle(className)) { + if (localStyle = this.__getClassStyle?.(className)) { if (localStyle._media?.length) { mergeResult(localStyle._default, getMediaStyle(localStyle._media)) } else { - mergeResult(localStyle._default) + mergeResult(localStyle) } - } else if (appStyle = getAppClassStyle(className)) { + } else if (appStyle = global.__getAppClassStyle?.(className)) { if (appStyle._media?.length) { mergeResult(appStyle._default, getMediaStyle(appStyle._media)) } else { - mergeResult(appStyle._default) + mergeResult(appStyle) } } else if (isObject(this.__props[className])) { // externalClasses必定以对象形式传递下来 diff --git a/packages/webpack-plugin/lib/platform/style/wx/index.js b/packages/webpack-plugin/lib/platform/style/wx/index.js index 500c3842f6..4dbbc9224e 100644 --- a/packages/webpack-plugin/lib/platform/style/wx/index.js +++ b/packages/webpack-plugin/lib/platform/style/wx/index.js @@ -33,7 +33,8 @@ module.exports = function getSpec({ warn, error }) { } // 值类型 const ValueType = { - number: 'number', + integer: 'integer', + length: 'length', color: 'color', enum: 'enum' } @@ -63,26 +64,27 @@ module.exports = function getSpec({ warn, error }) { 'align-items': ['flex-start', 'flex-end', 'center', 'stretch', 'baseline'], 'align-self': ['auto', 'flex-start', 'flex-end', 'center', 'stretch', 'baseline'], 'justify-content': ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly'], - 'background-size': ['contain', 'cover', 'auto', ValueType.number], - 'background-position': ['left', 'right', 'top', 'bottom', 'center', ValueType.number], + 'background-size': ['contain', 'cover', 'auto', ValueType.length], + 'background-position': ['left', 'right', 'top', 'bottom', 'center', ValueType.length], 'background-repeat': ['no-repeat'], - width: ['auto', ValueType.number], - height: ['auto', ValueType.number], - 'flex-basis': ['auto', ValueType.number], - margin: ['auto', ValueType.number], - 'margin-top': ['auto', ValueType.number], - 'margin-left': ['auto', ValueType.number], - 'margin-bottom': ['auto', ValueType.number], - 'margin-right': ['auto', ValueType.number], - 'margin-horizontal': ['auto', ValueType.number], - 'margin-vertical': ['auto', ValueType.number] + width: ['auto', ValueType.length], + height: ['auto', ValueType.length], + 'flex-basis': ['auto', ValueType.length], + margin: ['auto', ValueType.length], + 'margin-top': ['auto', ValueType.length], + 'margin-left': ['auto', ValueType.length], + 'margin-bottom': ['auto', ValueType.length], + 'margin-right': ['auto', ValueType.length], + 'margin-horizontal': ['auto', ValueType.length], + 'margin-vertical': ['auto', ValueType.length] } // 获取值类型 const getValueType = (prop) => { const propValueTypeRules = [ // 重要!!优先判断是不是枚举类型 [ValueType.enum, new RegExp('^(' + Object.keys(SUPPORTED_PROP_VAL_ARR).join('|') + ')$')], - [ValueType.number, /^((opacity|flex-grow|flex-shrink|gap|left|right|top|bottom)|(.+-(width|height|left|right|top|bottom|radius|spacing|size|gap|index|offset|opacity)))$/], + [ValueType.length, /^((gap|left|right|top|bottom)|(.+-(width|height|left|right|top|bottom|radius|spacing|size|gap|offset)))$/], + [ValueType.integer, /^((opacity|flex-grow|flex-shrink|z-index)|(.+-(index|opacity)))$/], [ValueType.color, /^(color|(.+-color))$/] ] for (const rule of propValueTypeRules) { @@ -144,21 +146,29 @@ module.exports = function getSpec({ warn, error }) { if (calcExp.test(valueForVerify) || envExp.test(valueForVerify)) return true const namedColor = ['transparent', 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen'] const valueExp = { - number: /^((-?(\d+(\.\d+)?|\.\d+))(rpx|px|%|vw|vh)?|hairlineWidth)$/, + integer: /^(-?(\d+(\.\d+)?|\.\d+))$/, + length: /^((-?(\d+(\.\d+)?|\.\d+))(rpx|px|%|vw|vh)?|hairlineWidth)$/, color: new RegExp(('^(' + namedColor.join('|') + ')$') + '|(^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$)|^(rgb|rgba|hsl|hsla|hwb)\\(.+\\)$') } const type = getValueType(prop) const tipsType = (type) => { const info = { - [ValueType.number]: '2rpx,10%,30rpx', + [ValueType.length]: '2rpx,10%,30rpx', [ValueType.color]: 'rgb,rgba,hsl,hsla,hwb,named color,#000000', [ValueType.enum]: `${SUPPORTED_PROP_VAL_ARR[prop]?.join(',')}` } - tips(`Value of ${prop} in ${selector} should be ${type}, eg ${info[type]}, received [${rawValue}], please check again!`) + tips(`Value of ${prop} in ${selector} should be ${type}${info[type] ? `, eg ${info[type]}` : ''}, received [${rawValue}], please check again!`) } switch (type) { - case ValueType.number: { - if (!valueExp.number.test(valueForVerify)) { + case ValueType.length: { + if (!valueExp.length.test(valueForVerify)) { + tipsType(type) + return false + } + return true + } + case ValueType.integer: { + if (!valueExp.integer.test(valueForVerify)) { tipsType(type) return false } diff --git a/packages/webpack-plugin/lib/react/processStyles.js b/packages/webpack-plugin/lib/react/processStyles.js index 457080232d..d0daabf90f 100644 --- a/packages/webpack-plugin/lib/react/processStyles.js +++ b/packages/webpack-plugin/lib/react/processStyles.js @@ -48,6 +48,11 @@ module.exports = function (styles, { }, (err) => { if (err) return callback(err) try { + output += ` + global.__classCaches = global.__classCaches || [] + var __classCache = new Map() + global.__classCaches.push(__classCache)` + const formatValueName = '_f' const classMap = getClassMap({ content, filename: loaderContext.resourcePath, @@ -55,19 +60,17 @@ module.exports = function (styles, { srcMode, ctorType, warn, - error + error, + formatValueName }) const classMapCode = Object.entries(classMap).reduce((result, [key, value]) => { result !== '' && (result += ',') - result += `${isValidIdentifierStr(key) ? `${key}` : `['${key}']`}: () => (${shallowStringify(value)})` + result += `${isValidIdentifierStr(key) ? `${key}` : `['${key}']`}: function(${formatValueName}){return ${shallowStringify(value)};}` return result }, '') if (ctorType === 'app') { output += ` - global.__classCaches = global.__classCaches || [] - const __classCache = new Map() - global.__classCaches.push(__classCache) - let __appClassMap + var __appClassMap global.__getAppClassStyle = function(className) { if(!__appClassMap) { __appClassMap = {${classMapCode}}; @@ -76,10 +79,7 @@ module.exports = function (styles, { };\n` } else { output += ` - global.__classCaches = global.__classCaches || [] - const __classCache = new Map() - global.__classCaches.push(__classCache) - let __classMap + var __classMap global.currentInject.injectMethods = { __getClassStyle: function(className) { if(!__classMap) { diff --git a/packages/webpack-plugin/lib/react/style-helper.js b/packages/webpack-plugin/lib/react/style-helper.js index 2664d6339b..07b779da81 100644 --- a/packages/webpack-plugin/lib/react/style-helper.js +++ b/packages/webpack-plugin/lib/react/style-helper.js @@ -8,14 +8,9 @@ const unitRegExp = /^\s*(-?\d+(?:\.\d+)?)(rpx|vw|vh|px)?\s*$/ const hairlineRegExp = /^\s*hairlineWidth\s*$/ const varRegExp = /^--/ const cssPrefixExp = /^-(webkit|moz|ms|o)-/ -function getClassMap ({ content, filename, mode, srcMode, ctorType, warn, error }) { +function getClassMap ({ content, filename, mode, srcMode, ctorType, formatValueName, warn, error }) { const classMap = ctorType === 'page' - ? { - [MPX_TAG_PAGE_SELECTOR]: { - _media: [], - _default: { flex: 1, height: "'100%'" } - } - } + ? { [MPX_TAG_PAGE_SELECTOR]: { flex: 1, height: "'100%'" } } : {} const root = postcss.parse(content, { @@ -30,12 +25,12 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, warn, error value = matched[1] needStringify = false } else { - value = `global.__formatValue(${+matched[1]}, '${matched[2]}')` + value = `${formatValueName}(${+matched[1]}, '${matched[2]}')` needStringify = false } } if (hairlineRegExp.test(value)) { - value = `global.__formatValue(${JSON.stringify(value)}, 'hairlineWidth')` + value = `${formatValueName}(${JSON.stringify(value)}, 'hairlineWidth')` needStringify = false } return needStringify ? JSON.stringify(value) : value @@ -93,7 +88,7 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, warn, error root.walkRules(rule => { const classMapValue = {} rule.walkDecls(({ prop, value }) => { - if (cssPrefixExp.test(prop) || cssPrefixExp.test(value)) return + if (value === 'undefined' || cssPrefixExp.test(prop) || cssPrefixExp.test(value)) return let newData = rulesRunner({ prop, value, selector: rule.selector }) if (!newData) return if (!Array.isArray(newData)) { @@ -140,19 +135,27 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, warn, error if (classMapKeys.length) { classMapKeys.forEach((key) => { if (Object.keys(classMapValue).length) { - const _default = classMap[key]?._default || {} - const _media = classMap[key]?._media || [] + let _default = classMap[key]?._default + let _media = classMap[key]?._media if (isMedia) { + // 当前是媒体查询 + _default = _default || {} + _media = _media || [] _media.push({ options, value: classMapValue }) - } else { + classMap[key] = { + _media, + _default + } + } else if (_default) { + // 已有媒体查询数据,此次非媒体查询 Object.assign(_default, classMapValue) - } - classMap[key] = { - _media, - _default + } else { + // 无媒体查询 + const val = classMap[key] || {} + classMap[key] = Object.assign(val, classMapValue) } } }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx index 2041fa3665..17dd92f06d 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx @@ -26,7 +26,7 @@ import { noop } from '@mpxjs/utils' import { SvgCssUri } from 'react-native-svg/css' import useInnerProps, { getCustomEvent } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' -import { SVG_REGEXP, useLayout, useTransformStyle, renderImage, extendObject } from './utils' +import { SVG_REGEXP, useLayout, useTransformStyle, renderImage, extendObject, isAndroid } from './utils' import Portal from './mpx-portal' export type Mode = @@ -190,7 +190,7 @@ const Image = forwardRef, ImageProps>((props, re normalStyle, setWidth, setHeight - } = useTransformStyle(styleObj, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight }) + } = useTransformStyle(styleObj, { enableVar, transformRadiusPercent: isAndroid && !isSvg && !isLayoutMode, externalVarContext, parentFontSize, parentWidth, parentHeight }) const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, diff --git a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx index 73c21c2150..2faa17581b 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx @@ -30,6 +30,7 @@ const varUseRegExp = /var\(/ const unoVarDecRegExp = /^--un-/ const unoVarUseRegExp = /var\(--un-/ const calcUseRegExp = /calc\(/ +const calcPercentExp = /^calc\(.*-?\d+(\.\d+)?%.*\)$/ const envUseRegExp = /env\(/ const filterRegExp = /(calc|env|%)/ @@ -40,6 +41,8 @@ const safeAreaInsetMap: Record = { 'safe-area-inset-left': 'left' } +export const extendObject = Object.assign + function getSafeAreaInset (name: string, navigation: Record | undefined) { const insets = extendObject({}, initialWindowMetrics?.insets, navigation?.insets) return insets[safeAreaInsetMap[name]] @@ -142,16 +145,17 @@ export function splitStyle> (styleObj: T): { innerStyle: Partial } } - -const selfPercentRule: Record = { - translateX: 'width', - translateY: 'height', +const radiusPercentRule: Record = { borderTopLeftRadius: 'width', borderBottomLeftRadius: 'width', borderBottomRightRadius: 'width', borderTopRightRadius: 'width', borderRadius: 'width' } +const selfPercentRule: Record = extendObject({ + translateX: 'width', + translateY: 'height' +}, radiusPercentRule) const parentHeightPercentRule: Record = { height: true, @@ -401,9 +405,10 @@ interface TransformStyleConfig { parentFontSize?: number parentWidth?: number parentHeight?: number + transformRadiusPercent?: boolean } -export function useTransformStyle (styleObj: Record = {}, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight }: TransformStyleConfig) { +export function useTransformStyle (styleObj: Record = {}, { enableVar, transformRadiusPercent, externalVarContext, parentFontSize, parentWidth, parentHeight }: TransformStyleConfig) { const varStyle: Record = {} const unoVarStyle: Record = {} const normalStyle: Record = {} @@ -451,14 +456,21 @@ export function useTransformStyle (styleObj: Record = {}, { enableV } } - function calcVisitor ({ value, keyPath }: VisitorArg) { + function calcVisitor ({ key, value, keyPath }: VisitorArg) { if (calcUseRegExp.test(value)) { + // calc translate & border-radius 的百分比计算 + if (hasOwn(selfPercentRule, key) && calcPercentExp.test(value)) { + hasSelfPercent = true + percentKeyPaths.push(keyPath.slice()) + } calcKeyPaths.push(keyPath.slice()) } } function percentVisitor ({ key, value, keyPath }: VisitorArg) { - if (hasOwn(selfPercentRule, key) && PERCENT_REGEX.test(value)) { + // fixme 去掉 translate & border-radius 的百分比计算 + // fixme Image 组件 borderRadius 仅支持 number + if (transformRadiusPercent && hasOwn(radiusPercentRule, key) && PERCENT_REGEX.test(value)) { hasSelfPercent = true percentKeyPaths.push(keyPath.slice()) } else if ((key === 'fontSize' || key === 'lineHeight') && PERCENT_REGEX.test(value)) { @@ -474,7 +486,6 @@ export function useTransformStyle (styleObj: Record = {}, { enableV // traverse var & generate normalStyle traverseStyle(styleObj, [varVisitor]) - hasVarDec = hasVarDec || !!externalVarContext enableVar = enableVar || hasVarDec || hasVarUse const enableVarRef = useRef(enableVar) @@ -538,9 +549,8 @@ export function useTransformStyle (styleObj: Record = {}, { enableV transformStringify(normalStyle) // transform rpx to px transformBoxShadow(normalStyle) - - // transform 字符串格式转化数组格式 - transformTransform(normalStyle) + // transform 字符串格式转化数组格式(先转数组再处理css var) + transformTransform(styleObj) return { hasVarDec, @@ -737,8 +747,6 @@ export function flatGesture (gestures: Array = []) { })) || [] } -export const extendObject = Object.assign - export function getCurrentPage (pageId: number | null | undefined) { if (!global.getCurrentPages) return const pages = global.getCurrentPages() diff --git a/packages/webpack-plugin/test/platform/wx/style-helper-rn.spec.js b/packages/webpack-plugin/test/platform/wx/style-helper-rn.spec.js index f16cf1994b..f414705409 100644 --- a/packages/webpack-plugin/test/platform/wx/style-helper-rn.spec.js +++ b/packages/webpack-plugin/test/platform/wx/style-helper-rn.spec.js @@ -52,7 +52,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '"var(--x, 2px)"', lineHeight: '"var(--y, 1.5)"' }) @@ -69,7 +69,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '"var(--x, 0)"' }) expect(config.error).not.toHaveBeenCalled() @@ -85,7 +85,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.btn._default).toEqual({ + expect(result.btn).toEqual({ '--dn-container-height': '"var(--dn-tag-height, auto)"' }) expect(config.error).not.toHaveBeenCalled() @@ -101,7 +101,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '"var(--x)"', lineHeight: '"var(--y)"' }) @@ -134,7 +134,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '"var(--x, var(--y, 2px))"' }) expect(config.error).not.toHaveBeenCalled() @@ -151,7 +151,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '"var(--a, var(--b, var(--c, var(--d, var(--e, 2px)))))"' }) expect(config.error).not.toHaveBeenCalled() @@ -191,7 +191,7 @@ describe('React Native style validation for CSS variables', () => { }) // 由于 var(--x) 没有 fallback,整个表达式是合法的,应该保留 - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '"var(--x, var(--x))"' }) expect(config.error).not.toHaveBeenCalled() @@ -213,7 +213,7 @@ describe('React Native style validation for CSS variables', () => { }) // 应该成功解析,因为最终 fallback 是有效的 2px - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '"var(--a, var(--b, var(--a, 2px)))"' }) expect(config.error).not.toHaveBeenCalled() @@ -247,7 +247,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.box._default).toHaveProperty('marginLeft') + expect(result.box).toHaveProperty('marginLeft') expect(config.error).not.toHaveBeenCalled() }) }) @@ -277,7 +277,7 @@ describe('React Native style validation for CSS variables', () => { ...config }) - expect(result.text._default).toEqual({ + expect(result.text).toEqual({ letterSpacing: '2' }) expect(config.error).not.toHaveBeenCalled()