Skip to main content
预处理器我用了几年,后来原生变量和 @layer 出来,才重新想它还剩什么价值。

Q1: CSS 预处理器的历史?为什么要有它?

1

原始 CSS 的蛮荒时代 (2000 年代初)

在 Web 开发的早期,CSS 只是一堆扁平的、没有变量、没有函数、没有嵌套的纯文本规则。开发者在几百行的小项目中尚能应付,但一旦项目规模膨胀到数万行,维护就变成了一场噩梦。你无法定义一个颜色变量在全局复用,无法把一段常用样式抽象成可调用的函数,也无法通过嵌套来表达父子选择器之间的归属关系。每个页面都要手写完整的路径选择器,#header .nav .item a:hover 这样的选择器堆满了整个样式表,修改一处牵动全身。
2

CSS 预处理器的诞生 (2006-2009)

2006 年,Hampton Catlin 发布了 Sass(Syntactically Awesome Style Sheets),引入了变量、嵌套、Mixin 等编程概念。2009 年,Alexis Sellier 发布了 LESS,语法更接近原生 CSS,学习成本更低。2013 年,PostCSS 登场,它不是一个预处理器,而是一个 CSS 转换平台——通过插件体系实现了从 Autoprefixer 到 CSS Modules 等一切 CSS 后处理能力。三者共同构成了 CSS 工程化的”三驾马车”。
3

繁荣与标准化反噬 (2015-2020)

随着 CSS 预处理器的大规模采用,W3C 和浏览器厂商看到了开发者的强烈需求。CSS Variables(自定义属性)、calc()、@supports、@layer 等原生特性被逐步纳入规范并落地到浏览器中。曾经必须依赖预处理器才能实现的”变量”和”函数计算”,现在原生 CSS 就能做。预处理器的核心卖点被一个个蚕食,它的角色从”必需品”降级为”可选项”。
4

现代格局:PostCSS 胜出 (2020 至今)

在今天,Sass 和 LESS 的使用率在下降,而 PostCSS 凭借其插件化架构成为事实上的 CSS 工程化标准。Tailwind CSS 通过 PostCSS 插件实现、Autoprefixer 通过 PostCSS 插件实现、CSS Modules 也通过 PostCSS 插件实现。预处理器不再是”必装工具”,而是”按需选用的功能模块”。理解这段历史,才能在面试中展现你对 CSS 工程化全貌的把握。
CSS 预处理器解决的核心问题:
CSS 预处理器是”编译期的编程语言”,它解决了原生 CSS 缺乏变量和函数的问题,但现代 CSS 原生特性正在逐步吞噬它的核心功能。

🚫 没有预处理器

原生 CSS 没有变量、没有嵌套、没有 Mixin。在大型项目中,开发者必须手动维护数千行重复的样式规则。改一个品牌色,全局搜索替换 200 个 #ff0000 字面量,极易遗漏和出错。

✅ 引入预处理器

通过 $primary-color: #ff0000 定义变量,@mixin 封装复用逻辑,嵌套语法表达 DOM 层级关系。编译器将高级语法转化为原生 CSS,开发效率提升数倍。改一个变量,全站生效。
回答时先说”为什么”(原生 CSS 缺乏编程能力的痛点),再说”怎么解决”(预处理器的变量/嵌套/函数机制),最后点出”趋势”(原生 CSS 逐步取代预处理器)。想考察的是你对 CSS 工程化演进的理解,而不是背 Sass 语法。

Q2: SASS/LESS/PostCSS 对比

三者的本质差异不在语法,而在架构设计哲学:
PostCSS 的本质是一个 CSS-to-CSS 的转换平台。它把 CSS 解析为 AST(抽象语法树),然后通过插件链依次对 AST 进行转换,最后输出新的 CSS。它不发明新语法,只做转换。这意味着你可以用 PostCSS 实现 Sass 的全部功能(通过 postcss-nested、postcss-simple-vars 等插件),也可以实现 Sass 无法做到的事(如 Autoprefixer 的浏览器前缀自动补全)。

基础梳理

专家视角深度剖析

很多人只能背下上面的表格,但实际更在意的是:为什么 PostCSS 在现代工程中胜出了? 答案在于架构设计的可组合性。Sass 和 LESS 是”一体式语言”——变量、嵌套、Mixin 都被硬编码在编译器中,你无法移除或替换其中任何一部分。而 PostCSS 是”插件平台”——每个功能都是独立插件,你可以自由组合。这意味着:当 CSS 原生支持了嵌套(2023 年浏览器已全面支持 & 嵌套),你只需移除 postcss-nested 插件,零代码改动。但如果你用了 Sass 的嵌套,迁移成本巨大。PostCSS 的插件架构让它能随着 CSS 规范的演进而”瘦身”,而 Sass/LESS 则背负着历史包袱无法卸载。

Q3: SASS 核心功能(变量、嵌套、Mixin、函数)

变量系统

嵌套语法

Sass 允许你无限嵌套,但这会产生极深的选择器路径(如 #app .main .content .card .title .text),导致选择器优先级过高、难以覆盖、性能下降。工业规范:嵌套不超过 3 层。 深层嵌套意味着你的 CSS 结构与 DOM 结构高度耦合,违反了关注点分离原则。

Mixin 与函数

Mixin 就像一份菜谱——你告诉厨师”做一道红烧肉”(@include),他就按步骤执行一系列操作(输出一堆 CSS 规则)。函数则像一台计算器——你输入”24 除以 16”(rem(24px)),它只返回一个结果值(1.5rem),不会产生额外的操作步骤。菜谱(Mixin)能输出成品菜(CSS 规则块),计算器(函数)只能输出一个数字(单个值)。

继承与组合


Q4: CSS 预处理器的局限性(为什么被 CSS 原生特性替代)

预处理器变量 vs 原生 CSS 变量: 预处理器变量在编译期被”焊死”为普通文本,运行时不存在。而原生 CSS 变量是运行时的活体对象,支持 JS 动态读写、继承层叠、transition 动画(配合 @property)。 嵌套 vs 原生 & 嵌套: 2023 年 CSS 原生支持了 & 选择器嵌套(Nesting 规范),且 Chrome 120+、Safari 17.2+、Firefox 117+ 全面支持。Sass 的嵌套不再是”唯一选择”。 计算 vs calc(): 原生 CSS 的 calc() 函数支持混合单位计算(calc(100% - 20px)),Sass 在编译期无法处理不同单位的混合运算。 关键区别: ❌/✅ 代码对比(展示原生替代):
新项目应优先使用原生 CSS 变量 + 原生嵌套 + calc() + color-mix()。只有在需要 Mixin、控制流等”编程能力”时才引入 Sass。PostCSS 作为构建层始终保留(用于 Autoprefixer、CSS Modules 等后处理能力)。渐进式迁移老项目:先替换变量($ → --),再替换嵌套,最后清理 Mixin。

🔍 底层源码:PostCSS 是如何工作的?

PostCSS 的核心是一个 CSS Parser → AST → Plugin Chain → Stringifier 的管线。它使用 postcss-parser 将 CSS 文本解析为一棵抽象语法树(AST),然后将 AST 依次传递给每个插件进行转换,最后通过 stringify 将修改后的 AST 还原为 CSS 文本(详见 PostCSS 源码 lib/parse.js):
这段管线做了 3 件事:解析(CSS 文本转为可操作的树结构)、转换(插件链依次修改树节点)、序列化(将修改后的树还原为 CSS 文本)。每个插件都是一个纯函数,接收 AST 并就地修改它。

十、及格回答 vs 满分回答

问题:“Sass 和 PostCSS 有什么区别?你在项目中是怎么做 CSS 工程化的?”

及格回答(60-70分):
“Sass 是 CSS 预处理器,支持变量、嵌套、Mixin。PostCSS 是一个后处理工具,可以加浏览器前缀。我们项目用 Sass 写样式,PostCSS 做 autoprefixer。”
问题出在:知道工具但不理解架构差异。 满分回答(90-100分):
Sass 和 PostCSS 的本质区别不在语法,而在架构设计哲学。Sass 是”一体式语言”——变量、嵌套、Mixin 被硬编码在编译器中,你无法移除或替换其中任何一部分。而 PostCSS 是”插件平台”——它把 CSS 解析为 AST,然后通过插件链依次对 AST 进行转换。每个功能(嵌套、变量、前缀补全)都是独立插件,你可以自由组合。[1]
这种架构差异决定了它们面对 CSS 规范演进时的命运。当 CSS 原生支持了嵌套(2023 年 Nesting 规范),你只需从 PostCSS 配置中移除 postcss-nested 插件,零代码改动。但如果你用了 Sass 的嵌套语法,迁移成本巨大——因为 Sass 的嵌套是编译器内建的,你无法”关闭”它而不影响其他功能。PostCSS 的插件架构让它能随着 CSS 规范的演进自然”瘦身”。[2]
在我负责的项目中,CSS 工程化策略是:原生 CSS 变量做主题系统(运行时动态切换),Sass 仅用于 Mixin 和控制流(@for 批量生成工具类),PostCSS 做后处理(Autoprefixer + CSS Modules + Tailwind)。这种”原生为主、Sass 为辅、PostCSS 兜底”的分层架构,既保证了运行时性能,又保留了编译期的编程能力。

常见追问

答题要点: 指出 @extend 生成组合选择器(体积小但不灵活),@mixin 生成重复代码(体积大但灵活)。高分回答: “@extend 的原理是将多个选择器合并为一条规则(.a, .b { 共享样式 }),减少 CSS 体积,但它的缺点是:不能传参数、不能包含动态逻辑、在媒体查询中有致命 bug(不能跨 @media 边界 extend)。@mixin 则是在每个 @include 处复制一份完整的样式块,体积更大,但支持传参、条件判断、循环等编程能力。经验法则:不需要传参的静态共享样式用 @extend(如 .success、.warning 共享基础样式);需要传参或包含动态逻辑的用 @mixin(如 @include text-ellipsis(2))。”
答题要点: 区分两种语法的文件格式差异和编译流程。高分回答: “Sass 有两种语法格式:.sass 使用缩进语法(类似 Python,无花括号和分号),.scss 使用花括号语法(与原生 CSS 完全兼容)。编译流程是:源文件 → 词法分析(Tokenization)→ 语法分析(Parsing)→ AST → 代码生成(Code Generation)→ 输出 .css 文件。.scss 是推荐格式,因为它与原生 CSS 语法兼容,你可以直接将一个 .css 文件重命名为 .scss 而不需要修改任何代码。在构建工具中(Webpack 的 sass-loader、Vite 内置支持),编译过程是自动化的,开发者只需关注 .scss 源文件。”
答题要点: 必须解释 Visitor 模式的 AST 操作。高分回答: “PostCSS 的插件本质上是一个函数,接收一个 AST 根节点(Root),然后通过 Visitor 模式遍历和修改树节点。每个插件通过 walkRules、walkDecls、walkAtRules 等方法遍历特定类型的节点,然后就地修改它们。例如编写一个自动添加 !important 的插件:
关键点:PostCSS 的插件是就地修改 AST(mutating),不是返回新树。这意味着插件的执行顺序很重要——先执行的插件会改变后续插件看到的 AST 状态。这也是为什么 postcss.config.js 中插件的数组顺序需要精心安排。”
答题要点: 结合实际场景(批量生成工具类)解释控制流。高分回答: “Sass 的控制流在批量生成 CSS 工具类时非常有用。例如批量生成间距工具类:
编译后会自动生成 48 个间距类和 8 个颜色类。但要注意:这种批量生成会显著增大 CSS 体积。现代方案中,Tailwind CSS 的 JIT 模式通过按需生成解决了这个问题——只生成你实际使用的类,而不是预生成所有组合。”
答题要点: 必须分场景讨论!编译期变量 vs 运行时变量的根本差异。高分回答: “Sass 变量在编译期被替换为普通文本,运行时根本不存在。这意味着你无法通过 JS 动态修改一个 Sass 变量的值来切换主题。变通方案有三种: 1. Class 切换方案:预编译多套主题的 CSS 文件(.theme-dark、.theme-light),通过切换 class 激活不同主题。缺点是 CSS 体积翻倍。 2. CSS 变量混合方案:Sass 变量用于编译期的静态计算(如 rem() 函数),主题相关的变量改用原生 CSS 变量(--primary: #1890ff)。这是最实用的方案。 3. CSS-in-JS 方案:彻底放弃 Sass,用 styled-components 或 Emotion 做运行时主题切换。 工业规范:方案 2 是主流选择——Sass 负责编译期的编程能力(Mixin、函数、控制流),原生 CSS 变量负责运行时的动态主题。”
答题要点: 对比”AST 转换平台”的共同设计哲学。高分回答: “PostCSS 和 Babel 在架构上是同一类东西——基于 AST 的转换平台。Babel 将 JavaScript 源码解析为 AST(通过 @babel/parser),然后通过插件链(如 @babel/plugin-transform-arrow-functions)对 AST 进行转换,最后将修改后的 AST 还原为 JS 代码。PostCSS 对 CSS 做同样的事:CSS 文本 → AST → 插件链转换 → CSS 文本。两者的共同设计哲学是:平台不提供功能,插件提供功能。平台只负责解析和序列化,所有实际的转换逻辑都由插件实现。这种架构的美妙之处在于可组合性——你可以自由组合任意插件,创建出无限种构建管线。”
答题要点: 解释模块化系统的命名空间隔离机制。高分回答: “@import 是 Sass 早期的模块导入方式,它有一个致命缺陷:没有命名空间隔离。当你 @import 'button' 时,button.scss 中的所有变量、Mixin、函数都会被注入到当前文件的全局作用域,极易产生命名冲突。@use 是 Sass 推出的新模块系统(Dart Sass 1.0+),它引入了命名空间隔离:
@use 的优势:命名空间隔离、只加载一次(避免重复编译)、支持 @forward 转发模块。Sass 官方已废弃 @import,新项目应全面使用 @use。迁移时需要注意:@import 的变量是全局可用的,而 @use 的变量必须通过命名空间访问。”
答题要点: 结合 Monorepo 工程化架构解释样式共享方案。高分回答: “在 Monorepo 中共享 Sass 工具集有三种方案: 1. 共享包方案:创建一个 @repo/styles 包,将变量、Mixin、函数放在其中,其他包通过 @use '@repo/styles' 导入。需要在 package.json 中配置 sassOptions.includePaths 指向 node_modules。 2. Symbolic Link 方案:通过 pnpm workspace 的软链接机制,让多个包共享同一份样式源文件。修改即生效,无需发版。 3. CSS 变量方案:将主题变量从 Sass 迁移到原生 CSS 变量(:root 定义),通过 CSS 的继承机制自然共享,不依赖 Sass 的模块系统。 最佳实践:方案 3 + 方案 1 组合。运行时主题变量用原生 CSS 变量(跨技术栈共享),编译期工具函数(如 rem()、text-ellipsis)用 Sass 的 @use 模块系统。”

📚 延伸阅读与引用参考

  • 🔗 Sass 官方文档:Sass 语言的权威参考,包含 @use 模块系统的完整规范。[1]
  • 📖 PostCSS 插件开发指南:PostCSS 核心团队编写的插件开发教程,详解 AST 操作 API。[2]
  • 🟢 CSS Nesting 规范 - W3C:W3C 官方对 CSS 原生嵌套的规范定义,解释了 & 选择器的语法规则。[3]