Skip to content

架构与转换公式

English · 简体中文

数字是怎么算出来的,以及编译器刻意不做的事。选项速查见配置参考

编译流程

  1. 插件初始化时把 libraries 展开成路由,追加在 routes 之后。
  2. 根据文件路径执行 include / exclude
  3. 为每条规则解析归属画布,优先级见配置参考@adaptive > 属性名路由 > 选择器路由 > 文件路由 > defaultProfile
  4. 使用 PostCSS AST 遍历声明,属性和值经过过滤器。
  5. 使用 postcss-value-parser 解析值,跳过字符串和 URL 函数。
  6. 把目标长度转换成有界流体表达式。
  7. 规则内声明处理完毕后,若启用 fixedContainingBlock 且该规则自身声明了 position: fixed,修正其行内轴 inset 与宽度。
  8. @adaptive 改写为 @media@container
  9. 如显式启用,最后追加低优先级根布局基础层。

第 7 步在第 6 步之后,因此它包裹的是换算后的流体值而不是原始像素。

普通长度

设设计宽度为 D、设计值为 P、流体下限和上限为 LU

text
preferred = P / D × 100vw
minimum   = P × L / D px
maximum   = P × U / D px
result    = clamp(minimum, preferred, maximum)

负数会对边界重新排序,保证 clamp() 的最小值始终小于最大值。

单调性

DLU 为正数时,上式的绝对值对视口宽度单调不减:区间内按 P 的符号线性跟随,区间外是常数,且全程不改变符号。

P 为正时这就是「单调不减」。P 为负时公式是单调不增的——-16px 在 375 稿上编译成 clamp(-20.48px, -4.26667vw, -13.65333px),视口越宽值越小。负长度(负外边距、外溢、反向偏移)本来就是靠远离零来变大的,所以真正守恒的量是绝对值,不是数值本身。

这条性质有个直接用处:产物里任何「视口变宽、尺寸绝对值反而变小」的现象都不可能来自公式本身,只可能来自跨断点换了画布——也就是两张设计稿在同一个元素上给了互相矛盾的数字。命令行据此做接缝检查(见断点处的倒退),而且这个检查在这一类问题上是完备的:不会漏到别的宽度上去。

按绝对值比较不是细节。最早那版检查直接比数值,于是对每一条负长度都是反的:PC 稿要求更深的外溢会被报出来,而外溢在断点处几乎消失反倒不报。

文字长度与可访问性

vw 文字无法充分响应浏览器文字缩放。设 FfontFluidity

text
preferred = P × (1 - F) rem-part + P × F / D × 100vw

静态部分和上下界使用 rem,流体部分使用 vw/cqi。默认 F = 0.35,在设计宽度处仍严格等于设计值,同时在窗口变化与浏览器缩放之间取得平衡。

上下界同样用 rem 是关键的一步。用户把默认字号调大后,流体部分不跟着变,但下界跟着变——于是在高缩放档位上,clamp() 会落到下界,公式退化成纯 rem,缩放重新完全生效。

Chrome 实测(375px 视口,默认配置,font-size: 16px):

根字号正文实际字号相对
16px16.00px100%
20px18.97px119%
24px22.77px142%
32px30.36px190%

同一组测量里,widthpadding 全程保持 343px / 16px 不变——只有文字随缩放变化,布局尺寸不会跟着膨胀。

这组数字只覆盖默认配置的一个断面。项目应继续执行 WCAG 200% 缩放验收;编译公式不能替代真实可访问性测试。

静态部分锚在哪张画布上

P × (1 - F) 是一段固定长度,不随视口变化,所以它必须相对某一个宽度才有意义。默认就是 profile 自己的设计宽度:1440 稿上的 16px 就是 1440 处的 16px,说的是什么就是什么。

但组件库画布不是这样。Vant 画在 375 上,页面画在 750 上,两者描述的是同一份设计的两套单位——Vant 的 16px 和页面的 32px 是同一个尺寸。此时若各自锚在自己的画布上,两个 rem 部分会差整整一倍,在任何视口下都对不上:

text
页面 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 的锚点。实现上等价于「先把长度换算成锚点画布的单位,再照常套公式」:

text
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.typecontainer 时,@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)内部的声明属于外层元素,照常转换:

css
.card {
  padding: 16px;          /* 默认画布 */

  @adaptive pc {
    padding: 32px;        /* pc 画布,并改写成 @media */
  }
}

插件没见过的 at-rule,内部的规则仍然会被处理,直接声明不会——未知语境下按元素样式处理是猜测。

fixed 修正的边界

fixedContainingBlock 是唯一一处编译器改写定位的地方,它刻意做得很窄:

  • 需要显式开启(appPcPreset 在建立居中列时默认开启,因为那正是问题出现的配置);
  • 只处理规则自身声明了 position: fixed 的情况。从其它规则继承定位无法静态观察,按选择器组合去推测会制造隐藏的运行时耦合;
  • 只处理行内轴;
  • 只在值为 0100% 这类无歧义形态上做替换,其余一律用 calc() 叠加,且幂等。

除此之外,编译器不猜测设计意图,不自动移动侧栏,也不注入 JavaScript。复杂布局应由 CSS Grid、Flexbox、容器查询和明确的端口规则表达。

基于 MIT 协议发布