你造不造PostCss?不造就進來看看,包你造

image.png

關于PostCss

在前端編寫css屬性時,又是為了兼容不同的瀏覽器,需要在屬性上加上不同前綴,有時為了添加一條屬性,需添加3~4條類似的屬性只是為了滿足瀏覽器的兼容,這不僅會增加的工作量,還容易遺漏,造成樣式兼容問題。

隨著前端工程化越來越強大,我們只需要在編譯工具(webpack,rollup等)中配置一下,就可以實現編譯過程中自動補全前綴的功能,我們就有更多的精力在更重要的地方。

多數情況下,這都是借用了PostCss的力量,PostCss是一個使用JS插件轉換css樣式的工具,這些插件可以實現css樣式合并,將px轉化成rem,支持變量,處理內聯圖像等等,換句話說,如果沒有這些小而美的插件,那么PostCss 就什么都不會做。

目前,PostSS有200多個插件,可以在插件列表或可搜索目錄中找到所有插件

工程化中怎么使用的?

舉個例子??

Webpack

webpack.config.js 使用 postcss-loader:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
          },
          {
            loader: 'postcss-loader'
          }
        ]
      }
    ]
  }
}

創建 postcss.config.js,配置postcss需要的插件:

module.exports = {
  plugins: [
    require('autoprefixer'),
    require('postcss-px2rem')
  ]
}
Gulp

在Gulp中使用 gulp-postcssgulp-sourcemaps.

gulp.task('css', () => {
  const postcss    = require('gulp-postcss')
  const sourcemaps = require('gulp-sourcemaps')

  return gulp.src('src/**/*.css')
    .pipe( sourcemaps.init() )
    .pipe( postcss([ require('autoprefixer'), require('postcss-nested') ]) )
    .pipe( sourcemaps.write('.') )
    .pipe( gulp.dest('build/') )
})
JS API

也可以直接使用JS API

const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const postcssNested = require('postcss-nested')
const fs = require('fs')

fs.readFile('src/app.css', (err, css) => {
  postcss([autoprefixer, postcssNested])
    .process(css, { from: 'src/app.css', to: 'dest/app.css' })
    .then(result => {
      fs.writeFile('dest/app.css', result.css, () => true)
      if ( result.map ) {
        fs.writeFile('dest/app.css.map', result.map.toString(), () => true)
      }
    })
})
大佬出場

在不同的編譯工具中,我們會使用不同的PostCss工具,webpack的postcss-loader,gulp的gulp-postcss,但是其實,他們內部都是使用了postcss,只是針對不同的編譯工具進行了適配,那就以最熟悉的postcss-loader為例,他內部源碼構造如下,可以看出也是使用了postcss

const path = require('path');
const { getOptions } = require('loader-utils');
const validateOptions = require('schema-utils');
const postcss = require('postcss');
const postcssrc = require('postcss-load-config');
const Warning = require('./Warning.js');
const SyntaxError = require('./Error.js');
const parseOptions = require('./options.js');

function loader(css, map, meta) {
    const options = Object.assign({}, getOptions(this));
    validateOptions(require('./options.json'), options, 'PostCSS Loader');
    const cb = this.async();
    const file = this.resourcePath;
    const sourceMap = options.sourceMap;

    Promise.resolve().then(() => {
        // webpack內置參數處理與擴展
        // 省略
        return postcssrc(rc.ctx, rc.path);
    }).then((config) => {
        // 省略
        return postcss(plugins)
            .process(css, options)
            .then((result) => {
                // ...
                cb(null, css, map, meta);

                return null;
            });
    }).catch((err) => {
        // 錯誤處理
    });
}
module.exports = loader;

API

PostCss接受一個CSS文件,通過將其轉換為抽象語法樹,提供一個API來分析和修改其規則。這個API可以被插件用來做很多有用的事情,例如,自動查找錯誤,或者插入瀏覽器前綴。

1、創建一個postcss實例,并傳入需要的plugins參數,初始化插件,并用這些插件后續去處理css文件

let processor = require('postcss')

processor有兩個屬性,兩個方法

  • plugins: 屬性,processor 接受到插件參數
const processor = postcss([autoprefixer, postcssNested])
processor.plugins.length //=> 2
  • version: 屬性,processor 的版本號
if (result.processor.version.split('.')[0] !== '6') {
  throw new Error('This plugin works only with PostCSS 6')
}
  • process:方法,解析css并返回一個promise實例,參數在代碼中,接受兩個參數
    • css:需要轉譯的css文件,或者一個帶有 toString() 方法的函數
    • 第二個參數是一個對象 processOptions,可有6個屬性,標注再例子中
  • use:方法,為processor添加插件,一般有四種形式(不常用)
    • 格式化插件
    • 一個包含pluginCreator.postcss = true的構造函數,
    • 一個函數,PostCSS 第一個參為 @{link Root},第二個參數為Result。
    • 其他的PostCSS實例,PostCSS會將這個實例的插件復制到本PostCSS實例中。

所以一般的使用方式如下

let processor = require('postcss')

const processOptions = { 
    from: 'a.css',  // 需要轉譯的css文件路徑
    to: 'a.out.css', // 產出路徑
    // parser : Parser 的相關設置,Parser主要用于將css字符串專為AST
    // stringifier: stringifier 的相關設置,stringifier主要用于將AST 轉成 css 字符串
    // map: , sourceMap的相關設置 SourceMapOptions
    // syntax: 包含parser和stringifier的對象
}
processor.process(css, processOptions)
  .then(result => {
     console.log(result.css)
  })

??

下面我們通過實際例子看看 PostCSS 會將 css 源碼轉換成的 AST 格式:

const postcss = require('postcss')
postcss().process(`
@media screen and (min-width: 480px) {
    body {
        background-color: lightgreen;
    }
}
/* 這是一段注釋 */
#app {
    border: 1px solid #000;
}
`).then(result => {
 console.log(result)
})

直接使用 PostCSS,在不使用任何插件的情況下將 css 源碼進行轉換的A ST 如下

{
  "raws": {
    "semicolon": false,  // raws.semicolon 最后是否是分號結束
    "after": ""          // raws.after 最后的空字符串
  },
  "type": "root",        // 當前對象的類型
  "nodes": [             // 子節點
    {
      "raws": {
        "before": "",    // raws.before  距離前一個兄弟節點的內容
        "between": " ",  // raws.between 選擇器與 { 之間的內容
        "afterName": " ",  // raws.afterName  記錄@name之間后的內容
        "semicolon": false,
        "after": "\n"
      },
      "type": "atrule",  // 當前節點類型
      "name": "media",   // @后的標識名稱
      "source": {        // source 字段記錄@語句的開始,以及當前文件的信息
        "inputId": 0,
        "start": {
          "offset": 0,
          "line": 1,
          "column": 1
        },
        "end": {
          "offset": 94,
          "line": 5,
          "column": 1
        }
      },
      "params": "screen and (min-width: 480px)", // // @后的標識參數
      "nodes": [
        {
          "raws": {
            "before": "\n    ",
            "between": " ",
            "semicolon": true,
            "after": "\n    "
          },
          "type": "rule",
          "nodes": [
            {
              "raws": {
                "before": "\n        ",
                "between": ": "
              },
              "type": "decl",
              "source": {
                "inputId": 0,
                "start": {
                  "offset": 58,
                  "line": 3,
                  "column": 9
                },
                "end": {
                  "offset": 86,
                  "line": 3,
                  "column": 37
                }
              },
              "prop": "background-color",
              "value": "lightgreen"
            }
          ],
          "source": {
            "inputId": 0,
            "start": {
              "offset": 43,
              "line": 2,
              "column": 5
            },
            "end": {
              "offset": 92,
              "line": 4,
              "column": 5
            }
          },
          "selector": "body"
        }
      ]
    },
    {
      "raws": {
        "before": "\n",
        "left": " ",
        "right": " "
      },
      "type": "comment",  //  注釋節點
      "source": {
        "inputId": 0,
        "start": {
          "offset": 96,
          "line": 6,
          "column": 1
        },
        "end": {
          "offset": 107,
          "line": 6,
          "column": 12
        }
      },
      "text": "這是一段注釋"  // 注釋節點內容
    },
    {
      "raws": {
        "before": "\n",
        "between": " ",
        "semicolon": true,
        "after": "\n"
      },
      "type": "rule",
      "nodes": [
        {
          "raws": {
            "before": "\n    ",
            "between": ": "
          },
          "type": "decl",
          "source": {
            "inputId": 0,
            "start": {
              "offset": 120,
              "line": 8,
              "column": 5
            },
            "end": {
              "offset": 142,
              "line": 8,
              "column": 27
            }
          },
          "prop": "border",         // 屬性
          "value": "1px solid #000" // 屬性值
        }
      ],
      "source": {
        "inputId": 0,
        "start": {
          "offset": 109,
          "line": 7,
          "column": 1
        },
        "end": {
          "offset": 144,
          "line": 9,
          "column": 1
        }
      },
      "selector": "#app"  // 選擇器名稱
    }
  ],
  "source": {
    "inputId": 0,
    "start": {
      "offset": 0,
      "line": 1,
      "column": 1
    }
  },
  "inputs": [    // 當前文件的相關信息
    {
      "hasBOM": false,
      "css": "@media screen and (min-width: 480px) {\n    body {\n        background-color: lightgreen;\n    }\n}\n/* 這是一段注釋 */\n#app {\n    border: 1px solid #000;\n}",
      "id": "<input css VT5Twy>"
    }
  ]
}

也可以直接使用在線轉

[圖片上傳失敗...(image-345b22-1649396282989)]

下面我們來介紹一下CSS AST 節點主要節點類型,相關節點屬性標記再上方代碼中

  • Root: 根結點,Commont,AtRule,Rule 都是它的子節點。
  • Commont: 注釋節點。
  • AtRule: 帶@標識的的節點。
  • Rule: 選擇器節點
  • Declaration:每個 css 屬性以及屬性值就代表一個 declaration

每個節點類型還有一些屬性和操作方法
具體可看官方文檔

獲取到 AST 后我們就可以對 AST 進行操作,從而實現相關功能

開發一個PostCss插件

PostCSS 插件格式規范及 API
PostCSS 插件其實就是一個 JS 對象,其基本形式和解析如下:

module.exports = (opts = {}) => {
    // 此處可對插件配置opts進行處理
    return {
        postcssPlugin: 'postcss-test', // 插件名字,以postcss-開頭

        Once(root, postcss) {
            // 此處root即為轉換后的AST,此方法轉換一次css將調用一次
        },

        Declaration(decl, postcss) {
           // postcss遍歷css樣式時調用,在這里可以快速獲得type為decl的節點
        },

        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,我們將根據實際需求來開發一個簡易的插件,有如下 css:

.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 中使用該插件:

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

運行結果如下:

.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) {
            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) {
            if (decl.value === '#ffffff') {
                    decl.value = 'white'
            }
        }
    }
}
module.exports.postcss = true

重新執行 PostCSS,結果如下,符合預期。

.demo {
     font-size: 14px;
     color: white;
}

總結

1、PostCss是一個使用JS插件轉換css樣式的工具
2、PostCss將CSS解析為抽象語法樹(AST)、并提供一套 API 操作 AST
3、PostCss通過任意數量的“插件”函數,操作并傳遞AST
4、然后將操作完成的AST轉換回字符串,并輸出到文件中
5、可以生成sourcemaps以跟蹤任何更改
6、PostCss就像是處理css的生態系統,沒有一個特定的插件能完全代表PostCss。

參考文章:
[如何編寫屬于自己的 PostCSS 8 插件?](https://zhuanlan.zhihu.com/p/426470972)  
[Postcss 運用以及原理解析](http://www.lxweimin.com/p/183af77a51ec)  
[官方文檔](https://www.postcss.com.cn/api/#processor-use)  
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容