笔者近期在将前端架构 webpack 升级到 5 时,一些配套模块也需要进行升级,其中包括了 css 处理模块 PostCSS。旧版本使用的是 PostCSS 7,在升级至 PostCSS 8 的过程中,笔者发现部分插件前置依赖还是停留在 7 版本,且年久失修,在 PostCSS 8 中出现各种各样的问题,无奈只能研究源码,将目前部分旧版本插件升级至新版本。这里,笔者将升级插件的过程进行简化和提炼,让读者自己也可以编写一个 PostCSS 8 插件。
1 插件工作原理
PostCSS 是一个允许使用 JS 插件转换样式的工具。开发者可以根据自己的实际需求,在编译过程将指定 css 样式进行转换和处理。目前 PostCSS 官方收录插件有 200 多款,其中包括使用最广泛的Autoprefixer
自动补全 css 前缀插件。
PostCSS 和插件的工作原理其实很简单,就是先将 css 源码转换为 AST,插件基于转换后 AST 的信息进行个性化处理,最后 PostCSS 再将处理后的 AST 信息转换为 css 源码,完成 css 样式转换,其流程可以归结为下图:
下面我们通过实际例子看看 PostCSS 会将 css 源码转换成的 AST 格式:
代码语言:javascript复制const postcss = require('postcss')
postcss().process(`
.demo {
font-size: 14px; /*this is a comment*/
}
`).then(result => {
console.log(result)
})
代码中直接引用 PostCSS,在不经过任何插件的情况下将 css 源码进行转换,AST 转换结果如下:
代码语言:javascript复制{
"processor": {
"version": "8.3.6",
"plugins": []
},
"messages": [],
"root": {
"raws": {
"semicolon": false,
"after": "n"
},
"type": "root",
// ↓ nodes 字段内容重点关注
"nodes": [
{
"raws": {
"before": "n",
"between": " ",
"semicolon": true,
"after": "n"
},
"type": "rule",
"nodes": [
{
"raws": {
"before": "n ",
"between": ": "
},
"type": "decl",
"source": {
"inputId": 0,
"start": {
"offset": 11,
"line": 3,
"column": 3
},
"end": {
"offset": 26,
"line": 3,
"column": 18
}
},
"prop": "font-size", // css 属性和值
"value": "14px"
},
{
"raws": {
"before": " ",
"left": "",
"right": ""
},
"type": "comment", // 注释类
"source": {
"inputId": 0,
"start": {
"offset": 28,
"line": 3,
"column": 20
},
"end": {
"offset": 48,
"line": 3,
"column": 40
}
},
"text": "this is a comment"
}
],
"source": {
"inputId": 0,
"start": {
"offset": 1,
"line": 2,
"column": 1
},
"end": {
"offset": 28,
"line": 4,
"column": 1
}
},
"selector": ".demo", // 类名
"lastEach": 1,
"indexes": {}
}
],
"source": {
"inputId": 0,
"start": {
"offset": 0,
"line": 1,
"column": 1
}
},
"lastEach": 1,
"indexes": {},
"inputs": [
{
"hasBOM": false,
"css": "n.demo {n font-size: 14px;n}n",
"id": "<input css vi1Oew>"
}
]
},
"opts": {},
"css": "n.demo {n font-size: 14px;n}n"
}
AST 对象中 nodes 字段里的内容尤为重要,其中存储了 css 源码的关键字、注释、源码的起始、结束位置以及 css 的属性和属性值,类名使用selector
存储,每个类下又存储一个 nodes 数组,该数组下存放的就是该类的属性 (prop
) 和属性值 (value
)。那么插件就可以基于 AST 字段对 css 属性进行修改,从而实现 css 的转换。
2 PostCSS 插件格式规范及 API
PostCSS 插件其实就是一个 JS 对象,其基本形式和解析如下:
代码语言:javascript复制module.exports = (opts = { }) => {
// 此处可对插件配置 opts 进行处理
return {
postcssPlugin: 'postcss-test', // 插件名字,以 postcss- 开头
Once (root, postcss) {
// 此处 root 即为转换后的 AST,此方法转换一次 css 将调用一次
},
Declaration (decl, postcss) {
// postcss 遍历 css 样式时调用,在这里可以快速获得 type 为 decl 的节点 (请参考第二节的 AST 对象)
},
Declaration: {
color(decl, postcss) {
// 可以进一步获得 decl 节点指定的属性值,这里是获得属性为 color 的值
}
},
Comment (comment, postcss) {
// 可以快速访问 AST 注释节点(type 为 comment)
},
AtRule(atRule, postcss) {
// 可以快速访问 css 如 @media,@import 等 @定义的节点(type 为 atRule)
}
}
}
module.exports.postcss = true
更多的 PostCSS 插件 API 可以详细参考官方 postcss8 文档,基本原理就是 PostCSS 会遍历每一个 css 样式属性值、注释等节点,之后开发者就可以针对个性需求对节点进行处理即可。
3 实际开发一个 PostCSS 8 插件
了解了 PostCSS 插件的格式和 API,我们将根据实际需求来开发一个简易的插件,有如下 css:
代码语言:javascript复制.demo {
font-size: 14px; /*this is a comment*/
color: #ffffff;
}
需求如下:
- 删除 css 内注释
- 将所有颜色为十六进制的
#ffffff
转为 css 内置的颜色变量white
根据第三节的插件格式,本次开发只需使用Comment
和Declaration
接口即可:
// plugin.js
module.exports = (opts = { }) => {
return {
postcssPlugin: 'postcss-test',
Declaration (decl, postcss) {
if (decl.value === '#ffffff') {
decl.value = 'white'
}
},
Comment(comment) {
comment.text = ''
}
}
}
module.exports.postcss = true
在 PostCSS 中使用该插件:
代码语言:javascript复制// index.js
const plugin = require('./plugin.js')
postcss([plugin]).process(`
.demo {
font-size: 14px; /*this is a comment*/
color: #ffffff;
}
`).then(result => {
console.log(result.css)
})
运行结果如下:
代码语言:javascript复制.demo {
font-size: 14px; /**/
color: white;
}
可以看到,字体颜色值已经成功做了转换,注释内容已经删掉,但注释标识符还依旧存在,这是因为注释节点是包含/**/
内容存在的,只要 AST 里注释节点还存在,最后 PostCSS 还原 AST 时还是会把这段内容还原,要做到彻底删掉注释,需要对 AST 的 nodes 字段进行遍历,将 type 为 comment 的节点进行删除,插件源码修改如下:
// plugin.js
module.exports = (opts = { }) => {
// Work with options here
// https://postcss.org/api/#plugin
return {
postcssPlugin: 'postcss-test',
Once (root, postcss) {
// Transform CSS AST here
root.nodes.forEach(node => {
if (node.type === 'rule') {
node.nodes.forEach((n, i) => {
if (n.type === 'comment') {
node.nodes.splice(i, 1)
}
})
}
})
},
Declaration (decl, postcss) {
// The faster way to find Declaration node
if (decl.value === '#ffffff') {
decl.value = 'white'
}
}
}
}
module.exports.postcss = true
重新执行 PostCSS,结果如下,符合预期。
代码语言:javascript复制.demo {
font-size: 14px;
color: white;
}
4 插件开发注意事项
通过实操开发可以看到,开发一个 PostCSS 插件其实很简单,但在实际的插件开发中,开发者需要注意以下事项:
1. 尽量使插件简单,使用者可以到手即用
Build code that is short, simple, clear, and modular.
尽量使你的插件和使用者代码解耦,开放有限的 API,同时开发者在使用你的插件时从名字就可以知道插件的功能。这里推荐一个简单而优雅的 PostCSS 插件 postcss-focus,读者可以从这个插件的源码中体会这个设计理念。
2. 开发插件前确认是否有现成的轮子
如果你对自己的项目有个新点子,想自己开发一个插件去实现,在开始写代码前,可以先到 PostCSS 官方注册的插件列表中查看是否有符合自己需求的插件,避免重复造轮子。不过截止目前 (2021.8),大部分插件依旧停留在 PostCSS 8 以下,虽然 PostCSS 8 已经对旧版本插件做了处理,但在 AST 的解析处理上还是有差异,从实际使用过程中我就发现 PostCss8 使用低版本插件会导致 AST 内的 source map 丢失,因此目前而言完全兼容 PostCSS 8 的插件还需各位开发者去升级。
5 从低版本 PostCSS 迁移
升级你的 PostCSS 插件具体可以参考官方给出的升级指引。这里只对部分关键部分做下解释:
1. 升级 API
- 将旧版
module.exports = postcss.plugin(name, creator)
替换为module.exports = creator
; - 新版插件将直接返回一个对象,对象内包含
Once
方法回调; - 将原插件逻辑代码转移至
Once
方法内; - 插件源码最后加上
module.exports.postcss = true
;
具体示例如下。
旧版插件:
代码语言:javascript复制- module.exports = postcss.plugin('postcss-dark-theme-class', (opts = {}) => {
- checkOpts(opts)
- return (root, result) => {
root.walkAtRules(atrule => { … })
- }
- })
升级后插件:
代码语言:javascript复制 module.exports = (opts = {}) => {
checkOpts(opts)
return {
postcssPlugin: 'postcss-dark-theme-class',
Once (root, { result }) {
root.walkAtRules(atrule => { … })
}
}
}
module.exports.postcss = true
2. 提取逻辑代码至新版 API
把逻辑代码都放在Once
回调内还不够优雅,PostCSS 8 已经实现了单个 css 的代码扫描,提供了Declaration()
, Rule()
, AtRule()
, Comment()
等方法,旧版插件类似root.walkAtRules
的方法就可以分别进行重构,插件效率也会得到提升:
module.exports = {
postcssPlugin: 'postcss-dark-theme-class',
- Once (root) {
- root.walkAtRules(atRule => {
- // Slow
- })
- }
AtRule (atRule) {
// Faster
}
}
module.exports.postcss = true
6 总结
通过本文的介绍,读者可以了解 PostCSS 8 工作的基本原理,根据具体需求快速开发一个 PostCSS 8 插件,并在最后引用官方示例中介绍了如何快速升级旧版 PostCSS 插件。目前 PostCSS 8 还有大量还没进行升级兼容的 PostCSS 插件,希望读者可以在阅读本文后可以获得启发,对 PostCSS 8 的插件生态做出贡献。