插件机制与 API:visitor、遍历、节点操作
基于 PostCSS 8.5.16 · 核于 2026-07
速查
- 插件本质:一个函数返回对象,对象含
postcssPlugin(名字)+ 访问器方法;函数上标记plugin.postcss = true(PostCSS 8 约定)。 - 访问器(visitor):以节点类型命名的方法,遍历到该类型节点即回调——
Once/Root/Rule/Declaration/AtRule/Comment。 - 进入 vs 退出:进入型(
Rule等)在处理子节点之前触发;退出型(RuleExit/DeclarationExit/AtRuleExit/RootExit/OnceExit)在子节点都处理完之后触发。 OncevsRoot:Once每轮遍历只触发一次(放一次性逻辑);Root可能因 re-visit 多次触发。- walk 系列(旧式/主动查找):
walkRules()/walkDecls()/walkAtRules()/walkComments()/walk(),递归遍历子孙。 - 节点操作:
decl.value = …(改)、replaceWith()(替换)、clone()/cloneBefore()(复制)、remove()(删)、容器append()/prepend()/insertBefore()(增)。 - re-visit:PostCSS 会重新访问你改过/新增的节点(让其他插件也能处理)→ 无条件改动可能死循环。
- 防死循环:先判「是否已达目标状态」,或用
WeakSet/ 节点上挂Symbol标记做幂等判重。 - source:新建/替换节点时复制原节点
source,保 source map。 - visitor 相比旧式优势:框架统一调度一趟遍历分发给各插件,减少重复遍历、组合性更好。
一、插件的标准结构(PostCSS 8)
一个现代 PostCSS 插件就是一个函数,返回带 postcssPlugin 名和访问器的对象,并在函数上打 postcss 标记:
// 一个把所有 color 值改成 red 的最小插件
const plugin = (opts = {}) => {
return {
postcssPlugin: 'to-red', // 插件名(必填)
Declaration(decl) { // 访问器:遇到每个声明节点回调
if (decl.prop === 'color') {
decl.value = 'red';
}
},
};
};
plugin.postcss = true; // 标记这是 PostCSS 插件(必填)
export default plugin;postcssPlugin:插件名字,用于报错与调试。- 访问器方法(这里是
Declaration):PostCSS 遍历 AST,遇到对应类型节点就调用。 plugin.postcss = true:告诉 PostCSS「这是个插件」,CommonJS 写法是module.exports.postcss = true。
二、访问器(visitor)体系
访问器是 PostCSS 8 的核心机制:你按节点类型命名方法,框架在遍历时把对应节点分发给你。
进入型 vs 退出型
| 进入型(子节点处理前触发) | 退出型(子节点处理后触发) |
|---|---|
Once(root) — 每轮开始一次 | OnceExit(root) — 每轮结束一次 |
Root(root) | RootExit(root) |
AtRule(atRule) | AtRuleExit(atRule) |
Rule(rule) | RuleExit(rule) |
Declaration(decl) | DeclarationExit(decl) |
Comment(comment) | —— |
- 进入型是前序遍历:适合在深入子树前就地改当前节点。
- 退出型是后序遍历:适合「先让子孙都处理完,再基于结果处理父节点」的逻辑。
Once 与 Root 的区别(易错点)
{
postcssPlugin: 'demo',
Once(root) {
// 每处理一个文件只跑一次 —— 放一次性初始化、或旧式 root.walkRules() 全量遍历
},
Root(root) {
// 遍历中每遇到 Root 触发;因 re-visit,树被改动后可能被【多次】触发
},
}需要保证只跑一遍的逻辑,放 Once 而不是 Root。
精确到值/属性名的访问器
访问器还支持用对象形式,按 prop / atRule name 精确过滤,减少无谓回调:
{
postcssPlugin: 'demo',
// 只在属性名是 color 时回调
Declaration: {
color(decl) { decl.value = 'red'; },
},
// 只在 @media 时回调
AtRule: {
media(atRule) { /* … */ },
},
}三、walk 系列:主动遍历与查找
除了被动等访问器回调,容器节点(Root/Rule/AtRule)还提供 walk 系列,用于主动递归查找节点(旧式插件常在 Once 里这么写):
Once(root) {
root.walkRules(rule => { /* 每条规则 */ });
root.walkDecls('color', decl => { /* 属性名为 color 的声明 */ });
root.walkAtRules('media', atRule => { /* 每个 @media */ });
root.walkComments(c => { /* 每条注释 */ });
root.walk(node => { /* 所有节点 */ });
}walkXxx递归深入所有子孙;each()只遍历直接子节点(不递归)。walkDecls/walkAtRules可传第一个参数做名字过滤(字符串或正则)。- 在遍历中删除/新增节点是安全的,PostCSS 会正确处理索引。
四、节点的增删改
写转换插件,本质就是对 AST 做 DOM 风格的增删改:
// 改:直接赋值
decl.value = 'red';
decl.prop = 'background-color';
// 替换:把当前节点换成新节点(记得复制 source)
const clean = decl.clone({ prop: 'color', value: 'red' });
clean.source = decl.source;
decl.replaceWith(clean);
// 复制:深拷贝 / 在前后插入副本
const copy = rule.clone();
decl.cloneBefore({ prop: '-webkit-color', value: decl.value });
// 删除:从父节点移除自己
decl.remove();
// 增(容器方法):追加 / 前插 / 指定位置插入
rule.append({ prop: 'display', value: 'flex' });
rule.prepend({ prop: 'margin', value: '0' });
root.insertBefore(rule, newRule);clone(overrides)深拷贝并可就地覆盖字段,是「基于旧节点造新节点」的常用手法(如 Autoprefixer 造带前缀的副本)。append/prepend接受节点对象或{ prop, value }简写,PostCSS 会帮你建Declaration。
五、re-visit 与防死循环(关键坑)
PostCSS 有一条重要语义:你在访问器里改过或新增的节点,会被重新访问(re-visit),以便其他插件(和你自己)也能处理到这些新内容。这很强大,但埋着一个经典陷阱:
// ❌ 反例:无条件加前缀 → 改动触发 re-visit → 又给它加一遍 → 死循环
Declaration(decl) {
decl.cloneBefore({ prop: '-webkit-' + decl.prop });
}规避的核心思想是幂等——保证同一节点不会被重复无意义地再处理。三种常见判重手段:
// 手段 1:先判目标状态是否已达成
Declaration(decl) {
if (decl.prop.startsWith('-webkit-')) return; // 已是前缀,跳过
decl.cloneBefore({ prop: '-webkit-' + decl.prop });
}
// 手段 2:WeakSet 记录处理过的节点
const seen = new WeakSet();
Declaration(decl) {
if (seen.has(decl)) return;
seen.add(decl);
/* … */
}
// 手段 3:在节点上挂 Symbol 标记
const DONE = Symbol('done');
Declaration(decl) {
if (decl[DONE]) return;
decl[DONE] = true;
/* … */
}死循环不会被框架自动救
PostCSS 不替你判断「业务上算不算已处理」,re-visit 是它的既定行为。判重是插件作者的责任。
六、为什么 visitor 优于旧式 walk
PostCSS 8 引入 visitor 前,每个插件都在自己的 Once 里对整棵树 walk 一遍——N 个插件 = N 趟全量遍历。visitor 让 PostCSS:
- 统一调度一趟遍历:把各插件注册的
Declaration/Rule等汇总,在同一趟遍历中分发给所有插件,减少重复遍历、通常更快。 - 改动正确 re-visit:一个插件的产出能被后续插件看到,插件间组合性更好。
新写插件优先用 visitor;只有需要「一次性全量扫描」或复杂主动查找时,才在 Once 里用 walk。
会写插件、懂遍历后,下一步进入 主流插件生态:Autoprefixer、postcss-preset-env(stage)、postcss-nesting/nested、cssnano、postcss-import 逐个拆解。