实现一个简单的babel插件

Babel 是一个通用的多功能的 JavaScript 编译器。


1681804197834.jpg

浏览器编译你的js代码,需要把js转化成ast。2015年es6语法发布,但浏览器还普遍不支持es6语法。于是催生出babel模块,默认支持js到ast的转化,并通过修改ast,将es6的特性代码转化为同效用的es5代码,同时也提供了ast的操作函数。

Babel操作流程:js代码 -> 原AST -> babel处理 -> 修改后的AST -> 修改后的js代码 -> 交给浏览器编译
即: 解析(parse),转换(transform),生成(generate)
@babel/core
从名称就可以看出这是babel的核心包。首先介绍一下这个包,这个包集成了上篇文章讲述的@babel/parser,@babel/traverse,@babel/generator,@babel/types这些包。也就是说这个包具备解析(parse),转换(traverse),生成代码的能力,而且还扩展了其它功能。

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

在线js转换到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(访问者模式)

当谈及“进入”一个节点时,实际上是说我们在访问他们,之所以使用这样的术语是因为有一个访问者模式(visitor)概念
上面AST小节示例有 “type”: “VariableDeclarator” ,诸如此类的树节点在访问时,就会进入visitor对象声明的对应类型的成员方法。此时你访问到的不是节点本身,而是一个Path,所以可以追踪树的父节点等其它信息。
常用写法是取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);
#生成对应的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"
}
#代码执行后的结果
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配置属性,支持传入自定义的插件而已。

单独抽离插件

在前端项目当中,babel插件都是以独立模块出现。这时候babel.config.js配置文件就派上用场了。transformFileSync第二个参数对象有一个babelrc属性,默认是true。这个属性代表是否从项目根目录读取babelrc文件获取presets和plugins配置。
这时候就已经把babel插件单独抽离出来了。如果你想发布到npm上,你只需要按照发布规范和步骤发布,然后安装下来,在babel.config.js配置就可以了。
接下来 我们编译一段es6 class语法,看看编译后的结果:

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();',

细心的小伙伴发现了,编译后的代码没有任何变化,我们之前也讲过babel/core其实只是提供了编译的流程。如果想要处理代码,还需要提供插件(在编译的第二部transform)对节点进行增删改查,才可以修改节点生成最终想要的可执行代码。
这里如果需要处理es6及以上的语法,需要使用babel/preset-env预设包(就是插件的集合),安装该包,修改.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) {}
}

由上图可知这个表达式对应CallExpression类型的节点,该节点有一个callee属性对应的是MemberExpression类型的节点,MemberExpression节点又有一个object属性为Identifier类型的节点,只有Identifier节点名字是console才可以判断当前节点是console类型的表达式。然后执行path.remove方法就可以删除当前节点。
代码如下

module.exports=function(babel){
    return{
        visitor:{
            CallExpression(path){
                if(path.node.callee&&babel.types.isIdentifier(path.node.callee.object,{name:'console'})){
                    path.remove()
                }
            }
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容