阅读 2197

从源码聊 postcss 和 babel 的 api 风格的不同

babel 是一个 js 到 js 的转译器,可以通过语法插件支持 es next、typescript、flow 等语法,支持 AST 转换插件,最后生成目标代码和 sourcemap。

postcss 是一个 css 到 css 的转译器,可以通过语法插件支持 less、sass、stylus 等语法,也支持 AST 转换插件,最后生成目标代码和 sourcemap。

image.png

postcss 之于 css,就像 babel 之于 js 一样,postcss 的插件体系也很繁荣,比如 autoprefixer、stylelint 等。

这俩工具在定位上类似,都是用于代码的转译和静态分析。但是在 api 设计上有所不同。

学完这篇文章你会了解到:

  • babel、postcss 的 api 的简易使用
  • babel、postcss 的编译流程
  • postcss 的源码架构
  • postcss 和 babel 在 api 设计上的不同
  • 链式和集中式的 api 风格各有什么好处

babel api

我们先从熟悉的 babel 开始,babel 7 的 api 一般这样用:

const babel = require('@babel/core');

const code = `
    console.log('hello world');
`;
const { code, map } = babel.transformSync(code, {
    plugins: [
        [
            pluginA,
            {
                // options
            }
        ]
    ],
    sourceMaps: true
});
复制代码

使用 babel core 包的 transformSync 的 api,传入源码和转换插件。babel 会在内部完成源码到 AST 的 parseAST 的 transform,以及目标代码和 sourcemap 的 generate 三个阶段。

我们可以用 babel3 的源码来了解下内部做了什么(babel3 的源码比较清晰,感兴趣可以去看看):

首先,babel 通过 File 对象来保存处理的文件的源码等信息,然后调用 parse

parse 方法里面会完成 parse、transform、generate 这 3 步:

而 parse 在 babel 3 的时候还是直接使用了 acorn(babel 7 的 parser 是 fork 了 acorn 做的修改):

transform 阶段会调用所有内置的 transformer 插件对 AST 进行转换:

而 generate 阶段会生成目标代码和 sourcemap:

babel 后续做了很多的迭代,比如分成了很多包,但是这个流程是没有变的。

我们有的时候也会直接用更底层的包,而不使用 babel core:

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

const code = `
    console.log('hello world');
`;

const ast = parser.parse(code);

traverse(ast, {
    visitor: {
        CallExpression(path) {
            //xxx
        }
    }
});

const { code, map } = generate(ast, {
    sourceMaps: true
});
复制代码

其实 @babel/core 是基于 @babel/parser、@babel/traverse、@babel/generator 等包做的封装,支持了插件能力。如果不需要插件,那么直接用这些包就行。

postcss api

postcss 的 api 是这样的:

const postcss = require('postcss');
const autoprefixer = require('autoprefixer')

const css = `
@import "aaa";
a {
   background: color;
}`;

postcss([autoprefixer]).process(css).then(result => {
 console.log(result.css)
})  
复制代码

首先调用 postcss 函数传入插件数组,然后调用 process 方法处理传入的 css,再调用 then 就可以在回调里面拿到结果(目标代码和 sourcemap)。

为什么会是这样的顺序呢,我们从源码角度看一下:

首先调用的 postcss 方法返回 Processor 对象,而 Processor 对象有 use(应用插件)、process(处理 css) 等方法,所以才是 postcss([]).process

然后我们可以看到 process 返回的值是 LazyResult 对象,而 LazyResult 对象会调用 parser 对传入的 css 进行 parse,提供了 then 方法来返回结果,所以是 postcss([]).process(css).then(() => {})。

then 里面会调用 aync,会应用插件,处理 AST(root),最终返回 stringifier 的结果。

stringifier 里会调用 MapGenerator 把 AST 打印成目标 css,并生成 sourcemap。

这就是 postcss 的编译流程。

postcss 其实依然是 parse、transform、generate 3 步,只不过 api 不同。

postcss 内部有这些类:

  • Processor:传入插件,返回 LazyResult,有 use(传入插件)、process(返回结果) 方法
  • LazyResult:传入源码,调用 Parser 对源码进行 parse,有 then(异步获取结果)方法,会用插件对 AST 转换,然后调用 MapGenerator 打印 AST
  • Parser:传入源码,返回 AST
  • MapGenerator: 传入 AST,打印成目标代码,并生成 sourcemap。

再回到开始,我们调用的方式是

postcss([autoprefixer]).process(css).then(result => {
 console.log(result.css)
}) 
复制代码

其实 postcss 就是个工厂函数,用于创建 Processsor 对象,Processor 对象的 process 方法返回 LazyResult 对象,LazyResult 对象有 then 方法来返回结果。

链式和集中式的 api 风格

postcss 和 babel 都是对代码(js、css)的转译,但是 api 却差别挺大,来对比下:

babel 是:

const { code, map } = babel.transformSync(code, {
    plugins: [
        [
            pluginA,
            {
                // options
            }
        ]
    ],
    sourceMaps: true
});
复制代码

postcss 是:

postcss([autoprefixer]).process(css).then(result => {
 console.log(result.css)
}) 
复制代码

可以看到 babel 只有一个 api,一次性传入源码和插件,在内部完成 parse、transform、generate 3个阶段。

postcss 则是提供了链式的 api,分多步传入插件和源码。

postcss 这种链式的风格的好处是代码紧凑,每一步做了什么都比较明确。babel 这种一次性传入的好处是使用简单,但是 option 耦合在一块,复杂度高。

链式的多次传入,还是集中式的一次传入,都有自己的好处,比如 jquery、chalk、jest、webpack-chain 都是链式的,而 webpack、babel 等都是一次性传入的。

jest api 风格(链式):

test(`jest`, () => {
    expect(a + b).not.toBeLessThan(expected);
}
复制代码

chalk api 风格(链式):

chalk.rgb(123, 45, 67).underline('Underlined reddish color')
复制代码

webpack-chain api 风格(链式):

config
  .entry('index')
    .add('src/index.js')
    .end()
  .output
    .path('dist')
    .filename('[name].bundle.js');
复制代码

webpack api 风格(集中式):

webpack({
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
})
复制代码

总结

本文我们通过 babel 和 postcss 的 api 用法分别探究了下内部的实现流程,知道了为什么 postcss 是 postcss([plugins]).process(css).then 的调用链。

之后分别梳理了 babel 的集中式风格 api、postcss 的链式风格 api 的优缺点,并且引申出了其他的一些库的 api 风格。知道了其实做同一件事情是可以设计出不同的 api 的。

希望这篇文章能够让你清楚 postcss 和 babel 内部的流程都是啥,为什么 postcss 的api 调用链条是那样的,它们都是什么 api 风格。链式和集中式都有什么优缺点。

文章分类
前端
文章标签