目前主流的前端框架在开发的时候都采用最新的 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 后的结构:
{
"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 节点,进行修改。先看一个例子:
mkdir babel-demo && cd babel-demo
npm i -D @babel/core @babel/types
touch index.js
// 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
}
]
});
node index.js
可以看到 path 的结构是:
{
"parent": { "type": "Program" },
"node": { "type": "ImportDeclaration" }
}
通过 node 节点可以访问到当前节点。
有同学要问了,我怎么知道我当前要修改的东西是什么类型呢??
先把对应的代码片段贴到 astexplorer,看到该语句是一个
ImportDeclaration,然后到 Babel Spec 查询这个语句的细节文档(这是 Babel 基于 ESTree Spec 做的修改版)。
我们要现在把 import React from "react"; 修改成 import React from "vue";,来看看怎么实现:
// 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 里,毕竟我们是要拿来用的。
// src/index.js
// 这里我们打算写一个插件将 "moduleA" 改成 "moduleB"
import module from "moduleA";
// src/moduleB.js
export default () => {
console.log("B");
};
// .babelrc
{
"presets": [["@babel/preset-env"]],
"plugins": ["myplugin"]
}
Babel 插件的命名方式为 babel-plugin-${your-plugin-name}。npm 打包发布方法可参考 使用Webpack4打包组件库并发布到npm 这篇文章,这里为了方便,直接在 node_modules 下写了
// node_modules/babel-plugin-myplugin/index.js
module.exports = function() {
return {
visitor: {
ImportDeclaration(path) {
path.node.source.value = "./moduleB";
}
}
};
};
// 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 插件已经可以正常使用了。
感谢&参考: