最近在进行大屏项目开发时,我们采用了 px2viewport
方案来适配超高分辨率。然而,在配置 postcss-px-to-viewport
插件时,遇到了一个有趣的CSS单位转换问题:明明配置了将 margin-top
转换为 vh
单位,但实际编译后却变成了 vw
单位,导致第二个配置失效。经过深入分析,我找到了原因并总结出解决方案。
配置与现象
在.umirc.ts
配置文件中,有两个pxToViewport
插件配置:
typescript
// 第一个配置:将px转换为vw
pxToViewport({
viewportWidth: 1920,
viewportUnit: 'vw',
propList: ['*']
}),
// 第二个配置:将px转换为vh
pxToViewport({
viewportWidth: 1080,
viewportUnit: 'vh',
propList: ['height', 'min-height', 'max-height', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'top', 'bottom']
})
然而,实际编译后的CSS中,margin-top: 55px
被转换为了1vw
而不是预期的vh
单位。
源码解析
为了查明问题,我深入看了一下 postcss-px-to-viewport
插件的核心源码。
这个插件的主要作用是将 CSS 中的
px
单位转换为vw
、vh
或其他视口单位,以实现响应式布局和多端适配。
1. 默认配置与选项合并
插件的默认配置定义了各种转换参数,用户可以通过 options
进行覆盖。插件启动时,会使用 object-assign
将用户传入的选项与默认值进行合并。
javascript
// index.js 源码片段
var defaults = {
unitToConvert: 'px', <---- 这是关键参数
viewportWidth: 320,
viewportHeight: 568,
unitPrecision: 5,
viewportUnit: 'vw',
fontViewportUnit: 'vw',
selectorBlackList: [],
propList: ['*'],
minPixelValue: 1,
mediaQuery: false,
replace: true,
landscape: false,
landscapeUnit: 'vw',
landscapeWidth: 568
};
module.exports = postcss.plugin('postcss-px-to-viewport', function (options) {
var opts = objectAssign({}, defaults, options);
// ...
});
2. px
转换核心逻辑 createPxReplace
createPxReplace
函数是实现 px
到目标单位转换的核心。它接收 options
、目标单位 viewportUnit
和视口尺寸 viewportSize
,返回一个替换函数,该函数用于 String.prototype.replace
方法。
javascript
// index.js 源码片段
function createPxReplace(opts, viewportUnit, viewportSize) {
return function (m, $1) {
if (!$1) return m;
var pixels = parseFloat($1);
if (pixels <= opts.minPixelValue) return m;
var parsedVal = toFixed((pixels / viewportSize * 100), opts.unitPrecision);
return parsedVal === 0 ? '0' : parsedVal + viewportUnit;
};
}
function toFixed(number, precision) {
var multiplier = Math.pow(10, precision + 1),
wholeNumber = Math.floor(number * multiplier);
return Math.round(wholeNumber / 10) * 10 / multiplier;
}
这个函数的核心逻辑是:
- 获取
px
值。 - 如果
px
值小于等于minPixelValue
,则不进行转换。 - 计算转换后的值:
(pixels / viewportSize * 100)
,并使用toFixed
进行精度处理。 - 返回转换后的值加上目标单位(如
vw
或vh
)。
3. 属性列表匹配 prop-list-matcher.js
插件通过 propList
选项来控制哪些 CSS 属性需要进行单位转换。prop-list-matcher.js
文件提供了 createPropListMatcher
函数,用于根据 propList
配置生成一个匹配器函数,该函数能够精确匹配、包含、起始或结束匹配以及排除属性。
javascript
// index.js 源码片段
var { createPropListMatcher } = require('./src/prop-list-matcher');
// ...
var satisfyPropList = createPropListMatcher(opts.propList);
在插件的 walkDecls
遍历过程中,会使用 satisfyPropList(decl.prop)
来判断当前 CSS 属性是否需要进行转换。
原因分析
通过查看postcss-px-to-viewport
插件源码和编译过程,我发现:
在问题排查过程中,影响失效的关键因素是 var pxRegex = getUnitRegexp(opts.unitToConvert);
这个正则匹配。由于第一个插件命中并转换了 px
单位,导致第二个插件在查找匹配时,因为目标属性已不含 px
单位而匹配失败。
以下是 pixel-unit-regexp.js
中的 getUnitRegexp
函数源码:
javascript
// excluding regex trick: http://www.rexegg.com/regex-best-trick.html
// Not anything inside double quotes
// Not anything inside single quotes
// Not anything inside url()
// Any digit followed by px
// !singlequotes|!doublequotes|!url()|pixelunit
function getUnitRegexp(unit) {
return new RegExp('"[^"]+"|\'[^\']+\'|url\\([^\\)]+\\)|(\\d*\\.?\\d+)' + unit, 'g');
}
module.exports = {
getUnitRegexp
};
- 插件执行顺序 :PostCSS插件按配置顺序执行,第一个
vw
转换插件先处理了所有属性 - 转换逻辑 :第一个插件将
margin-top: 55px
转换为1vw
后,第二个插件检测到该属性已不含px
单位,因此跳过处理 - propList配置 :虽然第二个插件指定了
margin-top
,但第一个插件的propList: ['*']
已经包含了所有属性
解决方案
方案1:调整插件顺序
typescript
// 先处理vh转换
pxToViewport({
viewportWidth: 1080,
viewportUnit: 'vh',
propList: ['height', 'min-height', 'max-height', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'top', 'bottom']
}),
// 再处理vw转换
pxToViewport({
viewportWidth: 1920,
viewportUnit: 'vw',
propList: ['*', '!height', '!min-height', '!max-height', '!padding-top', '!padding-bottom', '!margin-top', '!margin-bottom', '!top', '!bottom']
})
方案2:使用选择器黑名单
typescript
pxToViewport({
viewportWidth: 1920,
viewportUnit: 'vw',
selectorBlackList: ['.use-vh']
}),
pxToViewport({
viewportWidth: 1080,
viewportUnit: 'vh',
propList: ['height', 'min-height', 'max-height', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'top', 'bottom']
})
然后在HTML中:
html
<div class="use-vh">...</div>
经验总结
- 插件顺序很重要:PostCSS插件执行顺序会影响最终结果
- propList要精确 :避免使用
['*']
这种宽泛配置 - 测试编译结果:重要的样式转换一定要检查实际输出
通过这次调试,我更加理解了PostCSS插件的工作机制,也学会了如何更好地控制样式转换过程。