PostCSS 架构
致读者
本译文在忠于原文的基础上,融入了译者基于自身领域知识所添加的解释性内容和本地化表达。这些精心添加的内容旨在增进读者对原文核心信息的理解。如对内容有任何疑问,欢迎您参与评论区讨论,或查阅原文以作参考。
这篇文章主要面向想要为 postcss
核心贡献代码或深入理解这个工具的开发者。
概述
在深入 postcss
的开发之前,我们需要先明确 postcss
的定位:
不是像
sass
或less
那样的style
预处理器postcss
自身不自定义语法和语义规则且实际上并非是一种独立的编程语言。postcss
专注于处理标准的css
,并且可以很容易地与sass
、less
等工具集成。这意味着任何有效的css
代码都可以被postcss
处理。是
css
语法转译工具它允许开发者定义类似于自定义
css
的语法,这些语法可被插件理解和转换。postcss
并不局限于css
规范本身,而是专注于css
的语法定义方式。通过这种方式,开发者可以定义诸如at-rule
之类的自定义语法结构,在以postcss
作为上游转译工具的构建的工具中会非常有帮助。postcss
扮演着构建出色的用于css
操作工具框架角色。在
css
生态系统中占据着重要地位大量优秀的工具(如
autoprefixer
(自动添加浏览器前缀)、stylelint
(css
代码检查工具)、cssnano
(css
代码压缩工具))都是基于postcss
生态系统构建的。
工作流程
postcss
的工作流程设计得相当直观,但有些核心部分需要特别说明:
解析器(Parser
)是其中的关键组件。它能够理解 css
语法并创建相应的对象表示。解析器的实现主要有两种方式:
单文件字符串到
ast
转换这是一种较为简单的方法,比如 Rework 分析器 就采用这种方式。但随着代码库增大,这种方式会导致代码难以维护,且性能较差。
词法分析与解析分离(源字符串 → 词法标记(
tokens
) →ast
),将整个过程分为 词法分析和语法解析两个步骤这种方法在
postcss
中使用,也是目前最流行的做法。许多解析器, 如Babel
的 [@babel/parser]((https://github.com/babel/babel/tree/master/packages/babel-parser) 和CSSTree
都采用这种方式。转译的工作流程分为两步骤的的主要原因是:
性能优化:
字符串转换为词法标记(
tokenization
)是一个耗时操作,需要逐字符处理源代码。将这一步骤独立出来,确保只需执行一次。抽象复杂性:
词法标记到
ast
的转换在逻辑上更复杂,但通过分离,我们可以实现一个高性能的词法分析器(代码可能较难理读)和一个易于理解的解析器(虽然相对较慢)。
通过这种拆分,我们可以在性能和代码可读性之间取得平衡。词法分析保证速度,语法解析保证可读性。
总的来说,这种分离策略既提升了整体性能,又提高了代码的可维护性,是一种在
postcss
等工具中广泛采用的有效策略。
核心结构
接下来,文章提到将详细介绍在 postcss
工作流程中起主要作用的几个核心结构:
Tokenizer
Tokenizer
是 postcss
的词法分析器,实现目录可见 lib/tokenize.js
。主要负责将 css
字符串转换为标记(tokens
)列表。
- 也称为词法分析器(Lexer)
- 接收
css
字符串,返回标记(tokens
)列表 - 每个标记描述语法的一部分,如
at-rule
,comment
或word
- 标记可包含位置信息,便于生成更详细的错误信息
- 标记以列表形式表示,包含类型、内容、开始位置、结束位置等信息
postcss
的标记生成器优化了性能,代码可能看起来较复杂postcss
的Tokenizer
使用一种流/链接api
,在其中向Parser
公开nextToken()
方法。 通过这种方式,我们为Parser
提供了清晰的接口,并通过仅存储少量标记而不是整个标记列表来减少内存使用量。
例子:
.className {
color: #fff;
}
对应的PostCSS解析后的tokens如下
[
["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]
]
正如上述示例所见,一个 token
表示一个列表并且 space token
没有位置信息。
让我们更仔细地看一下像 word
这样的单个 token
。就像所说的那样,每个标记都表示为一个列表,并遵循这种模式。
const token = [
// represents token type
'word',
// represents matched word
'.className',
// This two numbers represent start position of token.
// It is optional value as we saw in the example above,
// tokens like `space` don't have such information.
// Here the first number is line number and the second one is corresponding column.
1,
1,
// Next two numbers also optional and represent end position for multichar tokens like this one. Numbers follow same rule as was described above
1,
10
];
特别值得注意的是,postcss
的词法分析器采用了流式 api
设计。这意味着它不会一次性生成所有标记,而是通过 nextToken()
方法按需提供标记给解析器。这种设计有两个重要优势:
- 降低内存使用:因为不需要同时在内存中保存所有标记,尤其是在处理大型 CSS 文件时。
- 提供清晰的接口:让解析器能够更优雅地处理标记流,解析器可以灵活地控制标记的生成,例如在需要时回滚到之前的标记。
Parser
Parser
是 postcss
的语法分析器,实现目录可见 lib/parse.js
、lib/parser.js
。它的主要任务是将 词法标记 转换成 抽象语法树(ast
)。这个过程可以类比为将散落的拼零件(词法标记)组装成完整的工具(抽象语法树(ast
))。
- 不直接处理源代码字符串,而是处理 词法分析器(
Tokenizer
) 提供的 标记(tokens
)。 - 使用词法分析器提供的
nextToken
和back
方法来获取和回溯标记。 - 根据获取的 标记(
tokens
) 构建ast
节点(ast node
)。
Processor
Processor
是 postcss
的调度中心,实现目录可见 lib/processor.js
。它的主要职责包括:
- 初始化插件。
- 协调执行语法转换。
- 提供公共
api
接口。
虽然处理器的结构相对简单,但它是连接各个组件的关键纽带。
Stringifier
Stringifier
是 postcss
的字符串化工具,实现目录可见 lib/stringify.js
、lib/stringifier.js
。它的主要职责是将经过修改的 css ast
转换回 css
字符串。这个过程就像是将抽象的树结构(cssom
)重新"具象化"为可以直接使用的 css
代码。
字符串生成器的工作方式是:
- 从提供的起始节点开始遍历
css ast
。 - 在遍历过程中根据不同类型的节点调用相应的生成方法。
- 最终拼接出完整的
css
字符串。
总结
通过这些核心组件的协同工作,postcss
能够灵活而高效地处理各种 css
转换需求,同时保持良好的可扩展性和可维护性。每个组件都专注于自己的职责,又能够无缝配合,这正是 postcss
设计的精妙之处。
Tokenizer
将css
字符串转换为标记(tokens
)。Parser
使用这些标记构建css ast
。Processor
应用插件对css ast
进行转换。Stringifier
将修改后的css ast
转换回css
字符串。