PostCSS 自定义插件开发 #

PostCSS 的强大之处在于其插件系统。本文将教你如何开发自己的 PostCSS 插件。

插件基础 #

插件结构 #

PostCSS 8+ 推荐使用新的插件 API:

javascript
// 简单插件
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Once(root) {
      // 处理整个 CSS AST
    }
  }
}
plugin.postcss = true

module.exports = plugin

带选项的插件 #

javascript
const plugin = (options = {}) => {
  return {
    postcssPlugin: 'my-plugin',
    Once(root) {
      // 使用 options
      const prefix = options.prefix || ''
      
      root.walkRules(rule => {
        rule.selector = prefix + rule.selector
      })
    }
  }
}
plugin.postcss = true

module.exports = plugin

使用 postcss.plugin(旧 API) #

javascript
// PostCSS 7 及更早版本
const postcss = require('postcss')

const plugin = postcss.plugin('my-plugin', (options = {}) => {
  return root => {
    // 处理 CSS
  }
})

module.exports = plugin

AST 节点类型 #

节点类型概览 #

text
Root(根节点)
├── AtRule(@ 规则)
│   ├── @media
│   ├── @keyframes
│   ├── @font-face
│   └── ...
├── Rule(规则)
│   └── Declaration(声明)
└── Comment(注释)

Root 节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Once(root) {
      // root 是整个 CSS 文件的根节点
      
      // 遍历所有节点
      root.walk(node => {
        console.log(node.type)
      })
      
      // 获取所有规则
      root.walkRules(rule => {
        console.log(rule.selector)
      })
      
      // 获取所有声明
      root.walkDecls(decl => {
        console.log(decl.prop, decl.value)
      })
      
      // 获取所有 @ 规则
      root.walkAtRules(atRule => {
        console.log(atRule.name, atRule.params)
      })
    }
  }
}
plugin.postcss = true

Rule 节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Rule(rule) {
      // rule.selector - 选择器
      // rule.nodes - 子节点
      
      // 修改选择器
      rule.selector = '.prefix-' + rule.selector
      
      // 遍历规则内的声明
      rule.walkDecls(decl => {
        console.log(decl.prop, decl.value)
      })
    }
  }
}
plugin.postcss = true

Declaration 节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Declaration(decl) {
      // decl.prop - 属性名
      // decl.value - 属性值
      // decl.important - 是否 !important
      
      // 修改值
      if (decl.prop === 'color' && decl.value === 'red') {
        decl.value = 'blue'
      }
      
      // 获取父节点
      const rule = decl.parent
      console.log(rule.selector)
    }
  }
}
plugin.postcss = true

AtRule 节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    AtRule(atRule) {
      // atRule.name - @ 规则名称(不含 @)
      // atRule.params - 参数
      // atRule.nodes - 子节点(如果有)
      
      if (atRule.name === 'media') {
        console.log('Media query:', atRule.params)
      }
    }
  }
}
plugin.postcss = true

Comment 节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Comment(comment) {
      // comment.text - 注释内容
      
      // 删除所有注释
      comment.remove()
    }
  }
}
plugin.postcss = true

节点操作 #

创建节点 #

javascript
const postcss = require('postcss')

const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Once(root) {
      // 创建规则
      const newRule = postcss.rule({
        selector: '.new-class'
      })
      
      // 创建声明
      const newDecl = postcss.decl({
        prop: 'color',
        value: 'blue'
      })
      
      // 创建 @ 规则
      const newAtRule = postcss.atRule({
        name: 'media',
        params: '(min-width: 768px)'
      })
      
      // 创建注释
      const newComment = postcss.comment({
        text: 'Auto-generated'
      })
      
      // 组合节点
      newRule.append(newDecl)
      root.append(newRule)
    }
  }
}
plugin.postcss = true

修改节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Declaration(decl) {
      // 修改属性
      decl.prop = 'background-color'
      
      // 修改值
      decl.value = 'red'
      
      // 添加 !important
      decl.important = true
    }
  }
}
plugin.postcss = true

删除节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Declaration(decl) {
      // 删除特定声明
      if (decl.prop === 'color') {
        decl.remove()
      }
    },
    Rule(rule) {
      // 删除空规则
      if (rule.nodes.length === 0) {
        rule.remove()
      }
    }
  }
}
plugin.postcss = true

替换节点 #

javascript
const postcss = require('postcss')

const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Declaration(decl) {
      if (decl.prop === 'color' && decl.value === 'red') {
        // 替换为多个声明
        decl.replaceWith(
          postcss.decl({ prop: 'color', value: 'blue' }),
          postcss.decl({ prop: 'background', value: 'red' })
        )
      }
    }
  }
}
plugin.postcss = true

克隆节点 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'my-plugin',
    Rule(rule) {
      // 克隆规则
      const cloned = rule.clone()
      
      // 修改克隆的规则
      cloned.selector = '.cloned-' + rule.selector.slice(1)
      
      // 添加到 root
      rule.root().append(cloned)
    }
  }
}
plugin.postcss = true

实用插件示例 #

示例 1:添加选择器前缀 #

javascript
const plugin = (options = {}) => {
  const prefix = options.prefix || ''

  return {
    postcssPlugin: 'postcss-prefix-selector',
    Rule(rule) {
      if (prefix && !rule.selector.startsWith(prefix)) {
        rule.selector = prefix + ' ' + rule.selector
      }
    }
  }
}
plugin.postcss = true

module.exports = plugin
css
/* 输入 */
.button {
  color: blue;
}

/* 输出(prefix: '.my-app') */
.my-app .button {
  color: blue;
}

示例 2:颜色转换 #

javascript
const plugin = (options = {}) => {
  const colors = options.colors || {}

  return {
    postcssPlugin: 'postcss-color-replace',
    Declaration(decl) {
      if (colors[decl.value]) {
        decl.value = colors[decl.value]
      }
    }
  }
}
plugin.postcss = true

module.exports = plugin
javascript
// 使用
require('postcss-color-replace')({
  colors: {
    '$primary': '#007bff',
    '$secondary': '#6c757d'
  }
})

礼例 3:响应式单位转换 #

javascript
const plugin = (options = {}) => {
  const baseSize = options.baseSize || 16
  const precision = options.precision || 4

  return {
    postcssPlugin: 'postcss-px-to-rem',
    Declaration(decl) {
      if (decl.value.includes('px')) {
        decl.value = decl.value.replace(/(\d+)px/g, (match, px) => {
          const rem = (parseInt(px) / baseSize).toFixed(precision)
          return rem + 'rem'
        })
      }
    }
  }
}
plugin.postcss = true

module.exports = plugin

示例 4:添加调试信息 #

javascript
const postcss = require('postcss')

const plugin = (options = {}) => {
  return {
    postcssPlugin: 'postcss-debug',
    Once(root, { result }) {
      const stats = {
        rules: 0,
        declarations: 0,
        selectors: new Set()
      }

      root.walkRules(rule => {
        stats.rules++
        stats.selectors.add(rule.selector)
        rule.walkDecls(() => {
          stats.declarations++
        })
      })

      // 添加注释
      const comment = postcss.comment({
        text: `Stats: ${stats.rules} rules, ${stats.declarations} declarations`
      })
      root.prepend(comment)

      // 添加到 result
      result.stats = stats
    }
  }
}
plugin.postcss = true

module.exports = plugin

示例 5:CSS 变量提取 #

javascript
const plugin = () => {
  const variables = {}

  return {
    postcssPlugin: 'postcss-extract-variables',
    Declaration(decl) {
      // 提取 CSS 变量
      if (decl.prop.startsWith('--')) {
        variables[decl.prop] = decl.value
      }
    },
    OnceExit(root, { result }) {
      result.variables = variables
    }
  }
}
plugin.postcss = true

module.exports = plugin

示例 6:自动 RTL 支持 #

javascript
const plugin = () => {
  const rtlMap = {
    'margin-left': 'margin-right',
    'margin-right': 'margin-left',
    'padding-left': 'padding-right',
    'padding-right': 'padding-left',
    'text-align': {
      'left': 'right',
      'right': 'left'
    },
    'float': {
      'left': 'right',
      'right': 'left'
    }
  }

  return {
    postcssPlugin: 'postcss-auto-rtl',
    Declaration(decl) {
      const rtlProp = rtlMap[decl.prop]
      
      if (rtlProp) {
        if (typeof rtlProp === 'string') {
          // 属性名转换
          const newDecl = decl.clone()
          newDecl.prop = rtlProp
          decl.parent.append(newDecl)
        } else if (rtlProp[decl.value]) {
          // 值转换
          const newDecl = decl.clone()
          newDecl.value = rtlMap[decl.prop][decl.value]
          decl.parent.append(newDecl)
        }
      }
    }
  }
}
plugin.postcss = true

module.exports = plugin

异步操作 #

异步插件 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'postcss-async',
    async Once(root) {
      // 异步操作
      const data = await fetchData()
      
      root.walkDecls(decl => {
        if (decl.value === '$data') {
          decl.value = data
        }
      })
    }
  }
}
plugin.postcss = true

使用 Promise #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'postcss-promise',
    Once(root) {
      return new Promise((resolve) => {
        setTimeout(() => {
          root.walkDecls(decl => {
            decl.value = decl.value.toUpperCase()
          })
          resolve()
        }, 1000)
      })
    }
  }
}
plugin.postcss = true

错误处理 #

抛出错误 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'postcss-validate',
    Declaration(decl) {
      if (decl.prop === 'color' && !isValidColor(decl.value)) {
        throw decl.error(`Invalid color value: ${decl.value}`, {
          plugin: 'postcss-validate',
          word: decl.value
        })
      }
    }
  }
}
plugin.postcss = true

添加警告 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'postcss-warn',
    Declaration(decl, { result }) {
      if (decl.prop === 'color' && decl.value === 'red') {
        decl.warn(result, 'Using red color is not recommended')
      }
    }
  }
}
plugin.postcss = true

访问结果对象 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'postcss-result',
    Once(root, { result }) {
      // result.css - 处理后的 CSS
      // result.opts - 选项
      // result.messages - 消息数组
      
      // 添加消息
      result.messages.push({
        type: 'dependency',
        plugin: 'postcss-result',
        file: 'dependency.css'
      })
    }
  }
}
plugin.postcss = true

插件测试 #

使用 Jest 测试 #

javascript
// plugin.test.js
const postcss = require('postcss')
const plugin = require('./plugin')

test('should add prefix to selectors', async () => {
  const input = '.button { color: blue; }'
  const expected = '.prefix .button { color: blue; }'
  
  const result = await postcss([plugin({ prefix: '.prefix' })])
    .process(input, { from: undefined })
  
  expect(result.css).toBe(expected)
})

test('should warn on deprecated values', async () => {
  const input = '.button { color: red; }'
  
  const result = await postcss([plugin()])
    .process(input, { from: undefined })
  
  const warnings = result.warnings()
  expect(warnings).toHaveLength(1)
  expect(warnings[0].text).toContain('deprecated')
})

测试错误处理 #

javascript
test('should throw on invalid input', async () => {
  const input = '.button { color: invalid; }'
  
  await expect(
    postcss([plugin()]).process(input, { from: undefined })
  ).rejects.toThrow('Invalid color value')
})

发布插件 #

package.json #

json
{
  "name": "postcss-my-plugin",
  "version": "1.0.0",
  "description": "A PostCSS plugin for ...",
  "keywords": [
    "postcss",
    "postcss-plugin",
    "css"
  ],
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "peerDependencies": {
    "postcss": "^8.0.0"
  },
  "devDependencies": {
    "postcss": "^8.0.0",
    "jest": "^29.0.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/user/postcss-my-plugin.git"
  },
  "license": "MIT"
}

README 模板 #

markdown
# postcss-my-plugin

[PostCSS] plugin for ...

## Installation

```bash
npm install postcss-my-plugin --save-dev

Usage #

javascript
// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-my-plugin')({
      // options
    })
  ]
}

Options #

Option Type Default Description
prefix string ‘’ Prefix to add

Example #

css
/* Input */
.button { color: blue; }

/* Output */
.prefix .button { color: blue; }

License #

MIT

text

## 最佳实践

### 1. 使用语义化命名

```javascript
// 好的命名
postcss-prefix-selector
postcss-color-replace
postcss-px-to-rem

// 不好的命名
postcss-plugin1
postcss-helper

2. 提供清晰的选项 #

javascript
const plugin = (options = {}) => {
  // 提供默认值
  const config = {
    prefix: options.prefix || '',
    exclude: options.exclude || [],
    include: options.include || ['**/*.css']
  }
  
  return {
    postcssPlugin: 'postcss-my-plugin',
    // ...
  }
}

3. 添加 Source Map 支持 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'postcss-my-plugin',
    Declaration(decl) {
      // 保留 source 信息
      const newDecl = decl.clone()
      newDecl.value = 'new value'
      decl.replaceWith(newDecl)
    }
  }
}

4. 处理边缘情况 #

javascript
const plugin = () => {
  return {
    postcssPlugin: 'postcss-my-plugin',
    Declaration(decl) {
      // 检查值是否存在
      if (!decl.value) return
      
      // 检查是否已处理
      if (decl.value.includes('processed')) return
      
      // 处理...
    }
  }
}

下一步 #

现在你已经学会了开发 PostCSS 插件,接下来学习 最佳实践 了解更多项目配置和优化技巧!

最后更新:2026-03-28