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