Skip to content

构建工具集成 ​

English · 简体中文

插件是标准 PostCSS 8 插件,凡是能配 PostCSS 的地方都能用。下面是各工具的接法,以及几个会静默出错的点。

Vite ​

postcss.config.mjs(推荐,与 vite.config.ts 解耦):

js
import adaptiveMatrix, { appPcPreset } from 'postcss-adaptive-matrix'
import autoprefixer from 'autoprefixer'

export default {
  plugins: [
    adaptiveMatrix(appPcPreset({ rootSelector: '#app' })),
    autoprefixer(),
  ],
}

或写在 vite.config.ts 里:

ts
export default defineConfig({
  css: {
    postcss: {
      plugins: [adaptiveMatrix(appPcPreset({ rootSelector: '#app' }))],
    },
  },
})

两者不能同时存在。 Vite 一旦发现 css.postcss 是内联对象,就不再读 postcss.config.*。

Nuxt ​

ts
// nuxt.config.ts
export default defineNuxtConfig({
  postcss: {
    plugins: {
      'postcss-adaptive-matrix': { /* 选项 */ },
      autoprefixer: {},
    },
  },
})

Nuxt 用的是对象形式,键是包名。要用 appPcPreset 就得改回 postcss.config.mjs 数组形式——预设返回的是一个对象,没法用包名键表达。

Webpack ​

js
// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-adaptive-matrix')(require('postcss-adaptive-matrix').appPcPreset()),
    require('autoprefixer'),
  ],
}

postcss-loader 要排在 css-loader 之后、预处理器 loader 之前:

js
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']

Taro ​

js
// config/index.js
const config = {
  mini: { postcss: { /* Taro 自己的插件配置 */ } },
  h5: {
    postcss: {
      'postcss-adaptive-matrix': {
        enable: true,
        config: { defaultProfile: 'app', profiles: { /* ... */ } },
      },
    },
  },
}

小程序端建议只用单画布。Taro 官方把 CSS 媒体查询列入小程序端不支持的 selector,因此只有当这份构建确实写了 @adaptive 块时才需要设置 query: false;不使用该指令、直接选择唯一画布的构建完全不用填写 query。rpx 也已经在做类似换算,不要再叠加第二套长度转换,否则效果会翻倍。


四个静默出错的点 ​

1. 插件顺序 ​

放在 autoprefixer 之前。autoprefixer 不会给 clamp() 加前缀,顺序反了不报错,只是白跑一趟。

放在 postcss-nesting 这类展开嵌套的插件之后也可以——两种顺序都正确,因为嵌套的 @adaptive 现在能正常处理(见架构)。

Sass / Less 不属于这个话题:预处理器在 PostCSS 之前跑完,PostCSS 拿到的已经是展开后的 CSS。

2. 只有文件相关配置才需要 from ​

普通转换以及 selector、property、media 路由都不依赖源文件路径。只有文件相关功能才依赖 PostCSS 的 from:routes 的 file 通道、include / exclude、root.injectTo、显式组件库路径匹配(无论组件库单独传入还是放在数组中),以及会读取文件名的函数型 designWidth、textAnchorWidth 或 rootValue 解析器。

Vite、Webpack、Nuxt、Taro 都会传。如果你手写 postcss(...).process(css),又显式配置了上述功能却没有传 from,编译器会给出一次告警:matcher 无法命中,或 resolver 无法按文件做选择。没有任何文件相关配置时,则既不告警也不要求 from。

js
// 配置了按路径匹配时错误
await postcss([adaptiveMatrix(options)]).process(css)

// 向这些 matcher 提供所需路径
await postcss([adaptiveMatrix(options)]).process(css, { from: '/abs/path/app.css' })

3. Vue SFC 的路径带 query 串 ​

Vite 给 <style> 块的 from 长这样:

/project/src/views/mobile/home/index.vue?vue&type=style&index=0&lang.scss

写 file 匹配时按包含去写,不要锚定结尾:

js
routes: [{ profile: 'mobile', file: [/[\\/]mobile[\\/]/] }]   // 对
routes: [{ profile: 'mobile', file: [/\.mobile\.scss$/] }]     // SFC 匹配不到

Windows 与 POSIX 的分隔符不同,所以用 [\\/] 而不是 /。

4. 组件化项目的根容器基础样式会逐文件重复 ​

配了 rootSelector / root 之后,插件会追加一段全局基础样式。它是全局的,但 PostCSS 一次只看见一个文件,没有跨文件去重的余地——所以默认每个文件各一份。

Vue / Svelte 里每个组件的 <style> 块都是独立文件,于是 150 个组件就是 150 份安全区变量和根列规则。不报错,只是产物白白变大。

js
appPcPreset({
  rootSelector: '#app',
  rootInjectTo: 'src/styles/main',   // 只注入入口这一份
})

反过来,匹配不上就一份都没有,同样不报错。用 npx adaptive-matrix src/styles/main.css 确认入口文件里出现了新增声明。


原子化 CSS:Tailwind 与 UnoCSS ​

工具类 CSS 同样经过 PostCSS,所以同样会被换算——这正是你要的:p-4 和你手写的 padding: 16px 说的是同一个尺寸,应该得到同一个结果。

但默认配置一个都读不到,而且不报错。 装上插件跑一遍,手写的部分被换算了、工具类原样不动,两套尺寸从此对不齐。原因有两个,取决于你用的大版本。

包一层就都解决了:

js
import adaptiveMatrix, { appPcPreset, withAtomicCss } from 'postcss-adaptive-matrix'

export default {
  plugins: [adaptiveMatrix(withAtomicCss(appPcPreset({ rootSelector: '#app' })))],
}

withAtomicCss 是包装不是替换:你原有的 profiles、routes、root 全部保留,它只往上加需要的两件事。下面解释这两件事分别在挡什么。

挡路的第一件事:单位是 rem ​

Tailwind 3、UnoCSS presetUno / presetWind3 把长度直接写成 rem:

css
.p-4      { padding: 1rem }          /* 不是 16px */
.text-lg  { font-size: 1.125rem; line-height: 1.75rem }
.border   { border-width: 1px }      /* 边框仍然是 px */
.p-\[13px\] { padding: 13px }        /* 方括号任意值也是 px */

同一张表里两种单位都有。所以要读的是两种,不是把 px 改成 rem:只读 rem 会漏掉所有边框宽度和方括号值,只读 px 会漏掉间距和字号。withAtomicCss 做的第一件事就是把 rem 加进 unitToConvert。

页面如果写了 html { font-size: 62.5% },再配一个 rootValue: 10,读和写两头一起改,见配置参考。

挡路的第二件事:长度根本不在工具类里 ​

Tailwind 4 和 UnoCSS presetWind4 换了形状——工具类里没有长度,只有一个变量引用:

css
:root { --spacing: 0.25rem; --text-lg: 1.125rem; --radius-lg: 0.5rem }

.p-4       { padding: calc(var(--spacing) * 4) }
.gap-8     { gap: calc(var(--spacing) * 8) }
.rounded-lg{ border-radius: var(--radius-lg) }
.text-lg   { font-size: var(--text-lg) }

var() 是不透明的,编译器看不进去。所以要在源头认领这些主题 token。自定义属性默认不换算(要么被显式认领,要么开 transformCustomProperties),withAtomicCss 加的正是这条认领路由。

认领 token 之后工具类不用动也对了:

css
--spacing: clamp(3.41333px, 1.06667vw, 5.12px);
.p-4 { padding: calc(var(--spacing) * 4) }

calc(clamp(a, b, c) * 4) 等价于 clamp(4a, 4b, 4c)(正系数下乘法可以穿过 clamp),结果与直接换算 16px 逐位相同。

被认领的 token 前缀:--spacing、--text-、--leading-、--radius-、--container-。三个故意不在列表里:

没认领原因
--breakpoint-*它是画布切换的宽度。缩放它等于移动断点本身,而且没有任何地方会提示
--tracking-*用 em 发布,而 em 依附的字号已经被做成流体了,再缩一次是叠加
--shadow-*阴影的像素是按屏幕尺度画的层次感,不是设计稿上量出来的长度

主题里自己扩展的长度族用 tokenPrefixes 补:

js
withAtomicCss(appPcPreset(), { tokenPrefixes: ['--gutter-', '--size-'] })

内置默认值已经合适时,不需要为了签名硬写一个空配置:withAtomicCss() 就是完整的零配置写法。原子 CSS 专属设置也可直接写成 withAtomicCss({ tokenPrefixes: '--size-' });只有多个额外前缀才需要数组,只有确实存在要保留的主配置时才使用 withAtomicCss(base, options)。

前缀必须是以 -- 开头的非空自定义属性前缀。包装器会先校验要展开的集合,字段名拼错时给出建议,并在不折叠大小写的前提下去重——自定义属性名本来就区分大小写。

字号 token 照样可缩放 ​

--text-lg 这种名字不长得像字体属性,但它承载的就是字号。默认的 textProperties 里包含 --text-* 与 --leading-*,所以它拿到的是和手写 font-size 完全相同的 rem + vw 混合公式,浏览器文字缩放不受影响:

css
--text-lg: clamp(1.06725rem, calc(0.73125rem + 1.68vw), 1.23525rem);

这一条对没有被认领的 token 不起作用——它只决定一个已经要换算的长度怎么写,不决定它换不换算。

这一节是对着真实产物写的 ​

conformance/cases/atomic/ 下的三个用例,输入是 Tailwind CSS 4.3.3、UnoCSS 66.7.5 presetWind3、UnoCSS 66.7.5 presetWind4 的真实产物原文,不是手写的仿制品——上面那两种形状的差别大到手写必然写成想象中的样子。用 scripts/capture-atomic.mjs 重新抓取。

这两个框架不是 devDependency:抓下来的 CSS 就是全部输入,把 npm test 绑在别人的发版节奏上换不来额外信息。

另一条路 ​

不想加配置也可以让框架输出 px:UnoCSS 的 @unocss/preset-rem-to-px,Tailwind 3 改 theme.spacing。Tailwind 4 没有这条路——它的形状是 token 间接引用,与单位无关。

工具类走哪张画布 ​

工具类没有类名前缀特征,所以它们走 defaultProfile。要让某一批工具类走别的画布,用 routes 的 selector 通道;主题 token 整体改派用 withAtomicCss(base, { profile: 'pc' })。

组件库按需引入 ​

unplugin-vue-components 之类的按需引入,最终仍然是从 node_modules 里引 CSS 文件,路径通道照常命中,不需要额外配置。

但不要为了"性能"加 exclude: [/node_modules/]——那会把组件库的 CSS 整个排除在外,组件就回到原始像素,和你缩放后的页面对不齐。这正是组件库适配要解决的问题。

校验 ​

改完配置,跑一遍产物比看配置可靠:

bash
npx adaptive-matrix src/styles/app.css -c postcss.config.mjs

输出是逐条声明的前后对照,不用启动构建、不用开浏览器。上面那个「from 必须传」的坑,也可以用 --from 先试一遍再上线。完整用法见命令行预览。

这一篇是跑过真实构建的 ​

本文关于 Vite 的每一条说法都由 test/vite.test.ts 里的真实 Vite 构建验证,而不是靠 postcss().process 模拟。构建脚手架包含一个自动发现的 postcss.config.mjs、一份从 node_modules 里引入的依赖 CSS、以及一个由 @vitejs/plugin-vue 同款方式提供的 <style> 块,然后断言:

  • 配置文件确实被 Vite 找到并生效(没生效时构建照样成功、样式表原样输出,没有任何地方会说话);
  • 依赖的 CSS 按 node_modules 路径落到组件库画布,产出与页面上等价尺寸逐字节相同;
  • 带 query 串的 SFC id 能被包含式 file 路由命中;
  • 同样的输入再构建一次,产物完全一致(watch / HMR 会反复跑同一条流水线);
  • 反过来,用 /\.mobile\.css$/ 这种锚定结尾的写法,SFC 块确实静默留在默认画布上——上面第 3 条说的就是这件事,这条测试让它成为验证过的事实而不是记忆。

Webpack 侧没有起真实构建。postcss-loader 做的事就是带着 from 调 postcss.process,而 Webpack 场景真正出过问题的是 CommonJS 入口能不能直接调用(0.4.0 修复),那一条由 test/package.test.ts 针对构建产物本身验证。为一条几乎没有独立风险的路径引入整套 Webpack 依赖,不划算。

程序化编译 ​

集中说明见程序化 API 指南。本节保留,已有链接继续有效。

构建脚本、编辑器或服务需要直接取得编译结果时,可以使用具名导出的辅助函数:

ts
import { compileAdaptiveCss, createAdaptiveCompiler } from 'postcss-adaptive-matrix'

const output = await compileAdaptiveCss('.card { padding: 24px }')
console.log(output.css)

const compile = createAdaptiveCompiler({ profiles: { app: 375 } })
const result = await compile('.card { padding: 24px }', {
  process: { from: 'src/card.css', to: 'dist/card.css', map: { inline: false } },
  targets: { safari: 12 },
})

只有 CSS 字符串必填,编译配置和请求选项均可省略。process 接受 PostCSS 处理选项,targets 开启已有的特性支持审计。结果包含 css、map、warnings、compatibility(未传 targets 时为 null)和完整的 PostCSS result。语法或配置错误会拒绝 Promise。兼容性问题以数据返回,不会自动抛错;请检查 findings 与 unknownBrowsers 决定是否阻止构建。审计只覆盖文档列出的特性,并非全部 CSS 行为。

多文件可复用同一个编译函数以保留转换缓存。动态画布宽度与根字号会在每次编译时重新求值,同一路径的重新构建也不会使用过时的值。

可选构建门禁 ​

源码映射沿用 PostCSS 约定:process.map: false 禁用映射;{ inline: true } 将映射嵌入 CSS 注释,此时返回的 map 为 undefined;{ inline: false } 返回独立映射对象。通过 process.map.prev 传入上游映射,可在预处理链路中保留原始来源路径与源码内容。测试验证了传入映射的链路,并未运行 Sass 编译器。

传入 failOn: ['warnings', 'compatibility'] 后,结果的 gate 包含所选类别与 passed 布尔值。此选项及每个类别均按需启用;省略或传入 [] 时为 gate: null。兼容性门禁必须同时传入 targets,不支持的特性或未知浏览器名都会导致门禁失败,避免把无法识别的目标当作通过。门禁失败不会拒绝编译,也不会丢弃 CSS、源码映射或诊断:

ts
const output = await compileAdaptiveCss(source, {}, {
  targets: { safari: 14 },
  failOn: 'compatibility',
})
if (output.gate?.passed === false) {
  console.error(output.compatibility)
  process.exitCode = 1
}

此接口门禁目前覆盖警告和兼容性,不包含 CLI 的断点接缝分析。语法与配置错误仍会拒绝 Promise;门禁失败则保留为可检查的结果数据。

基于 MIT 协议发布