小白也能看懂的AST抽象语法树+babel插件开发教程

前言

文本babel插件开发将用到babel-cli脚手架环境,并结合一个AST抽象语法树 查询网站AST Explorer,让你开发插件时能够快速定位对应js代码的AST节点, 并通过几个demo让你快速了解如何开发一个babel插件。

关键词: 速成babel、babel插件、AST/抽象语法树。

babel简介

Babel 是一个 JavaScript 编译器

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js
  • 源码转换(codemods)
  • ......

babel-cli环境搭建

新建一个babel项目目录hello-babel,并在该目录下运行以下命令安装@babel/cli脚手架:

cmd 复制代码
npm install --save-dev @babel/cli

在根目录下添加src文件目录,用来编写我们需要进行编译的js文件,并在该文件下添加一个index.js文件,下文我们将就在这个文件下进行编写需要进行代码转换的相关代码。

index.js

js 复制代码
const fn = () => {
  console.log('hello babel')
}

有了以上基础后,这时我们就可以通过在根目录的cmd命令行中输入./node_modules/.bin/babel src --out-dir lib来对编写的js代码进行编译了,如果你使用的是npm@5.2.0以上版本,可以通过npx babel src --out-dir lib来执行编译。

下面解释以上命令代表的含义:

src:代表编译该目录下的所有js文件

src/index.js:只编译该目录下的index.js文件

--out-dir lib:将编译后的js文件输出到根目录下的lib文件夹下

运行以上命令后,发现把src/index.js文件原样输出到了lib/index.js文件夹下:

这是由于我们还未进行插件编写,在编写插件之前有必要简单了解一下: 一个插件其实就是一个函数,该函数会返回一个对象,对象里具有一个visitor属性,该属性也是一个对象,用来观察对应节点的变化

第一个babel插件

下面来写一个简单的修改节点名称的插件说明如何观察节点对对其进行编辑。

这里给大家推荐一个查看AST抽象语法树结构的网站:AST Explorer

如下图所示,把src/index.js的代码copy到该网站下,左边是你的代码,右边可查看对应的AST树或者JSON结构。

我们需要修改fn的方法名,鼠标点击fn名称,右边会自动定位到对应的节点。

上图可得出,fn方法得节点类型为Identifier,我们可以编写以下代码,并观察Identifier节点。

js 复制代码
module.exports = function () {
  return {
    visitor: {
      Identifier(path) {
        // ...
      },
    },
  };
}

Identifier方法可接收到一个参数,记录Identifier类型节点的相关信息,我们可通过path.node获取当前节点信息。

js 复制代码
Identifier(path) {
  let name = path.node.name;
  if (name === 'fn') {
    console.log(path.node)
  }
},

// path.node打印信息如下
Node {
  type: 'Identifier',
  start: 6,
  end: 8,
  loc: SourceLocation {
    start: Position { line: 1, column: 6, index: 6 },
    end: Position { line: 1, column: 8, index: 8 },
    filename: undefined,
    identifierName: 'fn'
  },
  name: 'fn',
  leadingComments: undefined,
  innerComments: undefined,
  trailingComments: undefined
}

我们要修改Identifier类型的节点名称,只需需求path.node.name的值即可:

plugins/change-name.js

js 复制代码
module.exports = function () {
  return {
    visitor: {
      // 编辑节点名称
    Identifier(path) {
      let name = path.node.name;
      if (name === 'fn') {
        console.log(path.node)
        path.node.name = 'fn2'
      }
    },
    },
  };
}

以上我们就完成了一个把Identifier节点名称为fn的修改为fn2babel插件,想要使用这个插件还需要创建一个配置文件进行引用。

创建babel配置文件

babel配置可在根目录下的这4种文件里面进行配置:

babel.config.json

.babelrc.json

package.json

babel.config.js

这里我们选择在根目录下创建babel.config.js文件进行配置:

js 复制代码
module.exports = {
  // 引入当前目录下的plugins/change-name.js插件
  plugins: ['./plugins/change-name.js']
}

执行babel编译

cmd 复制代码
npx babel src src --out-dir lib

可在lib/index.js文件下看到名称修改成功了!

进阶:向console.log()方法下追加参数插件

还是老样子,copy代码先查看一下该观察哪个类型的节点:

上图可看出,我们可以通过观察CallExpression节点来获取console.log相关的信息,并且AST中有一个arguments数组存放着console.log的参数节点,所有我们可以向arguments数组添加我们想要打印的参数:

js 复制代码
// const t = require('@babel/types');
// 编辑进阶版:向console.log添加参数
module.exports = function({ types: t }) {
  return {
    visitor: {
      CallExpression({node}) {
        // 由上图得出的console.log所在节点类型条件,来查找出console.log方法
        if (t.isMemberExpression(node.callee)) {
          if (node.callee.object.name === "console") {
            // 找到console对象
            if (["log"].includes(node.callee.property.name)) {
              // 找到对应的方法和所在行数
              const { line } = node.loc.start; // 找到所处位置的行
              // 向arguments添加打印所在行信息
              node.arguments.push(t.stringLiteral(`line:${line}`));
            }
          }
        }
      },
    },
  };
}

以上我们从插件方法的参数当中解构出types参数,该参数和顶部引入的@babel/types包作用一样。我们可通过该对象提供的相关类型工具判断类型,以及向arguments数组添加一个stringLiteral节点,该节点的内容为line:${line}

配置文件中引入插件:

js 复制代码
module.exports = {
  // 插件列表,编译顺序从前到后
  plugins: [
    './plugins/change-name.js',
    './plugins/add-args.js'
  ]
}

执行后输出代码如下:

向代码块{}中添加方法插件

我们先修改一下src/index.js文件

js 复制代码
const fn = () => {
  var name = 'ikun'
  console.log(name)
}

目的是在以上代码的{}代码块最前面插入console.log('start')代码。

首先找出{}代码块对应的AST类型:

我们可看到{}的类型为BlockStatement,并且该节点下具有一个body属性存放在两个元素,分别为var name = 'ikun'console.log(name)节点,所有我们只需参考后者的节点信息,把console.log('start')节点创建出来,并放到body数组的最前面即可:

plugins/add-console.js

js 复制代码
module.exports = function markFnPlugin({ types: t }) {
  return {
    // 在babel把js源文件编译为AST抽象语法树后,我们可以根据抽象语法树的规则,对源文件进行增加/修改/删除代码的目的
    visitor: {
      // 添加代码:在代码块{}最前面插入一段console.log('start')代码
      BlockStatement(path) {
        // 在数组前面插入元素
        path.node.body.unshift(
          // 创建expressionStatement节点
          t.expressionStatement(
            t.callExpression(
              t.memberExpression(
                t.identifier('console'),
                t.identifier('log')
              ),
              [t.stringLiteral('start')]
            )
          )
        )
      }
    },
  };
}

babel.config.js

js 复制代码
module.exports = {
  plugins: [
    './plugins/add-console.js'
  ]
}

执行后可得到以下结果:

总结

以上通过修改对应节点名称console.log方法追加参数{}代码块追加代码 三个插件快速了解了如何编写babel插件,并结合AST Explorer网站,帮我们快速定位AST结构,通过编辑AST抽象语法树的节点信息,可让我们对源代码进行转换,这也是前端工程化的一大利器。

参考文献

babel中文文档

前端工程化基石 -- AST(抽象语法树)以及AST的广泛应用🔥

相关推荐
Justinc.4 分钟前
CSS3新增边框属性(五)
前端·css·css3
fruge12 分钟前
纯css制作声波扩散动画、js+css3波纹催眠动画特效、【css3动画】圆波扩散效果、雷达光波效果完整代码
javascript·css·css3
neter.asia21 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫21 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
光影少年40 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_41 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891144 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾1 小时前
前端基础-html-注册界面
前端·算法·html
Rattenking1 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu1 小时前
前端 Canvas 绘画 总结
前端