架构与转换公式
English · 简体中文
数字是怎么算出来的,以及编译器刻意不做的事。选项速查见配置参考。
编译流程
- 插件初始化时把
libraries展开成路由,追加在routes之后。 - 根据文件路径执行
include/exclude。 - 为每条规则解析归属画布,优先级见配置参考:
@adaptive> 属性名路由 > 选择器路由 > 文件路由 >defaultProfile。 - 使用 PostCSS AST 遍历声明,属性和值经过过滤器。
- 使用
postcss-value-parser解析值,跳过字符串和 URL 函数。 - 把目标长度转换成有界流体表达式。
- 规则内声明处理完毕后,若启用
fixedContainingBlock且该规则自身声明了position: fixed,修正其行内轴 inset 与宽度。 - 把
@adaptive改写为@media或@container。 - 如显式启用,最后追加低优先级根布局基础层。
第 7 步在第 6 步之后,因此它包裹的是换算后的流体值而不是原始像素。
普通长度
设设计宽度为 D、设计值为 P、流体下限和上限为 L、U。
preferred = P / D × 100vw
minimum = P × L / D px
maximum = P × U / D px
result = clamp(minimum, preferred, maximum)负数会对边界重新排序,保证 clamp() 的最小值始终小于最大值。
单调性
D、L、U 为正数时,上式的绝对值对视口宽度单调不减:区间内按 P 的符号线性跟随,区间外是常数,且全程不改变符号。
P 为正时这就是「单调不减」。P 为负时公式是单调不增的——-16px 在 375 稿上编译成 clamp(-20.48px, -4.26667vw, -13.65333px),视口越宽值越小。负长度(负外边距、外溢、反向偏移)本来就是靠远离零来变大的,所以真正守恒的量是绝对值,不是数值本身。
这条性质有个直接用处:产物里任何「视口变宽、尺寸绝对值反而变小」的现象都不可能来自公式本身,只可能来自跨断点换了画布——也就是两张设计稿在同一个元素上给了互相矛盾的数字。命令行据此做接缝检查(见断点处的倒退),而且这个检查在这一类问题上是完备的:不会漏到别的宽度上去。
按绝对值比较不是细节。最早那版检查直接比数值,于是对每一条负长度都是反的:PC 稿要求更深的外溢会被报出来,而外溢在断点处几乎消失反倒不报。
文字长度与可访问性
纯 vw 文字无法充分响应浏览器文字缩放。设 F 为 fontFluidity:
preferred = P × (1 - F) rem-part + P × F / D × 100vw静态部分和上下界使用 rem,流体部分使用 vw/cqi。默认 F = 0.35,在设计宽度处仍严格等于设计值,同时在窗口变化与浏览器缩放之间取得平衡。
上下界同样用 rem 是关键的一步。用户把默认字号调大后,流体部分不跟着变,但下界跟着变——于是在高缩放档位上,clamp() 会落到下界,公式退化成纯 rem,缩放重新完全生效。
Chrome 实测(375px 视口,默认配置,font-size: 16px):
| 根字号 | 正文实际字号 | 相对 |
|---|---|---|
| 16px | 16.00px | 100% |
| 20px | 18.97px | 119% |
| 24px | 22.77px | 142% |
| 32px | 30.36px | 190% |
同一组测量里,width 与 padding 全程保持 343px / 16px 不变——只有文字随缩放变化,布局尺寸不会跟着膨胀。
这组数字只覆盖默认配置的一个断面。项目应继续执行 WCAG 200% 缩放验收;编译公式不能替代真实可访问性测试。
静态部分锚在哪张画布上
P × (1 - F) 是一段固定长度,不随视口变化,所以它必须相对某一个宽度才有意义。默认就是 profile 自己的设计宽度:1440 稿上的 16px 就是 1440 处的 16px,说的是什么就是什么。
但组件库画布不是这样。Vant 画在 375 上,页面画在 750 上,两者描述的是同一份设计的两套单位——Vant 的 16px 和页面的 32px 是同一个尺寸。此时若各自锚在自己的画布上,两个 rem 部分会差整整一倍,在任何视口下都对不上:
页面 32px on 750 → clamp(1.59867rem, calc(1.3rem + 1.49333vw), 1.86rem)
Vant 16px on 375 → clamp(0.94867rem, calc(0.65rem + 1.49333vw), 1.098rem) ← 修正前流体项 1.49333vw 两边本来就相同(P × F / D 与画布同比例约掉了),差的全在静态项。实测 390px 视口下页面 26.62px、Vant 16.22px,Vant 的文字小了约 40%——而这是国内移动端相当常见的一组搭配。
所以静态部分锚定的是 textAnchorWidth,组件库画布一律继承所属 profile 的锚点。实现上等价于「先把长度换算成锚点画布的单位,再照常套公式」:
P_anchor = P × A / D A = textAnchorWidth
preferred = P_anchor × (1 - F) rem-part + P_anchor × F / A × 100vw流体项完全不变(P_anchor × F / A ≡ P × F / D),只有静态项被归一化。非文字长度 F = 1,静态项恒为 0,产物逐字节不变——这也解释了为什么此前只有文字对不上,padding 一直是对的。
textAnchorWidth 也可以在 profile 上显式设置,用于手写画布之间存在同样换算关系的场合。
容器查询
Profile 的 query.type 为 container 时,@adaptive 输出 @container。通常同时把 unit 设为 cqi,使组件尺寸依赖自身容器而不是浏览器窗口。
容器必须由应用已有布局或 root.container 建立。不要让元素查询自己;为可复用组件选择稳定的祖先容器。
不转换范围
url()、local()、format();- 引号字符串;
- 已含视口/容器单位的
clamp()、min()、max()——见下; @font-face、@page、@property、@counter-style里的声明——它们描述的是资源或页面盒子,不是元素。打印边距换成vw不是同一个边距;- 默认的 CSS 自定义属性(被组件库
tokenPrefix认领的除外——认领本身即是开关); - 小于
minPixelValue的值; - 不超过
hairline的绝对值; - 过滤器或注释明确排除的内容。
幂等
clamp() / min() / max() 内部只要已经出现视口或容器单位,里面的 px 就原样保留。
这类表达式是已经写好的有界流体值——可能出自作者之手,也可能出自本插件上一趟。它的 px 是这个表达式自己的边界,不是从设计稿上量来的尺寸,再换算一次等于缩放两次。
直接结果是产物幂等:同一段 CSS 跑一遍和跑三遍结果完全相同。插件在 PostCSS 链里被挂了两次、或者组件库预编译过又被消费方编译一次,都不会产生嵌套 clamp。
范围刻意只限这三个函数。calc(100vw - 32px) 照常转换——那里的 32px 确实是设计稿尺寸,只是恰好挨着一个视口单位。
幂等不止于长度换算,还包括另外两件事:
- 忽略注释保留在产物里。 被忽略的
40px和没人管过的40px长得一模一样,注释一旦被吃掉,第二趟就会把它换算了。注释会被任何压缩器去掉,而作者的「别动这里」必须活过一趟以上。 - 根容器基础样式只注入一次。 产物开头有
/* postcss-adaptive-matrix foundation */标记,第二趟见到它就整段跳过——既不会把max-inline-size: 480px这类固定上限当成设计稿尺寸再缩放,也不会叠出第二份。
一致性套件对每一个样例都断言了这一点:把产物再编译一遍必须原样返回。
嵌套
原生 CSS 嵌套里,@adaptive 与条件组规则(@media、@supports、@layer、@container、@scope、@starting-style)内部的声明属于外层元素,照常转换:
.card {
padding: 16px; /* 默认画布 */
@adaptive pc {
padding: 32px; /* pc 画布,并改写成 @media */
}
}插件没见过的 at-rule,内部的规则仍然会被处理,直接声明不会——未知语境下按元素样式处理是猜测。
fixed 修正的边界
fixedContainingBlock 是唯一一处编译器改写定位的地方,它刻意做得很窄:
- 需要显式开启(
appPcPreset在建立居中列时默认开启,因为那正是问题出现的配置); - 只处理规则自身声明了
position: fixed的情况。从其它规则继承定位无法静态观察,按选择器组合去推测会制造隐藏的运行时耦合; - 只处理行内轴;
- 只在值为
0或100%这类无歧义形态上做替换,其余一律用calc()叠加,且幂等。
除此之外,编译器不猜测设计意图,不自动移动侧栏,也不注入 JavaScript。复杂布局应由 CSS Grid、Flexbox、容器查询和明确的端口规则表达。