

Dec 18, 2021
PostCSS 是一个时常被提起的词,但与它的知名度不相匹配的则是相当多的对 PostCSS 认知上的错误。尤其是对于习惯了使用样式预处理器(例如 Sass、Less 或 Stylus)的开发者而言,熟悉并且在一定程度上享受这样的预处理器带来的便利时,对同类型工具和技术也就感到不那么好奇了。
但事实上,这也是对 PostCSS 的一个主要的误解,即把 PostCSS 当作和 Sass 等语言一样的样式预处理器。而当我们得知长久以来的印象被这样一行文字纠正时,很多人可能又会对 PostCSS 燃起兴趣。而我认为,每一个编写 CSS 的人都应当了解 PostCSS,认识它是什么、不是什么,以及在它的能力范围内能做到什么。
PostCSS 不是一门语言,没有定义一种语法规则,也不是预处理器的替代方案。
PostCSS 可以读取你的样式表(很可能包含额外的语法和特殊的内容),处理它并输出标准的、可以被浏览器所接受的 CSS。任何 类似于 CSS 的语法 的文件均可以被 PostCSS 所处理。
我已经拥有了样式预处理器(例如 Sass),所以我其实不需要 PostCSS?
这个想法很合理,在大多数情况下,样式预处理器带来的条件判断、循环、mixin、嵌套选择器和函数等足够有效和强大。但当预处理器无法施展拳脚的场合,PostCSS 却可以帮忙。
一个最流行的例子就是 Autoprefixer,一个 PostCSS 插件。在 npm 上,Autoprefixer 每周有超过 1100 万次的安装,并被包括阿里巴巴、推特和谷歌等在内的企业采用。它解析用户的 CSS,并使用来自于 Can I Use 的数据自动为部分样式规则添加浏览器前缀。
PostCSS 的插件并不总是需要转换和输出 CSS。例如 stylelint 就是一个依托 PostCSS 解析能力开发的样式检查器(当然它也包含纠正错误的能力)。
通过对样式规则的转换,可以将自定义的、浏览器不能识别的语法转换为可以正常工作的样式规则。这允许开发者们定制额外的样式声明和可能的值。例如 Lost Grid 通过lost-column,lost-align和lost-offset等属性实现了栅格布局(虽然现在可以通过flex和grid实现,但在以前人们需要使用float浮动来实现相同的布局,相比之下基于 PostCSS 的这种语法将大大减少工作量并且使页面结构更清晰)。
总而言之,PostCSS 承担了一个框架的角色和职能——允许用户在它之上构建用于调整和改变样式表的工具。
这是一个来自于 PostCSS 文档中的图,展示了整个 PostCSS 的工作流程。

其中,Parser(解析器)具备识别输入的源代码,并且创建一个可以描述源代码结构的对象表达的能力。在很多源代码的编译和解释器中这一步都是必不可少的。通常,我们将经过解析器创建的用于描述源代码结构的对象称为 AST(Abstract Syntax Tree,抽象语法树)。显然,几乎所有编程语言都可以做到轻松操作树型结构的数据,这正是 PostCSS 的插件处理用户样式的第一步。
Tokenizer(或称为 Lexer,“词法分析器”)在语法解析中扮演了十分重要的角色。它将输入的字符串转换为由标记构成的列表。
例如,考虑下面的 CSS 作为输入
.className {
color: #fff;
}
PostCSS 得到的标记列表将是这样的:
;[
['word', '.className', 1, 1, 1, 10][('space', ' ')][('{', '{', 1, 12)][('space', ' ')][
('word', 'color', 1, 14, 1, 18)
][(':', ':', 1, 19)][('space', ' ')][('word', '#FFF', 1, 21, 1, 23)][(';', ';', 1, 24)][
('space', ' ')
][('}', '}', 1, 26)],
]
我们可以看到对于每一个标记或单词,标记列表中均有一个子数组对其进行了描述,包括标记的类型(word、space等)、标记的内容以及标记的位置(单个符号如{,标记位置1, 12标识其位置是输入代码中第 1 行第 12 列;而对于多个符号如#FFF,标记位置1, 21, 1, 23表示其位置是第 1 行第 21 列开始到第 1 行第 23 列结束)。
标记化的实现方式和承载 token 的数据结构可以有多种选择,PostCSS 选择了看起来很“脏”的列表式。但事实上,标记化的过程将占据整个语法分析中约 90% 的时间,因此速度和性能至关重要,任何更复杂的高级构造(例如类)都会显著拖慢速度。
Parser(解释器)是 PostCSS 中对 CSS 进行语法分析的主要结构,它消费 Tokenizer 输出的 token,并构建 AST 供 PostCSS 的插件在随后进行处理。
Processor(处理器)在 PostCSS 中负责初始化插件和执行语法转换。它的职能和做所暴露的 API 有限。
由图可知当 PostCSS 完成了语法转换后,经过插件处理得到的新的 AST 需要被转换为字符串并输出为新的 CSS 文件。Stringifier 负责遍历 AST 树,并输出字符串以实现这一最后的步骤。
毫无疑问,单独安装的一份 PostCSS 并不能帮助到开发者——几乎所有的情况下,PostCSS 插件才是开发者真正需要接触的东西,也是PostCSS 的灵魂。
社区内已经有非常多的插件,涵盖了几乎各个方向和使用场景。不过为了真正了解 PostCSS 以及它如何在众多的项目中发挥作用,不妨尝试编写一个允许用户在样式表中使用mixin的插件。
Mixin 是指若干条可复用的样式,通过@include语法可以快速插入到样式声明中。例如,对于频繁使用的几条样式,定义一个名为flex-center的 mixin:
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/* 使用时 */
.selector {
@include flex-center;
color: #212121;
/* ...... */
}
/* 输出 */
.selector {
display: flex;
align-items: center;
justify-content: center;
color: #212121;
}
在此例中,构建 PostCSS 插件的目的是识别输入的 CSS 文件中所有的@mixin和@include,并进行相应的规则替换。因此,我们只需要遍历所有的AtRule声明即可达到目的。
简单的 PostCSS 插件代码实现如下:
const mixins = new Map()
return {
// 对于所有 @~:
AtRule: {
// 对于所有 @mixin:
mixin: (node) => {
mixins.set(node.params, node.nodes)
node.remove()
},
// 对于所有 @include
include: (node) => {
const name = node.params
if (mixins.get(name)) {
node.replaceWith(mixins.get(name))
}
},
},
}
.selector {
@include test;
}
@mixin test {
color: blue;
}
那么这条 mixin 无法被正确的处理。此外,一个真正好用的 mixin 插件还应当包括错误捕获并允许接收参数等。作为参考,可以查看PostCSS 官方 Repo 的 mixin 插件。PostCSS 的插件开发十分简单,大众对于处理 CSS 的需求又是五花八门的,因此 PostCSS 插件种类繁多,包括既可以被正式用于生产项目的例子(前面已然列举了不少),也包括单纯为了好玩而被创造的(例如PostCSS Australian Stylesheets)。
对于需要从头开发一个插件的用户来说,有一些需要遵守的准则。
postcss添加到插件的peerDependencies中。fs.readFile代替fs.readFileSync。color,那么使用Declaration: { color: /**/ }要比调用walkDeclaration快得多。node.error报告错误,使用result.warn输出警告。