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

推荐阅读更多精彩内容