Skip to content

带你玩转 babel 工具链(四)babel 插件和 preset

一、前言

在前面三章,我们串联了整个的代码转换流程: parse, transform, generator。这也是babel插件最核心的内容,都是基于上面的过程实现的。本文将带大家,详细了解插件的用法、执行机制以及一些细节。

往期回顾:

二、插件和 preset 的基本配置

如果你的插件和 preset 是 npm 模块,可以直接引用包名:

js
// .babelrc
{
  "plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"],
  "presets": ["@babel/preset-env"]
}
// .babelrc
{
  "plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"],
  "presets": ["@babel/preset-env"]
}

也支持配置相对路径的配置:

js
{
  "plugins": ["./node_modules/asdf/plugin"],
  "presets": ["./node_modules/asdf/plugin"]
}
{
  "plugins": ["./node_modules/asdf/plugin"],
  "presets": ["./node_modules/asdf/plugin"]
}

当然你也可以通过下面这种方式传递参数,在数组中再嵌套一个数组,第一项是包名,第二项是参数配置

js
{
  "plugins": [
    [
      "transform-async-to-module-method",
      // 参数配置
      {
        "module": "bluebird",
        "method": "coroutine"
      }
    ]
  ],
  "presets": [
    [
      "@babel/preset-env",
      // 参数配置
      {
        "xxx": "xxx"
      }
    ]
  ]
}
{
  "plugins": [
    [
      "transform-async-to-module-method",
      // 参数配置
      {
        "module": "bluebird",
        "method": "coroutine"
      }
    ]
  ],
  "presets": [
    [
      "@babel/preset-env",
      // 参数配置
      {
        "xxx": "xxx"
      }
    ]
  ]
}

三、插件和 preset 的执行顺序

  • pluginpreset先执行
  • 插件顺序从前往后排列。
  • presets 顺序是从后往前

image.png

四、插件的分类

babel 插件可以分为以下三类:

  • syntax plugin 语法插件
  • transform plugin 转换插件
  • proposal plugin 提案插件

语法插件

先来看下语法插件长啥样~

image.png

语法插件都是以@babel/plugin-syntax-xxx格式引入。

它的实现依赖了@babel/parser的支持,我们知道@babel/parser的配置中是有个plugins配置的,如下

js
const parser = require("@babel/parser");

const ast = parser.parse(
  `
  const a: number = 1
`,
  {
    plugins: ["typescript"], // 配置语法支持
  }
);

console.log(ast);
const parser = require("@babel/parser");

const ast = parser.parse(
  `
  const a: number = 1
`,
  {
    plugins: ["typescript"], // 配置语法支持
  }
);

console.log(ast);

另外我们也可以在.babelrc中配置:

js
{
  "parserOpts": {
    "plugins": ["jsx", "flow"]
  }
}
{
  "parserOpts": {
    "plugins": ["jsx", "flow"]
  }
}

常见的配置有:

  • objectRestSpread:对象的解构语法
  • jsx:jsx 的 dsl 支持
  • flow:flow 语法
  • typescript: ts 语法
  • optionalChaining:可选链语法
  • classProperties、classPrivateProperties、classPrivateMethods:类(私)属性、方法的语法支持
  • ...

具体的讲解请参考我的另一篇文章带你玩转 babel 工具链(一)@babel/parser

转换插件

这种插件的实现依赖了@babel/traverse,统一用@babel/plugin-transform-xxx开头。它们其实都是对 AST 进行了操作和转换。这样就能实现高版本语法到低版本语法的降级。

image.png

proposal 插件

可以看到 babel 下有很多plugin-proposal-xxx这种类型的插件

image.png

proposal 一般是基于语法插件转换插件实现的。

那么为什么还会有这种类型的插件呢。

就比如我们想使用可选链这种语法,但是这种语法还没有纳入标准。就需要实现proposal插件.

语言特性从提出到标准会有一个过程,分为几个阶段

  • 阶段 0 - Strawman: 只是一个想法,可能用 babel plugin 实现
  • 阶段 1 - Proposal: 值得继续的建议
  • 阶段 2 - Draft: 建立 spec
  • 阶段 3 - Candidate: 完成 spec 并且在浏览器实现
  • 阶段 4 - Finished: 会加入到下一年的 es20xx spec

目前optional-chaining这种语法处于stage-2阶段。所以需要proposal插件实现。

image.png

当这个语言特性成为标准就会加入到babel-preset-esXXX

但是!!!!

上面的流程中,从 proposal 过渡到babel-preset-esxxx已经不再适用,babel7将所有的babel-preset-esxxx统一为@babel/preset-env

babel6由于babel-preset-esxx每年都在变化,维护起来很不方便,babel7便将其全部打散,用各种transformsyntax插件来实现不同版本的语言特性支持。

image.png

五、插件和 preset 的基本写法

上面我们介绍了插件的简单用法、执行顺序、分类,那么下面我们来一块写一下插件和preset

插件

我们先看下一个简单的插件是怎么写的,有三种写法,最终都返回了一个对象

js
// 第一种写法
export default function(api, options, dirname) {
  return {
    inherits: parentPlugin,
    pre(file) {
       // TODO
    },
    manipulateOptions(options, parserOptions) {
    },
    post(file) {
      // TODO
    },
    visitor: {
      // visitor contents
    }
  };
};

// 第二种写法
export default {
    inherits: parentPlugin,
    pre(file) {
       // TODO
    },
    manipulateOptions(options, parserOptions) {
    },
    post(file) {
      // TODO
    },
    visitor: {
      // visitor contents
    }
};

// 第三种(写起来有类型提示)
const { declare } = require("@babel/helper-plugin-utils")

export default declare((api, options, dirname) => {
  return {
    inherits: parentPlugin,
    pre(file) {
       // TODO
    },
    manipulateOptions(options, parserOptions) {
    },
    post(file) {
      // TODO
    },
    visitor: {
      // visitor contents
    }
  };
})
// 第一种写法
export default function(api, options, dirname) {
  return {
    inherits: parentPlugin,
    pre(file) {
       // TODO
    },
    manipulateOptions(options, parserOptions) {
    },
    post(file) {
      // TODO
    },
    visitor: {
      // visitor contents
    }
  };
};

// 第二种写法
export default {
    inherits: parentPlugin,
    pre(file) {
       // TODO
    },
    manipulateOptions(options, parserOptions) {
    },
    post(file) {
      // TODO
    },
    visitor: {
      // visitor contents
    }
};

// 第三种(写起来有类型提示)
const { declare } = require("@babel/helper-plugin-utils")

export default declare((api, options, dirname) => {
  return {
    inherits: parentPlugin,
    pre(file) {
       // TODO
    },
    manipulateOptions(options, parserOptions) {
    },
    post(file) {
      // TODO
    },
    visitor: {
      // visitor contents
    }
  };
})

下面来一一解释参数和返回值的含义

参数:

  • api

    这个参数包含了很多 api,例如我们常用的traverse, types, template等工具方法

    image.png

  • options:这个参数就是我们给插件传入的参数对象

  • dirname:.babelrc所在的目录

返回值对象属性:

preset

preset 与插件的区别:preset 是多个 plugin 的集合,而 plugin 只对单一的功能进行转换

就比如我们常用的@babel/preset-env, @babel/preset-react,其内部包含了多个 babel转换插件,语法插件等。

那我们如何写一个 preset 呢?其实也很简单

js
export default function preset(api, options) {
  return {
    plugins: ["myPlugin1", "myPlugin2"],
    presets: [
      [
        "preset1",
        {
          a: 1,
        },
      ],
      "preset2",
    ],
  };
}
export default function preset(api, options) {
  return {
    plugins: ["myPlugin1", "myPlugin2"],
    presets: [
      [
        "preset1",
        {
          a: 1,
        },
      ],
      "preset2",
    ],
  };
}

可以看到,与插件的写法类似,也需要实现一个方法,并且返回一个对象。

而且preset的写法和.babelrc的配置很像,既可以配置plugins,也支持配置presets

六、插件上下文

在 babel 插件中,也有插件上下文的概念,类似rollup的插件上下文那样,可以获取到文件、插件的一些信息。

@babel/core的源码中,插件的上下文对象名为PluginPass

可以看到下面的源码中,包含了很多属性和方法

js
// node_modules/@babel/core/lib/transformation/plugin-pass.js
class PluginPass {
  constructor(file, key, options) {
    this._map = new Map();
    this.key = void 0;
    this.file = void 0;
    this.opts = void 0;
    this.cwd = void 0;
    this.filename = void 0;
    this.key = key;
    this.file = file;
    this.opts = options || {};
    this.cwd = file.opts.cwd;
    this.filename = file.opts.filename;
  }

  set(key, val) {
    this._map.set(key, val);
  }

  get(key) {
    return this._map.get(key);
  }

  availableHelper(name, versionRange) {
    return this.file.availableHelper(name, versionRange);
  }

  addHelper(name) {
    return this.file.addHelper(name);
  }

  addImport() {
    return this.file.addImport();
  }

  buildCodeFrameError(node, msg, _Error) {
    return this.file.buildCodeFrameError(node, msg, _Error);
  }
}
// node_modules/@babel/core/lib/transformation/plugin-pass.js
class PluginPass {
  constructor(file, key, options) {
    this._map = new Map();
    this.key = void 0;
    this.file = void 0;
    this.opts = void 0;
    this.cwd = void 0;
    this.filename = void 0;
    this.key = key;
    this.file = file;
    this.opts = options || {};
    this.cwd = file.opts.cwd;
    this.filename = file.opts.filename;
  }

  set(key, val) {
    this._map.set(key, val);
  }

  get(key) {
    return this._map.get(key);
  }

  availableHelper(name, versionRange) {
    return this.file.availableHelper(name, versionRange);
  }

  addHelper(name) {
    return this.file.addHelper(name);
  }

  addImport() {
    return this.file.addImport();
  }

  buildCodeFrameError(node, msg, _Error) {
    return this.file.buildCodeFrameError(node, msg, _Error);
  }
}

我们可以在插件中直接获取或使用,存在如下属性和方法:

js
module.exports = function (api, options, dirname) {
  return {
    pre(file) {
      this.key; // 插件模块名,如果是自定义的插件,那么就是一个绝对路径
      this.file; // File对象
      this.cwd; // 当前工作目录
      this.filename; // 需要被babel处理的文件路径
      this.opts; // 插件配置
      this._map; // 私有的属性
      this.addHelper("typeof"); // 添加运行时helper
      this.set("key", val); // 在上下文中添加一个公共属性
      this.get("key"); // 获取公共属性
      this.addImport(); // 添加一个导入(已废弃)
    },
    visitor: {
      enter() {},
    },
    post() {},
  };
};
module.exports = function (api, options, dirname) {
  return {
    pre(file) {
      this.key; // 插件模块名,如果是自定义的插件,那么就是一个绝对路径
      this.file; // File对象
      this.cwd; // 当前工作目录
      this.filename; // 需要被babel处理的文件路径
      this.opts; // 插件配置
      this._map; // 私有的属性
      this.addHelper("typeof"); // 添加运行时helper
      this.set("key", val); // 在上下文中添加一个公共属性
      this.get("key"); // 获取公共属性
      this.addImport(); // 添加一个导入(已废弃)
    },
    visitor: {
      enter() {},
    },
    post() {},
  };
};

以上的方法和属性其实都依赖了File, 我们定位源码node_modules/@babel/core/lib/transformation/file/file.js

image.png

可以看到 File 才是具体的实现,this只是方便我们获取属性和方法。

所以更推荐大家使用this.file.xxx来获取插件信息,能获取到的信息更加全面~

总结

这一章我们学习了插件presets的基本用法。了解了两者的执行顺序。

插件从左往右执行的,presets从右往左执行的。

再然后,我们知道插件分类三种类型:syntaxtransformproposal

最后学习了插件和preset的写法。

参考