Postcss 运用以及原理解析

这篇文章是我在公司内部的一个分享,大部分时间都在调试 postcss 源码,即 postcss 将 css 字符串 解析为 CSS AST 的过程,很可惜这部分是不可见的,我打算录制视频放到B站上面,后续再更新。

本文目标:

  • 掌握 postcss 的使用
  • 自定义 postcss 插件
  • 掌握 stylelint 的使用
  • 自定义 stylelint rule
  • 扩展 css parser 解释器

1. postcss 是什么

在聊 postcss 之前,我们需要知道什么是 CSS 后处理工具。我们比较熟悉的 Less/Sass/Stylus,这类工具都属于CSS 预处理工具。预处理指的是通过特殊的规则,将非 css 文本格式最终生成 css 文件,而 postcss 则是对 CSS 进行处理,最终生成CSS。
可能大部分前端开发者都使用过 Autoprefixer 这款插件,它以 Can I Use (浏览器兼容性支持) 为基础,自动处理兼容性问题,下面是一个简单的例子:

// Autoprefixer 处理前的CSS样式
.container {
    display: flex;
}
.item {
    flex: 1;
}

// Autoprefixer 处理后的CSS样式
.container {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
}
.item {
  -webkit-box-flex: 1;
  -webkit-flex: 1;
  -ms-flex: 1;
  flex: 1;
}

在这个例子中过,通过使用 Autoprefixer 插件,帮助我们自动处理浏览器前缀,极大的提高了编码效率。其实,Autoprefixer 正是 postcss 众多插件中的一款,postcss 提供的简洁明了API,并且文档十分详细,这为其生态建设提供了有力的支撑。点击 这里 查看更多可用插件。

2. postcss 如何使用

两个主要的功能:

  • 转换 css,这是我们最常使用的
  • 获取 css ast,当我们编写插件时需要掌握

2.1 转换 css

这里只介绍 postcss API,如果您使用 webpack,只需要将 postcss 包装为 postcss-loader 即可。

// 01-simple-demo
const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const precss = require('precss')
const fs = require('fs')
const path = require('path')

const src = path.resolve('./src/app.css');
const dest = path.resolve('./dest/app.css');

fs.readFile(src, (err, css) => {
  postcss([precss, autoprefixer]) // [precss, autoprefixer] 为使用的插件列表,返回 Processor 对象
    .process(css) // process 接收 css 资源
    .then(result => { // result 为 Result 实例
      fs.writeFile(dest, result.css, function(err) {
        if (err) throw err;
      });
    })
})

相关源码:

  • postcss/lib/postcss.js
  • postcss/lib/processor.js

2.2 获取 CSS AST

// 02-use-parser
const postcss = require('postcss')
const fs = require('fs')
const path = require('path')

const src = path.resolve('./src/app.css');
const css = fs.readFileSync(src);
const root = postcss.parse(css);

console.log(root); // CSS 抽象语法树

相关源码:

  • postcss/lib/parse.js
  • postcss/lib/parser.js

3. postcss 的运行过程

主要步骤:

  • 解释:接收输入的css,将css内容处理成css抽象语法树。
  • 转换:根据配置插件的顺序对树型结构的 AST 进行操作。
  • 输出:最终将处理后获得的 AST S 对象输出为 css 文件。

主要内容:

  • 标记器:将 css 拆解为 token 序列,为语法树提供基础(postcss/lib/tokenize.js ...)
  • 解释器:通过语法分享,将 token 序列转换为语法树(postcss/lib/parser.js ...)
  • 处理器:根据插件配置,对语法树做一些转换操作(postcss/lib/lazy-result.js ...)

最不容易理解的也是最难的点:CSS AST 的生成以及操作。

4. postcss CSS AST

你暂且将 AST 理解一个节点树,这些节点不完全相同,它们继承自同一个节点(源码中为Container)。
在学习 postcss 初期,通过查看可视化的 postcss css 语法树,可以帮助你理解。使用使用 css ast 在线工具,下图为一个很标准的 css 文档,有注释、媒体查询,以及选择器样式:

/**
 * Paste or drop some CSS here and explore
 * the syntax tree created by chosen parser.
 * Enjoy!
 */

@media screen and (min-width: 480px) {
    body {
        background-color: lightgreen;
    }
}

#main {
    border: 1px solid black;
}

ul li {
    padding: 5px;
}

上面的 css 最终会处理为下图结构,通过打印信息我们可以发现树型结构的 JS 对象是一个名为 Root 的构造函数,而起树型结构的 nodes 节点下还有 Common,AtRule, Rule 构造函数。


image.png
image.png

CSS AST 节点主要有以下构造类组成:

  • Root: 根结点,整个处理过程基本上都在围绕着 Root,Commont,AtRule,Rule 都是它的子节点。
  • Commont: css 中的注释信息,注释的内容在 comment.text 下。
  • AtRule: 带@标识的部分,name 为标识名称,params 为标识参数。nodes 为内部包含的其他子节点,可以是 Commont,AtRule,Rule,这让我们可以自定义更多的规则。
  • Declaration:每个 css 属性以及属性值就代表一个 declaration

4.1 Rule 选择器节点

一个选择器代表一个Rule,选择器对应的样式列表 nodes 为 Declaration构造函数


image.png
image.png
  • raws
    • before 距离前一个兄弟节点之间的内容
    • between 选择器与 { 之间的内容
    • semicolon 最后一个属性是否带分号
    • after 最后一个属性 和 } 之间的内容
image.png
image.png
  • type 节点类型
  • nodes 子节点
  • source
    • start 开始位置
    • end 结束位置
  • selecter 选择器

大部分节点结构是类似的,如果你理解了 Rule 节点的结构,相信其他类型的节点对你也是很轻松的!

4.2 Declaration 属性节点

Declaration 是 css 样式属性,prop为样式属性,value为样式值。可给 Rule 手动添加样式属性,也可以修改prop,value。上文提到的 Autoprefixer 就是通过 clone 当前属性,修改 prop 并添加到选择器下,Declaration 节点非常简单:


image.png
image.png

4.3 Comment 注释节点

image.png
image.png

5. 各构造器方法和属性

大部分节点都继承了 Container,因此我们先看看公共属性:

  • nodes 子节点
  • parent 父节点
  • raws 相当于分隔符集合
  • source 位置范围
  • type 节点类型
  • last 该节点的子节点的最后一个
  • first 该节点的子节点的第一个
  • after() 在当前节点的后面插入一个节点,等价 node.parent.insertAfter(node, add)
  • cleanRaws 代码格式化(保持缩进...)
  • clone 节点克隆
  • cloneBefore
  • cloneAfter
  • each 遍历儿子节点
  • error 抛出一个错误
  • every 条件遍历
  • index 获取节点在父节点中的索引
  • insertAfter
  • insertBefore
  • next
  • positionInside Convert string index to line/column
  • prepend
  • push
  • raw()
  • remove()
  • removeAll() Removes all children from the container and cleans their parent properties.
  • removeChild
  • replaceValues 用于遍历所有子孙节点 decl value
  • root()
  • some() Returns true if callback returns true for (at least) one of the container’s children.some()
  • toJSON() 打印 JSON,使用 JSON.stringify() 有循环依赖
  • toString() 获取转换后的 css
  • walk() 遍历所有子孙节点,这个接口非常有用哦
  • walkAtRules 遍历艾特节点
  • walkComments 遍历注释节点
  • walkRules 遍历选择器节点
  • walkDecls 遍历属性节点

5.1 Rule

  • 公共属性方法
  • selector 选择器
  • selectors 选择器数组

[图片上传失败...(image-ed56fe-1630568107685)]

5.2 Declaration

  • 公共属性方法
  • prop
  • value

5.3 Comment

  • 公共属性方法
  • text

6. 如何使用 postcss 插件

插件用于丰富 postcss 的功能。

插件编写文档

postcss([PluginA({}), PluginB({})])
  .process(css, {from: ``, to: ``})
  .then(result => {
    // do ...
    }).catch(error => {
    throw new Error(error)
    })

7. 如何编写一个 postcss 插件

上文中,我们对 css 处理后生成的 Root 以及其节点下的 Commont,AtRule,Rule, Declaration 有了基本的认识,那么我们是如何获得Root,又将拿这些构造函数做些什么呢。

7.1 案例:css选择器深度校验

const fs = require('fs')
const path = require('path')
const postcss = require('postcss')
const css = fs.readFileSync(path.resolve(__dirname, './main.css'), 'utf8')

const checkDepth = postcss.plugin('check-depth', (opt) => {
  opt = opt || { depth: 3 }
  return root => {
    root.walkRules(rule => {
      let selector = rule.selector.replace(/(^\s*)|(\s*$)/g, '')
      if (selector.split(/\s/).length > opt.depth) {
        throw rule.error(`css selector depth is too long`)
      }
    })
  }
})

postcss([checkDepth({
  depth: 3
})])
  .process(css, { from: ``, to: `` })
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    throw new Error(error)
  })

7.2 案例:px 转 rem

const postcss = require('postcss') // postcss

module.exports = postcss.plugin('px2rem', function(opts) {
  opts = opts || {};
  return function (root, result) {
    root.replaceValues(/\d+px/, { fast: 'px' }, string => {
      return opts.ratio * parseInt(string) + 'rem'
    })
  }
});

通过上述插件代码的示例,可以看出整个流程还是很清晰的

  • 重点对象:Root,Commont,AtRule,Rule, Declaration,Result;
  • 遍历方法:walkCommonts,walkAtRules,walkRules,walkDels

8. postcss 的运用

  • stylelint 的使用
  • 扩展 stylelint 规则
  • ...见代码

9. 参考文档

  1. http://api.postcss.org
  2. postcss API文档
  3. postcss 在线开发
  4. postcss 插件列表
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容