PostCSS Architecture
A Note to Our Readers
While maintaining fidelity to the source material, this translation incorporates explanatory content and localized expressions, informed by the translator's domain expertise. These thoughtful additions aim to enhance readers' comprehension of the original text's core messages. For any inquiries about the content, we welcome you to engage in the discussion section or consult the source text for reference.
这篇文章主要面向想要为 postcss
核心贡献代码或深入理解这个工具的开发者。
Overview
在深入 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
生态系统构建的。
WorkFlow
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
等工具中广泛采用的有效策略。
Core Structures
接下来,文章提到将详细介绍在 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
字符串。
Summary
通过这些核心组件的协同工作,postcss
能够灵活而高效地处理各种 css
转换需求,同时保持良好的可扩展性和可维护性。每个组件都专注于自己的职责,又能够无缝配合,这正是 postcss
设计的精妙之处。
Tokenizer
将css
字符串转换为标记(tokens
)。Parser
使用这些标记构建css ast
。Processor
应用插件对css ast
进行转换。Stringifier
将修改后的css ast
转换回css
字符串。