Appearance
带你玩转 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 的执行顺序
plugin
比preset
先执行- 插件顺序
从前往后
排列。 - presets 顺序是
从后往前
四、插件的分类
babel 插件可以分为以下三类:
- syntax plugin 语法插件
- transform plugin 转换插件
- proposal plugin 提案插件
语法插件
先来看下语法插件长啥样~
语法插件都是以@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 进行了操作和转换。这样就能实现高版本语法到低版本语法的降级。
proposal 插件
可以看到 babel 下有很多plugin-proposal-xxx
这种类型的插件
proposal
一般是基于语法插件
或转换插件
实现的。
那么为什么还会有这种类型的插件呢。
就比如我们想使用可选链
这种语法,但是这种语法还没有纳入标准。就需要实现proposal插件
.
语言特性从提出到标准会有一个过程,分为几个阶段。
- 阶段 0 - Strawman: 只是一个想法,可能用 babel plugin 实现
- 阶段 1 - Proposal: 值得继续的建议
- 阶段 2 - Draft: 建立 spec
- 阶段 3 - Candidate: 完成 spec 并且在浏览器实现
- 阶段 4 - Finished: 会加入到下一年的 es20xx spec
目前optional-chaining
这种语法处于stage-2
阶段。所以需要proposal
插件实现。
当这个语言特性成为标准就会加入到babel-preset-esXXX
但是!!!!
上面的流程中,从 proposal 过渡到babel-preset-esxxx
已经不再适用,babel7
将所有的babel-preset-esxxx
统一为@babel/preset-env
。
在babel6
由于babel-preset-esxx
每年都在变化,维护起来很不方便,babel7
便将其全部打散,用各种transform
、syntax
插件来实现不同版本的语言特性支持。
五、插件和 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
等工具方法options:这个参数就是我们给插件传入的参数对象
dirname:
.babelrc
所在的目录
返回值对象属性:
- inherits 需要继承的插件,其实就是把配置进行了合并。
- pre 在转换代码前调用,可以接收 file 对象,关于 File 可以参考我的另一篇文章,带你玩转 babel 工具链(二)@babel/traverse
- post 代码转换后调用
- manipulateOptions 第一个参数是我们传入的配置,第二个就是
@babel/parser
的配置,可以参考我的另一篇文章,带你玩转 babel 工具链(二)@babel/parser
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
可以看到 File 才是具体的实现,this
只是方便我们获取属性和方法。
所以更推荐大家使用this.file.xxx
来获取插件信息,能获取到的信息更加全面~
总结
这一章我们学习了插件
和presets
的基本用法。了解了两者的执行顺序。
插件
是从左往右
执行的,presets
是从右往左
执行的。
再然后,我们知道插件分类三种类型:syntax
、transform
、proposal
最后学习了插件和preset
的写法。