Article

样式构建 PostCSS

更新于:2026-07-08

第 1 章:PostCSS 简介与核心概念

1.1 什么是 PostCSS

概念名称说明注意事项
PostCSS一个基于插件的 CSS 转换工具,使用 JavaScript 插件来分析和转换 CSS 代码。它本身不提供语法,而是通过插件实现功能扩展。PostCSS 不是预处理器或后处理器,而是一个平台,允许开发者使用插件来增强 CSS 功能。
插件化架构PostCSS 的所有功能都由插件提供,如 autoprefixer、postcss-preset-env 等,用户可根据需要自由组合。插件需显式安装和配置,不会自动生效。
JavaScript 驱动PostCSS 使用 JavaScript 编写插件,因此可以利用 Node.js 生态系统进行高度定制。需要一定的 JavaScript 基础才能开发或调试插件。

PostCSS 的核心思想

PostCSS 的核心理念可以概括为一句话:

“CSS 本身就是一种格式,不需要新语言——只需要更好的工具。”

Sass 和 Less 的做法是”发明一种新语法,然后编译为 CSS”;PostCSS 的做法是”保持 CSS 语法不变,用插件在编译阶段优化它”。

一个典型的 PostCSS 处理流程

原始 CSS 文件(标准 CSS 语法)


 PostCSS 解析为 AST


 插件 1: autoprefixer(添加浏览器前缀)


 插件 2: postcss-nested(展开嵌套规则)


 插件 3: cssnano(压缩与优化)


 输出最终 CSS 字符串

🔑 关键洞察:PostCSS 不改变你写 CSS 的方式(语法仍是标准的),它改变的是 CSS 在被浏览器接收之前的”优化路径”。


1.2 PostCSS 的工作原理

概念名称说明注意事项
抽象语法树(AST)PostCSS 将 CSS 源码解析为 AST(Abstract Syntax Tree),插件通过操作 AST 节点来修改样式规则。所有转换操作都在 AST 层面进行,确保结构安全。
插件执行流程每个插件接收 AST,进行遍历和修改,最终由 PostCSS 将修改后的 AST 序列化为 CSS 字符串。插件顺序影响输出结果,应按逻辑顺序配置。
源码映射(Source Map)支持生成 source map,便于调试转换后的 CSS 对应原始源文件位置。需在构建工具中启用 source map 选项才能生效。

深入理解:AST 的结构

以下面这段 CSS 为例:

/* style.css */
:root {
  --primary-color: #3498db;
}

.button {
  background: var(--primary-color);
  padding: 10px 20px;
}

PostCSS 会将它解析为如下结构的 AST:

Root(根节点)
├── Comment: "style.css"
├── Rule(选择器: :root)
│   └── Declaration
│       ├── prop: "--primary-color"
│       └── value: "#3498db"
├── Rule(选择器: .button)
│   ├── Declaration
│   │   ├── prop: "background"
│   │   └── value: "var(--primary-color)"
│   └── Declaration
│       ├── prop: "padding"
│       └── value: "10px 20px"

插件的工作就是遍历这棵树。例如 autoprefixer 会检查每个 Declarationprop 是否需要加前缀,如发现 display: flex 且目标浏览器需要,则在同一 Rule 下新增 display: -webkit-boxdisplay: -ms-flexbox 等声明。

插件执行顺序:为什么顺序很重要?

❌ 错误的顺序示例(先压缩再加前缀):

// postcss.config.js
module.exports = {
  plugins: [
    require('cssnano')(),         // 1. 先压缩(可能移除空格合并声明)
    require('autoprefixer')()     // 2. 再加前缀(压缩后的 AST 结构可能被破坏)
  ]
};

✅ 正确的顺序示例(先转换再压缩):

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import')(),        // 1. 合并 @import
    require('postcss-nested')(),        // 2. 展开嵌套
    require('autoprefixer')(),          // 3. 添加浏览器前缀
    require('cssnano')()                // 4. 最后压缩
  ]
};

插件顺序的黄金法则

  1. 读取与合并类插件最先执行(如 postcss-import
  2. 语法扩展类插件其次(如 postcss-nestedpostcss-simple-vars
  3. 兼容性增强类插件再次(如 autoprefixerpostcss-preset-env
  4. 优化与压缩类插件最后执行(如 cssnanopostcss-calc

Source Map 作用

启用 Source Map 后,浏览器 DevTools 的”Sources”面板会显示原始 CSS 位置,而非编译后位置:

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')()
  ],
  map: true  // 生成 .map 文件
};

输出

  • style.css
  • style.css.map(包含原始位置映射信息)

1.3 PostCSS 与其他预处理器(Sass、Less)的对比

对比维度PostCSSSass / Less注意事项
语法扩展方式无内置语法,功能由插件提供提供自己的语法(如嵌套、变量、混合等)PostCSS 更灵活,可按需选择功能。
学习成本需理解插件机制和配置有固定语法和规则,学习曲线较陡PostCSS 初期配置较复杂。
可扩展性极高,可通过 JS 编写任意插件有限,依赖预处理器自身功能PostCSS 更适合定制化需求。
性能通常更快,仅运行所需插件固定功能集,可能包含未使用功能合理配置插件可提升构建速度。
社区生态插件丰富,与现代工具链集成好成熟稳定,文档齐全PostCSS 更贴近现代前端工程化。

场景对比:同一段样式的三种写法

写法 1:标准 CSS(浏览器直接解析)

/* style.css */
.card {
  padding: 16px;
  border-radius: 8px;
  background: #f4f4f4;
}

.card .title {
  font-size: 18px;
  color: #222;
}

.card .content {
  line-height: 1.5;
  color: #555;
}

写法 2:Sass 语法(需 sass 编译器翻译)

// style.scss
$card-bg: #f4f4f4;
$card-padding: 16px;
$title-color: #222;

.card {
  padding: $card-padding;
  border-radius: 8px;
  background: $card-bg;

  .title {
    font-size: 18px;
    color: $title-color;
  }

  .content {
    line-height: 1.5;
    color: #555;
  }
}

写法 3:PostCSS + 插件(标准 CSS + 编译时优化)

/* style.css —— 语法仍是标准 CSS,但通过插件增强 */
@value card-bg: #f4f4f4;
@value title-color: #222;

.card {
  padding: 16px;
  border-radius: 8px;
  background: card-bg;

  & .title {
    font-size: 18px;
    color: title-color;
  }

  & .content {
    line-height: 1.5;
    color: #555;
  }
}

处理后,PostCSS 输出

.card { padding: 16px; border-radius: 8px; background: #f4f4f4; }
.card .title { font-size: 18px; color: #222; }
.card .content { line-height: 1.5; color: #555; }

性能对比

以下是中等规模项目(约 200 个 CSS 文件)的构建时间对比(2025 年硬件):

工具典型配置平均构建时间
Sass (Dart)dart-sass --style compressed2.8s
Lesslessc --clean-css3.2s
PostCSSpostcss-import + autoprefixer + cssnano1.1s
PostCSS(仅 cssnano)只做压缩0.4s

PostCSS 更快的原因

  1. 插件按需加载,不解析未使用的语法
  2. 基于 Node.js Stream,支持增量处理
  3. AST 遍历一次完成多个插件工作(插件链式执行,而非多次解析)

何时选择 PostCSS,何时选择 Sass?

你的场景推荐工具原因
使用 Vite / Next.js / Nuxt 等现代框架PostCSS框架已内置 PostCSS 支持,零配置启用
项目大量使用嵌套、变量、MixinSass + PostCSS 组合Sass 提供编写效率,PostCSS 负责最终优化与前缀
追求最小 CSS 体积与最大灵活性PostCSS可按需组合插件,精确控制输出
团队熟悉 Sass 且有大量历史代码Sass保持一致性,避免迁移成本
企业级项目,需支持 IE11 等旧浏览器PostCSS(或 Sass + PostCSS)browserslist 驱动的兼容性方案最可靠

1.4 PostCSS 的生态系统与常用插件概述

插件名称说明注意事项
autoprefixer自动为 CSS 属性添加浏览器厂商前缀(如 -webkit--moz-依赖 caniuse 数据,需配置浏览器范围(browserslist)。
postcss-preset-env允许使用未来的 CSS 特性,并将其转换为当前浏览器支持的语法可替代部分 Sass 功能,支持 CSS 变量、嵌套等提案。
postcss-nested支持嵌套 CSS 规则,类似 Sass 的嵌套语法可与 postcss-preset-env 一起使用。
postcss-simple-vars支持 $variable 语法定义和使用变量与原生 CSS 变量不同,是编译时替换。
postcss-import支持 @import 导入多个 CSS 文件并合并可减少 HTTP 请求,提升可维护性。
postcss-calc解析 calc() 表达式并进行数学计算简化支持常量计算,提升性能。
postcss-url转换 URL 路径,支持内联、重写、复制等模式常用于资源路径处理或 base64 内联。
cssnano用于压缩和优化 CSS 的插件,类似 UglifyJS 对 JavaScript 的作用推荐在生产环境使用。

插件 1:autoprefixer(最常用)

作用:根据目标浏览器范围自动添加 -webkit--ms--moz- 等前缀。

配置示例

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      grid: 'autoplace',
      overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead']
    })
  ]
};

输入 / 输出对比

输入输出(目标包含 IE11 时)
display: flexdisplay: -webkit-box; display: -ms-flexbox; display: flex
display: griddisplay: -ms-grid; display: grid
user-select: none-webkit-user-select: none; -moz-user-select: none; user-select: none

插件 2:postcss-preset-env(面向未来 CSS)

作用:让你今天就可以写明天的 CSS 语法(如 :has()color-mix()、CSS 嵌套等),插件会自动转换为当前浏览器支持的语法。

配置示例

module.exports = {
  plugins: [
    require('postcss-preset-env')({
      stage: 2,                    // 仅使用稳定到 Stage 2 的提案
      features: {
        'nesting-rules': true,     // 启用 CSS 嵌套
        'custom-properties': true  // 启用 CSS 变量降级
      },
      browserslist: ['> 1%', 'last 2 versions']
    })
  ]
};

输入 / 输出对比

输入(未来 CSS 语法)

.card {
  background: color-mix(in srgb, #3498db 50%, #ffffff);

  & .title {
    font-weight: bold;
  }
}

输出(现代浏览器兼容语法)

.card { background: #9ecbe7; }
.card .title { font-weight: bold; }

插件 3:postcss-nested(Sass 风格嵌套)

作用:支持在 CSS 中使用 & 符号实现选择器嵌套,类似 Sass 的写法。

配置示例

module.exports = {
  plugins: [
    require('postcss-nested')()
  ]
};

输入 / 输出对比

输入(嵌套写法)

.nav {
  background: #333;

  & li {
    display: inline-block;

    & a {
      color: #fff;
      &:hover { text-decoration: underline; }
    }
  }
}

输出(展开后的标准 CSS)

.nav { background: #333; }
.nav li { display: inline-block; }
.nav li a { color: #fff; }
.nav li a:hover { text-decoration: underline; }

插件 4:postcss-import(文件合并)

作用:将多个 @import 文件合并为一个 CSS,减少 HTTP 请求。

项目结构

src/
  css/
    base.css        /* 基础样式 */
    layout.css      /* 布局样式 */
    components.css  /* 组件样式 */
    main.css        /* 通过 @import 引入上面三个 */

main.css 内容

@import 'base.css';
@import 'layout.css';
@import 'components.css';

/* 全局样式 */
body { margin: 0; }

postcss.config.js

module.exports = {
  plugins: [
    require('postcss-import')()
  ]
};

输出(合并后的 CSS):三个文件的内容依次出现在 main.css 中,最终只输出一个文件。

插件 5:postcss-calc(计算优化)

作用:在编译阶段解析 calc() 中的常量运算,减少浏览器运行时开销。

输入

.wrapper {
  width: calc(100% - 2 * 16px);
  padding: calc(8px + 8px);
}

输出

.wrapper {
  width: calc(100% - 32px);   /* 2 * 16px = 32px,编译时完成 */
  padding: 16px;               /* 8px + 8px = 16px,编译时完成 */
}

插件 6:postcss-url(资源路径处理)

作用:将 url('...') 中的图片内联为 base64,或自动重写路径。

postcss.config.js

module.exports = {
  plugins: [
    require('postcss-url')({
      url: 'inline',        // 将小于某个阈值的图片内联为 base64
      maxSize: 10           // 最大 10KB 的图片内联
    })
  ]
};

输入

.icon {
  background: url('./icons/arrow.svg');   /* 大小: 2KB */
}
.hero {
  background: url('./images/hero.png');   /* 大小: 120KB */
}

输出

.icon {
  background: url('data:image/svg+xml;base64,PHN2ZyB...');  /* 已内联 */
}
.hero {
  background: url('./images/hero.png');  /* 超过 10KB,保留原路径 */
}

插件 7:cssnano(压缩与优化)

作用:压缩 CSS 体积,包括去除空格、合并声明、简写属性、移除无用前缀等。

postcss.config.js

module.exports = {
  plugins: [
    require('autoprefixer')(),      // 先加前缀
    require('cssnano')({            // 后压缩
      preset: ['default', {
        discardComments: { removeAll: true },
        normalizeWhitespace: true,
        colormin: true              // 将 #ffffff 简化为 #fff
      }]
    })
  ]
};

输入 / 输出对比

输入输出
256 行,带有注释和空行1 行,无注释无空格
color: #ffffffcolor:#fff
margin-top: 10px; margin-right: 10px; margin-bottom: 10px; margin-left: 10px;margin:10px
体积: 24.8 KB体积: 17.2 KB(减少约 30%)

🔑 第一章总结

五个核心要点

  1. PostCSS 是平台,不是语言 — 它不提供新语法,而是通过插件增强标准 CSS 的处理能力
  2. 一切都是插件 — 从加前缀到压缩优化,每个功能都需要显式安装和配置对应插件
  3. AST 是核心机制 — 所有转换都在”抽象语法树”上完成,插件遍历节点进行修改
  4. 顺序至关重要 — 插件执行顺序影响输出,按”读取 → 转换 → 兼容 → 压缩”排列
  5. 与 Sass 互补而非替代 — PostCSS 可独立使用,也可在 Sass 编译后做最终优化

第 2 章:环境搭建与基础配置

2.1 安装 PostCSS(CLI 与 Node.js 集成)

方法名称语法用途注意事项
npm 安装(全局)npm install -g postcss全局安装 PostCSS CLI,便于命令行使用全局安装后可在任意目录使用 postcss 命令。
npm 安装(项目本地)npm install postcss --save-dev将 PostCSS 作为开发依赖安装到项目中推荐方式,便于版本控制和团队协作。
Node.js 中导入require('postcss')import postcss from 'postcss'在 JavaScript 脚本中使用 PostCSS API需配合插件数组和处理选项使用。
检查版本postcss --version查看已安装的 PostCSS 版本确保版本兼容插件要求。

推荐安装流程(本地项目)

第 1 步:初始化项目(如尚未初始化):

# 进入项目目录
cd your-project

# 初始化 package.json(如果没有)
npm init -y

第 2 步:安装 PostCSS 和常用插件到项目本地:

# 安装 PostCSS 核心 + CLI + 3 个常用插件
npm install --save-dev postcss postcss-cli autoprefixer cssnano

第 3 步:验证安装:

# 方法 1:通过 npx 运行(推荐,无需全局安装)
npx postcss --version
# 输出: 8.4.x

# 方法 2:在 package.json 的 scripts 中定义
# "build:css": "postcss src/style.css -o dist/style.css"

package.json 示例

{
  "name": "your-project",
  "version": "1.0.0",
  "scripts": {
    "build:css": "postcss src/style.css -o dist/style.css",
    "watch:css": "postcss src/style.css -o dist/style.css --watch"
  },
  "devDependencies": {
    "postcss": "^8.4.47",
    "postcss-cli": "^11.0.0",
    "autoprefixer": "^10.4.20",
    "cssnano": "^7.0.6"
  }
}

Node.js API 用法(自定义脚本场景)

当你需要将 PostCSS 嵌入到自定义构建脚本时:

// build-css.js
const fs = require('fs');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');

// 读取源 CSS
const css = fs.readFileSync('./src/style.css', 'utf8');

// 配置插件链
const plugins = [
  autoprefixer({ grid: 'autoplace' }),
  cssnano()
];

// 执行 PostCSS 处理
postcss(plugins)
  .process(css, {
    from: './src/style.css',    // 源文件路径(用于 source map)
    to: './dist/style.css',     // 目标文件路径
    map: { inline: false }      // 生成独立的 .map 文件
  })
  .then(result => {
    // 写入处理后的 CSS
    fs.writeFileSync('./dist/style.css', result.css);
    // 写入 source map(如果有)
    if (result.map) {
      fs.writeFileSync('./dist/style.css.map', result.map.toString());
    }
    console.log('✅ CSS 构建完成');
  })
  .catch(error => {
    console.error('❌ CSS 构建失败:', error);
    process.exit(1);
  });

运行脚本

node build-css.js
# 输出: ✅ CSS 构建完成

2.2 使用 postcss-cli 进行基本转换

方法名称语法用途注意事项
基本转换命令postcss input.css -o output.css将 input.css 转换并输出到 output.css若无插件,输出与输入相同。
指定多个输入文件postcss *.css -o output.css合并多个 CSS 文件为一个输出文件按字母顺序合并。
使用插件postcss input.css --use autoprefixer -o output.css在转换时应用指定插件需提前安装插件(如 npm install autoprefixer)。
启用 Source Mappostcss input.css -o output.css --map生成 source map 文件有助于调试转换后的 CSS。
输出到标准输出postcss input.css --no-map将结果输出到控制台可结合管道操作符用于调试或与其他工具集成。

实战 1:基本的文件转换

# 假设目录结构:
# src/
#   style.css      (源文件,标准 CSS)
# dist/            (构建输出目录)

# 创建输出目录
mkdir -p dist

# 基本转换(无插件,输出文件与源文件相同)
npx postcss src/style.css -o dist/style.css

# 带 autoprefixer 插件的转换
npx postcss src/style.css --use autoprefixer -o dist/style.css

# 验证输出
cat dist/style.css

实战 2:启用 Source Map

# 生成独立的 .map 文件
npx postcss src/style.css -o dist/style.css --map

# 生成的文件:
#   dist/style.css         (处理后的 CSS,末尾含 map 引用注释)
#   dist/style.css.map     (source map 文件)

dist/style.css 末尾会出现:

/*# sourceMappingURL=style.css.map */

实战 3:多文件合并

# src/
#   base.css
#   layout.css
#   components.css

# 合并三个文件为 bundle.css
npx postcss src/base.css src/layout.css src/components.css -o dist/bundle.css

# 或使用通配符(按字母顺序合并)
npx postcss src/*.css -o dist/bundle.css

实战 4:多插件组合

# 使用 autoprefixer 和 cssnano,输出到 dist/
npx postcss src/style.css \
  --use autoprefixer \
  --use cssnano \
  -o dist/style.css \
  --map

实战 5:输出到 stdout 并配合管道

# 直接在终端查看处理结果(不写入文件)
npx postcss src/style.css --use autoprefixer --no-map

# 使用管道将结果传给其他命令
npx postcss src/style.css --use autoprefixer --no-map | grep -c 'display'

# 结合 gzip 压缩(Linux/Mac)
npx postcss src/style.css --use cssnano --no-map | gzip > dist/style.css.gz

实战 6:监听模式(开发时自动重新编译)

# 监听文件变化,自动重新处理
npx postcss src/style.css -o dist/style.css --watch
# 输出: Waiting for file changes...

# 修改 src/style.css 后,终端会提示:
# ✔ Finished src/style.css (12ms)

开发流程建议

  • 开发环境:开启 --watch 模式并启用 --map,配合浏览器 DevTools 实时调试
  • 生产环境:关闭 --watch,启用 cssnano 做压缩,禁用 --map 减少体积

2.3 配置 postcss.config.js 文件

方法名称语法用途注意事项
基本配置对象module.exports = { plugins: [...] }定义插件列表插件需已安装并正确 require
配置插件选项require('plugin-name')(options)为插件传入选项参数不同插件支持的选项不同,需查阅文档。
条件加载插件根据 NODE_ENV 判断按环境启用不同插件推荐用于区分开发与生产环境。
使用插件字符串(需配合工具)'autoprefixer'简化配置(部分工具支持)需构建工具(如 webpack)支持自动 require

方式 1:最简配置

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
};

使用:有了这个文件后,postcss-cli 会自动读取它:

# 无需再用 --use 参数
npx postcss src/style.css -o dist/style.css
# autoprefixer 已被自动应用

方式 2:带插件选项的配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import')(),
    require('postcss-nested')(),
    require('autoprefixer')({
      grid: 'autoplace',
      overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead']
    }),
    require('cssnano')({
      preset: ['default', {
        discardComments: { removeAll: true }
      }]
    })
  ]
};

方式 3:按环境条件加载插件(推荐)

// postcss.config.js
module.exports = ({ env }) => ({
  plugins: [
    require('postcss-import')(),
    require('postcss-nested')(),
    require('autoprefixer')(),
    // 仅在 production 环境启用 cssnano 压缩
    env === 'production' && require('cssnano')()
  ].filter(Boolean)  // 过滤掉 false(当 env 不是 production 时)
});

使用方式

# 开发环境:不压缩(保留格式和空格,便于调试)
NODE_ENV=development npx postcss src/style.css -o dist/style.css

# 生产环境:启用 cssnano 压缩
NODE_ENV=production npx postcss src/style.css -o dist/style.css

方式 4:ES Module 格式(若项目使用 “type”: “module”)

// postcss.config.js(ES Module 写法)
export default {
  plugins: [
    (await import('autoprefixer')).default
  ]
};

或者使用异步函数:

// postcss.config.js
export default async () => {
  const autoprefixer = (await import('autoprefixer')).default;
  const cssnano = (await import('cssnano')).default;

  return {
    plugins: [
      autoprefixer({ grid: 'autoplace' }),
      cssnano()
    ]
  };
};

方式 5:字符串插件名(部分工具支持)

注意:此写法仅在部分构建工具(如 Next.js、Nuxt、Webpack postcss-loader)中有效,postcss-cli 本身不自动解析字符串。

// postcss.config.js
module.exports = {
  plugins: [
    'autoprefixer',                  // 等价于 require('autoprefixer')()
    ['autoprefixer', { grid: true }] // 等价于 require('autoprefixer')({ grid: true })
  ]
};

配置文件查找顺序

PostCSS 工具链会按以下顺序查找配置文件(优先级从高到低):

  1. 命令行显式指定的路径:--config ./path/to/postcss.config.js
  2. 当前目录的 postcss.config.js
  3. 当前目录的 postcss.config.cjs(CommonJS 格式)
  4. 当前目录的 postcss.config.mjs(ES Module 格式)
  5. 当前目录的 .postcssrc(JSON 格式)
  6. package.json 中的 postcss 字段

完整示例:中型项目推荐配置

// postcss.config.js
module.exports = ({ env }) => ({
  plugins: [
    require('postcss-import')({
      path: ['./src/css']              // @import 的搜索路径
    }),
    require('postcss-nested')(),       // 支持 & 嵌套
    require('postcss-preset-env')({    // 使用未来 CSS 语法
      stage: 2,
      features: {
        'nesting-rules': true,
        'custom-properties': true
      }
    }),
    require('autoprefixer')({          // 自动加前缀
      grid: 'autoplace'
    }),
    // 生产环境专用插件
    env === 'production' && require('postcss-calc')(),
    env === 'production' && require('cssnano')({
      preset: ['default', {
        discardComments: { removeAll: true },
        normalizeWhitespace: true,
        colormin: true
      }]
    })
  ].filter(Boolean)
});

2.4 在 Webpack 中集成 PostCSS

方法名称语法用途注意事项
安装 loadernpm install postcss-loader --save-dev安装 Webpack 的 PostCSS 加载器必须安装 postcss-loader 才能集成。
配置 module.rules{ test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] }在 Webpack 规则中使用 PostCSS执行顺序从右到左postcss-loader 应在 css-loader 后(即数组靠后位置)。
启用 source mapdevtool: 'source-map'确保生成 source map需在 css-loaderpostcss-loader 中启用 sourceMap 选项。
配置 loader 选项{ loader: 'postcss-loader', options: { postcssOptions: { plugins: [...] } } }指定配置文件路径或内联插件推荐使用外部 postcss.config.js 统一管理。

步骤 1:安装所需依赖

npm install --save-dev webpack webpack-cli css-loader style-loader postcss postcss-loader autoprefixer cssnano

步骤 2:创建 postcss.config.js

// postcss.config.js(与 2.3 节相同,Webpack 自动识别)
module.exports = {
  plugins: [
    require('autoprefixer')({ grid: 'autoplace' }),
    require('cssnano')()
  ]
};

步骤 3:配置 webpack.config.js

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  devtool: 'source-map',  // 启用 source map
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',          // 3. 将 CSS 注入到 DOM
          {
            loader: 'css-loader', // 2. 解析 @import 和 url()
            options: { sourceMap: true }
          },
          {
            loader: 'postcss-loader', // 1. ★ PostCSS 处理(最先执行)
            options: {
              sourceMap: true,
              postcssOptions: {
                config: path.resolve(__dirname, 'postcss.config.js')
              }
            }
          }
        ]
      }
    ]
  },
  mode: 'development'
};

⚠️ 重要提醒 — loader 执行顺序

Webpack 的 use 数组中,loader 执行顺序是从右向左(或从下往上):

use: ['style-loader', 'css-loader', 'postcss-loader']
        ↑              ↑                 ↑
       第3步          第2步              第1步

即:先由 postcss-loader 处理 CSS(加前缀、压缩),再由 css-loader 解析,最后由 style-loader 注入到页面。顺序错误会导致前缀未生效或 Source Map 异常。

步骤 4:在入口文件中引入 CSS

// src/index.js
import './style.css';  // Webpack 会依次通过 postcss-loader → css-loader → style-loader 处理

console.log('✅ 项目已加载');

步骤 5:构建并验证

# 运行构建
npx webpack --config webpack.config.js

# 输出:
#   Asset       Size
#   bundle.js   123 KiB
# ✔ 成功编译

内联配置方式(不推荐,但可用于快速测试)

// webpack.config.js(不使用外部 postcss.config.js,直接内联)
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  ['autoprefixer', { grid: 'autoplace' }],
                  ['cssnano']
                ]
              }
            }
          }
        ]
      }
    ]
  }
};

为什么不推荐内联

  • PostCSS 配置无法被其他工具(如 postcss-cli、Stylelint)共享
  • webpack.config.js 变得臃肿
  • 不同团队成员查看配置时需打开多个文件

2.5 在 Vite 中集成 PostCSS

方法名称语法用途注意事项
创建 postcss.config.jsmodule.exports = { plugins: [...] }Vite 自动识别该配置文件Vite 原生支持 PostCSS,无需额外 loader
内联配置(vite.config.js)css: { postcss: { plugins: [...] } }在 Vite 配置中直接定义 PostCSS 插件优先级高于 postcss.config.js
环境区分配置根据 mode 参数动态返回配置不同环境使用不同插件需注意插件在生产环境的副作用。
使用 .postcssrc 文件.postcssrc.json.postcssrc.js支持 JSON 或 JS 格式的配置文件postcss.config.js 互斥,优先级较低。

方式 1:使用 postcss.config.js(推荐)

第 1 步:安装依赖

npm install --save-dev postcss autoprefixer cssnano

第 2 步:创建 postcss.config.js

// postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: { grid: 'autoplace' },
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
  }
};

或使用数组风格

// postcss.config.js(更灵活,支持条件判断)
module.exports = ({ env }) => ({
  plugins: [
    require('autoprefixer')({ grid: 'autoplace' }),
    env === 'production' && require('cssnano')()
  ].filter(Boolean)
});

第 3 步:验证

# 启动开发服务器
npx vite
# 输出: VITE v5.x.x  ready in 300 ms
#       ➜  Local:   http://localhost:5173

# 或生产构建
npx vite build
# 输出: dist/index.html 及相关 CSS

Vite 会自动发现项目根目录下的 postcss.config.js 并应用到所有 CSS 文件处理中,无需在 vite.config.js 中做任何配置

方式 2:在 vite.config.js 中内联配置

// vite.config.js
export default {
  css: {
    postcss: {
      plugins: [
        require('autoprefixer')({ grid: 'autoplace' }),
        require('cssnano')()
      ]
    }
  }
};

⚠️ 注意:如果同时存在 vite.config.js 中的内联配置和 postcss.config.js 文件,Vite 会以内联配置为准,忽略 postcss.config.js

方式 3:按环境动态区分配置

// vite.config.js
export default ({ mode }) => ({
  css: {
    postcss: {
      plugins: [
        require('autoprefixer')({ grid: 'autoplace' }),
        // 仅在 production 模式启用 cssnano
        mode === 'production' && require('cssnano')()
      ].filter(Boolean)
    }
  }
});

使用

# 开发模式 (mode = 'development'):不压缩 CSS,速度更快
npx vite

# 生产构建 (mode = 'production'):启用 cssnano 压缩
npx vite build

方式 4:使用 .postcssrc(JSON 格式,简单场景)

// .postcssrc
{
  "plugins": {
    "autoprefixer": {
      "grid": "autoplace"
    },
    "cssnano": {}
  }
}

Vite 中 PostCSS 与 Tailwind CSS 配合使用

这是 Vite 项目中最常见的场景:

postcss.config.js

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {}
  }
};

vite.config.js

import { defineConfig } from 'vite';

export default defineConfig({
  // Vite 会自动读取 postcss.config.js,此处无需重复配置
  server: {
    port: 5173
  },
  build: {
    outDir: 'dist'
  }
});

验证构建

npx vite build
# 输出 dist/assets/index-[hash].css,已包含 Tailwind 原子类 + autoprefixer 前缀 + 默认压缩

🔑 第二章总结

五个核心要点

  1. 推荐本地安装 — 使用 npm install --save-dev postcss postcss-cli 而非全局安装,确保团队成员使用相同版本
  2. postcss.config.js 是配置中心 — 将所有 PostCSS 插件和选项统一管理,便于被 Webpack、Vite、Next.js 等多个工具共享
  3. 条件加载是最佳实践 — 用 env === 'production' && require('cssnano') 区分开发/生产环境,兼顾开发速度与构建体积
  4. Webpack loader 顺序至关重要postcss-loader 必须在 use 数组中排在 css-loader 之后(即先执行)
  5. Vite 原生支持 — Vite 已内置 PostCSS 支持,只需创建 postcss.config.js,无需安装额外 loader 或插件

第 3 章:PostCSS 插件机制详解

3.1 插件的工作原理与执行流程

概念名称说明注意事项
CSS 解析为 ASTPostCSS 将 CSS 源码解析成抽象语法树(AST),每个规则、声明、选择器等都成为节点对象。AST 是插件操作的基础,所有修改都在树结构上进行。
插件函数结构插件是一个函数,接收选项并返回一个 PostCSS 插件对象,包含 OnceAtRuleRule 等钩子。插件必须遵循 PostCSS 插件规范,返回正确的结构。
插件执行顺序插件按 postcss.config.js 中定义的顺序依次执行,前一个插件的输出作为下一个插件的输入。顺序错误可能导致预期外结果(如 autoprefixer 应在变量替换之后)。
节点遍历与修改插件通过访问 AST 节点(如 RuleDeclaration)并调用其方法(如 replaceWithremove)进行修改。修改需谨慎,避免破坏原有结构或引入语法错误。
AST 序列化为 CSS所有插件执行完成后,PostCSS 将最终的 AST 重新生成为标准 CSS 字符串。生成过程可配置(如缩进、换行),默认保持可读性。

深入理解 1:从 CSS 源码到 AST

以一段简单 CSS 为例:

/* header.css */
.wrapper {
  display: flex;
  padding: 16px;
}

@media (min-width: 768px) {
  .wrapper {
    padding: 32px;
  }
}

PostCSS 解析后得到的 AST 结构(简化表示):

Root
├── Comment: " header.css "
├── Rule (selector: ".wrapper")
│   ├── Declaration (prop: "display", value: "flex")
│   └── Declaration (prop: "padding", value: "16px")
└── AtRule (name: "media", params: "(min-width: 768px)")
    └── Rule (selector: ".wrapper")
        └── Declaration (prop: "padding", value: "32px")

深入理解 2:插件函数的基本结构

一个最小的 PostCSS 插件结构如下:

// my-plugin.js
module.exports = (opts = {}) => {
  // opts 接收插件配置(如 { grid: 'autoplace' })
  return {
    // 插件标识符(用于调试和错误提示)
    postcssPlugin: 'my-plugin',

    // ⭐ 全局钩子:在开始处理前执行一次
    Once(root, { result }) {
      // root: 整个 AST 的根节点
      // result: PostCSS 的结果对象
      console.log('开始处理 CSS,共', root.nodes.length, '个顶级节点');
    },

    // ⭐ 节点钩子:遍历所有 Rule(选择器)时执行
    Rule(rule) {
      // 例如:给所有选择器加上前缀
      if (opts.prefix && !rule.selector.startsWith(opts.prefix)) {
        rule.selector = rule.selector
          .split(',')
          .map(s => `${opts.prefix} ${s.trim()}`)
          .join(', ');
      }
    },

    // ⭐ 节点钩子:遍历所有 Declaration(属性声明)时执行
    Declaration(decl) {
      // 例如:将 px 单位缩放为 rem
      if (opts.scale && decl.value.includes('px')) {
        const num = parseFloat(decl.value) / opts.scale;
        decl.value = `${num.toFixed(3)}rem`;
      }
    },

    // ⭐ 节点钩子:遍历所有 @规则(如 @media、@keyframes)
    AtRule(atRule) {
      if (atRule.name === 'keyframes' && opts.removeKeyframes) {
        atRule.remove();  // 移除所有 @keyframes
      }
    },

    // ⭐ 全局钩子:在所有节点处理完成后执行一次
    OnceExit(root, { result }) {
      console.log('处理完成,CSS 大小:', result.css.length, '字符');
    }
  };
};

// ⚠️ 必需:标记此模块为 PostCSS 插件
module.exports.postcss = true;

使用自定义插件

// postcss.config.js
module.exports = {
  plugins: [
    require('./my-plugin')({
      prefix: '.my-app',
      scale: 16
    }),
    require('autoprefixer')()
  ]
};

深入理解 3:插件执行顺序的影响

错误的顺序可能导致问题

场景插件 1(先执行)插件 2(后执行)问题
❌ 错误cssnano(压缩)autoprefixer(加前缀)压缩后 AST 结构改变,autoprefixer 可能无法识别需要加前缀的属性
✅ 正确autoprefixer(加前缀)cssnano(压缩)先加前缀再压缩,结果正确且体积最小
❌ 错误autoprefixerpostcss-nestednested 展开嵌套规则时需要带前缀,此时 autoprefixer 已经执行过
✅ 正确postcss-nestedautoprefixer先展开嵌套,再对所有规则统一加前缀

推荐的通用插件顺序

// postcss.config.js
module.exports = {
  plugins: [
    // 1. 读取与合并:@import、文件合并
    require('postcss-import')(),

    // 2. 语法扩展:变量、嵌套、未来 CSS 特性
    require('postcss-simple-vars')(),
    require('postcss-nested')(),
    require('postcss-preset-env')({ stage: 2 }),

    // 3. 兼容性增强:前缀、特性降级
    require('autoprefixer')(),

    // 4. 计算优化:常量解析
    require('postcss-calc')(),

    // 5. 资源处理:URL、内联
    require('postcss-url')({ url: 'inline', maxSize: 10 }),

    // 6. 最终优化:压缩、去重
    require('cssnano')()
  ]
};

深入理解 4:节点遍历与常用方法

插件在 AST 上的典型操作:

// example-plugin.js
module.exports = () => ({
  postcssPlugin: 'example-plugin',

  Declaration(decl, { Warning }) {
    // 1. 读取属性
    const prop = decl.prop;           // "display"
    const value = decl.value;         // "flex"
    const raw = decl.raws.value;      // 原始字符串(含注释、空格)

    // 2. 修改属性
    decl.prop = prop.toLowerCase();
    decl.value = value.trim();

    // 3. 新增同级声明(在当前声明之后插入)
    decl.after('margin: 0');

    // 4. 新增同级声明(在当前声明之前插入)
    decl.before('position: relative');

    // 5. 删除节点
    if (prop === 'opacity' && value === '1') {
      decl.remove();  // opacity: 1 无需存在
    }

    // 6. 替换节点
    if (prop === 'font-weight' && value === 'bold') {
      decl.replaceWith('font-weight: 700');
    }

    // 7. 生成警告(出现在构建日志中)
    if (value.includes('!important')) {
      decl.warn(decl.root(), '避免使用 !important', { word: '!important' });
    }
  },

  Rule(rule) {
    // 1. 获取选择器字符串
    const selector = rule.selector;    // ".btn, .button"

    // 2. 获取所有子声明(直接子节点)
    const declarations = rule.nodes;

    // 3. 克隆一个规则
    const cloned = rule.clone();
    cloned.selector = `.alternative-${selector}`;
    rule.parent.insertAfter(rule, cloned);

    // 4. 移动到其他位置
    // rule.moveTo(otherRule);

    // 5. 获取父节点(如 @media)
    const parent = rule.parent;
    if (parent && parent.type === 'atrule' && parent.name === 'media') {
      console.log('选择器', selector, '位于 @media 中:', parent.params);
    }
  }
});

module.exports.postcss = true;

深入理解 5:AST 序列化

所有插件处理完成后,PostCSS 重新生成 CSS:

const postcss = require('postcss');
const autoprefixer = require('autoprefixer');

const css = `
  .demo {
    display: flex;
    opacity: 0.9;
  }
`;

// 执行处理
postcss([autoprefixer])
  .process(css, {
    from: undefined,
    to: undefined,
    map: false
  })
  .then(result => {
    // result.css: 序列化后的标准 CSS 字符串
    console.log(result.css);

    // result.root: 最终的 AST,可继续操作
    console.log('节点总数:', result.root.nodes.length);

    // result.messages: 插件传递的消息(如 linter 警告)
    result.messages.forEach(msg => console.log(msg));
  });

输出

.demo {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    opacity: 0.9;
}

3.2 如何选择和使用 PostCSS 插件

方法名称语法用途注意事项
安装插件npm install plugin-name --save-dev安装指定 PostCSS 插件必须先安装才能在配置中使用。
在配置中引入require('plugin-name')在 postcss.config.js 中导入插件插件名需与 npm 包名一致。
使用插件数组plugins: [require('plugin1'), require('plugin2')]按顺序注册多个插件推荐将 @import 处理插件放在最前。
传入插件选项require('plugin-name')(options)为插件配置功能参数选项需符合插件文档要求,否则可能无效或报错。
条件启用插件env === 'production' && require('cssnano')根据环境启用压缩或优化插件利用构建工具传入环境变量(如 cross-env)。

步骤 1:发现与评估插件

如何发现插件

# 方法 A:在 npm 官网搜索 "postcss-" 前缀
# https://www.npmjs.com/search?q=postcss-

# 方法 B:使用 PostCSS 插件列表
# https://github.com/postcss/postcss/blob/main/docs/plugins.md

# 方法 C:在 GitHub 搜索 "postcss plugin"
# 关注 Star 数量、最近更新时间、Issues 是否活跃

# 方法 D:npx 直接探索(不会安装到项目)
npx postcss-cli --help

评估插件的五个标准

评估项✅ 可接受❌ 需警惕
星标与下载量≥ 1k star,月下载 ≥ 10k数百 star,月下载不足 1000
最近更新3 个月内有提交超过 1 年未更新
维护者知名组织或 PostCSS 团队个人维护且 issues 长期未响应
是否支持 PostCSS 8+package.json"postcss": "^8.x"仅支持 postcss@6 或更早
是否有测试覆盖有 CI 流程和测试文件无测试或测试覆盖率极低

步骤 2:安装插件

命令

# 单个插件
npm install --save-dev postcss-nested

# 多个插件(一次性安装常用插件组合)
npm install --save-dev \
  postcss-import \
  postcss-nested \
  postcss-preset-env \
  autoprefixer \
  postcss-calc \
  cssnano

package.json 结果

{
  "devDependencies": {
    "postcss": "^8.4.47",
    "postcss-calc": "^10.0.0",
    "postcss-import": "^16.1.0",
    "postcss-nested": "^6.2.0",
    "postcss-preset-env": "^10.0.3",
    "autoprefixer": "^10.4.20",
    "cssnano": "^7.0.6"
  }
}

步骤 3:在 postcss.config.js 中引入并配置

方法 A:直接调用(最常见)

module.exports = {
  plugins: [
    require('postcss-import')(),
    require('postcss-nested')(),
    require('autoprefixer')({ grid: 'autoplace' }),
    require('cssnano')()
  ]
};

方法 B:对象风格(部分工具支持)

// key 是插件名(字符串),value 是配置对象
module.exports = {
  plugins: {
    'postcss-import': {},
    'postcss-nested': {},
    'autoprefixer': { grid: 'autoplace' },
    'cssnano': {}
  }
};

方法 C:条件加载(区分环境)

module.exports = ({ env }) => ({
  plugins: [
    require('postcss-import')(),
    require('postcss-nested')(),
    require('autoprefixer')(),

    // 仅在生产环境启用压缩与优化
    env === 'production' && require('postcss-calc')(),
    env === 'production' && require('cssnano')({
      preset: ['default', {
        discardComments: { removeAll: true },
        normalizeWhitespace: true,
        colormin: true
      }]
    })
  ].filter(Boolean)
});

使用方法 C 的构建命令

# 跨平台设置环境变量(推荐使用 cross-env)
# npm install --save-dev cross-env

# 开发构建:无 cssnano
npx cross-env NODE_ENV=development postcss src/style.css -o dist/style.css

# 生产构建:启用 cssnano 和 postcss-calc
npx cross-env NODE_ENV=production postcss src/style.css -o dist/style.css

步骤 4:阅读插件文档并传入正确选项

以 autoprefixer 为例

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      // 选项 1:是否为 grid 布局加前缀
      //   - false: 不加前缀(默认)
      //   - true: 为 grid 属性加 -ms 前缀(含旧语法转换)
      //   - "autoplace": 智能加前缀,避免旧语法陷阱(推荐)
      grid: 'autoplace',

      // 选项 2:是否为 flexbox 布局加前缀
      //   - true: 加前缀(默认)
      //   - false: 不加
      //   - "no-2009": 仅加标准 flex 前缀(推荐),不加 2009 草案的 -webkit-box
      flexbox: 'no-2009',

      // 选项 3:覆盖 browserslist,仅在 PostCSS 内生效
      overrideBrowserslist: [
        '> 1%',
        'last 2 versions',
        'not dead',
        'ie >= 11'
      ],

      // 选项 4:是否在 CSS 中添加已移除前缀的提示(调试用)
      cascade: true,

      // 选项 5:是否移除过时的旧前缀(默认 true)
      remove: true
    })
  ]
};

📌 操作建议:每个插件都应阅读其 README.md,常见插件的常用选项不超过 5 个,掌握即可覆盖 99% 的场景。

步骤 5:验证插件是否生效

方法 A:检查构建输出

/* src/style.css(不含前缀) */
.flex-container {
  display: flex;
  padding: 1rem;
}
npx postcss src/style.css -o dist/style.css
cat dist/style.css
/* 预期输出(包含 -webkit- / -ms- 前缀) */
.flex-container {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  padding: 1rem;
}

方法 B:使用 —map 在浏览器中查看 Source Map

npx postcss src/style.css -o dist/style.css --map
# 在浏览器开发者工具中,Styles 面板会显示原始选择器与行号

方法 C:编写 Node.js 脚本直接调试

// debug-plugin.js
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');

const css = '.test { display: flex; }';

postcss([
  autoprefixer({
    overrideBrowserslist: ['ie 11', 'chrome 80'],
    grid: 'autoplace'
  })
])
  .process(css, { from: undefined })
  .then(result => {
    console.log('输出 CSS:');
    console.log(result.css);
    console.log('\n警告数:', result.warnings().length);
  });

运行:

node debug-plugin.js

3.3 常用插件分类与推荐

分类插件名称说明注意事项
浏览器兼容性autoprefixer自动添加厂商前缀,基于 caniuse 数据和 browserslist 配置必须配置 .browserslistrc 文件以明确目标浏览器。
现代 CSS 支持postcss-preset-env支持未来 CSS 特性(如嵌套、自定义属性、颜色函数等)可替代部分 Sass/Less 功能,推荐作为基础插件。
代码组织postcss-import支持 @import 合并 CSS 文件有助于模块化开发,建议放在插件链开头。
语法增强postcss-nested支持嵌套 CSS 规则postcss-preset-env 兼容,提升可读性。
变量支持postcss-simple-vars支持 $var: value 语法定义变量注意与原生 CSS 变量冲突,建议统一使用一种。
数学计算postcss-calc解析并简化 calc() 表达式中的常量运算提升性能,减少运行时计算负担。
资源处理postcss-url转换 URL 路径,支持内联(data-uri)、重写、复制等常用于图片、字体等资源优化。
代码压缩cssnano轻量级 CSS 压缩器,移除空格、注释、重复规则等推荐仅在生产环境启用,避免影响开发调试。
代码检查stylelintCSS 代码风格检查工具(需配合 postcss 插件)提升代码一致性,建议集成到 CI/CD 流程。

分类 1:浏览器兼容性插件

核心插件:autoprefixer

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      grid: 'autoplace',            // 智能处理 -ms-grid
      flexbox: 'no-2009',           // 仅加标准 flex 前缀
      overrideBrowserslist: [
        '> 1%',
        'last 2 versions',
        'ie >= 11'
      ]
    })
  ]
};

配套配置:.browserslistrc

> 1%
last 2 versions
not dead
ie >= 11

推荐度:⭐⭐⭐⭐⭐(几乎所有生产项目必备)

分类 2:现代 CSS 语法支持

核心插件:postcss-preset-env

module.exports = {
  plugins: [
    require('postcss-preset-env')({
      // stage 0: 实验性功能(草案阶段)
      // stage 1: 早期实现(如 :has()、颜色函数)
      // stage 2: 稳定提案(如 CSS 嵌套、嵌套媒体查询)
      // stage 3: 候选推荐(推荐默认值)
      // stage 4: 已成为标准(无转换)
      stage: 2,

      // 显式启用/禁用某些特性
      features: {
        'nesting-rules': true,          // & 嵌套
        'custom-properties': true,       // CSS 变量降级
        'color-mix': true,               // color-mix() 函数
        'gap-properties': true,          // flexbox gap 支持
        'logical-properties': true       // 逻辑属性 margin-block
      },

      browserslist: ['> 1%', 'last 2 versions']
    })
  ]
};

示例:从未来 CSS 到现代浏览器兼容

/* 输入:使用 stage 2 的未来 CSS 语法 */
.card {
  background: color-mix(in srgb, #3498db 30%, white);

  & .title {
    font-weight: bold;
  }

  &:has(.image) {
    display: flex;
    gap: 1rem;
  }
}
/* 输出:现代浏览器兼容语法 */
.card {
  background: #bcdcf0;
}
.card .title {
  font-weight: bold;
}
.card:has(.image) {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-gap: 1rem;
  gap: 1rem;
}

推荐度:⭐⭐⭐⭐(面向现代浏览器时推荐,避免 Sass 依赖)

分类 3:代码组织与模块化

核心插件:postcss-import

module.exports = {
  plugins: [
    require('postcss-import')({
      // 搜索路径(默认 node_modules 和当前目录)
      path: ['src/css', 'src/components'],

      // 是否跳过部分 @import(如包含 URL 的)
      skipDuplicates: true,

      // 插件:在导入的子文件中也应用插件
      plugins: [
        require('postcss-nested')()
      ]
    })
  ]
};

目录结构与使用示例

src/
  css/
    base/
      reset.css        /* 基础重置样式 */
      variables.css    /* 全局变量(CSS 原生变量) */
    components/
      button.css       /* 按钮组件 */
      card.css         /* 卡片组件 */
    layout/
      grid.css         /* 网格布局 */
    main.css           /* 通过 @import 汇总所有模块 */

main.css

@import 'base/reset.css';
@import 'base/variables.css';

@import 'components/button.css';
@import 'components/card.css';

@import 'layout/grid.css';

/* 全局样式 */
body {
  font-family: system-ui, sans-serif;
}

构建后,所有子文件合并到一个 dist/main.css

推荐度:⭐⭐⭐⭐(模块化 CSS 项目必备)

分类 4:语法增强

核心插件:postcss-nested + postcss-simple-vars

module.exports = {
  plugins: [
    // $name 变量(编译时替换)
    require('postcss-simple-vars')({
      variables: {
        primary: '#3498db',
        radius: '8px'
      }
    }),

    // & 嵌套选择器(类似 Sass)
    require('postcss-nested')()
  ]
};

输入

.button {
  background: $primary;
  border-radius: $radius;
  padding: 8px 16px;

  &:hover {
    background: darken(#3498db, 10%);
  }

  &--large {
    padding: 16px 32px;
  }
}

输出

.button {
  background: #3498db;
  border-radius: 8px;
  padding: 8px 16px;
}
.button:hover {
  background: #2980b9;
}
.button--large {
  padding: 16px 32px;
}

⚠️ 注意postcss-simple-vars 使用 $name 语法,与原生 CSS 变量 --name 不同。$name 在编译时替换为字面量,--name 保留为 CSS 变量(浏览器运行时解析)。建议项目中只选其一,避免混淆。

推荐度:⭐⭐⭐(项目不使用 Sass 但需要变量/嵌套时启用)

分类 5:数学计算优化

核心插件:postcss-calc

module.exports = {
  plugins: [
    require('postcss-calc')({
      preserve: false,        // 保留原始 calc()(默认 false)
      precision: 5,            // 保留小数位
      warnWhenCannotResolve: true  // 无法解析时警告
    })
  ]
};

输入

.example {
  width: calc(100% - 2 * 16px);
  padding: calc(8px + 8px);
  margin: calc(100% / 3);
  height: calc(100vh - 60px - 20px - 20px);
}

输出

.example {
  width: calc(100% - 32px);    /* 2 * 16 = 32(编译时计算) */
  padding: 16px;                /* 8 + 8 = 16,完全无 calc() */
  margin: calc(100% / 3);       /* 单位 % 无法进一步简化 */
  height: calc(100vh - 100px);  /* 60 + 20 + 20 = 100 */
}

推荐度:⭐⭐⭐⭐(包含复杂计算的项目推荐启用)

分类 6:资源处理

核心插件:postcss-url

module.exports = {
  plugins: [
    require('postcss-url')({
      // url 模式:
      //   - "copy": 复制资源到输出目录
      //   - "inline": 内联为 base64 data-URI
      //   - "rebase": 重写相对路径(默认)
      url: 'inline',

      // 小于 10KB 的图片内联,超过的保留为外链
      maxSize: 10,        // 10 KB

      // base64 过滤器(可选)
      filter: /\.(svg|png|jpg|jpeg)$/i,

      // 内联失败时的回退策略
      fallback: 'copy',

      // "copy" 模式使用的输出路径
      assetsPath: 'assets'
    })
  ]
};

输入

.icon {
  background: url('./icons/check.svg') center no-repeat;  /* 2 KB */
}
.banner {
  background: url('./images/hero.jpg') center/cover;       /* 180 KB */
}

输出

.icon {
  background: url('data:image/svg+xml;base64,PHN2ZyB4bWxuc...') center no-repeat;
}
.banner {
  background: url('assets/hero.jpg') center/cover;   /* 被复制到 assets/ */
}

推荐度:⭐⭐⭐(包含大量图标/图片的项目推荐启用)

分类 7:代码压缩

核心插件:cssnano

module.exports = {
  plugins: [
    require('cssnano')({
      // preset 预设:
      //   - default: 平衡压缩与安全
      //   - advanced: 更激进的压缩(需额外配置)
      //   - lite: 最轻量,仅去除空格与注释
      preset: ['default', {
        discardComments: { removeAll: true },  // 移除所有注释
        normalizeWhitespace: true,             // 标准化空格
        colormin: true,                        // 颜色压缩 #ffffff → #fff
        mergeLonghand: true,                   // 合并 padding/margin 等
        discardDuplicates: true,               // 删除重复规则
        discardEmpty: true,                    // 删除空规则
        reduceIdents: true                     // 简化动画名称
      }]
    })
  ]
};

压缩效果对比

文件压缩前压缩后压缩率
style.css245 KB186 KB24%
Tailwind 原子类4.8 MB380 KB92%
自定义组件12 KB8.6 KB28%

推荐度:⭐⭐⭐⭐⭐(生产构建必备,开发环境禁用)

分类 8:代码质量检查

核心工具:stylelint + postcss-stylelint

// .stylelintrc.js(stylelint 配置,独立于 postcss.config.js)
module.exports = {
  extends: [
    'stylelint-config-standard'
  ],
  rules: {
    'no-empty-source': null,
    'property-no-unknown': [true, {
      ignoreProperties: ['composes']  // 允许 CSS Modules 特性
    }]
  }
};

与 PostCSS 集成

// postcss.config.js
module.exports = ({ env }) => ({
  plugins: [
    // 开发环境启用 lint(生产跳过以加快构建)
    env === 'development' && require('stylelint')(),
    require('autoprefixer')(),
    env === 'production' && require('cssnano')()
  ].filter(Boolean)
});

运行

npx postcss src/**/*.css -o /dev/null
# 终端中会输出 stylelint 警告

推荐度:⭐⭐⭐⭐(团队协作项目推荐启用,保持代码风格一致)


🔑 第三章总结

五个核心要点

  1. PostCSS 插件 = 一个带钩子的函数 — 通过 OnceRuleDeclarationAtRule 等钩子访问 AST,读取、修改、新增或删除节点
  2. 插件顺序是一门艺术 — 推荐顺序:读取合并 → 变量嵌套 → 语法扩展 → 前缀加兼容 → 计算优化 → 资源处理 → 最终压缩
  3. 选择插件需谨慎 — 评估标准:Star ≥ 1k、月下载 ≥ 10k、最近更新 ≤ 3 月、支持 PostCSS 8+
  4. 条件加载区分开发/生产 — 开发时保留格式与 Source Map,生产时启用压缩和优化,通过 env === 'production' && require(...) 简洁实现
  5. 验证插件是否生效 — 检查生成的 CSS 文件是否包含预期前缀或转换,必要时编写 Node.js 脚本直接测试单个插件

第 4 章:常用官方与社区插件实践

4.1 postcss-preset-env:渐进式使用现代 CSS

方法名称语法用途注意事项
启用插件require('postcss-preset-env')使用现代 CSS 特性并转换为兼容语法需配合 .browserslistrc 使用。
配置 stagestage: number (0-4)控制支持的 CSS 提案阶段stage 2 及以上特性较稳定,推荐使用。
启用特定功能features: { 'nesting-rules': true }精确控制启用哪些 CSS 特性可避免引入不需要的转换逻辑。
禁用功能features: { 'feature-name': false }显式禁用某些特性用于规避某些浏览器兼容问题。

完整配置示例

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-preset-env')({
      // stage 0: 实验性(草案),语法可能变化
      // stage 1: 早期实现(如 :has() 初版)
      // stage 2: 稳定提案(推荐默认值)
      // stage 3: 候选推荐(即将成为标准)
      // stage 4: 已成为标准,无转换
      stage: 2,

      // 显式启用/禁用特定特性
      features: {
        'nesting-rules': true,          // 启用 & 嵌套语法
        'custom-properties': true,       // CSS 变量(--name)降级为静态值
        'color-mix': true,               // color-mix(in srgb, ...)
        'color-functional-notation': true,  // 将 rgb(0, 0, 0) 转为 rgb(0 0 0)
        'gap-properties': true,          // flexbox 中的 gap 属性
        'logical-properties': true,      // 逻辑属性(margin-block / padding-inline)
        'focus-visible-pseudo-class': true,  // :focus-visible
        'any-link-pseudo-class': true    // :any-link
      },

      // 目标浏览器(也可通过 .browserslistrc 定义)
      browserslist: ['> 1%', 'last 2 versions', 'not dead']
    })
  ]
};

场景 1:使用嵌套语法

输入

.card {
  background: #fff;
  padding: 1rem;

  & .title {
    font-size: 1.25rem;
    font-weight: 600;
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
}

输出(postcss-preset-env 转换后):

.card {
  background: #fff;
  padding: 1rem;
}
.card .title {
  font-size: 1.25rem;
  font-weight: 600;
}
.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

场景 2:CSS 自定义属性降级

输入

:root {
  --primary: #3498db;
  --success: #2ecc71;
}

.btn {
  background: var(--primary);
  color: white;
}

.btn-success {
  background: var(--success);
}

输出(当目标浏览器不支持 var() 时,被替换为字面量):

.btn {
  background: #3498db;
  color: white;
}
.btn-success {
  background: #2ecc71;
}

场景 3:color-mix() 函数

输入

.theme {
  background: color-mix(in srgb, #3498db 30%, white);
  border: 1px solid color-mix(in srgb, #3498db 80%, black);
}

输出

.theme {
  background: #bcdcf0;
  border: 1px solid #113750;
}

推荐使用场景

项目类型是否推荐配置建议
现代 Web 应用⭐⭐⭐⭐⭐stage: 2,支持嵌套和变量,精简前缀
企业级应用(IE11)⭐⭐⭐⭐stage: 3 + autoprefixer,禁用实验特性
开源组件库⭐⭐⭐⭐stage: 2,对 :has() 等特性做特性检测
静态博客/文档⭐⭐⭐stage: 3,启用少量语法糖即可

4.2 autoprefixer:自动添加浏览器前缀

方法名称语法用途注意事项
基本使用require('autoprefixer')自动为支持的属性添加前缀依赖 .browserslistrc 配置目标浏览器。
配置 grid 布局grid: 'autoplace'true / false控制是否处理 Grid 布局相关前缀'autoplace' 更智能,避免冗余前缀。
禁用 flexbox 前缀flexbox: false禁用 Flexbox 相关前缀添加适用于只支持现代浏览器的项目。
覆盖 browserslistoverrideBrowserslist: [...]覆盖项目中的 browserslist 配置优先级高于 .browserslistrc

完整配置示例

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      // grid 布局处理策略:
      //   - false: 不添加前缀(默认)
      //   - true: 为 grid 属性添加 -ms- 前缀(含 IE11 旧语法转换)
      //   - 'autoplace': 智能加前缀(推荐),避免 -ms-grid 的旧语法陷阱
      grid: 'autoplace',

      // flexbox 布局处理:
      //   - true: 添加前缀(默认),包括 2009 年草案的 -webkit-box 前缀
      //   - false: 不添加前缀
      //   - 'no-2009': 仅添加标准 flex 前缀(推荐),不生成 -webkit-box
      flexbox: 'no-2009',

      // 覆盖 browserslist 配置(仅作用于 Autoprefixer)
      overrideBrowserslist: [
        '> 1%',
        'last 2 versions',
        'not dead',
        'ie >= 11'
      ],

      // 是否保留前缀对齐(视觉上对齐,开发阶段可选)
      cascade: true,

      // 是否移除过时的前缀(如 -webkit-border-radius 等)
      remove: true
    })
  ]
};

场景 1:Flexbox 前缀

输入

.flex-container {
  display: flex;
  justify-content: center;
  align-items: center;
}

输出(目标包含 IE11):

.flex-container {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
}

场景 2:Grid 布局前缀

输入

.grid-layout {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

输出grid: 'autoplace'):

.grid-layout {
  display: -ms-grid;
  display: grid;
  -ms-grid-columns: 1fr 1rem 1fr 1rem 1fr;
  grid-template-columns: repeat(3, 1fr);
  -webkit-grid-gap: 1rem;
  grid-gap: 1rem;
  gap: 1rem;
}

场景 3:用户选择和变换

输入

.user-area {
  user-select: none;
  transform: translateY(10px);
  transition: transform 0.3s ease;
}

输出

.user-area {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-transform: translateY(10px);
  -ms-transform: translateY(10px);
  transform: translateY(10px);
  -webkit-transition: transform 0.3s ease;
  transition: transform 0.3s ease;
}

与 postcss-preset-env 的协作建议

两者配合使用时,推荐的插件顺序为:

// postcss.config.js
module.exports = {
  plugins: [
    // 1. 先处理 @import 和变量
    require('postcss-import')(),
    require('postcss-nested')(),

    // 2. postcss-preset-env:将现代 CSS 语法转换为标准语法
    require('postcss-preset-env')({
      stage: 2,
      features: {
        'nesting-rules': true,
        'custom-properties': true
      }
    }),

    // 3. autoprefixer:基于标准语法添加浏览器前缀
    require('autoprefixer')({
      grid: 'autoplace',
      flexbox: 'no-2009'
    }),

    // 4. cssnano:最后压缩
    require('cssnano')()
  ]
};

为什么这样排序?

  • postcss-preset-env 将 color-mix()、嵌套等语法转换为标准 CSS
  • autoprefixer 基于转换后的标准 CSS 添加 -webkit--ms- 前缀
  • cssnano 最后压缩,保证所有前缀都被优化

4.3 postcss-nested:嵌套 CSS 规则

方法名称语法用途注意事项
启用嵌套require('postcss-nested')支持类似 Sass 的嵌套语法推荐与 postcss-preset-env 配合使用。
嵌套规则写法& 表示父选择器在嵌套中引用父级& 会被替换为完整父选择器。
多层嵌套多级 {} 嵌套组织复杂组件样式避免过深嵌套,影响性能和可维护性。
支持 @media 嵌套将媒体查询嵌套在规则内简化响应式样式组织转换后会将 @media 提升到顶层。

基础配置

// postcss.config.js
module.exports = {
  plugins: [
    // postcss-nested 不需要显式配置,默认即可
    require('postcss-nested')()
  ]
};

场景 1:基础嵌套 + BEM 修饰符

输入

.card {
  background: #fff;
  padding: 1rem;
  border-radius: 8px;

  &__title {
    font-size: 1.25rem;
    font-weight: 600;
    color: #333;
  }

  &__content {
    color: #666;
    line-height: 1.6;
  }

  &--highlighted {
    background: #fffbe6;
    border: 1px solid #ffd666;
  }
}

输出

.card {
  background: #fff;
  padding: 1rem;
  border-radius: 8px;
}
.card__title {
  font-size: 1.25rem;
  font-weight: 600;
  color: #333;
}
.card__content {
  color: #666;
  line-height: 1.6;
}
.card--highlighted {
  background: #fffbe6;
  border: 1px solid #ffd666;
}

场景 2:伪类 / 伪元素嵌套

输入

.button {
  padding: 8px 16px;
  background: #3498db;
  color: #fff;

  &:hover {
    background: #2980b9;
  }

  &:active {
    transform: translateY(1px);
  }

  &::before {
    content: '» ';
    display: inline-block;
  }
}

输出

.button {
  padding: 8px 16px;
  background: #3498db;
  color: #fff;
}
.button:hover {
  background: #2980b9;
}
.button:active {
  transform: translateY(1px);
}
.button::before {
  content: '» ';
  display: inline-block;
}

场景 3:多层嵌套(导航栏示例)

输入

.header {
  display: flex;
  align-items: center;

  & .logo {
    width: 48px;
    height: 48px;
  }

  & .nav {
    display: flex;
    gap: 1rem;

    & a {
      color: #333;
      text-decoration: none;

      &:hover {
        text-decoration: underline;
      }
    }
  }
}

输出

.header {
  display: flex;
  align-items: center;
}
.header .logo {
  width: 48px;
  height: 48px;
}
.header .nav {
  display: flex;
  gap: 1rem;
}
.header .nav a {
  color: #333;
  text-decoration: none;
}
.header .nav a:hover {
  text-decoration: underline;
}

场景 4:@media 嵌套(推荐场景)

输入

.hero {
  padding: 2rem;
  font-size: 16px;

  @media (min-width: 768px) {
    padding: 3rem;
    font-size: 18px;
  }

  @media (min-width: 1024px) {
    padding: 4rem;
    font-size: 20px;
  }
}

输出

.hero {
  padding: 2rem;
  font-size: 16px;
}
@media (min-width: 768px) {
  .hero {
    padding: 3rem;
    font-size: 18px;
  }
}
@media (min-width: 1024px) {
  .hero {
    padding: 4rem;
    font-size: 20px;
  }
}

最佳实践:嵌套深度建议

嵌套层级推荐场景说明
1 层✅ 推荐用于伪类(:hover)、伪元素(::before)、BEM 修饰符
2 层⚠️ 可以接受用于组件 → 子元素结构,如 .card.title
3 层及以上❌ 不推荐样式耦合度过高,性能下降,难以维护。建议抽取新的根规则

4.4 postcss-simple-vars:CSS 变量语法支持

方法名称语法用途注意事项
定义变量$var-name: value;在 CSS 中定义变量变量作用域为整个文件。
使用变量$var-name在属性值中引用变量不支持嵌套作用域。
变量拼接$var + value拼接变量与单位必须使用 + 连接,空格无效。
作用域限制无块级作用域所有变量全局可见命名需避免冲突,建议加前缀。

基础配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-simple-vars')({
      // 可选:在配置中定义变量(可被 CSS 中的 $name 覆盖)
      variables: {
        'primary-color': '#3498db',
        'success-color': '#2ecc71',
        'warning-color': '#f1c40f',
        'danger-color': '#e74c3c',
        'radius-s': '4px',
        'radius-m': '8px',
        'radius-l': '16px',
        'font-size-base': '16px'
      },

      // 可选:变量定义后是否保留在输出中(默认 false,不保留)
      // 保留可用于调试:$primary-color: #3498db;
      silent: true
    })
  ]
};

场景 1:基本变量替换

输入

$primary-color: #3498db;
$spacing: 16px;

.btn {
  background: $primary-color;
  color: #fff;
  padding: $spacing;
  border-radius: 8px;
}

输出

.btn {
  background: #3498db;
  color: #fff;
  padding: 16px;
  border-radius: 8px;
}

场景 2:变量与单位拼接

输入

$font-size-scale: 1.5;

.title {
  font-size: $font-size-scale + rem;
  line-height: $font-size-scale + 0.5;
}

输出

.title {
  font-size: 1.5rem;
  line-height: 2;
}

场景 3:在多个属性中复用

输入

$radius-m: 8px;

.card {
  border-radius: $radius-m;
  padding: $radius-m * 2;
  box-shadow: 0 0 $radius-m rgba(0, 0, 0, 0.1);
}

输出

.card {
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
}

⚠️ 注意事项

1. 与原生 CSS 变量(--name)的区别

特性postcss-simple-vars ($name)原生 CSS 变量 (--name)
解析时机编译时替换(PostCSS 构建阶段)浏览器运行时解析
作用域全局(整个文件)支持块级作用域(基于选择器)
动态修改不可(编译为字面量后固定)可通过 JS:element.style.setProperty('--color', 'red')
浏览器兼容性完全兼容(编译后就是普通 CSS)需要现代浏览器(IE11 不支持)

2. 避免与其他插件冲突

如果同时使用 Sass(.scss),Sass 本身已支持 $name 变量,不需要再使用 postcss-simple-vars

3. 建议命名规范

推荐使用前缀避免冲突:

/* ❌ 不推荐:变量名过于通用 */
$color: #3498db;
$size: 16px;

/* ✅ 推荐:带有语义前缀 */
$color-primary: #3498db;
$color-success: #2ecc71;
$space-md: 16px;
$radius-md: 8px;

场景 4:与 postcss-calc 配合

postcss.config.js(正确顺序)

module.exports = {
  plugins: [
    require('postcss-simple-vars')({  // 1. 先替换 $name 为字面量
      variables: { 'base-space': 16 }
    }),
    require('postcss-nested')(),        // 2. 展开嵌套
    require('postcss-calc')(),          // 3. 简化 calc()(必须在 simple-vars 之后)
    require('autoprefixer')(),          // 4. 加前缀
    require('cssnano')()                // 5. 压缩
  ]
};

输入

$base-space: 16px;

.wrapper {
  width: calc(100% - 2 * $base-space);
  padding: $base-space;
  margin: calc($base-space / 2);
}

输出

.wrapper {
  width: calc(100% - 32px);
  padding: 16px;
  margin: 8px;
}

4.5 postcss-calc:简化 calc 表达式

方法名称语法用途注意事项
启用 calc 简化require('postcss-calc')简化 calc() 中的常量运算自动计算并替换可简化部分。
简化常量表达式calc(10px + 20px) → 30px合并相同单位的数值若单位一致或可转换,则合并。
嵌套 calc 处理calc(calc(...))扁平化嵌套 calc 表达式提升可读性和运行效率。
与变量结合calc($var * 2)支持变量参与计算(需其他插件先替换)执行顺序至关重要。

基础配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-calc')({
      // 保留原始 calc() 作为回退(默认 false)
      preserve: false,

      // 精度:保留小数位数(默认 5)
      precision: 5,

      // 无法解析时发出警告(默认 false)
      warnWhenCannotResolve: true,

      // 是否允许跨单位转换(如 px → rem,需要额外配置)
      mediaQueries: false
    })
  ]
};

场景 1:同单位常量合并

输入

.spacing {
  padding: calc(8px + 8px);
  margin: calc(16px * 2);
  width: calc(100px / 4);
}

输出

.spacing {
  padding: 16px;
  margin: 32px;
  width: 25px;
}

场景 2:混合单位保留 calc()

输入

.layout {
  width: calc(100% - 2 * 16px);
  padding: calc(16px + 1rem);
  font-size: calc(16px * 1.25);
}

输出

.layout {
  width: calc(100% - 32px);     /* 2 * 16px = 32px(编译时完成) */
  padding: calc(16px + 1rem);   /* 不同单位,保留 calc(),由浏览器计算 */
  font-size: 20px;              /* 16 * 1.25 = 20,完全简化 */
}

场景 3:嵌套 calc 扁平化

输入

.hero {
  height: calc(calc(100vh - 60px) / 2);
  width: calc(calc(100% - calc(16px + 16px)) / 3);
}

输出

.hero {
  height: calc((100vh - 60px) / 2);
  width: calc((100% - 32px) / 3);
}

场景 4:与变量、嵌套插件的协作链

postcss.config.js

module.exports = {
  plugins: [
    require('postcss-simple-vars')({    // 1. 先替换 $name 为字面量
      variables: { 'base-space': 16 }
    }),
    require('postcss-nested')(),          // 2. 展开嵌套
    require('postcss-calc')(),            // 3. 简化 calc()
    require('postcss-preset-env')({       // 4. 处理现代 CSS
      stage: 2,
      features: { 'nesting-rules': false }  // 注意:已由 postcss-nested 处理
    }),
    require('autoprefixer')(),            // 5. 添加前缀
    require('cssnano')()                  // 6. 压缩
  ]
};

输入

$base-space: 16px;
$grid-gap: 1rem;

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: $grid-gap;
  padding: calc($base-space + 8px);
  max-width: calc(1200px - $base-space * 2);

  & .item {
    padding: $base-space;
    font-size: calc(14px + 2px);
  }
}

输出

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
  padding: 24px;
  max-width: 1168px;
}
.container .item {
  padding: 16px;
  font-size: 16px;
}

性能收益

在大型 CSS 文件(> 500KB)中,启用 postcss-calc 可以带来以下优化:

指标无 postcss-calc启用 postcss-calc提升
CSS 体积(典型场景)256 KB228 KB-11%
浏览器解析时间(Chrome 120)18 ms14 ms-22%
运行时计算(每个 calc())1-2 µs0 µs(已编译为字面量)-100%

4.6 postcss-import:支持 @import 导入

方法名称语法用途注意事项
启用 @import 处理require('postcss-import')将多个 CSS 文件合并为一个建议放在插件链最前面。
导入本地文件@import "file.css";引入同目录下的 CSS 文件文件路径相对于当前 CSS 文件。
禁止处理特定 import@import url("https://...")外部 URL 不会被内联保留原样,由浏览器加载。
自定义解析逻辑resolve: function() {}控制文件查找方式高级用法,用于自定义模块路径。

基础配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import')({
      // 查找 @import 文件的路径(除了当前目录)
      path: ['src/css', 'src/components', 'node_modules'],

      // 是否跳过重复 import(默认 true)
      skipDuplicates: true,

      // 是否将 @import 的内容内联(默认 true)
      // 设为 false 则保留 @import 语句不展开
      inline: true,

      // 在导入的子文件中也应用的插件
      plugins: [
        require('postcss-simple-vars')()
      ]
    })
  ]
};

场景 1:模块化组织 CSS 文件

项目结构

src/
  css/
    base/
      reset.css       /* 浏览器重置 */
      variables.css   /* 变量定义 */
    components/
      button.css      /* 按钮 */
      card.css        /* 卡片 */
    layout/
      grid.css        /* 栅格 */
      header.css      /* 头部 */
    main.css          /* 入口文件 */

main.css

/* base 模块 */
@import 'base/reset.css';
@import 'base/variables.css';

/* layout 模块 */
@import 'layout/grid.css';
@import 'layout/header.css';

/* components 模块 */
@import 'components/button.css';
@import 'components/card.css';

/* 全局样式 */
body {
  font-family: system-ui, sans-serif;
  line-height: 1.6;
  color: #333;
}

构建命令

npx postcss src/css/main.css -o dist/style.css

输出:所有子文件的内容按 @import 顺序合并到单个 dist/style.css 中。

场景 2:与 node_modules 中的 CSS 配合

/* main.css */
@import 'normalize.css';              /* 从 node_modules/normalize.css 加载 */
@import 'swiper/swiper-bundle.css';   /* 从 node_modules/swiper 加载 */
@import './base/reset.css';           /* 本地文件 */

postcss-import 会自动:

  1. node_modules 中查找 normalize.css
  2. 解析 @import 语句并内联
  3. 对外部 URL(如 Google Fonts)保留原样不展开

场景 3:混合本地与外部 import

输入

/* 外部 URL — 保留不动,由浏览器加载 */
@import url('https://fonts.googleapis.com/css2?family=Inter');

/* 本地文件 — 内联合并 */
@import 'base/reset.css';
@import 'components/button.css';

body {
  font-family: 'Inter', system-ui, sans-serif;
}

输出

@import url('https://fonts.googleapis.com/css2?family=Inter');

/* ... base/reset.css 内容 ... */
/* ... components/button.css 内容 ... */

body {
  font-family: 'Inter', system-ui, sans-serif;
}

场景 4:自定义 resolve 函数

// postcss.config.js
const path = require('path');

module.exports = {
  plugins: [
    require('postcss-import')({
      // 自定义文件查找逻辑
      resolve(id, basedir, importOptions) {
        // 1. 如果以 ~ 开头,从项目根目录查找
        if (id.startsWith('~')) {
          return path.resolve(__dirname, id.slice(1));
        }

        // 2. 如果以 @/ 开头,从 src/css 查找
        if (id.startsWith('@/')) {
          return path.resolve(__dirname, 'src/css', id.slice(2));
        }

        // 3. 默认行为:在 basedir 下查找
        return path.resolve(basedir, id);
      },

      // 过滤:只处理 CSS 文件
      filter: (url) => url.endsWith('.css') || !url.startsWith('http')
    })
  ]
};

使用

/* main.css */
@import '~/node_modules/normalize.css';  /* 使用 ~ 从项目根目录查找 */
@import '@/components/button.css';        /* 使用 @/ 从 src/css 查找 */
@import './base/reset.css';               /* 相对路径 */

⚠️ 插件链顺序

postcss-import 必须放在插件链的最前面,否则:

  • 子文件中的变量、嵌套等语法无法被后续插件处理
  • 可能导致 @import 语句位置错乱(如出现在压缩后的 CSS 中间)

正确

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import')(),  // 1. 首先合并 @import
    require('postcss-simple-vars')(),
    require('postcss-nested')(),
    require('postcss-calc')(),
    require('autoprefixer')(),
    require('cssnano')()
  ]
};

4.7 postcss-url:处理 URL 资源路径

方法名称语法用途注意事项
内联小资源url: 'inline'将小文件转为 data-uri 内联减少 HTTP 请求,但增加 CSS 体积。
重写路径url: 'rebase'调整资源路径相对位置适用于构建后资源移动场景。
复制并重命名url: 'copy'复制资源到目标目录并更新路径需配置目标路径和文件哈希。
自定义转换逻辑url: function() {}完全控制 URL 转换行为灵活性最高,可用于 CDN 分发。

基础配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-url')({
      // 核心模式:'inline' | 'copy' | 'rebase' | 'asset' | function
      url: 'inline',

      // 小于 10KB 的文件内联,超过则保留外链(仅 inline 模式)
      maxSize: 10,  // 单位:KB

      // 文件过滤(仅处理匹配的后缀)
      filter: /\.(svg|png|jpg|jpeg|gif|woff2?)$/i,

      // 内联失败时的回退模式
      fallback: 'copy',

      // 目标路径(copy 模式下使用)
      assetsPath: 'assets',

      // 是否使用 base64 编码(默认 true)
      // SVG 可设为 false 使用 UTF-8 编码(体积更小)
      basePath: '.'
    })
  ]
};

场景 1:内联小图标为 data-uri

输入

.icon-check {
  background: url('./icons/check.svg') center no-repeat;
  width: 16px;
  height: 16px;
}

.icon-large {
  background: url('./images/hero.png') center/cover;  /* 180 KB */
}

postcss.config.js

module.exports = {
  plugins: [
    require('postcss-url')({
      url: 'inline',
      maxSize: 10  // 10KB 阈值
    })
  ]
};

输出

.icon-check {
  background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...') center no-repeat;
  width: 16px;
  height: 16px;
}

.icon-large {
  background: url('./images/hero.png') center/cover;  /* 超过 10KB,保留 */
}

SVG 优化:使用 UTF-8 而非 base64

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-url')({
      url: 'inline',
      maxSize: 10,
      encodeType: 'base64',       // 默认 base64
      // 对 SVG 使用 UTF-8 URL 编码(体积更小)
      // 需配合自定义函数使用,见下方"自定义逻辑"
      filter: /\.(png|jpg|jpeg|gif)$/i  // 仅对这些类型使用 base64
    })
  ]
};

场景 2:重写路径(构建输出目录不同)

典型场景:

  • 源 CSS 位于 src/css/style.css
  • 引用的图片位于 src/images/
  • 构建后 CSS 输出到 dist/style.css
  • 图片复制到 dist/assets/images/

postcss.config.js

module.exports = {
  plugins: [
    require('postcss-url')({
      url: 'copy',
      assetsPath: 'assets/images',  // 复制到的目标目录
      useHash: true,                // 使用文件哈希命名(缓存优化)
      hashOptions: {
        append: true
      }
    })
  ]
};

输入

/* src/css/style.css */
.logo {
  background: url('../images/logo.svg');
}

输出

/* dist/style.css */
.logo {
  background: url('assets/images/logo-a3b5c7d.svg');  /* 路径已重写 + 含哈希 */
}

同时生成文件:dist/assets/images/logo-a3b5c7d.svg

场景 3:rebase 模式 — 调整相对路径

当 CSS 文件被移动到其他目录时,rebase 模式会自动修正相对路径:

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-url')({
      url: 'rebase',
      // 源文件目录
      from: 'src/css/style.css',
      // 目标文件目录
      to: 'dist/style.css',
      assetsPath: 'assets'
    })
  ]
};

场景 4:自定义转换逻辑(CDN 分发)

// postcss.config.js
const path = require('path');

module.exports = {
  plugins: [
    require('postcss-url')({
      // 完全自定义 URL 转换逻辑
      url: (asset) => {
        // asset 对象包含:
        //   - url: 原始 URL(如 './images/logo.svg')
        //   - pathname: 文件绝对路径
        //   - relativePath: 相对路径
        //   - search: 查询参数部分
        //   - hash: hash 部分

        // 1. SVG 文件内联为 UTF-8(体积比 base64 更小)
        if (asset.pathname && asset.pathname.endsWith('.svg')) {
          return 'data:image/svg+xml;utf8,' + encodeURIComponent(
            require('fs').readFileSync(asset.pathname, 'utf8')
          );
        }

        // 2. 字体文件指向 CDN
        if (/\.(woff2?|ttf|eot)$/i.test(asset.pathname || '')) {
          const fileName = path.basename(asset.pathname || '');
          return `https://cdn.example.com/fonts/${fileName}`;
        }

        // 3. 图片文件指向 CDN
        if (/\.(png|jpg|jpeg|gif|webp)$/i.test(asset.pathname || '')) {
          const fileName = path.basename(asset.pathname || '');
          return `https://cdn.example.com/images/${fileName}`;
        }

        // 4. 其他保持原 URL
        return asset.url;
      }
    })
  ]
};

输入

/* src/style.css */
.icon-check {
  background: url('./icons/check.svg');
}

.font-primary {
  font-family: 'Inter', sans-serif;
  src: url('./fonts/Inter-Regular.woff2') format('woff2');
}

.hero {
  background: url('./images/hero.png') center/cover;
}

输出

/* dist/style.css */
.icon-check {
  background: url('data:image/svg+xml;utf8,<svg xmlns=...>');
}

.font-primary {
  font-family: 'Inter', sans-serif;
  src: url('https://cdn.example.com/fonts/Inter-Regular.woff2') format('woff2');
}

.hero {
  background: url('https://cdn.example.com/images/hero.png') center/cover;
}

不同模式的使用建议

模式适用场景优点注意事项
inline小图标、小字体减少 HTTP 请求内联内容直接写入 CSS,体积增大,需控制 maxSize
copy中大型图片、字体支持文件哈希,便于缓存需要构建工具配合复制文件
rebaseCSS 文件被移动到其他目录简单,自动修正路径需配置 fromto
functionCDN 分发、自定义编码策略灵活性最高需要一定 Node.js 编程基础

🔑 第四章总结

五个核心要点

  1. postcss-preset-env 是”现代 CSS 引擎” — 基于 stage 配置渐进支持新特性,推荐 stage: 2 平衡稳定性与功能覆盖
  2. autoprefixer 是”兼容性守门员” — 配合 grid: 'autoplace'flexbox: 'no-2009',在正确性与体积之间取得平衡
  3. postcss-nested + postcss-simple-vars 提供 Sass 体验 — 无需安装 Sass 编译器,即可获得嵌套和变量能力,但注意变量作用域为全局
  4. postcss-import 必须放在插件链最前面 — 它负责合并所有 @import 文件,后续插件才能对完整的 CSS 进行处理
  5. postcss-url 是资源管理利器 — 通过 inline 内联小资源、copy 处理大资源、function 模式适配 CDN

完整的推荐配置(七插件协作链)

// postcss.config.js
module.exports = ({ env }) => ({
  plugins: [
    // 1. 合并 @import
    require('postcss-import')({ path: ['src/css'] }),

    // 2. 变量替换($name → 字面量)
    require('postcss-simple-vars')({
      variables: { 'primary': '#3498db' }
    }),

    // 3. 嵌套展开(& → 完整选择器)
    require('postcss-nested')(),

    // 4. 现代 CSS 转换(color-mix、:has()、嵌套等)
    require('postcss-preset-env')({ stage: 2 }),

    // 5. calc 简化
    require('postcss-calc')(),

    // 6. 浏览器前缀
    require('autoprefixer')({ grid: 'autoplace', flexbox: 'no-2009' }),

    // 7. 仅生产环境:资源内联 + 压缩
    env === 'production' && require('postcss-url')({
      url: 'inline',
      maxSize: 10,
      filter: /\.(svg|png|jpg)$/i
    }),
    env === 'production' && require('cssnano')()
  ].filter(Boolean)
});

第 5 章:CSS 自定义语法扩展

5.1 使用插件实现类似 Sass 的功能

Sass 功能对应 PostCSS 插件用途注意事项
嵌套规则postcss-nested支持选择器嵌套,提升可读性使用 & 引用父选择器,支持多层嵌套
变量定义postcss-simple-vars支持 $variable: value 语法变量为全局作用域,不支持块级作用域
计算表达式postcss-calc支持 calc() 内数学运算简化需确保变量插件在 calc 插件之前执行
混合(Mixin)postcss-mixins定义可复用的样式片段可带参数,但功能较 Sass 简单
条件与循环postcss-conditionals / postcss-each支持 @if/@else@each 循环非标准功能,需谨慎使用以保证可维护性

完整配置示例(模拟 Sass 开发环境)

// postcss.config.js
module.exports = {
  plugins: [
    // 1. @import 合并(最先执行)
    require('postcss-import')({
      path: ['src/css', 'src/components']
    }),

    // 2. 变量定义与替换($name: value)
    require('postcss-simple-vars')({
      variables: {
        'primary': '#007bff',
        'secondary': '#6c757d',
        'success': '#28a745',
        'danger': '#dc3545',
        'warning': '#ffc107',
        'info': '#17a2b8',
        'light': '#f8f9fa',
        'dark': '#343a40',
        'breakpoint-sm': '576px',
        'breakpoint-md': '768px',
        'breakpoint-lg': '992px',
        'breakpoint-xl': '1200px',
        'font-size-base': '16px',
        'line-height-base': '1.5',
        'spacing-sm': '0.5rem',
        'spacing-md': '1rem',
        'spacing-lg': '1.5rem',
        'spacing-xl': '2rem',
        'radius-sm': '4px',
        'radius-md': '8px',
        'radius-lg': '16px',
        'shadow-sm': '0 2px 4px rgba(0, 0, 0, 0.05)',
        'shadow-md': '0 4px 12px rgba(0, 0, 0, 0.1)',
        'shadow-lg': '0 8px 24px rgba(0, 0, 0, 0.15)'
      },
      silent: true
    }),

    // 3. Mixin 定义与调用(必须在嵌套之前)
    require('postcss-mixins')(),

    // 4. 条件判断(@if / @else)
    require('postcss-conditionals')(),

    // 5. 循环遍历(@each / @for)
    require('postcss-each')(),

    // 6. 嵌套规则(& 替换)
    require('postcss-nested')(),

    // 7. calc() 表达式简化
    require('postcss-calc')(),

    // 8. 现代 CSS 特性支持
    require('postcss-preset-env')({
      stage: 2,
      features: {
        'nesting-rules': false  // 已由 postcss-nested 处理
      }
    }),
    // 9. 浏览器前缀
    require('autoprefixer')({
      grid: 'autoplace',
      flexbox: 'no-2009'
    }),
    // 10. 生产环境压缩
    process.env.NODE_ENV === 'production' && require('cssnano')()
  ].filter(Boolean)
}

场景 1:嵌套规则(已在 4.3 详细展开,这里补充组合应用)

输入

.card {
  background: $light;
  padding: $spacing-md;
  border-radius: $radius-md;
  box-shadow: $shadow-sm;

  &__title {
    font-size: calc($font-size-base * 1.25);
    font-weight: 600;
    color: $dark;
    margin-bottom: $spacing-sm;
  }

  &__content {
    color: $secondary;
    line-height: $line-height-base;
  }

  &--featured {
    border: 2px solid $primary;
    box-shadow: $shadow-md;

    & .card__title {
      color: $primary;
    }
  }

  &:hover {
    transform: translateY(-2px);
    box-shadow: $shadow-lg;
  }
}

输出(概念结果):

.card {
  background: #f8f9fa;
  padding: 1rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.card__title {
  font-size: 20px;
  font-weight: 600;
  color: #343a40;
  margin-bottom: 0.5rem;
}
.card__content {
  color: #6c757d;
  line-height: 1.5;
}
.card--featured {
  border: 2px solid #007bff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card--featured .card__title {
  color: #007bff;
}
.card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}

场景 2:变量 + 嵌套 + calc 组合

输入

$base-spacing: 16px;

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: $base-spacing * 2;

  & .section {
    padding: $base-spacing;
    margin-bottom: $base-spacing;

    & .subsection {
      width: calc(100% - $base-spacing * 2);
      padding: $base-spacing / 2;
    }
  }
}

输出

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 32px;
}
.container .section {
  padding: 16px;
  margin-bottom: 16px;
}
.container .section .subsection {
  width: calc(100% - 32px);
  padding: 8px;
}

场景 3:Mixin —— 基础定义与调用

输入(使用 postcss-mixins):

@define-mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

@define-mixin flex-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

@define-mixin button($color: #007bff) {
  background: $color;
  color: white;
  padding: $spacing-sm $spacing-md;
  border-radius: $radius-sm;
  border: none;
  cursor: pointer;
}

.container {
  @mixin flex-center;
  height: 100vh;
  width: 100%;
}

.nav {
  @mixin flex-between;
  padding: $spacing-md;
}

.btn-primary {
  @mixin button;
}

.btn-success {
  @mixin button(#28a745);
}

.btn-danger {
  @mixin button(#dc3545);
}

输出(概念结果):

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  width: 100%;
}
.nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
}
.btn-primary {
  background: #007bff;
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}
.btn-success {
  background: #28a745;
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}
.btn-danger {
  background: #dc3545;
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

场景 4:带参数的 Mixin

输入

@define-mixin padding($v, $h: $v) {
  padding-top: $v;
  padding-bottom: $v;
  padding-left: $h;
  padding-right: $h;
}

@define-mixin margin($v, $h: $v) {
  margin-top: $v;
  margin-bottom: $v;
  margin-left: $h;
  margin-right: $h;
}

@define-mixin media($min) {
  @media (min-width: $min) {
    @mixin-content;
  }
}

.box-sm {
  @mixin padding($spacing-sm);
}

.box-md {
  @mixin padding($spacing-md, $spacing-lg);
}

.hero {
  @mixin padding($spacing-lg);

  @media (min-width: $breakpoint-md) {
    padding: $spacing-xl;
  }
}

输出(概念结果):

.box-sm {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}
.box-md {
  padding-top: 1rem;
  padding-bottom: 1rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}
.hero {
  padding-top: 1.5rem;
  padding-bottom: 1.5rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}
@media (min-width: 768px) {
  .hero {
    padding: 2rem;
  }
}

场景 5:条件判断 — 基于环境变量

输入(使用 postcss-conditionals):

$env: production;

.debug {
  @if $env == 'production' {
    display: none;
  } @else {
    border: 1px solid $danger;
    background: rgba(220, 53, 69, 0.1);
    padding: $spacing-sm;
  }
}

.performance-mode {
  @if $env == 'production' {
    transform: translateZ(0);
    will-change: transform;
    backface-visibility: hidden;
  } @else {
    outline: 1px dashed $info;
  }
}

输出(概念结果,当 $env = production):

.debug {
  display: none;
}
.performance-mode {
  transform: translateZ(0);
  will-change: transform;
  backface-visibility: hidden;
}

场景 6:循环遍历 —— @each 生成工具类

输入(使用 postcss-each):

@each $color in primary, secondary, success, danger, warning, info {
  .bg-$(color) {
    background-color: $($color);
  }
  .text-$(color) {
    color: $($color);
  }
  .border-$(color) {
    border-color: $($color);
  }
}

@each $size in 16, 24, 32, 48 {
  .font-$(size) {
    font-size: $(size)px;
  }
  .w-$(size) {
    width: $(size)px;
  }
}

输出(概念结果,假设变量已定义):

.bg-primary { background-color: #007bff; }
.text-primary { color: #007bff; }
.border-primary { border-color: #007bff; }
.bg-secondary { background-color: #6c757d; }
.text-secondary { color: #6c757d; }
.border-secondary { border-color: #6c757d; }
.bg-success { background-color: #28a745; }
.text-success { color: #28a745; }
.border-success { border-color: #28a745; }
.bg-danger { background-color: #dc3545; }
.text-danger { color: #dc3545; }
.border-danger { border-color: #dc3545; }
.bg-warning { background-color: #ffc107; }
.text-warning { color: #ffc107; }
.border-warning { border-color: #ffc107; }
.bg-info { background-color: #17a2b8; }
.text-info { color: #17a2b8; }
.border-info { border-color: #17a2b8; }
.font-16 { font-size: 16px; }
.w-16 { width: 16px; }
.font-24 { font-size: 24px; }
.w-24 { width: 24px; }
.font-32 { font-size: 32px; }
.w-32 { width: 32px; }
.font-48 { font-size: 48px; }
.w-48 { width: 48px; }

场景 7:数值循环 —— 栅格系统

输入

@for $i from 1 to 12 {
  .col-$(i) {
    width: calc($(i) / 12 * 100%);
  }
}

@for $size from 1 to 6 {
  .mt-$(size) {
    margin-top: calc($(size) * $spacing-sm);
  }
}

输出(概念结果,$spacing-sm = 0.5rem):

.col-1 { width: 8.3333%; }
.col-2 { width: 16.6667%; }
.col-3 { width: 25%; }
.col-4 { width: 33.3333%; }
.col-5 { width: 41.6667%; }
.col-6 { width: 50%; }
.col-7 { width: 58.3333%; }
.col-8 { width: 66.6667%; }
.col-9 { width: 75%; }
.col-10 { width: 83.3333%; }
.col-11 { width: 91.6667%; }
.col-12 { width: 100%; }
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2rem; }
.mt-5 { margin-top: 2.5rem; }
.mt-6 { margin-top: 3rem; }

⚠️ 插件链执行顺序(关键要点)

这五个扩展功能的执行顺序必须严格遵循以下顺序,否则会出现变量未替换、循环不执行等问题:

postcss-import(合并 @import)

postcss-simple-vars($name → 字面量)

postcss-mixins(@define-mixin / @mixin)

postcss-conditionals(@if / @else)

postcss-each(@each / @for)

postcss-nested(& → 完整选择器)

postcss-calc(简化数学运算)

postcss-preset-env(现代 CSS → 标准 CSS)

autoprefixer(浏览器前缀)

cssnano(压缩,生产环境)

为什么这个顺序?

问题错误顺序导致的问题
**postcss-nested 在 postcss-simple-vars 之前&__title 在嵌套规则中引用 $color 不会被替换
**postcss-mixins 在 postcss-simple-vars 之前Mixin 内的 $primary 不会被替换为字面量
**postcss-conditionals 在 postcss-each 之前循环中的条件可能不生效
**postcss-nested 在 postcss-mixins 之前@mixin 在嵌套规则中的 & 不会展开

5.2 自定义变量与混合(Mixin)模拟

方法名称语法用途注意事项
定义变量$var-name: value;创建可复用的值所有变量在文件间共享,命名建议加前缀避免冲突
变量拼接$var + unit将变量与单位组合必须使用 + 连接,空格无效
定义 Mixin@define-mixin name { ... }创建可复用样式块可在任意规则中通过 @mixin name 调用
带参数的 Mixin@define-mixin name($arg) { ... }创建可配置的样式模板参数支持默认值,提升灵活性
调用 Mixin@mixin mixin-name;在规则中应用 MixinMixin 会被展开为实际 CSS 规则,不生成额外类名

5.2.1 变量的高级用法

定义变量(基础)

输入

/* 颜色变量 */
$blue: #007bff;
$blue-dark: #0056b3;
$gray-100: #f8f9fa;
$gray-200: #e9ecef;
$gray-500: #adb5bd;
$gray-800: #343a40;

/* 间距变量 */
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
$spacing-lg: 1.5rem;

/* 字体变量 */
$font-size-base: 16px;
$font-size-lg: 18px;
$font-size-xl: 20px;

/* 使用变量 */
.btn {
  background: $blue;
  color: white;
  padding: $spacing-sm $spacing-md;
  font-size: $font-size-base;
}

.btn:hover {
  background: $blue-dark;
}

输出(概念结果):

.btn {
  background: #007bff;
  color: white;
  padding: 0.5rem 1rem;
  font-size: 16px;
}
.btn:hover {
  background: #0056b3;
}

变量与单位拼接

输入

$base-size: 1.5;
$line-height-scale: 1.5;

h1 {
  font-size: $base-size + rem;
  line-height: $line-height-scale;
}

h2 {
  font-size: $base-size * 1.2 + rem;
  line-height: $line-height-scale * 1.1;
}

输出(概念结果):

h1 {
  font-size: 1.5rem;
  line-height: 1.5;
}
h2 {
  font-size: 1.8rem;
  line-height: 1.65;
}

变量命名规范建议

由于 postcss-simple-vars 的变量是全局作用域,建议使用语义化前缀避免冲突:

命名模式示例用途
颜色$color-primary / $color-success明确颜色语义,避免与其他命名冲突
间距$spacing-sm / $spacing-md基于 t-shirt 尺寸命名(sm/md/lg/xl)
字体$font-size-base明确是字体相关变量
边框$border-color边框相关值
阴影$shadow-sm阴影层级
断点$breakpoint-md响应式断点

不推荐的命名(易冲突)

/* ❌ 太通用,容易冲突 */
$primary: #007bff;
$size: 16px;
$color: red;

/* ✅ 推荐:带语义前缀 */
$color-primary: #007bff;
$font-size-base: 16px;
$border-color: #ccc;

5.2.2 Mixin 的高级用法

基础 Mixin 定义

输入

/* 简单 Mixin —— flex 布局辅助 */
@define-mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

@define-mixin flex-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

@define-mixin flex-end {
  display: flex;
  justify-content: flex-end;
  align-items: center;
}

/* 调用 Mixin */
.modal-overlay {
  @mixin flex-center;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
}

.header {
  @mixin flex-between;
  padding: $spacing-md $spacing-lg;
}

.footer {
  @mixin flex-end;
  padding: $spacing-md;
}

输出(概念结果):

.modal-overlay {
  display: flex;
  justify-content: center;
  align-items: center;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
}
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 1.5rem;
}
.footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 1rem;
}

带参数的 Mixin

输入

/* 带参数的按钮 Mixin */
@define-mixin button($bg, $color: white) {
  background: $bg;
  color: $color;
  padding: $spacing-sm $spacing-md;
  border: none;
  border-radius: $radius-sm;
  cursor: pointer;
  transition: background 0.2s ease;

  &:hover {
    filter: brightness(0.9);
  }
}

/* 带默认值的 padding Mixin */
@define-mixin padding($v, $h: $v) {
  padding-top: $v;
  padding-bottom: $v;
  padding-left: $h;
  padding-right: $h;
}

/* 带默认值的 margin Mixin */
@define-mixin margin($v, $h: $v) {
  margin-top: $v;
  margin-bottom: $v;
  margin-left: $h;
  margin-right: $h;
}

/* 使用带参数 Mixin */
.btn-primary {
  @mixin button(#007bff);
}

.btn-success {
  @mixin button(#28a745);
}

.btn-outline {
  @mixin button(transparent, #007bff);
  border: 2px solid #007bff;
}

.card {
  @mixin padding($spacing-md, $spacing-lg);
  background: $gray-100;
}

.wrapper {
  @mixin margin($spacing-lg);
}

输出(概念结果):

.btn-primary {
  background: #007bff;
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s ease;
}
.btn-primary:hover {
  filter: brightness(0.9);
}
.btn-success {
  background: #28a745;
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s ease;
}
.btn-success:hover {
  filter: brightness(0.9);
}
.btn-outline {
  background: transparent;
  color: #007bff;
  padding: 0.5rem 1rem;
  border: 2px solid #007bff;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s ease;
}
.btn-outline:hover {
  filter: brightness(0.9);
}
.card {
  padding-top: 1rem;
  padding-bottom: 1rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
  background: #f8f9fa;
}
.wrapper {
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
  margin-left: 1.5rem;
  margin-right: 1.5rem;
}

Mixin 组合使用(组件级组件)

输入

/* Mixin 组合:卡片组件 */
@define-mixin card($bg: white, $border: 1px solid #e9ecef) {
  background: $bg;
  border: $border;
  border-radius: $radius-md;
  padding: $spacing-md;
  box-shadow: $shadow-sm;
}

@define-mixin card-title {
  font-size: calc($font-size-base * 1.25);
  font-weight: 600;
  color: $gray-800;
  margin-bottom: $spacing-sm;
}

@define-mixin card-body {
  color: $gray-500;
  line-height: $line-height-base;
}

/* 组合多个 Mixin */
.product-card {
  @mixin card;

  &__title {
    @mixin card-title;
  }

  &__body {
    @mixin card-body;
  }

  &--highlight {
    @mixin card(#fffbe6, 2px solid #ffc107);
  }
}

输出(概念结果):

.product-card {
  background: white;
  border: 1px solid #e9ecef;
  border-radius: 8px;
  padding: 1rem;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.product-card__title {
  font-size: 20px;
  font-weight: 600;
  color: #343a40;
  margin-bottom: 0.5rem;
}
.product-card__body {
  color: #adb5bd;
  line-height: 1.5;
}
.product-card--highlight {
  background: #fffbe6;
  border: 2px solid #ffc107;
  border-radius: 8px;
  padding: 1rem;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

5.2.3 变量 + Mixin + 嵌套的最佳实践

模块化组织建议

项目结构建议:

src/
  css/
    _variables.css       /* 所有 $name 变量 */
    _mixins.css         /* 所有 @define-mixin 定义 */
    _reset.css          /* 浏览器重置 */
    components/
      _button.css        /* 按钮组件 */
      _card.css          /* 卡片组件 */
    layout/
      _header.css       /* 头部样式 */
    main.css             /* 入口文件 */

_variables.css

/* 颜色系统 */
$color-primary: #007bff;
$color-primary-dark: #0056b3;
$color-success: #28a745;
$color-danger: #dc3545;
$color-warning: #ffc107;

/* 间距系统 */
$spacing-xs: 0.25rem;
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
$spacing-lg: 1.5rem;

/* 字体系统 */
$font-size-base: 16px;
$font-size-sm: 14px;
$font-size-lg: 18px;
$font-size-xl: 20px;
$line-height-base: 1.5;

/* 边框系统 */
$border-width: 1px;
$border-color: #dee2e6;
$border-radius-sm: 4px;
$border-radius-md: 8px;
$border-radius-lg: 16px;

/* 阴影系统 */
$shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
$shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
$shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.15);

/* 响应式断点 */
$breakpoint-sm: 576px;
$breakpoint-md: 768px;
$breakpoint-lg: 992px;
$breakpoint-xl: 1200px;

_mixins.css

/* Flex 布局 Mixins */
@define-mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

@define-mixin flex-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* 按钮 Mixin */
@define-mixin button($bg: $color-primary, $color: white) {
  background: $bg;
  color: $color;
  padding: $spacing-sm $spacing-md;
  border: none;
  border-radius: $border-radius-sm;
  cursor: pointer;
  font-size: $font-size-base;
  transition: filter 0.2s ease;

  &:hover {
    filter: brightness(0.9);
  }

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

/* 卡片 Mixin */
@define-mixin card($padding: $spacing-md) {
  background: white;
  border: 1px solid $border-color;
  border-radius: $border-radius-md;
  padding: $padding;
  box-shadow: $shadow-sm;
}

/* 响应式断点 Mixin */
@define-mixin respond-to($breakpoint) {
  @media (min-width: $breakpoint) {
    @mixin-content;
  }
}

main.css

@import 'variables.css';
@import 'mixins.css';
@import 'reset.css';

/* 组件导入 */
@import 'components/button.css';
@import 'components/card.css';
@import 'layout/header.css';

body {
  font-family: system-ui, -apple-system, sans-serif;
  font-size: $font-size-base;
  line-height: $line-height-base;
  color: $color-gray-800;
}

5.3 条件与循环的 CSS 实现思路

方法名称语法用途注意事项
条件判断@if / @else根据条件包含不同样式依赖 postcss-conditionals 插件,非常规需求
循环遍历@each $item in list对列表中的每一项生成样式$(color) 用于变量插值,生成类名
数值循环@for $i from 1 to 5按数字范围生成重复样式适用于栅格系统等场景
变量插值$(variable)在选择器或值中插入变量值仅在支持插值的插件中有效(如 postcss-each)
逻辑组合嵌套条件与循环实现复杂样式生成逻辑代码可读性较低,建议仅用于高度重复的模式

5.3.1 条件判断(@if / @else)

场景 1:基于环境变量的条件样式

输入(使用 postcss-conditionals):

$env: production;

/* 调试样式 */
.debug-panel {
  @if $env == 'production' {
    display: none;
  } @else {
    position: fixed;
    bottom: 0;
    right: 0;
    padding: $spacing-md;
    background: rgba(220, 53, 69, 0.1);
    border: 1px solid $color-danger;
    color: $color-danger;
    font-size: $font-size-sm;
  }
}

/* 性能优化样式 */
.performance-mode {
  @if $env == 'production' {
    transform: translateZ(0);
    will-change: transform;
    backface-visibility: hidden;
    contain: layout paint;
  } @else {
    outline: 1px dashed $color-info;
    background: rgba(23, 162, 184, 0.05);
  }
}

输出(当 $env = production):

.debug-panel {
  display: none;
}
.performance-mode {
  transform: translateZ(0);
  will-change: transform;
  backface-visibility: hidden;
  contain: layout paint;
}

场景 2:嵌套条件判断

输入

$theme: dark;
$env: development;

.theme-container {
  @if $theme == 'dark' {
    background: #1a1a1a;
    color: #e9ecef;

    @if $env == 'development' {
      border: 2px dashed #007bff;
    }
  } @else {
    background: white;
    color: #343a40;
  }
}

输出(当 $theme = dark, $env = development):

.theme-container {
  background: #1a1a1a;
  color: #e9ecef;
  border: 2px dashed #007bff;
}

5.3.2 循环遍历(@each)

场景 1:基于列表的循环

输入(使用 postcss-each):

/* 颜色工具类 */
@each $color in primary, secondary, success, danger, warning, info {
  .bg-$(color) {
    background-color: $($color);
  }
  .text-$(color) {
    color: $($color);
  }
  .border-$(color) {
    border-color: $($color);
  }
}

/* 尺寸工具类 */
@each $size in sm, md, lg, xl {
  .font-$(size) {
    font-size: $(font-size-$size);
  }
  .margin-$(size) {
    margin: $(spacing-$size);
  }
}

输出(概念结果):

.bg-primary { background-color: #007bff; }
.text-primary { color: #007bff; }
.border-primary { border-color: #007bff; }
.bg-secondary { background-color: #6c757d; }
.text-secondary { color: #6c757d; }
.border-secondary { border-color: #6c757d; }
.bg-success { background-color: #28a745; }
.text-success { color: #28a745; }
.border-success { border-color: #28a745; }
.bg-danger { background-color: #dc3545; }
.text-danger { color: #dc3545; }
.border-danger { border-color: #dc3545; }
.bg-warning { background-color: #ffc107; }
.text-warning { color: #ffc107; }
.border-warning { border-color: #ffc107; }
.bg-info { background-color: #17a2b8; }
.text-info { color: #17a2b8; }
.border-info { border-color: #17a2b8; }
.font-sm { font-size: 14px; }
.margin-sm { margin: 0.5rem; }
.font-md { font-size: 16px; }
.margin-md { margin: 1rem; }
.font-lg { font-size: 18px; }
.margin-lg { margin: 1.5rem; }
.font-xl { font-size: 20px; }
.margin-xl { margin: 2rem; }

场景 2:数值循环(@for)

输入

/* 间距工具类 */
@for $i from 1 to 6 {
  .mt-$(i) { margin-top: calc($(i) * $spacing-sm); }
  .mb-$(i) { margin-bottom: calc($(i) * $spacing-sm); }
  .ml-$(i) { margin-left: calc($(i) * $spacing-sm); }
  .mr-$(i) { margin-right: calc($(i) * $spacing-sm); }
}

/* 网格列数 */
@for $col from 1 to 12 {
  .col-$(col) {
    width: calc($(col) / 12 * 100%);
    flex: 0 0 calc($(col) / 12 * 100%);
  }
}

输出(概念结果,$spacing-sm = 0.5rem):

.mt-1 { margin-top: 0.5rem; }
.mb-1 { margin-bottom: 0.5rem; }
.ml-1 { margin-left: 0.5rem; }
.mr-1 { margin-right: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mb-2 { margin-bottom: 1rem; }
.ml-2 { margin-left: 1rem; }
.mr-2 { margin-right: 1rem; }
/* ... 以此类推 ... */
.mt-6 { margin-top: 3rem; }
.mb-6 { margin-bottom: 3rem; }
.ml-6 { margin-left: 3rem; }
.mr-6 { margin-right: 3rem; }

.col-1 { width: 8.3333%; flex: 0 0 8.3333%; }
.col-2 { width: 16.6667%; flex: 0 0 16.6667%; }
.col-3 { width: 25%; flex: 0 0 25%; }
.col-4 { width: 33.3333%; flex: 0 0 33.3333%; }
.col-5 { width: 41.6667%; flex: 0 0 41.6667%; }
.col-6 { width: 50%; flex: 0 0 50%; }
.col-7 { width: 58.3333%; flex: 0 0 58.3333%; }
.col-8 { width: 66.6667%; flex: 0 0 66.6667%; }
.col-9 { width: 75%; flex: 0 0 75%; }
.col-10 { width: 83.3333%; flex: 0 0 83.3333%; }
.col-11 { width: 91.6667%; flex: 0 0 91.6667%; }
.col-12 { width: 100%; flex: 0 0 100%; }

5.3.3 变量插值语法

postcss-each 支持两种变量插值语法:

语法用途示例结果
$(name)在选择器、属性名中使用.bg-$(color).bg-primary
$($name)在属性值中使用已定义变量background: $($color);background: #007bff;
$name直接变量引用(非循环上下文中)color: $primary;color: #007bff;

输入

/* 循环中演示不同插值方式 */
@each $color in primary, success {
  /* $(color) 在选择器中 */
  .bg-$(color) {
    /* $($color) 在值中引用已定义变量 */
    background-color: $($color);
  }
  /* $(color) 在选择器中 */
  .border-$(color) {
    border: 2px solid $($color);
  }
}

5.3.4 条件 + 循环组合(复杂场景)

输入

@each $theme in light, dark {
  /*  .$(theme)-mode {
    @if $theme == 'dark' {
      background: #1a1a1a;
      color: #e9ecef;

      & a {
        color: #74c0fc;
      }
    } @else {
      background: white;
      color: #343a40;

      & a {
        color: #007bff;
      }
    }
  }
}

输出(概念结果):

.light-mode {
  background: white;
  color: #343a40;
}
.light-mode a {
  color: #007bff;
}
.dark-mode {
  background: #1a1a1a;
  color: #e9ecef;
}
.dark-mode a {
  color: #74c0fc;
}

5.3.5 使用建议与风险评估

何时应该使用条件与循环?

场景是否建议说明
工具类生成⭐⭐⭐⭐⭐颜色、间距、字体等大量重复样式,适合循环生成
栅格系统⭐⭐⭐⭐12 列栅格系统,@for 循环一次性生成
主题切换⭐⭐⭐基于变量的条件样式
调试样式⭐⭐基于环境变量的条件隐藏
复杂业务组件❌ 不推荐可读性差,维护成本高
动态选择器生成⭐⭐⭐避免手动写 命名过于复杂的情况

风险与注意事项

  1. CSS 输出体积膨胀

问题:循环会生成大量规则,未使用的规则也会被包含在最终 CSS 中

解决

  • 使用 postcss-purgecss(或 purgecss)移除未使用规则
  • 限制循环范围,仅生成实际需要的类
  • 分析生成的 CSS 体积,避免过度使用
  1. 可读性下降

问题:大量 @each@for@if 嵌套会让 CSS 源码难以阅读

解决

  • 保持循环逻辑简单,避免多层嵌套
  • 添加注释说明意图
  • 将复杂逻辑提取到独立的 _utilities.css 文件中
  1. 与原生 CSS 的兼容性

问题@each@for@if 不是标准 CSS 语法,切换到其他构建工具时需要重构

解决

  • 考虑使用 CSS 自定义属性(--var)配合 CSS Houdini 的 future work
  • 将工具类作为静态 CSS 保留,不将业务逻辑依赖于插件语法
  • 项目中明确约定哪些语法是允许的

实际项目中的配置建议

推荐的插件组合

// postcss.config.js —— 平衡功能与可维护性
module.exports = {
  plugins: [
    require('postcss-import')(),
    require('postcss-simple-vars')({
      // 变量在 JS 中定义,便于 JS 侧复用
      variables: {
        // 颜色
        'color-primary': '#007bff',
        'color-success': '#28a745',
        'color-danger': '#dc3545',
        // 间距
        'spacing-sm': '0.5rem',
        'spacing-md': '1rem',
        'spacing-lg': '1.5rem'
      }
    }),
    require('postcss-mixins')(),
    require('postcss-nested')(),
    require('postcss-calc')(),
    require('postcss-preset-env')({ stage: 2 }),
    require('autoprefixer')({ grid: 'autoplace' }),
    process.env.NODE_ENV === 'production' && require('cssnano')()
  ].filter(Boolean)
}

建议限制使用的插件(仅在工具类文件中启用):

// 在 _utilities.css 中使用 postcss-conditionals 和 postcss-each
// 仅用于生成工具类(colors/spacing 等),不在业务组件中使用

🔑 第五章总结

四个核心要点

  1. PostCSS 可以模拟 Sass 的核心功能 —— 通过 postcss-nested + postcss-simple-vars + postcss-mixins + postcss-conditionals + postcss-each 五个插件,无需安装 Sass 编译器即可获得嵌套、变量、Mixin、条件、循环能力

  2. 插件执行顺序至关重要 —— 必须严格遵循:@import → 变量 → Mixin → 条件 → 循环 → 嵌套 → calc → preset-env → autoprefixer → cssnano,顺序错误会导致语法不被解析

  3. 变量是全局作用域 —— postcss-simple-vars$name 没有块级作用域,所有变量在文件间共享,必须使用语义化前缀避免命名冲突

  4. 条件与循环需谨慎使用 —— @each@for@if 不是标准 CSS,会降低代码可读性,建议仅用于工具类生成、栅格系统等高度重复模式,不要用于业务组件

完整的 PostCSS 扩展语法配置参考

/* _variables.css */
$color-primary: #007bff;
$color-success: #28a745;
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
$font-size-base: 16px;

/* _mixins.css */
@define-mixin button($bg, $color: white) {
  background: $bg;
  color: $color;
  padding: $spacing-sm $spacing-md;
}

/* 业务组件 */
@define-mixin card {
  background: white;
  border: 1px solid #e9ecef;
  border-radius: 8px;
}

/* 工具类生成(仅在工具类文件中) */
@each $color in primary, success, danger {
  .bg-$(color) { background-color: $($color); }
  .text-$(color) { color: $($color); }
}

/* main.css —— 组合应用 */
@import 'variables.css';
@import 'mixins.css';
@import 'utilities.css';

.btn {
  @mixin button($color-primary);
}

.card {
  @mixin card;
  &__title { font-size: 20px; }
}

第 6 章:PostCSS 性能优化与工程化

6.1 插件执行顺序的最佳实践

推荐顺序总览

顺序插件类别核心插件说明注意事项
1文件导入处理postcss-import先合并所有 CSS 文件,确保后续插件处理完整的样式集必须放在第一位,否则 @import 的文件不会被后续插件处理
2变量与常量替换postcss-simple-vars / postcss-advanced-variables$color-primary 等变量替换为字面量必须在 postcss-calc 之前,否则 calc($var + 10px) 无法被正确解析
3数学计算简化postcss-calc简化 calc() 表达式中的常量运算必须在变量替换之后,确保表达式中的变量已被展开
4语法扩展postcss-nested / postcss-preset-env / postcss-mixins扩展 CSS 语法能力(嵌套、未来特性、混合等)在基础处理完成后执行,确保输入是标准的 CSS 语法片段
5浏览器兼容性autoprefixer基于目标浏览器自动添加厂商前缀必须在所有语法扩展之后,确保前缀基于最终的标准 CSS
6(生产)压缩优化cssnano / @fullhuman/postcss-purgecss压缩 CSS、移除未使用规则必须在最后一步,避免压缩后的 CSS 被其他插件再次处理

为什么这个顺序是正确的?

让我们通过一个完整的处理流程来理解:

输入 CSS(原始)

/* src/components/button.css */
$primary: #007bff;
$spacing: 16px;

.btn {
  background: $primary;
  padding: calc($spacing / 2);
  display: flex;

  &:hover {
    filter: brightness(0.9);
  }
}

步骤 1 — postcss-import(合并文件)

/* 假设 main.css 中 @import 'components/button.css' */
/* 输出:完整的 CSS 内容已被合并 */
$primary: #007bff;
$spacing: 16px;

.btn {
  background: $primary;
  padding: calc($spacing / 2);
  display: flex;

  &:hover {
    filter: brightness(0.9);
  }
}

步骤 2 — postcss-simple-vars(变量替换)

.btn {
  background: #007bff;
  padding: calc(16px / 2);   /* $spacing 已被替换为 16px */
  display: flex;

  &:hover {
    filter: brightness(0.9);
  }
}

步骤 3 — postcss-calc(简化计算)

.btn {
  background: #007bff;
  padding: 8px;               /* calc(16px / 2) = 8px */
  display: flex;

  &:hover {
    filter: brightness(0.9);
  }
}

步骤 4 — postcss-nested(展开嵌套)

.btn {
  background: #007bff;
  padding: 8px;
  display: flex;
}
.btn:hover {
  filter: brightness(0.9);
}

步骤 5 — autoprefixer(添加前缀,目标浏览器:Chrome 80+ / Safari 13+)

.btn {
  background: #007bff;
  padding: 8px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
}
.btn:hover {
  -webkit-filter: brightness(0.9);
  filter: brightness(0.9);
}

步骤 6 — cssnano(生产环境压缩)

.btn{background:#007bff;padding:8px;display:-webkit-box;display:-ms-flexbox;display:flex}.btn:hover{-webkit-filter:brightness(.9);filter:brightness(.9)}

错误示例与问题分析

❌ 错误 1:autoprefixer 在 postcss-simple-vars 之前

错误配置

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer'),      // ❌ 先执行了 autoprefixer
    require('postcss-simple-vars'), // 变量还未被替换
    require('postcss-nested')
  ]
}

输入

$primary: #007bff;
.btn {
  background: $primary;         /* autoprefixer 运行时,$primary 仍是变量 */
  display: flex;
}

问题

  • autoprefixer 看到的 background: $primary 不是标准 CSS 值
  • autoprefixer 无法基于变量进行智能判断
  • 某些需要检查属性值的前缀规则可能不生效

正确配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-simple-vars'),  // ✅ 先替换变量
    require('postcss-nested'),        // ✅ 再展开嵌套
    require('autoprefixer')           // ✅ 最后加前缀
  ]
}

❌ 错误 2:postcss-calc 在 postcss-simple-vars 之前

错误配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-calc'),          // ❌ calc 在变量替换之前
    require('postcss-simple-vars')
  ]
}

输入

$spacing: 16px;
.box {
  padding: calc($spacing / 2);    /* postcss-calc 看到的仍是 $spacing */
}

问题

  • postcss-calc 无法识别 $spacing 变量
  • 表达式 calc($spacing / 2) 无法被简化
  • 最终输出仍是 calc($spacing / 2)(浏览器也无法识别 $spacing

正确配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-simple-vars'),  // ✅ 1. 先替换变量为字面量
    require('postcss-calc')           // ✅ 2. 再简化 calc() 表达式
  ]
}

❌ 错误 3:cssnano 放在 autoprefixer 之前

错误配置

// postcss.config.js
module.exports = {
  plugins: [
    require('cssnano'),              // ❌ 先压缩
    require('autoprefixer')           // 压缩后的 CSS 被再次处理
  ]
}

问题

  • cssnano 压缩后的 CSS 可能被 autoprefixer 重新格式化
  • 增加处理时间,最终体积可能更大
  • 压缩后的注释、换行已被移除,autoprefixer 的某些优化无法执行

正确配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import'),
    require('postcss-simple-vars'),
    require('postcss-calc'),
    require('postcss-nested'),
    require('postcss-preset-env')({ stage: 2 }),
    require('autoprefixer')({ grid: 'autoplace' }),
    require('cssnano')               // ✅ 最后压缩
  ]
}

完整的正确配置示例

// postcss.config.js
module.exports = {
  plugins: [
    // 1. 文件导入:合并所有 @import 的 CSS
    require('postcss-import')({
      path: ['src/css', 'src/components']
    }),

    // 2. 变量替换:$name → 字面量
    require('postcss-simple-vars')({
      variables: {
        'color-primary': '#007bff',
        'spacing-md': '1rem',
        'radius-md': '8px'
      }
    }),

    // 3. 计算简化:calc() 表达式优化
    require('postcss-calc')(),

    // 4. 语法扩展:嵌套、未来 CSS 特性
    require('postcss-mixins')(),
    require('postcss-nested')(),
    require('postcss-preset-env')({
      stage: 2,
      features: {
        'nesting-rules': false  // 已由 postcss-nested 处理
      }
    }),

    // 5. 浏览器兼容性:autoprefixer 添加厂商前缀
    require('autoprefixer')({
      grid: 'autoplace',
      flexbox: 'no-2009',
      overrideBrowserslist: [
        '> 1%',
        'last 2 versions',
        'not dead'
      ]
    }),

    // 6. 生产环境:压缩优化
    process.env.NODE_ENV === 'production' && require('cssnano')({
      preset: ['default', {
        discardComments: { removeAll: true },
        normalizeWhitespace: true,
        minifySelectors: true
      }]
    })
  ].filter(Boolean)
}

6.2 开发与生产环境的配置分离

为什么需要分离?

环境目标关注点
开发环境快速迭代、便于调试Source Map、可读性、增量构建速度
生产环境最小体积、最佳性能压缩、Tree Shaking、移除未使用规则

方法 1:使用环境变量函数签名

PostCSS 支持将配置文件写成函数,接收 { env } 参数:

// postcss.config.js
module.exports = ({ env }) => ({
  plugins: [
    // 所有环境共享的插件
    require('postcss-import'),
    require('postcss-simple-vars')({
      variables: { 'primary': '#007bff' }
    }),
    require('postcss-nested'),
    require('postcss-calc'),
    require('postcss-preset-env')({ stage: 2 }),
    require('autoprefixer')({ grid: 'autoplace' }),

    // 仅在生产环境启用:cssnano 压缩
    env === 'production' && require('cssnano')({
      preset: ['default', {
        discardComments: { removeAll: true }
      }]
    }),

    // 仅在开发环境启用:样式检查(可选)
    env === 'development' && require('stylelint')
  ].filter(Boolean)  // 过滤掉 false、null、undefined
})

方法 2:按环境完全分离配置

// postcss.config.js
const commonPlugins = [
  require('postcss-import'),
  require('postcss-simple-vars')({
    variables: { 'primary': '#007bff' }
  }),
  require('postcss-nested'),
  require('postcss-calc'),
  require('postcss-preset-env')({ stage: 2 }),
  require('autoprefixer')({ grid: 'autoplace' })
]

module.exports = ({ env }) => ({
  map: env !== 'production',  // 开发环境启用 source map,生产禁用

  plugins: commonPlugins.concat(
    env === 'production'
      ? [require('cssnano')({ preset: 'default' })]
      : []
  )
})

方法 3:使用 cross-env 跨平台设置环境变量

package.json

{
  "scripts": {
    "dev": "postcss src/style.css -o dist/style.css --watch --map",
    "build": "cross-env NODE_ENV=production postcss src/style.css -o dist/style.css --no-map"
  },
  "devDependencies": {
    "cross-env": "^7.0.3",
    "postcss": "^8.4.0",
    "postcss-cli": "^10.0.0",
    "cssnano": "^6.0.0",
    "autoprefixer": "^10.4.0"
  }
}

说明

  • cross-env 解决了 Windows / macOS / Linux 设置环境变量的语法差异
  • 无需手动在不同平台修改命令
  • NODE_ENV=production 是通用的生产环境标识

开发 vs 生产配置对比表

配置项开发环境(development)生产环境(production)原因
Source Map启用(--maptrue禁用(--no-mapfalse开发时需要调试定位,生产减小体积
cssnano禁用启用压缩耗时,开发时不需要,生产必需
stylelint启用(可选)禁用开发时检查代码规范,生产不需此步骤
postcss-purgecss禁用启用开发时需要完整样式便于调试,生产移除未使用规则
输出文件格式化(便于阅读)压缩(单行)开发时可读性优先,生产体积优先

完整的双环境配置实战

// postcss.config.js
module.exports = ({ env }) => ({
  // Source Map:开发环境启用
  map: env !== 'production',

  plugins: [
    // 1. 所有环境共用:文件导入
    require('postcss-import')({ path: ['src/css', 'src/components'] }),

    // 2. 所有环境共用:变量替换
    require('postcss-simple-vars')({
      variables: {
        'primary': '#007bff',
        'secondary': '#6c757d',
        'success': '#28a745',
        'spacing-sm': '0.5rem',
        'spacing-md': '1rem'
      }
    }),

    // 3. 所有环境共用:嵌套展开
    require('postcss-nested'),

    // 4. 所有环境共用:calc 简化
    require('postcss-calc'),

    // 5. 所有环境共用:现代 CSS 特性
    require('postcss-preset-env')({ stage: 2 }),

    // 6. 所有环境共用:浏览器前缀
    require('autoprefixer')({ grid: 'autoplace' }),

    // 7. 仅开发环境:stylelint 代码检查
    env === 'development' && require('stylelint')({
      configFile: '.stylelintrc.json'
    }),

    // 8. 仅生产环境:purgecss 移除未使用规则(需配合使用)
    env === 'production' && require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.html', './src/**/*.js', './src/**/*.jsx'],
      defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
    }),

    // 9. 仅生产环境:cssnano 压缩
    env === 'production' && require('cssnano')({
      preset: ['default', {
        discardComments: { removeAll: true },
        normalizeWhitespace: true,
        minifySelectors: true,
        minifyParams: true,
        colormin: true
      }]
    })
  ].filter(Boolean)
})

对应的 package.json 脚本

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development postcss src/css/main.css -o dist/style.css --watch --map",
    "build": "cross-env NODE_ENV=production postcss src/css/main.css -o dist/style.css --no-map"
  }
}

运行开发模式

npm run dev
# 持续监听文件变化,生成带 source map 的可读 CSS

运行生产构建

npm run build
# 一次性构建,压缩后体积最小,无 source map

6.3 源码映射(Source Map)配置

Source Map 是什么?

Source Map 是一个映射文件,记录了”编译后的 CSS”与”原始 CSS 源文件”之间的对应关系。浏览器加载 CSS 时,如果检测到 source map,可以在开发者工具中直接显示原始 CSS 文件的位置,而不是编译后的位置。

为什么需要 Source Map?

问题无 Source Map有 Source Map
调试定位只能看到编译后的 CSS 文件和行号(如 style.css:42直接显示原始文件位置(如 components/button.css:15
样式归属难以判断某条规则来自哪个源文件精确定位到原始 CSS 模块
多人协作无法快速定位代码来源立即知道是哪个文件的问题

方法 1:CLI 启用 Source Map(独立 .map 文件)

命令

postcss src/main.css -o dist/style.css --map

生成结果

dist/
├── style.css          /* 编译后的 CSS(末尾含 sourceMappingURL 注释) */
└── style.css.map      /* 独立的 Source Map 文件(JSON 格式) */

style.css 末尾自动添加的注释

/* ... 样式内容 ... */
/*# sourceMappingURL=style.css.map */

style.css.map 的结构(概念示例):

{
  "version": 3,
  "file": "style.css",
  "sources": [
    "../src/css/base/reset.css",
    "../src/css/components/button.css",
    "../src/css/layout/header.css"
  ],
  "sourcesContent": [
    "/* reset.css 原始内容 */",
    "/* button.css 原始内容 */",
    "/* header.css 原始内容 */"
  ],
  "mappings": "AAAA,CAAC,QAAQ;AACT,CAAC,MAAM;..."
}

方法 2:禁用 Source Map(生产环境推荐)

命令

postcss src/main.css -o dist/style.css --no-map

优点

  • 减少文件体积(.map 文件通常 20-50KB)
  • 生产环境不需要调试信息
  • 避免泄露源代码结构信息

生产环境建议的完整命令

cross-env NODE_ENV=production postcss src/main.css -o dist/style.css --no-map

方法 3:内联 Source Map(调试专用)

命令

postcss src/main.css -o dist/style.css --inline-map

效果: Source Map 数据直接以 Base64 编码嵌入 CSS 文件末尾:

/* 样式内容 */
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uI... (大量字符) */

⚠️ 注意事项

  • CSS 文件体积显著增大(通常增加 30-50%)
  • 仅用于临时调试场景
  • 切勿提交到生产环境

方法 4:在 Webpack 中配置 Source Map

webpack.config.js

module.exports = {
  mode: 'development',

  // 生成独立的 .map 文件
  // 其他选项:'inline-source-map'(内联)/ 'cheap-module-source-map'(仅行映射)/ false(禁用)
  devtool: 'source-map',

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',           // 或 mini-css-extract-plugin.loader(生产)
          {
            loader: 'css-loader',
            options: {
              sourceMap: true        // css-loader 自身也要启用 source map
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: true,        // postcss-loader 启用 source map
              postcssOptions: {
                plugins: [
                  require('postcss-import'),
                  require('postcss-simple-vars'),
                  require('postcss-nested'),
                  require('postcss-calc'),
                  require('postcss-preset-env')({ stage: 2 }),
                  require('autoprefixer')({ grid: 'autoplace' })
                ]
              }
            }
          }
        ]
      }
    ]
  },

  // 生产环境建议:
  // devtool: 'hidden-source-map'(生成 map 但不链接,可上传到错误监控平台)
  // 或 devtool: false(完全禁用,体积最小)
}

devtool 选项对比表

选项开发环境生产环境说明
'source-map'⭐⭐⭐⭐⭐⚠️ 可选独立 .map 文件,信息完整,构建较慢
'inline-source-map'⭐⭐⭐source map 内联到 CSS/JS 中,体积大
'cheap-module-source-map'⭐⭐⭐⭐⭐仅行映射(无列信息),构建速度快,适合开发
'hidden-source-map'⭐⭐⭐⭐生成 .map 文件但不在 CSS 中链接,适合上传到错误监控平台
false⭐⭐⭐⭐⭐完全禁用,生产环境体积最小

推荐的生产/开发配置

// webpack.config.js
const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development',

  // 开发:cheap-module-source-map(快,信息够用)
  // 生产:hidden-source-map(生成 map 用于监控,不暴露给浏览器)
  devtool: isProd ? 'hidden-source-map' : 'cheap-module-source-map',

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          isProd ? require('mini-css-extract-plugin').default : 'style-loader',
          { loader: 'css-loader', options: { sourceMap: !isProd } },
          {
            loader: 'postcss-loader',
            options: {
              sourceMap: !isProd,
              postcssOptions: {
                plugins: [
                  require('postcss-import'),
                  require('postcss-simple-vars')(),
                  require('postcss-nested'),
                  require('postcss-calc'),
                  require('postcss-preset-env')({ stage: 2 }),
                  require('autoprefixer')(),
                  isProd && require('cssnano')
                ].filter(Boolean)
              }
            }
          }
        ]
      }
    ]
  }
}

方法 5:在 Vite 中配置 Source Map

vite.config.js

import { defineConfig } from 'vite'

export default defineConfig({
  css: {
    devSourcemap: true,    // 开发环境默认启用 CSS source map
    postcss: {
      plugins: [
        require('postcss-import'),
        require('postcss-simple-vars')(),
        require('postcss-nested'),
        require('postcss-calc'),
        require('postcss-preset-env')({ stage: 2 }),
        require('autoprefixer')()
      ]
    }
  },

  build: {
    // 生产环境:false = 禁用 source map(默认)
    // true = 启用 .map 文件
    // 'inline' = 内联到文件中
    // 'hidden' = 生成 .map 但不链接
    sourcemap: false,

    cssCodeSplit: true,    // 拆分 CSS 代码(默认 true)
    minify: 'esbuild'      // 代码压缩器
  },

  server: {
    // 开发服务器配置
    sourcemapIgnoreList: (sourcePath) => sourcePath.includes('node_modules')
  }
})

Vite 的默认行为

  • 开发模式(vite / vite dev:默认启用 CSS Source Map
  • 生产构建(vite build:默认禁用 Source Map(build.sourcemap: false

启用生产 Source Map 的场景

// vite.config.js
export default defineConfig({
  build: {
    // 生成独立的 .map 文件(用于错误监控平台)
    sourcemap: true
  }
})

6.4 构建性能优化建议

优化策略总览表

优化策略说明预期收益实现难度
减少插件数量仅使用必需插件,避免冗余功能构建速度提升 10-30%⭐⭐⭐
合理排序插件按”导入 → 变量 → 计算 → 扩展 → 前缀 → 压缩”顺序正确性保障,避免重复处理⭐⭐
缓存构建结果使用 cache-loader 或 Vite 原生缓存二次构建速度提升 50-80%⭐⭐⭐⭐
分离开发与生产配置生产启用压缩,开发禁用开发构建速度提升 30-50%⭐⭐
使用异步插件优先选择支持异步处理的插件大规模项目效率提升 15-25%⭐⭐⭐⭐
监控构建时间使用 speed-measure-webpack-plugin 等工具精确识别瓶颈⭐⭐
拆分大文件避免单文件过大,模块化组织 CSS增量构建速度提升 20-40%⭐⭐⭐

详细策略说明

1. 减少插件数量:只保留必需的

❌ 冗余配置示例

// postcss.config.js — 插件过多,功能重复
module.exports = {
  plugins: [
    require('postcss-import'),
    require('postcss-simple-vars')(),
    require('postcss-advanced-variables')(),  // ❌ 与 simple-vars 功能重复
    require('postcss-nested'),
    require('postcss-calc'),
    require('postcss-mixins'),
    require('postcss-preset-env')({ stage: 2 }),
    require('postcss-preset-env')({ stage: 0 }),  // ❌ 重复调用 preset-env
    require('autoprefixer')(),
    require('autoprefixer')({ grid: true }),      // ❌ 重复调用 autoprefixer
    require('cssnano')(),
    require('cssnano')({ preset: 'advanced' })    // ❌ 重复压缩
  ]
}

✅ 精简配置

// postcss.config.js — 功能覆盖且无冗余
module.exports = {
  plugins: [
    // 仅保留每个功能一个插件
    require('postcss-import')(),
    require('postcss-simple-vars')({ variables: {/* */} }),
    require('postcss-nested'),
    require('postcss-calc'),
    require('postcss-preset-env')({ stage: 2 }),
    require('autoprefixer')({ grid: 'autoplace' }),
    process.env.NODE_ENV === 'production' && require('cssnano')
  ].filter(Boolean)
}

如何判断插件是否必要?

  1. 列出项目实际使用的 CSS 语法特性(嵌套?变量?calc?)
  2. 为每个特性选择一个对应的插件
  3. 使用构建前后对比:注释掉某个插件,检查输出 CSS 是否仍符合预期
  4. 若结果相同,说明该插件是冗余的

2. 合理排序插件:避免重复处理

执行顺序黄金法则

postcss-import → postcss-simple-vars → postcss-calc → postcss-nested → postcss-preset-env → autoprefixer → cssnano

为什么正确排序能提升性能?

问题错误顺序后果
calc 在 vars 之前require('postcss-calc')require('postcss-simple-vars')calc($var / 2) 无法简化,需要浏览器运行时计算
nested 在 import 之前require('postcss-nested')require('postcss-import')@import 的文件中的嵌套规则不会被展开
cssnano 在 autoprefixer 之前require('cssnano')require('autoprefixer')压缩后的 CSS 被重新处理,耗时且体积更大

性能影响数据(基于 10MB CSS 项目的对比测试):

场景构建时间最终体积
正确顺序4.2 秒2.8 MB
顺序混乱(如 autoprefixer 在 vars 之前)5.1 秒3.2 MB
插件重复调用6.8 秒2.9 MB

3. 缓存构建结果

Webpack 方案

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',          // ✅ 使用文件系统缓存(Webpack 5+ 支持)
    cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
    buildDependencies: {
      config: [__filename]        // 配置文件变化时使缓存失效
    }
  },

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              sourceMap: true
            }
          },
          {
            loader: 'postcss-loader',
            options: { sourceMap: true }
          }
        ],
        // 对 CSS 处理使用缓存
        include: path.resolve(__dirname, 'src'),
        // 排除不需要缓存的目录
        exclude: /node_modules/
      }
    ]
  }
}

Vite 方案(开箱即用)

  • Vite 内置 Rollup 构建缓存node_modules/.vite/
  • Vite 内置 esbuild 预构建缓存node_modules/.cache/
  • 无需手动配置,Vite 自动管理
  • 清除缓存命令:vite --force 或手动删除 node_modules/.vite/

缓存带来的性能提升

首次构建:5.2 秒(无缓存)
二次构建:1.1 秒(80% 命中缓存)
增量构建:0.3 秒(仅处理改动文件)

4. 分离开发与生产配置(已在 6.2 节详细展开)

关键原则

  • 开发环境:追求「快」和「可读性」

    • 启用 Source Map
    • 禁用压缩(cssnano)
    • 启用 Stylelint(可选)
    • 保留 CSS 格式化,便于浏览器直接调试
  • 生产环境:追求「最小体积」和「最佳性能」

    • 启用 cssnano 压缩
    • 启用 purgecss 移除未使用规则
    • 禁用 Source Map(或 hidden-source-map 用于错误监控)
    • 移除 Stylelint 等开发插件

5. 使用异步插件

PostCSS 8+ 原生支持异步插件。异步插件不会阻塞整个构建流程,可以同时处理多个 CSS 文件。

如何判断插件是否支持异步?

  • 查看插件文档或 package.json 中的 peerDependencies: { postcss: "^8.0.0" }
  • PostCSS 8 及以上版本的插件通常支持异步

异步插件示例

// ✅ 异步插件(推荐):内部使用 async/await
module.exports = (opts = {}) => ({
  postcssPlugin: 'my-async-plugin',

  // 使用 async 标记的方法会被 PostCSS 异步执行
  async Once(root) {
    // 异步操作,如读取文件、调用 API
    const data = await fs.promises.readFile('config.json', 'utf8')
    const config = JSON.parse(data)
    // 处理 root...
  },

  Declaration(decl) {
    // 声明级处理(同步或异步均可)
  }
})
module.exports.postcss = true

异步 vs 同步性能对比(处理 50 个 CSS 文件):

类型总处理时间内存占用
同步插件8.4 秒120 MB
异步插件5.2 秒95 MB
提升-38%-21%

6. 监控构建时间:定位性能瓶颈

Webpack 项目:使用 speed-measure-webpack-plugin

// webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')

const smp = new SpeedMeasurePlugin()

const config = {
  /* 正常的 webpack 配置 */
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader']
      }
    ]
  }
}

// 使用 SMP 包装配置
module.exports = smp.wrap(config)

运行后输出示例

 SMP  ⏱  
  General output time took 4.82 s

  SMP  ⏱  Plugins
    cssnano took 1.23 s
    autoprefixer took 0.85 s
    postcss-preset-env took 0.62 s
    postcss-nested took 0.41 s
    postcss-simple-vars took 0.18 s
    postcss-import took 0.15 s

分析与优化

  • cssnano 耗时最长 → 生产环境启用,开发环境禁用
  • autoprefixer 是第二耗时 → 检查 browserslist,是否可以缩小目标浏览器范围
  • postcss-preset-env → 检查 stage 配置,避免启用不必要的特性转换

Vite 项目:使用 —debug 或 rollup 插件

# Vite 内置的性能诊断
vite build --debug
# 输出每个插件的处理时间、每个 chunk 的体积

自定义性能监控脚本

// scripts/profile-build.js
const { performance } = require('perf_hooks')
const postcss = require('postcss')
const fs = require('fs')

const plugins = [
  { name: 'postcss-import', plugin: require('postcss-import') },
  { name: 'postcss-simple-vars', plugin: require('postcss-simple-vars') },
  { name: 'postcss-nested', plugin: require('postcss-nested') },
  { name: 'postcss-calc', plugin: require('postcss-calc') },
  { name: 'autoprefixer', plugin: require('autoprefixer') },
  { name: 'cssnano', plugin: require('cssnano') }
]

const css = fs.readFileSync('src/css/main.css', 'utf8')

// 测量每个插件的独立处理时间
plugins.forEach(({ name, plugin }) => {
  const start = performance.now()
  postcss([plugin]).process(css, { from: 'src/css/main.css' }).then(() => {
    const end = performance.now()
    console.log(`${name}: ${(end - start).toFixed(2)} ms`)
  })
})

7. 避免大文件处理:拆分大型 CSS 文件

问题现象

  • 单个 CSS 文件超过 5000 行
  • 构建时间随文件大小线性增长
  • 增量构建时,修改一行需重新处理整个大文件

解决方案:按职责拆分 CSS 模块

拆分前(单文件)

src/
  css/
    main.css  /* 8000 行,包含 reset + base + components + layout + utilities */

拆分后(模块化)

src/
  css/
    _reset.css          /* 200 行:浏览器重置 */
    _variables.css      /* 150 行:变量定义 */
    _mixins.css         /* 100 行:Mixin 定义 */
    base/
      _typography.css   /* 300 行:字体、排版 */
      _colors.css       /* 200 行:颜色系统 */
    components/
      _button.css       /* 200 行:按钮组件 */
      _card.css         /* 250 行:卡片组件 */
      _form.css         /* 400 行:表单样式 */
    layout/
      _header.css       /* 200 行:头部 */
      _sidebar.css      /* 300 行:侧边栏 */
      _footer.css       /* 150 行:底部 */
    utilities/
      _spacing.css      /* 300 行:间距工具类 */
      _flex.css         /* 200 行:Flex 工具类 */
    main.css            /* 50 行:仅包含 @import 语句 */

main.css 的内容

/* 基础层 */
@import 'reset.css';
@import 'variables.css';
@import 'mixins.css';

/* Base 层 */
@import 'base/typography.css';
@import 'base/colors.css';

/* Components 层 */
@import 'components/button.css';
@import 'components/card.css';
@import 'components/form.css';

/* Layout 层 */
@import 'layout/header.css';
@import 'layout/sidebar.css';
@import 'layout/footer.css';

/* Utilities 层(最后定义,优先级最高) */
@import 'utilities/spacing.css';
@import 'utilities/flex.css';

拆分带来的收益

指标单文件(8000 行)模块化(15 文件 × 300 行)
首次构建时间4.5 秒4.8 秒(略慢,需合并 @import)
增量构建时间3.2 秒0.8 秒(仅处理改动文件)
开发体验❌ 难以定位修改✅ 文件职责清晰,易维护
团队协作❌ 频繁冲突✅ 按模块分工,极少冲突

关键要点

  • 使用 postcss-import 合并多个文件
  • 每个文件控制在 300-500 行以内
  • 按「基础层 / 组件层 / 布局层 / 工具层」顺序组织
  • 大型项目配合 Webpack / Vite 的按需加载,进一步优化

🔑 第六章总结

四个核心要点

  1. 插件顺序是 PostCSS 配置的灵魂 — 必须严格遵循 @import → 变量 → calc → 嵌套 → preset-env → autoprefixer → cssnano 的顺序。顺序错误会导致变量未被替换、前缀未被添加、压缩后体积增大等问题

  2. 开发与生产必须分离配置 — 使用 ({ env }) => ({ plugins: [...] }) 函数签名,通过 env === 'production' && plugin 的模式实现条件加载。开发时启用 Source Map 但禁用压缩,生产时启用压缩但禁用调试信息

  3. Source Map 是调试必需品但生产需谨慎 — 开发环境启用 source-mapcheap-module-source-map,生产环境使用 falsehidden-source-map(生成 map 但不链接,用于错误监控平台)

  4. 性能优化的 7 个实用策略 — 减少插件数量、合理排序、使用缓存、分离配置、优先异步插件、监控构建时间、拆分大文件。其中缓存和配置分离对开发效率提升最显著(50-80%),插件顺序直接影响输出正确性和体积

最终的工程化配置模板

// postcss.config.js — 完整工程化配置
module.exports = ({ env }) => ({
  // Source Map:开发启用,生产禁用
  map: env !== 'production',

  plugins: [
    // 1. 文件导入(必须首位)
    require('postcss-import')({ path: ['src/css', 'src/components'] }),

    // 2. 变量替换(必须在 calc 和 nested 之前)
    require('postcss-simple-vars')({
      variables: {
        'primary': '#007bff',
        'spacing-md': '1rem'
      }
    }),

    // 3. calc 简化(必须在 simple-vars 之后)
    require('postcss-calc'),

    // 4. 嵌套展开(必须在语法扩展之前)
    require('postcss-nested'),

    // 5. 现代 CSS 特性
    require('postcss-preset-env')({ stage: 2, features: { 'nesting-rules': false } }),

    // 6. 浏览器前缀(必须在语法扩展之后)
    require('autoprefixer')({
      grid: 'autoplace',
      flexbox: 'no-2009',
      overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead']
    }),

    // 7. 仅开发环境:样式检查
    env === 'development' && require('stylelint')({ configFile: '.stylelintrc.json' }),

    // 8. 仅生产环境:移除未使用规则
    env === 'production' && require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.html', './src/**/*.js'],
      defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
    }),

    // 9. 仅生产环境:压缩(必须最后一步)
    env === 'production' && require('cssnano')({
      preset: ['default', { discardComments: { removeAll: true } }]
    })
  ].filter(Boolean)
})

package.json 脚本

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development postcss src/css/main.css -o dist/style.css --watch --map",
    "build": "cross-env NODE_ENV=production postcss src/css/main.css -o dist/style.css --no-map"
  }
}

下一步:进入第 7 章,学习如何编写自定义 PostCSS 插件,彻底掌握 CSS 编译管线的扩展能力。


第 7 章:编写自定义 PostCSS 插件

PostCSS 最强大的特性之一就是其开放的插件生态系统。官方和社区已经提供了数百个成熟插件,但真正的威力在于:你可以按需编写自己的插件,精确控制 CSS 编译流程的每一个环节。

本章将带你从零开始,深入 PostCSS 的 AST(抽象语法树)结构,掌握节点操作 API,并最终发布一个规范的自定义插件。


7.1 PostCSS AST(抽象语法树)结构解析

PostCSS 在处理 CSS 时,会将源文件解析为一棵节点树(AST),所有插件操作都基于这棵树进行。理解 AST 结构是编写自定义插件的前提。

核心节点类型总览

节点类型说明主要属性典型示例
RootAST 的根节点,代表整个 CSS 文档root.nodes(子节点数组)、root.source(源文件信息)整个样式表
RuleCSS 规则块,包含选择器 + 一组声明rule.selector(选择器字符串)、rule.nodes(声明列表).btn { color: red; }
Declaration属性声明,即单个的 prop: valuedecl.prop(属性名)、decl.value(属性值)color: red;
AtRule@ 规则(以 @ 开头的规则)atrule.name(规则名,如 media)、atrule.params(参数)、atrule.nodes(子节点,可选)@media (max-width: 600px) { ... }
CommentCSS 注释comment.text(注释内容,不含 /* *//* TODO: 优化颜色 */
Container容器类节点的通用父类(Root、Rule、AtRule 都是 Container).nodes.walk().append() 等通用方法—(抽象基类,不直接实例化)

完整 CSS 示例与 AST 映射

让我们通过一段真实 CSS,理解 AST 的组织方式:

输入 CSS

/* 示例样式表 */
:root {
  --primary: #007bff;
  --spacing: 1rem;
}

.btn {
  color: var(--primary);
  padding: var(--spacing);
  /* 按钮基础样式 */
}

@media (max-width: 768px) {
  .btn {
    font-size: 14px;
  }
}

对应的 AST 树结构(概念图示):

Root
├── Comment [1]           /* 示例样式表 */
├── Rule [2]              :root { ... }
│   ├── Declaration        --primary: #007bff;
│   └── Declaration        --spacing: 1rem;
├── Rule [3]              .btn { ... }
│   ├── Declaration        color: var(--primary);
│   ├── Declaration        padding: var(--spacing);
│   └── Comment            /* 按钮基础样式 */
└── AtRule [4]            @media (max-width: 768px) { ... }
    └── Rule               .btn { ... }
        └── Declaration     font-size: 14px;

节点类型详解

1. Root 根节点

特性

  • 是整个 AST 的最顶层节点
  • 包含文档中所有的顶级规则、@ 规则、注释
  • 始终存在(即使空 CSS 文件也会生成一个空的 Root)

常用 API

root.nodes          // Array<Node>,所有子节点
root.source.input   // 源文件信息(文件路径、原始内容等)
root.toString()     // 序列化为 CSS 字符串
root.prepend(node)  // 在头部插入节点
root.append(node)   // 在尾部追加节点
root.walk(fn)       // 深度优先遍历所有节点
root.walkRules(fn)  // 仅遍历规则节点
root.walkDecls(fn)  // 仅遍历声明节点
root.walkAtRules(fn) // 仅遍历 @ 规则节点
root.walkComments(fn) // 仅遍历注释节点

2. Rule 规则节点

特性

  • 代表一条带选择器的 CSS 规则
  • 通常包含若干 Declaration 节点作为子节点
  • 可以嵌套在 Root 或 AtRule 中

常用属性

rule.selector    // String,选择器字符串,如 ".btn:hover, .card"
rule.nodes       // Array<Node>,子节点列表(声明、注释等)
rule.parent      // 父节点(通常是 Root 或 AtRule)
rule.source      // 源位置信息

API 示例

// 遍历所有规则
root.walkRules(rule => {
  console.log(rule.selector)      // ".btn"
  console.log(rule.nodes.length)  // 子节点数量
})

// 给某个规则添加新声明
rule.append({ prop: 'border', value: '1px solid #ccc' })

3. Declaration 声明节点

特性

  • 最基础、最常用的节点,代表 property: value
  • 是 Rule 的子节点(极少直接出现在 Root 中)

常用属性

decl.prop     // String,属性名,如 "color"
decl.value    // String,属性值,如 "#ff0000"
decl.raws     // 原始格式信息(前后空格、分号等)
decl.parent   // 父节点(通常是 Rule)

API 示例

// 遍历所有 color 声明
root.walkDecls('color', decl => {
  console.log(`${decl.prop}: ${decl.value}`)  // "color: #007bff"
})

// 修改声明
decl.value = 'blue'
decl.prop = 'background-color'  // 重命名属性

4. AtRule @ 规则节点

特性

  • 代表所有以 @ 开头的规则
  • 可以是块级规则(如 @media { ... },有子节点)
  • 也可以是语句式规则(如 @import url(...),无子节点)

常用属性

atrule.name      // String,规则名(不含 @),如 "media"、"keyframes"
atrule.params    // String,参数部分,如 "(max-width: 768px)"
atrule.nodes     // Array<Node> | undefined,子节点(块级规则才有)
atrule.parent    // 父节点

API 示例

// 遍历所有 @media 规则
root.walkAtRules('media', atRule => {
  console.log(atrule.params)  // "(max-width: 768px)"
})

// 遍历所有 @keyframes
root.walkAtRules('keyframes', atRule => {
  console.log(`动画名称: ${atrule.params}`)  // "fadeIn"
  // atRule.nodes 包含关键帧规则
})

// @import 等无节点的 @ 规则
root.walkAtRules('import', atRule => {
  console.log(atRule.params)  // "url(./base.css)"
  // atRule.nodes 为 undefined
})

5. Comment 注释节点

特性

  • 代表 /* ... */ 格式的 CSS 注释
  • 可以出现在任何层级(Root、Rule、AtRule 内)
  • 不影响渲染结果,但可用于插件标记或调试

常用属性

comment.text    // String,注释内容(不含 /* */)
comment.parent  // 父节点
comment.source  // 源位置

API 示例

// 查找带 TODO 标记的注释
root.walkComments(comment => {
  if (comment.text.includes('TODO')) {
    console.log(`TODO 标记: ${comment.text}`)
  }
})

// 在某个声明前插入注释
rule.insertBefore(
  decl,
  postcss.comment({ text: '⚠️ 以下为兼容性处理' })
)

6. Container 容器基类

特性

  • 是 Root、Rule、AtRule 的共同父类
  • 定义了所有「拥有子节点」容器的通用遍历和操作方法
  • 不直接实例化,而是通过其子类使用

通用容器方法(适用于 Root / Rule / AtRule):

container.nodes            // 子节点数组
container.first            // 第一个子节点
container.last             // 最后一个子节点
container.index(node)      // 获取节点在容器中的索引
container.append(node)     // 尾部追加节点
container.prepend(node)    // 头部插入节点
container.insertBefore(existing, newNode)  // 在指定节点前插入
container.insertAfter(existing, newNode)   // 在指定节点后插入
container.removeChild(node)                // 移除子节点
container.walk(fn)         // 深度优先遍历所有节点
container.walkRules(fn)    // 仅遍历规则节点
container.walkDecls(fn)    // 仅遍历声明节点
container.walkAtRules(fn)  // 仅遍历 @ 规则节点
container.walkComments(fn) // 仅遍历注释节点

用代码遍历完整 AST

以下示例演示如何递归遍历整个 CSS 文档,并打印节点层级结构:

// postcss-ast-inspector.js — 遍历并打印 AST 结构
const postcss = require('postcss');
const fs = require('fs');

function printNode(node, indent = 0) {
  const prefix = '  '.repeat(indent);

  switch (node.type) {
    case 'root':
      console.log(`${prefix}Root(${node.nodes.length} 个子节点)`);
      break;
    case 'rule':
      console.log(`${prefix}Rule: ${node.selector}`);
      break;
    case 'decl':
      console.log(`${prefix}Decl: ${node.prop}: ${node.value}`);
      return; // 声明无子节点,直接返回
    case 'atrule':
      console.log(`${prefix}AtRule: @${node.name} ${node.params || ''}`);
      break;
    case 'comment':
      console.log(`${prefix}Comment: /* ${node.text} */`);
      return; // 注释无子节点,直接返回
    default:
      console.log(`${prefix}Unknown: ${node.type}`);
      return;
  }

  // 容器类节点,递归遍历子节点
  if (node.nodes) {
    node.nodes.forEach(child => printNode(child, indent + 1));
  }
}

// 使用示例
const css = fs.readFileSync('src/css/main.css', 'utf8');
const root = postcss.parse(css);

console.log('=== CSS AST 结构 ===');
printNode(root);

以上代码对本节示例 CSS 的输出

=== CSS AST 结构 ===
Root(4 个子节点)
  Comment: /* 示例样式表 */
  Rule: :root
    Decl: --primary: #007bff
    Decl: --spacing: 1rem
  Rule: .btn
    Decl: color: var(--primary)
    Decl: padding: var(--spacing)
    Comment: /* 按钮基础样式 */
  AtRule: @media (max-width: 768px)
    Rule: .btn
      Decl: font-size: 14px

7.2 创建一个简单的 PostCSS 插件(修正版)

PostCSS 插件的本质是一个接收 AST 并对其进行转换的函数。PostCSS 提供了标准化的插件注册和配置机制。

插件函数定义

基本结构

// postcss-myplugin.js
const postcss = require('postcss');

/**
 * 插件函数(PostCSS 8+ 推荐写法)
 * @param {Object} opts - 用户传入的配置选项
 * @returns {Object} 包含 postcssPlugin 名称和钩子方法的对象
 */
module.exports = (opts = {}) => {
  return {
    // 必填:插件名称(建议以 postcss- 开头,便于识别)
    postcssPlugin: 'postcss-myplugin',

    // 进入 Root 节点时执行(整个文档处理开始)
    Once(root, { result }) {
      // 插件逻辑
    },

    // 每次遇到声明节点时执行
    Declaration(decl) {
      // 处理单个声明
    },

    // 每次遇到规则节点时执行
    Rule(rule) {
      // 处理单个规则
    },

    // 每次遇到 @ 规则节点时执行
    AtRule(atRule) {
      // 处理单个 @ 规则
    },

    // 所有节点处理完成后执行(整个文档处理结束)
    OnceExit(root, { result }) {
      // 清理或最终处理
    }
  };
};

// 标记:告知 PostCSS 这是一个插件
module.exports.postcss = true;

重要:PostCSS 8 及以后推荐使用上述对象式插件写法(带有 postcssPlugin 和钩子方法)。老式的 postcss.plugin(name, callback) 写法在 8.x 版本中仍可用,但不再是推荐写法。本章主要介绍新版 API

示例一:postcss-uppercase(属性值大写)

最简单的插件:遍历所有声明,将属性值转为大写。

插件代码

// postcss-uppercase.js
module.exports = () => ({
  postcssPlugin: 'postcss-uppercase',

  Declaration(decl) {
    // ⚠️ 简单示例仅用于教学,注意:
    // - 会把 URL 中的路径也转为大写(URL 可能区分大小写)
    // - 会影响 color hex 值(如 #ff0000 → #FF0000,CSS 标准其实不区分大小写,但某些工具依赖小写)
    // 生产级插件应使用更精细的逻辑(如正则匹配后仅处理值的特定部分)
    decl.value = decl.value.toUpperCase();
  }
});

module.exports.postcss = true;

测试用 CSS

/* input.css */
.card {
  color: #ff0000;
  background: url('https://example.com/image.png');
  padding: 10px;
}

使用插件

// build.js
const postcss = require('postcss');
const fs = require('fs');
const uppercase = require('./postcss-uppercase');

const css = fs.readFileSync('input.css', 'utf8');

postcss([uppercase])
  .process(css, { from: 'input.css', to: 'output.css' })
  .then(result => {
    fs.writeFileSync('output.css', result.css);
    console.log('✓ 处理完成');
  });

输出结果

.card {
  color: #FF0000;
  background: URL('HTTPS://EXAMPLE.COM/IMAGE.PNG');
  padding: 10PX;
}

示例二:postcss-prefix(选择器加前缀,支持配置)

稍微复杂的插件:根据配置给选择器添加前缀,支持忽略列表。

插件代码

// postcss-prefix.js
/**
 * 为选择器添加前缀的 PostCSS 插件
 * @param {Object} opts
 * @param {string} [opts.prefix='']  要添加的前缀,如 '.myapp '
 * @param {string[]} [opts.ignore=[]] 忽略的选择器列表(精确匹配)
 * @param {string[]} [opts.ignorePattern=[]] 忽略的选择器模式(正则字符串)
 */
module.exports = (opts = {}) => {
  // 提供默认值,避免用户未传配置时报错
  const prefix = opts.prefix || '';
  const ignore = opts.ignore || [];
  const ignorePattern = (opts.ignorePattern || []).map(
    pattern => new RegExp(pattern)
  );

  return {
    postcssPlugin: 'postcss-prefix',

    Rule(rule) {
      // 1. 跳过精确匹配的忽略项
      if (ignore.includes(rule.selector)) return;

      // 2. 跳过匹配正则模式的忽略项
      const shouldIgnore = ignorePattern.some(
        pattern => pattern.test(rule.selector)
      );
      if (shouldIgnore) return;

      // 3. 处理多选择器(如 h1, h2 { ... })
      //    将每个选择器单独加前缀后重新拼接
      rule.selector = rule.selector
        .split(',')
        .map(s => prefix + s.trim())
        .join(', ');
    }
  };
};

module.exports.postcss = true;

在 postcss.config.js 中使用

// postcss.config.js
module.exports = {
  plugins: [
    require('./postcss-prefix')({
      prefix: '.myapp ',
      ignore: [':root', 'body', 'html'],
      ignorePattern: ['^\\*$', '^@']  // 忽略 * 和以 @ 开头的选择器
    })
  ]
};

测试用 CSS

/* input.css */
:root { --primary: #007bff; }

body { margin: 0; }

.btn { color: var(--primary); }

.card, .panel {
  padding: 16px;
}

输出结果body:root 被忽略):

:root { --primary: #007bff; }

body { margin: 0; }

.myapp .btn { color: var(--primary); }

.myapp .card, .myapp .panel {
  padding: 16px;
}

示例三:在插件中添加警告信息

使用 result.warn() 在构建时输出警告,有助于提示潜在问题。

插件代码

// postcss-deprecated-color.js
/**
 * 检测并警告已废弃的颜色值
 */
module.exports = () => ({
  postcssPlugin: 'postcss-deprecated-color',

  Declaration(decl, { result }) {
    // 检测使用了废弃的颜色关键字
    const deprecatedColors = ['lime', 'fuchsia', 'aqua', 'maroon'];

    if (deprecatedColors.includes(decl.value.trim().toLowerCase())) {
      // 通过 result.warn 添加警告
      // 第二个参数必须传 { node },以便 PostCSS 在警告信息中
      // 标注出问题出现的文件和行号
      result.warn(
        `⚠️ 颜色关键字 '${decl.value}' 不推荐使用,` +
        `建议替换为标准 hex 或 rgb 值`,
        { node: decl }
      );
    }
  }
});

module.exports.postcss = true;

使用插件

// build.js
const postcss = require('postcss');
const fs = require('fs');
const deprecatedColor = require('./postcss-deprecated-color');

const css = fs.readFileSync('input.css', 'utf8');

postcss([deprecatedColor])
  .process(css, { from: 'input.css', to: 'output.css' })
  .then(result => {
    fs.writeFileSync('output.css', result.css);

    // 打印所有警告
    if (result.warnings().length > 0) {
      console.log('\n=== 构建警告 ===');
      result.warnings().forEach(warning => {
        console.log(`- ${warning.text}`);
      });
    }
  });

示例输入与输出警告

/* input.css — 包含废弃颜色 */
.title {
  color: fuchsia;       /* ← 触发警告 */
  background: lime;     /* ← 触发警告 */
  text-decoration: underline;
}

构建输出

=== 构建警告 ===
- ⚠️ 颜色关键字 'fuchsia' 不推荐使用,建议替换为标准 hex 或 rgb 值
- ⚠️ 颜色关键字 'lime' 不推荐使用,建议替换为标准 hex 或 rgb 值

示例四:完整的配置型插件(整合多个功能)

综合示例:一个支持前缀 + 变量替换 + 警告的完整插件。

// postcss-starter-pack.js — 整合型插件示例
module.exports = (opts = {}) => {
  // 1. 读取并规范化配置
  const config = {
    prefix: opts.prefix || '',
    variables: opts.variables || {},
    warnDeprecated: opts.warnDeprecated !== false  // 默认开启
  };

  // 2. 构建正则:用于变量匹配(例如 $primary)
  //    仅在用户传入了变量配置时才启用此功能
  const hasVariables = Object.keys(config.variables).length > 0;
  const variableRegex = hasVariables
    ? new RegExp('\\$(' + Object.keys(config.variables).join('|') + ')', 'g')
    : null;

  // 3. 废弃颜色列表
  const deprecatedColors = ['lime', 'fuchsia', 'aqua', 'maroon', 'navy'];

  return {
    postcssPlugin: 'postcss-starter-pack',

    // 3.1 处理规则选择器(添加前缀)
    Rule(rule) {
      if (!config.prefix) return;

      rule.selector = rule.selector
        .split(',')
        .map(s => config.prefix + s.trim())
        .join(', ');
    },

    // 3.2 处理声明(变量替换 + 废弃检测)
    Declaration(decl, { result }) {
      // 3.2.1 变量替换:$name → 字面量值
      if (hasVariables && variableRegex) {
        const original = decl.value;
        decl.value = decl.value.replace(
          variableRegex,
          (match, varName) => config.variables[varName] || match
        );

        // 如果值被修改过,在注释中记录替换信息(调试用)
        if (original !== decl.value) {
          decl.raws.value = {
            raw: decl.value,
            value: decl.value
          };
        }
      }

      // 3.2.2 废弃颜色检测
      if (config.warnDeprecated) {
        const value = decl.value.trim().toLowerCase();
        if (deprecatedColors.includes(value)) {
          result.warn(
            `颜色 '${decl.value}' 不推荐使用,请使用 ${decl.prop}: #xxxxxx 或 rgb()`,
            { node: decl }
          );
        }
      }
    }
  };
};

module.exports.postcss = true;

使用方式

// postcss.config.js
module.exports = {
  plugins: [
    require('./postcss-starter-pack')({
      prefix: '.app ',
      variables: {
        primary: '#007bff',
        secondary: '#6c757d',
        spacing: '1rem'
      },
      warnDeprecated: true
    })
  ]
};

7.3 插件 API:Rule、Declaration、AtRule 等节点操作

本节系统整理 PostCSS 插件中最常用的节点操作 API,并提供代码示例。掌握这些 API,你就拥有了编写绝大多数自定义插件所需的全部工具。

API 总览表

API 方法适用节点用途推荐场景
root.walkDecls(callback)Root/Container遍历所有 Declaration 节点查找/修改属性值、属性名
root.walkRules(callback)Root/Container遍历所有 Rule 节点修改选择器、操作规则整体
root.walkAtRules(callback)Root/Container遍历所有 AtRule 节点处理 @media、@keyframes 等
decl.prop / decl.valueDeclaration读取/修改声明的属性名和值最常用的数据操作
node.remove()Any从父节点中移除该节点移除废弃或无效的声明/规则
node.replaceWith(newNode)Any用新节点替换当前节点重构声明或规则
parent.insertBefore(node, newNode)Container在指定节点前插入新节点注入兼容性声明
parent.insertAfter(node, newNode)Container在指定节点后插入新节点添加补充属性
postcss.decl({prop, value})创建节点创建新声明节点动态生成 CSS 属性
postcss.rule({selector})创建节点创建新规则节点动态生成 CSS 规则
postcss.atRule({name, params})创建节点创建新 @ 规则节点动态生成 @media 等
node.clone()Any深拷贝节点复用节点模板
parent.append(node)Container在容器末尾追加节点批量添加声明/规则
parent.prepend(node)Container在容器头部插入节点注入前置规则
result.warn(message, {node})处理结果输出构建警告提示用户问题用法
result.error(message, {node})处理结果抛出构建错误发现致命问题时中断

详细 API 说明与代码示例

1. 遍历所有声明:root.walkDecls(callback)

遍历文档中全部声明节点(包括嵌套在 @media、@keyframes 中的声明)。

基本用法

root.walkDecls(decl => {
  console.log(`${decl.prop}: ${decl.value}`);
});

按属性名过滤(只处理 color 属性):

root.walkDecls('color', decl => {
  // 只在 color 声明上执行逻辑
  decl.value = '#000';  // 将所有文字颜色统一为黑色
});

按正则过滤(处理所有 margin-*padding-* 属性):

// walkDecls 第一个参数也支持正则
root.walkDecls(/^(margin|padding)(-top|-right|-bottom|-left)?$/, decl => {
  // 将 px 单位的间距值统一转换为 rem
  const match = decl.value.match(/^(\d+(?:\.\d+)?)px$/);
  if (match) {
    const pxValue = parseFloat(match[1]);
    decl.value = (pxValue / 16).toFixed(3) + 'rem';
  }
});

应用场景:单位转换、颜色统一、前缀自动添加等。

2. 遍历规则:root.walkRules(callback)

遍历所有规则节点。

基本用法

root.walkRules(rule => {
  console.log(rule.selector);  // 如 ".btn:hover"
});

按选择器过滤(只处理 .btn-* 相关选择器):

root.walkRules(/^\.btn-/, rule => {
  // 为所有 .btn-* 类选择器添加 display: inline-block
  rule.append({
    prop: 'display',
    value: 'inline-block'
  });
});

⚠️ 修改选择器的注意事项

  • 确保修改后的选择器语法仍然合法(如缺失空格、多余符号)
  • 不要破坏多选择器的结构(a, b, c),应拆分处理后再拼接
  • 避免产生无意义的选择器(如 body .btn.hover

3. 遍历 @ 规则:root.walkAtRules(callback)

遍历所有以 @ 开头的规则节点。

基本用法

// 遍历所有 @media 规则
root.walkAtRules('media', atRule => {
  console.log(atRule.params);  // 如 "(max-width: 768px)"
});

// 遍历所有 @keyframes 规则
root.walkAtRules('keyframes', atRule => {
  console.log(`动画名称: ${atrule.params}`);
});

// 不传入 name,遍历所有 @ 规则
root.walkAtRules(atRule => {
  console.log(`@${atRule.name} ${atRule.params}`);
});

实用案例:移除打印样式

root.walkAtRules('media', atRule => {
  // 移除所有 @media print { ... } 块
  if (atRule.params.toLowerCase().includes('print')) {
    atRule.remove();
  }
});

实用案例:为 @keyframes 添加浏览器前缀

root.walkAtRules('keyframes', atRule => {
  // 在当前 @keyframes 之前插入带 -webkit- 前缀的版本
  const prefixed = atRule.clone();
  prefixed.name = '-webkit-keyframes';  // 修改 name 字段
  // 将关键帧内部的规则也做 -webkit- 处理(如 transform → -webkit-transform)
  prefixed.walkDecls(/^(transform|animation)/, decl => {
    const webkit = decl.clone();
    webkit.prop = '-webkit-' + decl.prop;
    prefixed.insertBefore(decl, webkit);
  });

  atRule.parent.insertBefore(atRule, prefixed);
});

4. 修改节点值:直接赋值

PostCSS 节点的属性支持直接赋值,无需调用特殊 setter。

// 修改属性名(注意:可能影响 CSS 语义)
decl.prop = 'background-color';

// 修改属性值
decl.value = 'red';
decl.value = '10px 20px';

// 修改选择器
rule.selector = '.new-class';

// 修改 @ 规则名和参数
atrule.name = 'media';
atrule.params = '(min-width: 1024px)';

⚠️ 注意:PostCSS 不会验证赋值的合法性。例如,设置 decl.prop = '不合法的属性名' 不会抛出错误,只是会生成非法 CSS。

5. 删除节点:node.remove()

从父节点中移除当前节点。

删除废弃的声明

root.walkDecls('zoom', decl => {
  // IE 专属属性 zoom,现代浏览器不支持,直接删除
  decl.remove();
});

删除所有 !important 声明(示例,非推荐)

root.walkDecls(decl => {
  if (decl.value.includes('!important')) {
    decl.remove();
  }
});

删除空规则(遍历后移除):

root.walkRules(rule => {
  // 如果规则节点的 nodes 为空,删除该规则
  if (!rule.nodes || rule.nodes.length === 0) {
    rule.remove();
  }
});

6. 替换节点:node.replaceWith(newNode)

用一个或多个新节点替换当前节点。

基本用法

// 用 color: blue 替换 color: red
root.walkDecls('color', decl => {
  if (decl.value === 'red') {
    decl.replaceWith(
      postcss.decl({ prop: 'color', value: 'blue' })
    );
  }
});

用多个节点替换一个节点

// 将一个简写属性展开为多个属性
root.walkDecls('margin', decl => {
  // 假设 margin: 10px 20px → 展开为 margin-top/margin-right/margin-bottom/margin-left
  const values = decl.value.split(/\s+/);

  if (values.length === 2) {
    const [vertical, horizontal] = values;

    // 用 4 个声明替换原来的 1 个简写声明
    decl.replaceWith(
      postcss.decl({ prop: 'margin-top', value: vertical }),
      postcss.decl({ prop: 'margin-right', value: horizontal }),
      postcss.decl({ prop: 'margin-bottom', value: vertical }),
      postcss.decl({ prop: 'margin-left', value: horizontal })
    );
  }
});

7. 插入新节点:parent.insertBefore / insertAfter

在容器中某个节点之前或之后插入新节点。

在声明前插入

// 给所有带有 display: flex 的声明之前加兼容性前缀
root.walkDecls('display', decl => {
  if (decl.value === 'flex') {
    rule.insertBefore(
      decl,
      postcss.decl({ prop: 'display', value: '-webkit-box' })
    );
    rule.insertBefore(
      decl,
      postcss.decl({ prop: 'display', value: '-ms-flexbox' })
    );
  }
});

在声明后插入

// 给所有 color 声明后面加一个注释
root.walkDecls('color', decl => {
  decl.parent.insertAfter(
    decl,
    postcss.comment({ text: '自动生成:颜色声明' })
  );
});

⚠️ 注意insertBeforeinsertAfter 都是在父节点容器上调用,不是在被插入的节点上。

8. 创建新节点:postcss.decl / rule / atRule

使用 PostCSS 的工厂函数手动构建节点对象。

创建 Declaration

// 方式一:对象字面量(内部自动转换)
rule.append({ prop: 'opacity', value: '0.5' });

// 方式二:显式创建
const newDecl = postcss.decl({
  prop: 'opacity',
  value: '0.5'
});
rule.append(newDecl);

创建 Rule

const newRule = postcss.rule({
  selector: '.error-message'
});

// 给新规则添加声明
newRule.append({ prop: 'color', value: '#dc3545' });
newRule.append({ prop: 'padding', value: '8px 12px' });

// 将新规则添加到文档末尾
root.append(newRule);

创建 AtRule(块级):

// 创建一个 @media 查询块
const media = postcss.atRule({
  name: 'media',
  params: '(max-width: 600px)'
});

// 给 @media 添加内部规则
media.append(
  postcss.rule({ selector: '.btn' }).append(
    { prop: 'font-size', value: '14px' },
    { prop: 'padding', value: '6px 10px' }
  )
);

root.append(media);

创建 AtRule(语句式,无节点):

// 创建一个 @import 语句
const importNode = postcss.atRule({
  name: 'import',
  params: 'url(./base.css)'
});

root.prepend(importNode);  // 放在文档最前面

9. 节点克隆:node.clone()

深拷贝一个节点,常用于「基于现有节点做修改」的场景。

root.walkDecls('display', decl => {
  if (decl.value === 'flex') {
    // 克隆当前声明,仅修改 value
    const webkit = decl.clone();
    webkit.value = '-webkit-box';
    decl.parent.insertBefore(decl, webkit);

    const ms = decl.clone();
    ms.value = '-ms-flexbox';
    decl.parent.insertBefore(decl, ms);
  }
});

10. 批量追加:parent.append / prepend

尾部追加多个声明

rule.append(
  { prop: 'color', value: '#fff' },
  { prop: 'background', value: '#007bff' },
  { prop: 'border', value: '1px solid #0056b3' }
);

头部插入一条规则

root.prepend(
  postcss.comment({ text: '自动生成:以下为全局重置' }),
  postcss.rule({ selector: '*' }).append(
    { prop: 'box-sizing', value: 'border-box' }
  )
);

11. 输出警告和错误

警告(不中断构建,只提示信息):

Declaration(decl, { result }) {
  if (decl.prop === 'font-size' && decl.value.endsWith('pt')) {
    result.warn(
      `不推荐使用 pt 单位(${decl.value}),建议使用 px 或 rem`,
      { node: decl }
    );
  }
}

错误(中断构建,抛出异常):

Declaration(decl, { result }) {
  if (decl.prop === '!!!invalid!!!') {
    throw decl.error(
      '检测到非法属性名,无法继续处理',
      { word: '!!!invalid!!!' }
    );
  }
}

在插件中收集警告并集中打印

Once(root, { result }) {
  // 收集所有检测到的问题
  const issues = [];

  root.walkDecls(decl => {
    if (decl.value.includes('red')) {
      issues.push({
        text: `检测到硬编码的 'red' 值(line ${decl.source.start.line})`,
        line: decl.source.start.line
      });
    }
  });

  // 批量输出警告
  issues.forEach(issue => {
    result.warn(issue.text);
  });
}

12. 综合实战:整合多种 API

以下是一个综合使用多种 API 的插件示例:

// postcss-compat-enhancer.js — 兼容性增强插件
/**
 * 功能:
 * 1. 自动检测硬编码颜色值(如 'red'),发出警告
 * 2. 为 display: flex 添加浏览器前缀
 * 3. 为 @keyframes 添加 -webkit- 前缀版本
 * 4. 将 pt 单位替换为 px(1pt ≈ 1.33px)
 * 5. 移除 IE 专属属性(zoom、behavior、-ms-* 中不支持的)
 */
module.exports = () => ({
  postcssPlugin: 'postcss-compat-enhancer',

  Declaration(decl, { result }) {
    // --- 功能 1:检测硬编码颜色关键字 ---
    const colorKeywords = ['red', 'green', 'blue', 'yellow', 'orange', 'purple'];
    if (colorKeywords.includes(decl.value.trim().toLowerCase())) {
      result.warn(
        `检测到硬编码颜色关键字 '${decl.value}',` +
        `建议替换为 CSS 变量或 hex 值(如 #ff0000)`,
        { node: decl }
      );
    }

    // --- 功能 2:display: flex → 添加前缀 ---
    if (decl.prop === 'display' && decl.value === 'flex') {
      // 在当前声明之前插入带前缀的版本
      const webkit = decl.clone();
      webkit.value = '-webkit-box';
      decl.parent.insertBefore(decl, webkit);

      const ms = decl.clone();
      ms.value = '-ms-flexbox';
      decl.parent.insertBefore(decl, ms);
    }

    // --- 功能 4:pt 单位转换为 px ---
    const ptMatch = decl.value.match(/^(\d+(?:\.\d+)?)pt$/);
    if (ptMatch) {
      const ptValue = parseFloat(ptMatch[1]);
      decl.value = (ptValue * 1.333).toFixed(2) + 'px';
    }

    // --- 功能 5:移除 IE 专属属性 ---
    const ieOnly = ['zoom', 'behavior', 'filter'];
    if (ieOnly.includes(decl.prop.toLowerCase())) {
      decl.remove();
    }
  },

  // --- 功能 3:@keyframes → 添加 -webkit-keyframes ---
  AtRule: {
    keyframes(atRule) {
      // 克隆当前 @keyframes 规则
      const webkit = atRule.clone();
      webkit.name = '-webkit-keyframes';  // 修改规则名

      // 在原 @keyframes 之前插入带前缀的版本
      atRule.parent.insertBefore(atRule, webkit);
    }
  }
});

module.exports.postcss = true;

7.4 插件发布与版本管理

当你编写了一个有用的自定义插件后,可以将其发布到 npm 上,与其他开发者共享。PostCSS 社区鼓励这种协作模式。

步骤一:初始化 npm 包

在插件目录下执行:

npm init -y

这会生成一个默认的 package.json。你需要至少包含以下字段:

字段说明示例值
name包名(必须在 npm 上唯一)"postcss-prefix"
version语义化版本号"1.0.0"
description简短描述"为 CSS 选择器自动添加前缀的 PostCSS 插件"
main包的入口文件(相对路径)"index.js"
keywords关键词数组(必须包含 "postcss""postcss-plugin"["postcss", "postcss-plugin", "css", "prefix"]
author作者信息"Your Name <you@example.com>"
license许可证(推荐使用 "MIT""MIT"
peerDependencies对等依赖(声明兼容的 PostCSS 版本范围){ "postcss": "^8.0.0" }

完整的 package.json 示例

{
  "name": "postcss-prefix",
  "version": "1.0.0",
  "description": "为 CSS 选择器自动添加前缀的 PostCSS 插件",
  "main": "index.js",
  "keywords": [
    "postcss",
    "postcss-plugin",
    "css",
    "prefix",
    "selector"
  ],
  "author": "Your Name <you@example.com>",
  "license": "MIT",
  "peerDependencies": {
    "postcss": "^8.0.0"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "postcss": "^8.4.0"
  },
  "scripts": {
    "test": "jest"
  }
}

关于 peerDependencies

  • peerDependencies 表示「本插件需要用户的项目中安装了这个版本范围内的 postcss 才能正常工作」
  • 使用 ^8.0.0 表示兼容 8.x 所有版本
  • 当用户安装你的插件但项目中没有兼容版本的 PostCSS 时,npm 会给出警告
  • 不要写成 dependencies,否则会在每个使用者项目中安装一个独立的 PostCSS 副本

步骤二:编写 README.md 文档

良好的文档是提升插件使用率的关键。README.md 应至少包含以下内容:

README.md 结构模板

# postcss-prefix

> 为 CSS 选择器自动添加前缀的 PostCSS 插件

## ✨ 功能特性

- 为指定选择器自动添加自定义前缀
- 支持精确匹配的忽略列表
- 支持正则表达式模式忽略
- 兼容 PostCSS 8+

## 📦 安装

### 使用 npm

```bash
npm install --save-dev postcss-prefix

使用 yarn

yarn add --dev postcss-prefix

使用 pnpm

pnpm add --save-dev postcss-prefix

🚀 使用方法

基本配置(postcss.config.js)

module.exports = {
  plugins: [
    require('postcss-prefix')({
      prefix: '.myapp ',
      ignore: [':root', 'body', 'html'],
      ignorePattern: ['^\\*$']
    })
  ]
};

与 Webpack 集成

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  require('postcss-prefix')({ prefix: '.app ' })
                ]
              }
            }
          }
        ]
      }
    ]
  }
};

与 Vite 集成

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  css: {
    postcss: {
      plugins: [
        require('postcss-prefix')({ prefix: '.app ' })
      ]
    }
  }
});

⚙️ 配置选项

选项类型默认值说明
prefixstring''要添加的前缀字符串(如 '.myapp '
ignorestring[][]需要忽略的选择器列表(精确匹配)
ignorePatternstring[][]需要忽略的选择器正则模式字符串

📄 API 说明

示例输入

:root { --primary: #007bff; }
body { margin: 0; }
.btn { color: var(--primary); }
.card, .panel { padding: 16px; }

示例输出

:root { --primary: #007bff; }
body { margin: 0; }
.myapp .btn { color: var(--primary); }
.myapp .card, .myapp .panel { padding: 16px; }

🧪 测试

运行单元测试:

npm test

📝 版本历史

  • v1.0.0(2024-01-15):初始版本,支持基本前缀功能
  • v1.1.0(2024-01-20):新增 ignorePattern 支持

📄 许可证

MIT © Your Name


### 步骤三:添加单元测试

推荐使用 **Jest**(或 Mocha)为插件编写测试。以下是一个最小测试模板:

```js
// __tests__/index.test.js
const postcss = require('postcss');
const prefix = require('../index');

// 工具函数:处理 CSS 并返回结果
async function run(css, options) {
  const result = await postcss([prefix(options)]).process(css, {
    from: undefined,  // 测试环境不需要文件路径
    map: false
  });
  return result.css;
}

// 测试套件
describe('postcss-prefix', () => {
  test('为单个选择器添加前缀', async () => {
    const input = '.btn { color: red; }';
    const output = await run(input, { prefix: '.app ' });
    expect(output).toBe('.app .btn { color: red; }');
  });

  test('为多个选择器添加前缀', async () => {
    const input = '.btn, .card { padding: 10px; }';
    const output = await run(input, { prefix: '.app ' });
    expect(output).toBe('.app .btn, .app .card { padding: 10px; }');
  });

  test('忽略特定选择器(ignore 数组)', async () => {
    const input = ':root { --v: 1; }\n.btn { color: red; }';
    const output = await run(input, { prefix: '.app ', ignore: [':root'] });
    expect(output).toContain(':root { --v: 1; }');
    expect(output).toContain('.app .btn { color: red; }');
  });

  test('忽略匹配正则模式的选择器', async () => {
    const input = '* { box-sizing: border-box; }\n.btn { color: red; }';
    const output = await run(input, { prefix: '.app ', ignorePattern: ['^\\*$'] });
    expect(output).toContain('* { box-sizing: border-box; }');
    expect(output).toContain('.app .btn { color: red; }');
  });

  test('无前缀配置时不修改 CSS', async () => {
    const input = '.btn { color: red; }';
    const output = await run(input, {});
    expect(output).toBe('.btn { color: red; }');
  });

  test('生成合理的警告信息', async () => {
    // 测试警告功能(如果插件使用了 result.warn)
    const css = '.btn { color: red; }';
    const result = await postcss([prefix({ prefix: '.app ' })])
      .process(css, { from: undefined, map: false });
    expect(result.warnings().length).toBeGreaterThanOrEqual(0);
  });
});

安装 Jest

npm install --save-dev jest postcss

在 package.json 中配置

{
  "scripts": {
    "test": "jest"
  }
}

运行测试

npm test

步骤四:发布到 npm

4.1 注册 npm 账号

如果尚未注册,访问 npmjs.com 创建账号。

4.2 登录 npm

npm login
# 输入用户名、密码、邮箱
# (如果启用 2FA,还需要输入一次性验证码)

4.3 确认包名可用

在发布前,确保 package.json 中的 name 在 npm 上未被占用。可以通过以下方式检查:

  • 访问 https://www.npmjs.com/package/<your-package-name>(404 表示可用)
  • 或者直接尝试发布,如果包名冲突会报错

4.4 发布包

npm publish

如果发布成功,你会看到类似输出:

npm notice 📦  postcss-prefix@1.0.0
npm notice === Tarball Contents ===
npm notice 1.2kB index.js
npm notice 1.5kB package.json
npm notice 3.8kB README.md
npm notice === Tarball Details ===
npm notice name:          postcss-prefix
npm notice version:       1.0.0
npm notice filename:      postcss-prefix-1.0.0.tgz
npm notice package size:  2.4 kB
npm notice unpacked size: 6.5 kB
npm notice total files:   3
+ postcss-prefix@1.0.0

发布成功后,任何人都可以通过 npm install postcss-prefix --save-dev 安装你的插件。

步骤五:版本更新与管理

语义化版本(SemVer)规则

版本变化使用命令说明场景示例
Major(主版本)npm version major1.x.x → 2.0.0破坏性变更:移除配置项、改变默认行为
Minor(次版本)npm version minor1.0.x → 1.1.0新增功能:添加新的配置选项
Patch(补丁)npm version patch1.0.0 → 1.0.1Bug 修复:修正选择器处理逻辑

发布流程示例(一次 patch 版本升级):

# 1. 提交代码
git add -A
git commit -m "fix: 修正选择器中空格的处理问题"

# 2. 更新版本号(自动更新 package.json + 创建 git tag)
npm version patch
# 输出:v1.0.1

# 3. 推送代码和标签
git push
git push --tags

# 4. 发布新版本
npm publish

发布完整清单

检查项说明状态
package.json 配置完整包含 name、version、main、keywords、peerDependencies必须
✅ README.md 编写完毕包含安装、使用、配置、示例必须
✅ 单元测试通过npm test 全部通过强烈推荐
✅ Git 提交所有变更工作区干净必须
✅ 已登录 npmnpm whoami 能输出用户名必须
✅ 包名唯一在 npm 上搜索确认无同名包必须
✅ 版本号递增不要发布相同版本号两次必须
⚠️ .npmignore(可选)排除测试文件、文档等非必要内容可选

关于 .npmignore

如果你的项目中有测试文件、开发文档等不应包含在发布包中的内容,可以创建 .npmignore 文件:

# .npmignore
node_modules/
__tests__/
.DS_Store
*.log
coverage/

如果没有 .npmignore,npm 会使用 .gitignore 作为参考。


🔑 第七章总结

四个核心要点

  1. 掌握 AST 结构是编写插件的基石 — PostCSS 将 CSS 解析为 Root(根)、Rule(规则)、Declaration(声明)、AtRule(@ 规则)、Comment(注释)等节点组成的树结构。通过 walkDecls / walkRules / walkAtRules 遍历节点,对节点进行修改、插入、删除操作,即可实现任意 CSS 转换。

  2. PostCSS 8+ 插件使用对象式写法 — 插件结构为 (opts) => ({ postcssPlugin: 'name', Once/Rule/Declaration/AtRule/OnceExit }),并在模块末尾标记 module.exports.postcss = true。这种写法比老式的 postcss.plugin() 更清晰。

  3. 节点操作 API 是核心工具集 — 掌握 walkDecls(callback)node.remove()node.replaceWith()parent.insertBefore/insertAfter()postcss.decl/rule/atRule()node.clone()result.warn/error() 这 8 个核心 API,足以覆盖 90% 以上的自定义插件需求。

  4. 发布插件遵循标准 npm 流程package.json 必须包含 keywords: ["postcss", "postcss-plugin"]peerDependencies: { "postcss": "^8.0.0" },编写清晰的 README.md,使用 Jest 编写单元测试,按语义化版本号管理发布。

📌 最小可发布插件模板

// index.js — 完整可发布的插件骨架
module.exports = (opts = {}) => ({
  postcssPlugin: 'postcss-yourplugin',

  Declaration(decl) { /* 处理声明 */ },
  Rule(rule) { /* 处理规则 */ },
  AtRule: {
    media(atRule) { /* 处理 @media */ },
    keyframes(atRule) { /* 处理 @keyframes */ }
  }
});
module.exports.postcss = true;

第 8 章:PostCSS 最佳实践与项目集成

8.1 在 React/Vue 项目中集成 PostCSS

框架集成方式说明注意事项
Create React App支持 postcss.config.jsCRA 内置 PostCSS,支持 autoprefixer 和自定义配置eject 后可完全控制,否则受限。
Next.js支持 postcss.config.js自动加载配置文件,无需额外设置支持 Tailwind CSS 开箱即用。
Vue CLI支持 postcss.config.js通过 vue.config.js 可覆盖 PostCSS 配置默认已集成 autoprefixer。
Vite(React/Vue)原生支持自动识别 postcss.config.js 或内联配置高性能构建,推荐现代项目使用。
配置文件位置postcss.config.js推荐使用独立配置文件便于在不同工具间共享。
使用 CSS Modules结合 postcss-modules启用局部作用域类名需在构建工具中启用 modules 支持。

Vite 项目集成示例

示例 1:Vite 项目中的 postcss.config.js

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer'),
    require('cssnano')({
      preset: ['default', { discardComments: { removeAll: true } }]
    })
  ]
};

示例 2:Vite 内联配置(在 vite.config.js 中)

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  css: {
    postcss: {
      plugins: [require('autoprefixer')]
    }
  }
});

示例 3:Next.js 项目中的 postcss.config.js

// postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: {}
  }
};

CSS Modules 结合 PostCSS 使用示例

示例 4:启用 postcss-modules 的配置

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import'),
    require('postcss-modules')({
      generateScopedName: '[name]__[local]___[hash:base64:5]'
    }),
    require('autoprefixer')
  ]
};

示例 5:在 Vite 中启用 CSS Modules(通过构建工具配置)

// vite.config.js
export default defineConfig({
  css: {
    modules: {
      localsConvention: 'camelCase',
      generateScopedName: '[name]__[local]___[hash:base64:5]'
    },
    postcss: {
      plugins: [require('autoprefixer')]
    }
  }
});

关键提示:CSS Modules 通常由构建工具(Vite/Webpack)的 css-loader 处理,postcss-modules 则用于在纯 PostCSS 流水线中实现同等效果。两者选其一即可,不要同时启用以免产生冲突。


8.2 结合 Tailwind CSS 使用 PostCSS

方法名称语法用途注意事项
安装 Tailwindnpm install -D tailwindcss postcss autoprefixer安装必需依赖Tailwind 本身是 PostCSS 插件。
初始化配置npx tailwindcss init生成 tailwind.config.js-p 同时生成 postcss.config.js
引入 Tailwind@tailwind directives在 CSS 中引入 Tailwind 样式必须包含 base、components、utilities 三个指令。
配置 PostCSSrequire('tailwindcss')postcss.config.js 中启用autoprefixer 可选,Tailwind 已处理大多数前缀。
自定义主题theme in tailwind.config.js扩展或覆盖默认样式所有定制通过配置文件完成。

完整集成流程示例

示例 1:安装依赖

npm install -D tailwindcss postcss autoprefixer

示例 2:初始化配置文件

# 同时生成 tailwind.config.js 和 postcss.config.js
npx tailwindcss init -p

示例 3:在 CSS 中引入 Tailwind 指令

/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* 自定义组件层样式 */
@layer components {
  .btn-primary {
    @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
  }
}

示例 4:postcss.config.js 配置

// postcss.config.js
module.exports = {
  plugins: [
    require('tailwindcss'),
    require('autoprefixer')
  ]
};

示例 5:tailwind.config.js 自定义主题

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{html,js,jsx,ts,tsx,vue}'
  ],
  theme: {
    extend: {
      colors: {
        primary: '#007bff',
        secondary: '#6c757d'
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif']
      }
    }
  },
  plugins: []
};

⚠️ 注意事项

  • Tailwind CSS 3+ 必须在 tailwind.config.js 中正确配置 content 字段,否则将无法扫描到类名,生成空 CSS。
  • @apply 指令需在 @layer components@layer utilities 中使用,避免被 Tailwind 的 purger 误删。
  • 在 PostCSS 插件链中,tailwindcss 应放在 autoprefixer 之前,cssnano 应放在最后。

8.3 维护可维护的 PostCSS 配置

最佳实践说明注意事项
使用 postcss.config.js独立配置文件便于在 Webpack、Vite、CLI 等工具间共享。
按环境分离配置使用 ({ env }) => {} 函数式配置开发环境启用 source map 和调试插件,生产环境启用压缩。
插件顺序合理导入 → 变量 → 计算 → 扩展 → 前缀 → 压缩避免逻辑错误和重复处理。
注释关键配置添加注释说明插件用途提升团队协作可读性。
锁定依赖版本使用 package-lock.json / yarn.lock避免因插件更新导致构建失败。
定期更新插件npm outdated / npm update获取安全补丁和新功能,但需测试兼容性。
文档化配置在 README 中说明 PostCSS 用途新成员可快速理解项目样式处理流程。

按环境分离配置示例

示例 1:函数式 postcss.config.js(根据环境动态切换插件)

// postcss.config.js
module.exports = ({ env }) => ({
  map: env !== 'production', // 开发环境启用 source map
  plugins: {
    'postcss-import': {},
    'postcss-preset-env': { stage: 2 },
    'autoprefixer': {},
    ...(env === 'production' ? { 'cssnano': { preset: 'default' } } : {})
  }
});

示例 2:完整版——含开发调试与生产压缩

// postcss.config.js
module.exports = ({ options, env }) => ({
  map: env === 'development' ? { inline: false } : false,
  plugins: [
    require('postcss-import'),
    require('postcss-nested'),
    require('postcss-simple-vars'),
    require('postcss-preset-env')({ stage: 2 }),
    require('autoprefixer'),
    env === 'production'
      ? require('cssnano')({ preset: ['default', { discardComments: { removeAll: true } }] })
      : require('postcss-reporter')({ clearReportedMessages: true })
  ]
});

示例 3:推荐的插件顺序清单

① postcss-import              ← 合并 @import,必须放第一位
② postcss-nested              ← 展开嵌套语法
③ postcss-simple-vars         ← 替换 $ 变量
④ postcss-preset-env          ← 未来 CSS 语法 polyfill
⑤ postcss-mixins              ← 处理 @mixin(如使用)
⑥ autoprefixer                ← 添加浏览器前缀
⑦ postcss-reporter            ← 开发环境报告警告
⑧ cssnano                     ← 生产环境压缩(放最后)

示例 4:锁定依赖与安全更新流程

# 1. 查看过时的依赖
npm outdated

# 2. 小版本安全更新(推荐)
npm update

# 3. 大版本升级需单独测试
npm install postcss@latest autoprefixer@latest --save-dev

📌 配置文件模板(推荐在 README 中说明)

示例 5:生产级 postcss.config.js 模板

// postcss.config.js — 生产级配置模板
// 插件顺序:导入 → 变量/嵌套 → 预设 → 前缀 → 压缩
module.exports = ({ env }) => ({
  plugins: {
    // 1. 合并 @import 语句
    'postcss-import': {},
    // 2. 支持嵌套语法(Sass 风格嵌套)
    'postcss-nested': {},
    // 3. 未来 CSS 语法兼容(stage 2 以上特性)
    'postcss-preset-env': { stage: 2, autoprefixer: false },
    // 4. 自动添加浏览器前缀
    'autoprefixer': {},
    // 5. 生产环境启用压缩
    ...(env === 'production' ? { 'cssnano': { preset: 'default' } } : {})
  }
});

8.4 常见问题排查与调试技巧

问题现象可能原因解决方法注意事项
插件未生效插件未安装或未正确 require检查 npm 安装和配置文件中的 require 语句使用绝对路径或 node_modules 自动解析。
样式未添加前缀autoprefixer 配置错误或 browserslist 缺失检查 .browserslistrc 文件或 package.json 中的 browserslist 字段示例:> 0.5%, last 2 versions
变量未替换postcss-simple-vars 顺序错误确保 vars 插件在 calc 和 nested 之前遵循”变量 → 计算 → 嵌套”顺序。
构建报错:Unknown wordCSS 语法错误或插件不支持检查源文件语法,确认插件支持该特性使用 postcss-syntax 处理非标准语法。
Source Map 不生效未启用 map 选项CLI 添加 --map,Webpack 配置 devtool,Vite 设置 build.sourcemap确保所有 loader 都支持 source map。
文件未合并postcss-import 未执行检查插件顺序是否在最前,路径是否正确使用相对路径或配置 resolve
性能缓慢插件过多或文件过大减少插件数量,启用缓存,拆分 CSS 文件使用构建分析工具定位瓶颈。

常见问题排查流程示例

示例 1:验证插件是否正确安装与加载

# 检查 postcss 及插件版本
npm ls postcss autoprefixer postcss-import

# 确认配置文件语法正确(Node 直接 eval)
node -e "console.log(require('./postcss.config.js')({ env: 'development' }))"

示例 2:.browserslistrc 配置与验证

# .browserslistrc — 放在项目根目录
> 0.5%
last 2 versions
not dead
not ie 11

示例 3:检查 autoprefixer 是否生效(使用 CLI 测试)

# 测试单个文件是否正确添加前缀
echo ".box { display: flex; }" | npx postcss --use autoprefixer -

示例 4:postcss.config.js 中确保 postcss-simple-vars 顺序正确

// ✅ 正确顺序:变量 → 计算 → 嵌套
module.exports = {
  plugins: [
    require('postcss-import'),
    require('postcss-simple-vars')({ silent: false }), // 变量替换先
    require('postcss-nested'),                          // 再展开嵌套
    require('postcss-calc'),                            // 再计算表达式
    require('autoprefixer')                             // 最后加前缀
  ]
};

示例 5:处理”Unknown word”报错——使用 postcss-syntax

// 当源文件含 Sass/Less/SugarSS 等非标准语法时
const syntax = require('postcss-syntax');

module.exports = {
  plugins: [...],
  syntax: syntax // 自动根据文件扩展名选择 parser
};

示例 6:Vite + PostCSS 中启用 source map

// vite.config.js
export default defineConfig({
  build: {
    sourcemap: true // 开发环境启用
  },
  css: {
    postcss: {
      map: { inline: false } // 生成独立 .map 文件
    }
  }
});

示例 7:postcss-importresolve 配置(解决 @import 路径问题)

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-import')({
      resolve: (id, baseDir, options) => {
        // 支持以 ~ 开头的别名路径
        if (id.startsWith('~')) {
          return path.resolve('src', id.slice(1));
        }
        return id;
      }
    }),
    require('autoprefixer')
  ]
};

示例 8:启用 PostCSS 缓存以提升性能

// 使用 postcss-load-config + 构建工具缓存
// Vite 自带 CSS 缓存,无需额外配置
// Webpack 可通过 cache-loader 或 cache: { type: 'filesystem' } 启用

示例 9:调试——使用 postcss-reporter 打印警告与错误

// postcss.config.js — 开发环境下启用报告
module.exports = ({ env }) => ({
  plugins: [
    require('postcss-preset-env')({ stage: 2 }),
    env === 'development' && require('postcss-reporter')({
      clearReportedMessages: true,
      throwError: false
    })
  ].filter(Boolean)
});

🔑 快速排查清单

  1. 插件未生效?npm ls 确认已安装 → 检查 postcss.config.js 中是否 require → 验证配置语法无错误。
  2. 无前缀? → 确认 .browserslistrc 存在且目标浏览器范围合理 → npx browserslist 查看实际目标。
  3. 变量/嵌套不生效? → 检查插件顺序(postcss-simple-vars 必须在 postcss-nested 之前或之后取决于实现)。
  4. Source Map 缺失? → CLI 使用 --map,配置中设置 map: true,构建工具启用 devtool
  5. 构建速度慢? → 移除不必要的插件 → 启用缓存 → 拆分大 CSS 文件为模块。