教練我想寫一個 HelloWorld Babel 外掛

lnyx發表於2018-07-30

本文首發於 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 外掛已經可以正常使用了。

感謝&參考:

相關文章