如何编写 Babel 插件

371 阅读4分钟

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

本文首发于 www.lvdawei.com/post/build-…

本文将带大家了解 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 插件已经可以正常使用了。

感谢&参考: