postcss
postcss
workflow
PostCSS parses CSS to the tree of nodes (we call it AST). This tree may content:
- Root: node of the top of the tree, which represent CSS file.
- AtRule: statements begin with @ like @charset "UTF-8" or @media (screen) {}.
- Rule: selector with declaration inside. For instance input, button {}.
- Declaration: key-value pair like color: black;
- Comment: stand-alone comment. Comments inside selectors, at-rule parameters and values are stored in node’s raws property.
Core Structures
-
Tokenizer
lib/tokenize.js
Tokenizer (aka Lexer) plays important role in syntax analysis.
It accepts CSS string and returns a list of tokens.
Token is a simple structure that describes some part of syntax like
at-rule
,comment
orword
. It can also contain positional information for more descriptive errors.For example, if we consider following CSS
.className { color: #FFF; }
corresponding tokens from PostCSS will be
[ ["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] ]
As you can see from the example above a single token represented as a list and also
space
token doesn't have positional information.Let's look more closely on single token like
word
. As it was said each token represented as a list and follow such pattern.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 ]
There are many patterns how tokenization could be done, PostCSS motto is performance and simplicity. Tokenization is a complex computing operation and takes a large amount of syntax analysis time ( ~90% ), that why PostCSS' Tokenizer looks dirty but it was optimized for speed. Any high-level constructs like classes could dramatically slow down tokenizer.
PostCSS' Tokenizer uses some sort of streaming/chaining API where you expose
nextToken()
method to Parser. In this manner, we provide a clean interface for Parser and reduce memory usage by storing only a few tokens and not the whole list of tokens. -
Parser
lib/parse.js
,lib/parser.js
Parser is the main structure responsible for syntax analysis of incoming CSS. Parser produces a structure called Abstract Syntax Tree (AST) that could then be transformed by plugins later on.
Parser works in common with Tokenizer and operates over tokens, not source string, as it would be a very inefficient operation.
It uses mostly
nextToken
andback
methods provided by Tokenizer for obtaining single or multiple tokens and then construct part of AST calledNode
.There are multiple Node types that PostCSS could produce but all of them inherit from base Node class.
-
Processor
lib/processor.js
Processor is a very plain structure that initializes plugins and runs syntax transformations
It exposes only a few public API methods. Description of them could be found on API
-
Stringifier
lib/stringify.js
,lib/stringifier.js
Stringifier is a base class that translates modified AST to pure CSS string. Stringifier traverses AST starting from provided Node and generates a raw string representation of it calling corresponding methods.
The most essential method is
Stringifier.stringify
that accepts initial Node and semicolon indicator.
You can learn more by checking stringifier.js
how to use
new plugin
module.exports = (opts = { }) => {
// Work with options here
return {
postcssPlugin: 'PLUGIN_NAME',
/*
Root (root, postcss) {
// Transform CSS AST here
}
*/
/*
Declaration (decl, postcss) {
// The faster way to find Declaration node
}
*/
/*
Declaration: {
color: (decl, postcss) {
// The fastest way find Declaration node if you know property name
}
}
*/
}
}
module.exports.postcss = true
use plugin
await postcss([plugin]).process('a { color: black }', { from })
源码阅读
function postcss (...plugins) {
if (plugins.length === 1 && Array.isArray(plugins[0])) {
plugins = plugins[0]
}
return new Processor(plugins, postcss)
}
class Processor {
constructor (plugins = []) {
this.version = '8.1.10'
this.plugins = this.normalize(plugins)
}
normalize (plugins) {
let normalized = []
......
// 添加插件
normalized.push(i)
......
return normalized
}
.....
process (css, opts = {}) {
......
return new LazyResult(this, css, opts)
}
}
class LazyResult {
constructor (processor, css, opts) {
this.stringified = false
this.processed = false
let root
......
// parser css
root = parser(css, opts)
......
this.result = new Result(processor, root, opts)
this.helpers = { ...postcss, result: this.result, postcss }
// 获取plugin代码
this.plugins = this.processor.plugins.map(plugin => {
if (typeof plugin === 'object' && plugin.prepare) {
return { ...plugin, ...plugin.prepare(this.result) }
} else {
return plugin
}
})
}
then (onFulfilled, onRejected) {
....
// 执行
return this.async().then(onFulfilled, onRejected)
}
async () {
if (this.error) return Promise.reject(this.error)
if (this.processed) return Promise.resolve(this.result)
if (!this.processing) {
this.processing = this.runAsync()
}
return this.processing
}
stringify () {
if (this.error) throw this.error
if (this.stringified) return this.result
this.stringified = true
this.sync()
let opts = this.result.opts
let str = stringify
if (opts.syntax) str = opts.syntax.stringify
if (opts.stringifier) str = opts.stringifier
if (str.stringify) str = str.stringify
let map = new MapGenerator(str, this.result.root, this.result.opts)
let data = map.generate()
this.result.css = data[0]
this.result.map = data[1]
return this.result
}
async runAsync () {
this.plugin = 0
for (let i = 0; i < this.plugins.length; i++) {
let plugin = this.plugins[i]
// 执行插件代码
let promise = this.runOnRoot(plugin)
if (isPromise(promise)) {
try {
await promise
} catch (error) {
throw this.handleError(error)
}
}
}
this.prepareVisitors()
this.processed = true
// 返回结果
return this.stringify()
}
......
}
function parse (css, opts) {
let input = new Input(css, opts)
let parser = new Parser(input)
......
//css 解析
parser.parse()
//第一步解析为token
//第二步根据token来解析css
......
return parser.root
}
autoprefixer
PostCSS plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use.
源代码
module.exports = (...reqs) => {
......
// 返回 postcss 插件类型
return {
postcssPlugin: 'autoprefixer',
prepare (result) {
let prefixes = loadPrefixes({
from: result.opts.from,
env: options.env
})
return {
Once (root) {
timeCapsule(result, prefixes)
if (options.remove !== false) {
prefixes.processor.remove(root, result)
}
if (options.add !== false) {
prefixes.processor.add(root, result)
}
}
}
},
info (opts) {
opts = opts || {}
opts.from = opts.from || process.cwd()
return info(loadPrefixes(opts))
},
options,
browsers: reqs
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构