关于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 运用以及原理解析](https://www.jianshu.com/p/183af77a51ec)
[官方文档](https://www.postcss.com.cn/api/#processor-use)