如何编写 Babel 插件

目前主流的前端框架在开发的时候都采用最新的 ES6+ 语法,大部分的向下兼容工作都交给了 Babel 来处理。通过引入 Babel 插件,我们可以大胆地使用最新或是正在起草中,甚至是根本不在标准中的 jsx 等语法,跟甚至是你自己胡诌的写法!

本文将带大家了解 Babel 是怎么工作的、Babel 插件是怎么工作又是怎么编写的,并写一个与 webpack 集成的最简单的 Babel 插件。

Babel 是怎么工作的

Babel 是一个 JavaScript 编译器。Babel 通过读取源代码,生成抽象语法树(AST),根据插件对 AST 上对应的节点进行修改,修改完毕后根据新的 AST 输出新的代码。

@babel/parse 原名 babylon,Babel 的解析器,用于读取源代码,生成 AST。

来看看 import React from "react"; 转换成 AST 后的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"type": "Program",
"start": 0,
"end": 26,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 26,
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 7,
"end": 12,
"local": {
"type": "Identifier",
"start": 7,
"end": 12,
"name": "React"
}
}
],
"source": {
"type": "Literal",
"start": 18,
"end": 25,
"value": "react",
"raw": "\"react\""
}
}
],
"sourceType": "module"
}

@babel/traverse,Babel 的遍历器,用于维护 AST 的状态,并且负责替换、移除和添加节点。
@babel/types,Babel 的 helper 工具集,包含了构造、验证以及变换 AST 节点的方法。

Babel 插件又是怎么工作的

Babel 为插件提供了访客模式,可以轻松的访问对应类型的 AST 节点,进行修改。先看一个例子:

1
2
3
4
5
mkdir babel-demo && cd babel-demo

npm i -D @babel/core @babel/types

touch index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// index.js
const babel = require("@babel/core");
const code = 'import React from "react";';

const visitor = {
// 我们要修改的节点是 import 声明节点。
ImportDeclaration(path) {
console.log(path.parent.type);
console.log(path.node.type);
console.log(path.node.specifiers[0].local.name);
console.log(path.node.source.value);
}
};

babel.transform(code, {
plugins: [
{
visitor
}
]
});
1
node index.js

可以看到 path 的结构是:

1
2
3
4
{
"parent": { "type": "Program" },
"node": { "type": "ImportDeclaration" }
}

通过 node 节点可以访问到当前节点。

有同学要问了,我怎么知道我当前要修改的东西是什么类型呢??

先把对应的代码片段贴到 astexplorer,看到该语句是一个 ImportDeclaration,然后到 Babel Spec 查询这个语句的细节文档(这是 Babel 基于 ESTree Spec 做的修改版)。

我们要现在把 import React from "react"; 修改成 import React from "vue";,来看看怎么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.js
const babel = require("@babel/core");
const code = 'import React from "react";';

const visitor = {
ImportDeclaration(path) {
path.node.source.value = "vue";
}
};

const res = babel.transform(code, {
plugins: [
{
visitor
}
]
});

console.log(res.code);
// import React from "vue";

Babel 插件是怎么写的

来看看我们写的插件如何集成到 webpack 里,毕竟我们是要拿来用的。

1
2
3
4
5
6
7
8
9
// src/index.js
// 这里我们打算写一个插件将 "moduleA" 改成 "moduleB"
import module from "moduleA";


// src/moduleB.js
export default () => {
console.log("B");
};
1
2
3
4
5
// .babelrc
{
"presets": [["@babel/preset-env"]],
"plugins": ["myplugin"]
}

Babel 插件的命名方式为 babel-plugin-${your-plugin-name}。npm 打包发布方法可参考 使用 Webpack4.0 打包组件库并发布到 npm 这篇文章,这里为了方便,直接在 node_modules 下写了

1
2
3
4
5
6
7
8
9
10
// node_modules/babel-plugin-myplugin/index.js
module.exports = function() {
return {
visitor: {
ImportDeclaration(path) {
path.node.source.value = "./moduleB";
}
}
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// dist/main.js
/******/ (function(modules) {
// webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {}; // The require function
/******/
/******/ /******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if (installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/
} // Create a new module (and put it into the cache)
/******/ /******/ var module = (installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/
}); // Execute the module function
/******/
/******/ /******/ modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
); // Flag the module as loaded
/******/
/******/ /******/ module.l = true; // Return the exports of the module
/******/
/******/ /******/ return module.exports;
/******/
} // expose the modules object (__webpack_modules__)
/******/
/******/
/******/ /******/ __webpack_require__.m = modules; // expose the module cache
/******/
/******/ /******/ __webpack_require__.c = installedModules; // define getter function for harmony exports
/******/
/******/ /******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if (!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
enumerable: true,
get: getter
});
/******/
}
/******/
}; // define __esModule on exports
/******/
/******/ /******/ __webpack_require__.r = function(exports) {
/******/ if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module"
});
/******/
}
/******/ Object.defineProperty(exports, "__esModule", { value: true });
/******/
}; // create a fake namespace object // mode & 1: value is a module id, require it // mode & 2: merge all properties of value into the ns // mode & 4: return value when already ns object // mode & 8|1: behave like require
/******/
/******/ /******/ /******/ /******/ /******/ /******/ __webpack_require__.t = function(
value,
mode
) {
/******/ if (mode & 1) value = __webpack_require__(value);
/******/ if (mode & 8) return value;
/******/ if (
mode & 4 &&
typeof value === "object" &&
value &&
value.__esModule
)
return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, "default", {
enumerable: true,
value: value
});
/******/ if (mode & 2 && typeof value != "string")
for (var key in value)
__webpack_require__.d(
ns,
key,
function(key) {
return value[key];
}.bind(null, key)
);
/******/ return ns;
/******/
}; // getDefaultExport function for compatibility with non-harmony modules
/******/
/******/ /******/ __webpack_require__.n = function(module) {
/******/ var getter =
module && module.__esModule
? /******/ function getDefault() {
return module["default"];
}
: /******/ function getModuleExports() {
return module;
};
/******/ __webpack_require__.d(getter, "a", getter);
/******/ return getter;
/******/
}; // Object.prototype.hasOwnProperty.call
/******/
/******/ /******/ __webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}; // __webpack_public_path__
/******/
/******/ /******/ __webpack_require__.p = ""; // Load entry module and return exports
/******/
/******/
/******/ /******/ return __webpack_require__(
(__webpack_require__.s = "./src/index.js")
);
/******/
})(
/************************************************************************/
/******/ {
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ function(module, exports, __webpack_require__) {
"use strict";
// **关键代码在这里,这里的 moduleA 已经被改成 moduleB 了**
eval(
'\n\nvar _moduleB = _interopRequireDefault(__webpack_require__(/*! ./moduleB */ "./src/moduleB.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n//# sourceURL=webpack:///./src/index.js?'
);

/***/
},

/***/ "./src/moduleB.js":
/*!************************!*\
!*** ./src/moduleB.js ***!
\************************/
/*! no static exports found */
/***/ function(module, exports, __webpack_require__) {
"use strict";
eval(
'\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.default = void 0;\n\nvar _default = function _default() {\n console.log("B");\n};\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./src/moduleB.js?'
);

/***/
}

/******/
}
);

可以看到 moduleB 已经被打包进来了。

至此,我们最简单的 Babel 插件已经可以正常使用了。

感谢&参考:

评论