實現(xiàn)一個簡單的babel插件

Babel 是一個通用的多功能的 JavaScript 編譯器。


1681804197834.jpg

瀏覽器編譯你的js代碼,需要把js轉(zhuǎn)化成ast。2015年es6語法發(fā)布,但瀏覽器還普遍不支持es6語法。于是催生出babel模塊,默認(rèn)支持js到ast的轉(zhuǎn)化,并通過修改ast,將es6的特性代碼轉(zhuǎn)化為同效用的es5代碼,同時也提供了ast的操作函數(shù)。

Babel操作流程:js代碼 -> 原AST -> babel處理 -> 修改后的AST -> 修改后的js代碼 -> 交給瀏覽器編譯
即: 解析(parse),轉(zhuǎn)換(transform),生成(generate)
@babel/core
從名稱就可以看出這是babel的核心包。首先介紹一下這個包,這個包集成了上篇文章講述的@babel/parser,@babel/traverse,@babel/generator,@babel/types這些包。也就是說這個包具備解析(parse),轉(zhuǎn)換(traverse),生成代碼的能力,而且還擴展了其它功能。

npm install --save-dev @babel/cli @babel/core
AST解析示例

在線js轉(zhuǎn)換到AST工具:https://astexplorer.net/#/

let a = 1
{
  "type": "Program",
  "start": 0,
  "end": 10,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 10,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}

visitor(訪問者模式)

當(dāng)談及“進入”一個節(jié)點時,實際上是說我們在訪問他們,之所以使用這樣的術(shù)語是因為有一個訪問者模式(visitor)概念
上面AST小節(jié)示例有 “type”: “VariableDeclarator” ,諸如此類的樹節(jié)點在訪問時,就會進入visitor對象聲明的對應(yīng)類型的成員方法。此時你訪問到的不是節(jié)點本身,而是一個Path,所以可以追蹤樹的父節(jié)點等其它信息。
常用寫法是取path.node,如上即取到type = VariableDeclarator 的對象

舉個??

const babel = require("@babel/core")
const code = `a+b+c`
const obj = babel.transformSync(code, {
    plugins: [
        function MyPlugin(babel) {
            return {
                visitor: {
                    Identifier(path) {
                      console.log('visiting====>',path.node.name)
                    },
                }
            }
        }
    ]
});

console.log(obj.code);
#生成對應(yīng)的AST
{
  "type": "Program",
  "start": 0,
  "end": 5,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 5,
      "expression": {
        "type": "BinaryExpression",
        "start": 0,
        "end": 5,
        "left": {
          "type": "BinaryExpression",
          "start": 0,
          "end": 3,
          "left": {
            "type": "Identifier",
            "start": 0,
            "end": 1,
            "name": "a"
          },
          "operator": "+",
          "right": {
            "type": "Identifier",
            "start": 2,
            "end": 3,
            "name": "b"
          }
        },
        "operator": "+",
        "right": {
          "type": "Identifier",
          "start": 4,
          "end": 5,
          "name": "c"
        }
      }
    }
  ],
  "sourceType": "module"
}
#代碼執(zhí)行后的結(jié)果
visiting====> a
visiting====> b
visiting====> c

下邊修改一下代碼,改為從文件讀取源代碼,使用babel.transformFileSync直接讀取
舉個??

//file.js
a+b+c
//index.js
const babel = require("@babel/core")
const path = require("path")
const file = path.resolve(__dirname,'./file.js')
const obj = babel.transformFileAsync(file, {
    plugins: [
        function MyPlugin(babel) {
            return {
                visitor: {
                    Identifier(path) {
                      console.log('visiting====>',path.node.name)
                    },
                }
            }
        }
    ]
});
  console.log(obj.code);

從以上講述清楚的知道了babel插件是如何工作的。以代碼的角度分析,其實就是babel/core提供的api有一個plugins配置屬性,支持傳入自定義的插件而已。

單獨抽離插件

在前端項目當(dāng)中,babel插件都是以獨立模塊出現(xiàn)。這時候babel.config.js配置文件就派上用場了。transformFileSync第二個參數(shù)對象有一個babelrc屬性,默認(rèn)是true。這個屬性代表是否從項目根目錄讀取babelrc文件獲取presets和plugins配置。
這時候就已經(jīng)把babel插件單獨抽離出來了。如果你想發(fā)布到npm上,你只需要按照發(fā)布規(guī)范和步驟發(fā)布,然后安裝下來,在babel.config.js配置就可以了。
接下來 我們編譯一段es6 class語法,看看編譯后的結(jié)果:

code: 'class Person {\n' +
    '  constructor(name) {\n' +
    '    this.name = name;\n' +
    '  }\n' +
    '  say() {\n' +
    '    console.log(this.name);\n' +
    '  }\n' +
    '}\n' +
    'const person = new Person("張三");\n' +
    'person.say();',

細(xì)心的小伙伴發(fā)現(xiàn)了,編譯后的代碼沒有任何變化,我們之前也講過babel/core其實只是提供了編譯的流程。如果想要處理代碼,還需要提供插件(在編譯的第二部transform)對節(jié)點進行增刪改查,才可以修改節(jié)點生成最終想要的可執(zhí)行代碼。
這里如果需要處理es6及以上的語法,需要使用babel/preset-env預(yù)設(shè)包(就是插件的集合),安裝該包,修改.babelrc配置

npm install --save-dev @babel/preset-env
  module.exports = {
    "presets": [
        "@babel/preset-env"
    ],
    "plugins":["./plugin.js"]
  }
//編譯之后的代碼
code: '"use strict";\n' +
    '\n' +
    'function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }\n' +
    'function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n' +
    'function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }\n' +
    'function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }\n' +
    'function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }\n' +
    'function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }\n' +
    'var Person = /*#__PURE__*/function () {\n' +
    '  function Person(name) {\n' +
    '    _classCallCheck(this, Person);\n' +
    '    this.name = name;\n' +
    '  }\n' +
    '  _createClass(Person, [{\n' +
    '    key: "say",\n' +
    '    value: function say() {\n' +
    '      console.log(this.name);\n' +
    '    }\n' +
    '  }]);\n' +
    '  return Person;\n' +
    '}();\n' +
    'var person = new Person("張三");\n' +
    'person.say();',
  map: null,
  sourceType: 'script',
  externalDependencies: Set(0) {}
}

由上圖可知這個表達(dá)式對應(yīng)CallExpression類型的節(jié)點,該節(jié)點有一個callee屬性對應(yīng)的是MemberExpression類型的節(jié)點,MemberExpression節(jié)點又有一個object屬性為Identifier類型的節(jié)點,只有Identifier節(jié)點名字是console才可以判斷當(dāng)前節(jié)點是console類型的表達(dá)式。然后執(zhí)行path.remove方法就可以刪除當(dāng)前節(jié)點。
代碼如下

module.exports=function(babel){
    return{
        visitor:{
            CallExpression(path){
                if(path.node.callee&&babel.types.isIdentifier(path.node.callee.object,{name:'console'})){
                    path.remove()
                }
            }
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容