實(shí)現(xiàn)一個(gè)簡(jiǎn)單的babel插件

Babel 是一個(gè)通用的多功能的 JavaScript 編譯器。


1681804197834.jpg

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

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

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)談及“進(jìn)入”一個(gè)節(jié)點(diǎn)時(shí)柏锄,實(shí)際上是說我們?cè)?strong>訪問他們酿箭,之所以使用這樣的術(shù)語是因?yàn)橛幸粋€(gè)訪問者模式(visitor)概念
上面AST小節(jié)示例有 “type”: “VariableDeclarator” 立莉,諸如此類的樹節(jié)點(diǎn)在訪問時(shí),就會(huì)進(jìn)入visitor對(duì)象聲明的對(duì)應(yīng)類型的成員方法七问。此時(shí)你訪問到的不是節(jié)點(diǎn)本身蜓耻,而是一個(gè)Path,所以可以追蹤樹的父節(jié)點(diǎn)等其它信息械巡。
常用寫法是取path.node刹淌,如上即取到type = VariableDeclarator 的對(duì)象

舉個(gè)??

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);
#生成對(duì)應(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直接讀取
舉個(gè)??

//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插件是如何工作的讥耗。以代碼的角度分析有勾,其實(shí)就是babel/core提供的api有一個(gè)plugins配置屬性,支持傳入自定義的插件而已古程。

單獨(dú)抽離插件

在前端項(xiàng)目當(dāng)中蔼卡,babel插件都是以獨(dú)立模塊出現(xiàn)。這時(shí)候babel.config.js配置文件就派上用場(chǎng)了挣磨。transformFileSync第二個(gè)參數(shù)對(duì)象有一個(gè)babelrc屬性雇逞,默認(rèn)是true。這個(gè)屬性代表是否從項(xiàng)目根目錄讀取babelrc文件獲取presets和plugins配置茁裙。
這時(shí)候就已經(jīng)把babel插件單獨(dú)抽離出來了塘砸。如果你想發(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其實(shí)只是提供了編譯的流程壕翩。如果想要處理代碼蛉迹,還需要提供插件(在編譯的第二部transform)對(duì)節(jié)點(diǎn)進(jìn)行增刪改查,才可以修改節(jié)點(diǎn)生成最終想要的可執(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) {}
}

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

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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市模聋,隨后出現(xiàn)的幾起案子肩民,更是在濱河造成了極大的恐慌,老刑警劉巖链方,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件持痰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡祟蚀,警方通過查閱死者的電腦和手機(jī)工窍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來前酿,“玉大人患雏,你說我怎么就攤上這事“瘴” “怎么了淹仑?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)肺孵。 經(jīng)常有香客問我匀借,道長(zhǎng),這世上最難降的妖魔是什么悬槽? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任怀吻,我火速辦了婚禮瞬浓,結(jié)果婚禮上初婆,老公的妹妹穿的比我還像新娘。我一直安慰自己猿棉,他們只是感情好磅叛,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著萨赁,像睡著了一般弊琴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杖爽,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天敲董,我揣著相機(jī)與錄音,去河邊找鬼慰安。 笑死腋寨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的化焕。 我是一名探鬼主播萄窜,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了查刻?” 一聲冷哼從身側(cè)響起键兜,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穗泵,沒想到半個(gè)月后普气,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡佃延,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年棋电,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苇侵。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赶盔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情强霎,我是刑警寧澤融求,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站闷叉,受9級(jí)特大地震影響脊阴,放射性物質(zhì)發(fā)生泄漏嘿期。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一萄传、第九天 我趴在偏房一處隱蔽的房頂上張望蜜猾。 院中可真熱鬧,春花似錦衍菱、人聲如沸梦碗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斩例。三九已至,卻和暖如春础钠,著一層夾襖步出監(jiān)牢的瞬間叉谜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工很钓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留董栽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓袁稽,卻偏偏與公主長(zhǎng)得像推汽,于是被迫代替她去往敵國(guó)和親民泵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子槽畔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容