Postcss 運(yùn)用以及原理解析

這篇文章是我在公司內(nèi)部的一個(gè)分享,大部分時(shí)間都在調(diào)試 postcss 源碼,即 postcss 將 css 字符串 解析為 CSS AST 的過(guò)程,很可惜這部分是不可見的,我打算錄制視頻放到B站上面,后續(xù)再更新。

本文目標(biāo):

  • 掌握 postcss 的使用
  • 自定義 postcss 插件
  • 掌握 stylelint 的使用
  • 自定義 stylelint rule
  • 擴(kuò)展 css parser 解釋器

1. postcss 是什么

在聊 postcss 之前,我們需要知道什么是 CSS 后處理工具。我們比較熟悉的 Less/Sass/Stylus,這類工具都屬于CSS 預(yù)處理工具。預(yù)處理指的是通過(guò)特殊的規(guī)則,將非 css 文本格式最終生成 css 文件,而 postcss 則是對(duì) CSS 進(jìn)行處理,最終生成CSS。
可能大部分前端開發(fā)者都使用過(guò) Autoprefixer 這款插件,它以 Can I Use (瀏覽器兼容性支持) 為基礎(chǔ),自動(dòng)處理兼容性問(wèn)題,下面是一個(gè)簡(jiǎn)單的例子:

// 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;
}

在這個(gè)例子中過(guò),通過(guò)使用 Autoprefixer 插件,幫助我們自動(dòng)處理瀏覽器前綴,極大的提高了編碼效率。其實(shí),Autoprefixer 正是 postcss 眾多插件中的一款,postcss 提供的簡(jiǎn)潔明了API,并且文檔十分詳細(xì),這為其生態(tài)建設(shè)提供了有力的支撐。點(diǎn)擊 這里 查看更多可用插件。

2. postcss 如何使用

兩個(gè)主要的功能:

  • 轉(zhuǎn)換 css,這是我們最常使用的
  • 獲取 css ast,當(dāng)我們編寫插件時(shí)需要掌握

2.1 轉(zhuǎn)換 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 對(duì)象
    .process(css) // process 接收 css 資源
    .then(result => { // result 為 Result 實(shí)例
      fs.writeFile(dest, result.css, function(err) {
        if (err) throw err;
      });
    })
})

相關(guān)源碼:

  • 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 抽象語(yǔ)法樹

相關(guān)源碼:

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

3. postcss 的運(yùn)行過(guò)程

主要步驟:

  • 解釋:接收輸入的css,將css內(nèi)容處理成css抽象語(yǔ)法樹。
  • 轉(zhuǎn)換:根據(jù)配置插件的順序?qū)湫徒Y(jié)構(gòu)的 AST 進(jìn)行操作。
  • 輸出:最終將處理后獲得的 AST S 對(duì)象輸出為 css 文件。

主要內(nèi)容:

  • 標(biāo)記器:將 css 拆解為 token 序列,為語(yǔ)法樹提供基礎(chǔ)(postcss/lib/tokenize.js ...)
  • 解釋器:通過(guò)語(yǔ)法分享,將 token 序列轉(zhuǎn)換為語(yǔ)法樹(postcss/lib/parser.js ...)
  • 處理器:根據(jù)插件配置,對(duì)語(yǔ)法樹做一些轉(zhuǎn)換操作(postcss/lib/lazy-result.js ...)

最不容易理解的也是最難的點(diǎn):CSS AST 的生成以及操作。

4. postcss CSS AST

你暫且將 AST 理解一個(gè)節(jié)點(diǎn)樹,這些節(jié)點(diǎn)不完全相同,它們繼承自同一個(gè)節(jié)點(diǎn)(源碼中為Container)。
在學(xué)習(xí) postcss 初期,通過(guò)查看可視化的 postcss css 語(yǔ)法樹,可以幫助你理解。使用使用 css ast 在線工具,下圖為一個(gè)很標(biāo)準(zhǔn)的 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 最終會(huì)處理為下圖結(jié)構(gòu),通過(guò)打印信息我們可以發(fā)現(xiàn)樹型結(jié)構(gòu)的 JS 對(duì)象是一個(gè)名為 Root 的構(gòu)造函數(shù),而起樹型結(jié)構(gòu)的 nodes 節(jié)點(diǎn)下還有 Common,AtRule, Rule 構(gòu)造函數(shù)。


image.png
image.png

CSS AST 節(jié)點(diǎn)主要有以下構(gòu)造類組成:

  • Root: 根結(jié)點(diǎn),整個(gè)處理過(guò)程基本上都在圍繞著 Root,Commont,AtRule,Rule 都是它的子節(jié)點(diǎn)。
  • Commont: css 中的注釋信息,注釋的內(nèi)容在 comment.text 下。
  • AtRule: 帶@標(biāo)識(shí)的部分,name 為標(biāo)識(shí)名稱,params 為標(biāo)識(shí)參數(shù)。nodes 為內(nèi)部包含的其他子節(jié)點(diǎn),可以是 Commont,AtRule,Rule,這讓我們可以自定義更多的規(guī)則。
  • Declaration:每個(gè) css 屬性以及屬性值就代表一個(gè) declaration

4.1 Rule 選擇器節(jié)點(diǎn)

一個(gè)選擇器代表一個(gè)Rule,選擇器對(duì)應(yīng)的樣式列表 nodes 為 Declaration構(gòu)造函數(shù)


image.png
image.png
  • raws
    • before 距離前一個(gè)兄弟節(jié)點(diǎn)之間的內(nèi)容
    • between 選擇器與 { 之間的內(nèi)容
    • semicolon 最后一個(gè)屬性是否帶分號(hào)
    • after 最后一個(gè)屬性 和 } 之間的內(nèi)容
image.png
image.png
  • type 節(jié)點(diǎn)類型
  • nodes 子節(jié)點(diǎn)
  • source
    • start 開始位置
    • end 結(jié)束位置
  • selecter 選擇器

大部分節(jié)點(diǎn)結(jié)構(gòu)是類似的,如果你理解了 Rule 節(jié)點(diǎn)的結(jié)構(gòu),相信其他類型的節(jié)點(diǎn)對(duì)你也是很輕松的!

4.2 Declaration 屬性節(jié)點(diǎn)

Declaration 是 css 樣式屬性,prop為樣式屬性,value為樣式值??山o Rule 手動(dòng)添加樣式屬性,也可以修改prop,value。上文提到的 Autoprefixer 就是通過(guò) clone 當(dāng)前屬性,修改 prop 并添加到選擇器下,Declaration 節(jié)點(diǎn)非常簡(jiǎn)單:


image.png
image.png

4.3 Comment 注釋節(jié)點(diǎn)

image.png
image.png

5. 各構(gòu)造器方法和屬性

大部分節(jié)點(diǎn)都繼承了 Container,因此我們先看看公共屬性:

  • nodes 子節(jié)點(diǎn)
  • parent 父節(jié)點(diǎn)
  • raws 相當(dāng)于分隔符集合
  • source 位置范圍
  • type 節(jié)點(diǎn)類型
  • last 該節(jié)點(diǎn)的子節(jié)點(diǎn)的最后一個(gè)
  • first 該節(jié)點(diǎn)的子節(jié)點(diǎn)的第一個(gè)
  • after() 在當(dāng)前節(jié)點(diǎn)的后面插入一個(gè)節(jié)點(diǎn),等價(jià) node.parent.insertAfter(node, add)
  • cleanRaws 代碼格式化(保持縮進(jìn)...)
  • clone 節(jié)點(diǎn)克隆
  • cloneBefore
  • cloneAfter
  • each 遍歷兒子節(jié)點(diǎn)
  • error 拋出一個(gè)錯(cuò)誤
  • every 條件遍歷
  • index 獲取節(jié)點(diǎn)在父節(jié)點(diǎn)中的索引
  • 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 用于遍歷所有子孫節(jié)點(diǎn) decl value
  • root()
  • some() Returns true if callback returns true for (at least) one of the container’s children.some()
  • toJSON() 打印 JSON,使用 JSON.stringify() 有循環(huán)依賴
  • toString() 獲取轉(zhuǎn)換后的 css
  • walk() 遍歷所有子孫節(jié)點(diǎn),這個(gè)接口非常有用哦
  • walkAtRules 遍歷艾特節(jié)點(diǎn)
  • walkComments 遍歷注釋節(jié)點(diǎn)
  • walkRules 遍歷選擇器節(jié)點(diǎn)
  • walkDecls 遍歷屬性節(jié)點(diǎn)

5.1 Rule

  • 公共屬性方法
  • selector 選擇器
  • selectors 選擇器數(shù)組

[圖片上傳失敗...(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. 如何編寫一個(gè) postcss 插件

上文中,我們對(duì) css 處理后生成的 Root 以及其節(jié)點(diǎn)下的 Commont,AtRule,Rule, Declaration 有了基本的認(rèn)識(shí),那么我們是如何獲得Root,又將拿這些構(gòu)造函數(shù)做些什么呢。

7.1 案例:css選擇器深度校驗(yàn)

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 轉(zhuǎn) 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'
    })
  }
});

通過(guò)上述插件代碼的示例,可以看出整個(gè)流程還是很清晰的

  • 重點(diǎn)對(duì)象:Root,Commont,AtRule,Rule, Declaration,Result;
  • 遍歷方法:walkCommonts,walkAtRules,walkRules,walkDels

8. postcss 的運(yùn)用

  • stylelint 的使用
  • 擴(kuò)展 stylelint 規(guī)則
  • ...見代碼

9. 參考文檔

  1. http://api.postcss.org
  2. postcss API文檔
  3. postcss 在線開發(fā)
  4. postcss 插件列表
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容