編寫自己的Babel外掛(一)

長可發表於2018-12-04

教你如何寫元件按需載入外掛,let/const轉var,箭頭函式轉普通函式

Note:

前置知識:瞭解babel的使用,瞭解JavaScript語法樹 安裝babel-cli, babel-core

我們的打包檔案

import antd, { Table } from 'antd';

let arrow = () => {};
const component = <Table />;
複製程式碼

.babelrc配置

{
  "presets": ["react"],
  "plugins": [
    [
      "lessimport", //這是你開發的plugin名稱,包名命名為babel-plugin-xxxx
      {
        // 這些屬性可以隨意,最後可以在opts裡面訪問得到
        "libraryName": "antd",
        "modulePath": "/lib/{moduleName}",
        "styleSuffix": "css",
        "stylePath": "/lib/{moduleName}/style/index.{styleSuffix}"
      }
    ]
  ]
}
複製程式碼

外掛程式碼編寫

這是進行轉換操作的程式碼,在你新建的專案node_modules下面新建babel-plugin-lessimport資料夾 新建index.js,寫入以下程式碼

const babel = require('babel-core');
const type = require('babel-types');

const visitor = {
  ImportDeclaration(path, ref = { opts: {} }) {
    const specifiers = path.node.specifiers;
    const source = path.node.source;
    const libraryName = source.value;
    /**
     * 第二個判斷條件是判斷import語句裡面有沒有使用 import {xxx} 的語法,如果有,就替換
     * 不加這個條件的後果就是,死迴圈
     */
    if (libraryName === ref.opts.libraryName && specifiers.find(specifier => type.isImportSpecifier(specifier))) {
      const declarationNodes = [];
      specifiers.forEach(specifier => {
        /** 不是預設匯入的
         *  為什麼要這麼判斷,因為可能會有這種寫法,import React, { Component } from 'react';
         */
        if (!type.isImportDefaultSpecifier(specifier)) {
          declarationNodes.push(
            /**
             * importDeclaration 第一個引數是import xxx from module 裡面的xxx
             * xxx可以是 {yyy} => [importSpecifier], yyy => [importDefaultSpecifier], 空 => []
             * 第二個引數是module字串
             */
            type.importDeclaration(
              // 新增一個預設匯入的 specifier,可以多個,這樣就是import xxx, yyy from "test"
              [type.importDefaultSpecifier(specifier.local)],
              // type.stringLiteral 返回一個字面量字串
              type.stringLiteral( // {moduleName} 是在配置裡面配置的,代表需要引入模組的名字
                `${libraryName}${ref.opts.modulePath.replace('{moduleName}', specifier.local.name.toLowerCase())}`
              )
            )
          );
          // 引入css 或者 less,可配置
          if (ref.opts.styleSuffix) {
            declarationNodes.push(
              type.importDeclaration(
                [], // 空的specifier, 這樣就是 import from "xxxx"
                type.stringLiteral(
                  `${libraryName}${ref.opts.stylePath
                    .replace('{moduleName}', specifier.local.name.toLowerCase())
                    .replace('{styleSuffix}', ref.opts.styleSuffix)}`
                )
              )
            );
          }
          return;
        }
        declarationNodes.push(
          type.importDeclaration([type.importDefaultSpecifier(specifier.local)], type.stringLiteral(libraryName))
        );
      });
      // 一個節點替換成多個
      path.replaceWithMultiple(declarationNodes);
    }
  },
  /**
   * 轉換箭頭函式很簡單,直接把id, 引數,函式體,是否generator,是否async函式都賦給新函式
   * 然後替換節點即可
   */ 
  ArrowFunctionExpression(path) {
    const node = path.node;
    if (node.type === 'ArrowFunctionExpression') {
      path.replaceWith(type.functionExpression(node.id, node.params, node.body, node.generator, node.async));
    }
  },
  /**
   * 把let, const轉換成var 
   */
  VariableDeclaration(path) {
    const node = path.node;

    if (node.kind === 'let' || node.kind === 'const') { 
      // 變數宣告的kind, 可以是var let const
      // 然後第二個引數是宣告的變數,let a, b, c這裡的a, b, c就是node.declarations
      const varNode = type.variableDeclaration('var', node.declarations);
      path.replaceWith(varNode);
    }
  },
};
module.exports = function() {
  // 名稱必須是visitor
  return { visitor };
};
複製程式碼

第一個 Node 是 source 的值,第二個是 specifiers 的值 你也可以去astexplorer看對應的語法樹(網址在文末給出)

20181130232913.png

執行,檢視外掛轉換效果

對程式碼執行

babel index.js --out-file out.js
複製程式碼

轉換後的程式碼為,說明轉換成功了

import antd from 'antd';
import Table from 'antd/lib/table';
import 'antd/lib/table/style/index.css';

var arrow = function (x = 5) {
  console.log(x);
};

var component = React.createElement(Table, null);
複製程式碼

看到這就要恭喜你啦,再也不用寫這種囉裡囉嗦,打包出來檔案體積還大的程式碼了

import {Table} from "antd";
import from "antd/lib/table/style/index.css";
複製程式碼

使用外掛後只需要第一句即可完成按需引入和自動引入css 還可以配置多個不同的庫

以後面試當面試官問你你寫過babel外掛時再也不用慌了(๑•̀ㅂ•́)و✧

總結:

  • 外掛名稱必須是babel-plugin-xxx
  • .babelrc可以新增你的外掛以及配置引數
  • 外掛編寫必須遵循規範
  • 當你相改變某個節點時,使用該節點名(首字母大寫)稱作為函式名進行訪問,使用path.replaceWith或者repalceWithMultiple替換

如果你覺得我講的還不錯的話,歡迎watch,star,fork一條龍 (*/ω\*)

參考資料:

babel-types: 官網

babel-handbook: Babel 手冊

ast-explorer: AST 視覺化

es-tree JavaScript 語法樹規則

相關文章