關于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-postcss
和 gulp-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)