本文首發於 dawei.lv
目前主流的前端框架在開發的時候都採用最新的 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.0 打包元件庫併發布到 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 外掛已經可以正常使用了。
感謝&參考: