Babel插件指南

Babel插件指南

Babel简介

Babel 是一个通用的多功能的 JavaScript 编译器。此外它还拥有众多模块可用于不同形式的静态分析。

静态分析是在不需要执行代码的前提下对代码进行分析的处理过程。静态分析的目的是多种多样的, 它可用于语法检查,编译,代码高亮,代码转换,优化,压缩等等场景。

  • 通过图进一步理解:

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

Babel操作流程:js代码 -> 原AST -> babel处理 -> 修改后的AST -> 修改后的js代码 -> 交给浏览器编译

即: 解析(parse)转换(transform)生成(generate)

实现上述功能的插件见小节:Babel API

Babel 更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 js代码,Babel 更改这些代码,然后返回给你新生成的js代码。

由于babel提供了操作AST的函数,所以开发者可以由此做各种各样的静态分析。

AST(Abstract syntax tree)简介

常见编译型语言(例如:Java)编译程序一般步骤分为:词法分析->语法分析->语义检查->代码优化和字节码生成。具体的编译流程如下图:

js的AST生成和java略有不同,java是走上图所有流程,通过本地jdk将**.java文件编译成.class后缀的文件,然后交给jvm(虚拟机)处理。js默认是通过浏览器**编译,流程到上图的语义检查器为止,生成最终的AST

  • AST解析示例

js代码

let a = 1;

解析后AST

{
  "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"
}

在线js转换到AST工具:https://astexplorer.net/#/

JS生态里,基于 AST 实现功能的工具有很多,babel只是其中一个工具。其他例如:

  • ESlint: 代码错误或风格的检查,发现一些潜在的错误
  • IDE 的错误提示、格式化、高亮、自动补全等
  • UglifyJS 压缩代码
  • 代码打包工具 webpack

ESTree AST Node

babel完整的流程,最后一步是将修改后的AST生成为js代码,即深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

Visitors(访问者)

当谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个**访问者模式(visitor)**的概念。

visitor是一个用于 AST 遍历的跨语言的模式。 简单的说visitor就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。例如:

visitor: {
  // 变量声明函数
  VariableDeclarator(path, state) {
		// todo
  }
}

上面AST小节示例有 “type”: “VariableDeclarator” ,诸如此类的树节点在访问时,就会进入visitor对象声明的对应类型的成员方法。此时你访问到的不是节点本身,而是一个Path,所以可以追踪树的父节点等其它信息。

常用写法是取path.node,如上即取到type = VariableDeclarator 的对象

举个栗子:

// 设定一个访问者对象
const visitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};
// 声明js代码
a + b + c;
// js编译成ast
{
  "type": "Program",
  "start": 0,
  "end": 10,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 10,
      "expression": {
        "type": "BinaryExpression",
        "start": 0,
        "end": 9,
        "left": {
          "type": "BinaryExpression",
          "start": 0,
          "end": 5,
          "left": {
            "type": "Identifier",
            "start": 0,
            "end": 1,
            "name": "a"
          },
          "operator": "+",
          "right": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "b"
          }
        },
        "operator": "+",
        "right": {
          "type": "Identifier",
          "start": 8,
          "end": 9,
          "name": "c"
        }
      }
    }
  ],
  "sourceType": "module"
}
// 运行后打印结果:
Visiting: a
Visiting: b
Visiting: c

js声明后被编译成AST,AST被访问时,Identifier类型有3次,执行visitor对象的Identifier成员方法3次,打印对应日志

Babel API

babylon

Babylon 是 Babel 的解析器,作用是把js字符串解析成AST

$ npm install --save babylon
import * as babylon from "babylon";
const code = `function square(n) {
  return n * n;
}`;
babylon.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }
babel-traverse

Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。就是触发访问者(Visitors)的步骤

$ npm install --save babel-traverse

遍历更新节点:

// 把变量 n 改成 x
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `function square(n) {
  return n * n;
}`;
const ast = babylon.parse(code);
traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});
babel generator

Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)

$ npm install --save babel-generator

通过visitors处理后的AST再转成js

import * as babylon from "babylon";
import generate from "babel-generator";
import traverse from "babel-traverse";
const code = `function square(n) {
  return n * n;
}`;
const ast = babylon.parse(code);
traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});
generate(ast, {}, code);
// {
//   code: "...",
//   map: "..."
// }

项目中实践

引用自定义的babel插件

babel-core版本是6.x的,vue项目有.babelrc文件,在plugins属性里直接配置路径即可,如下babelPluginConsole:

{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime", "./src/plugins/babel/babelPluginConsole"]
}

其他版本的babel也一个思路,找到plugins配置位置,对应加入即可。

新增插件js文件

如上配置,我们在 /src/plugins/babel/ 路径下新建 babelPluginConsole.js

module.exports = function(babel) {
  let t = babel.types;
  return {
    visitor: {
  		// todo      
    }
  };
};
确定要实现的功能,编译成AST进行分析

这里我们对console.log()进行处理,默认打印变量名,如下:

let test = 'hi babel'
console.log(test);
//打印: test hi babel

所以我们将console.log(test)console.log('test',test); 解析成AST进行比较

{
  "type": "Program",
  "start": 0,
  "end": 67,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 21,
      "declarations": [
       // 变量定义的AST,忽略
      ],
      "kind": "let"
    },
    // console.log(test) 的AST
    {
      "type": "ExpressionStatement",
      "start": 22,
      "end": 40,
      "expression": {
        "type": "CallExpression",
        "start": 22,
        "end": 39,
        "callee": {
          "type": "MemberExpression",
          "start": 22,
          "end": 33,
          "object": {
            "type": "Identifier",
            "start": 22,
            "end": 29,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 30,
            "end": 33,
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [
          {
            "type": "Identifier",
            "start": 34,
            "end": 38,
            "name": "test"
          }
        ],
        "optional": false
      }
    },
    // console.log('test', test) 的AST
    {
      "type": "ExpressionStatement",
      "start": 41,
      "end": 67,
      "expression": {
        "type": "CallExpression",
        "start": 41,
        "end": 66,
        "callee": {
          "type": "MemberExpression",
          "start": 41,
          "end": 52,
          "object": {
            "type": "Identifier",
            "start": 41,
            "end": 48,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 49,
            "end": 52,
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 53,
            "end": 59,
            "value": "test",
            "raw": "'test'"
          },
          {
            "type": "Identifier",
            "start": 61,
            "end": 65,
            "name": "test"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

比较后发现,arguments属性多了一个字符串对象。所以我们要做的就是,获取console.log(test)的AST然后定位到arguments数组,给它添加一个对象。

编写和调试插件

按上述分析,得到如下一种解决逻辑

module.exports = function(babel) {
  let t = babel.types;
  return {
    visitor: {
      ExpressionStatement(path) {
        if (path.node && path.node.expression && path.node.expression.callee
            && path.node.expression.callee.object && path.node.expression.callee.property
            && path.node.expression.arguments 
            && path.node.expression.callee.object.name === 'console' 
            && path.node.expression.callee.property.name === 'log') {
          if (path.node.expression.arguments[0].type === 'Identifier') {
            path.node.expression.arguments = [t.stringLiteral(path.node.expression.arguments[0].name), ...path.node.expression.arguments]
            console.log('path.node.expression.arguments2', path.node.expression.arguments);
          }
        }
      },      
    }
  };
};

注意
  • 插件实现的逻辑非常灵活,可以使用自带的方法处理节点,也可以使用推荐的babel插件处理节点,如babel-template
  • visitor访问的是所有的节点,vue框架下,npm run后每次修改逻辑,热响应都会执行visitor的成员方法
  • 插件本身的逻辑修改,需要重新npm run才能响应

额外说明polyfill

首先我们来理清楚这三个概念:

  • 最新ES语法,比如:箭头函数,let/const
  • 最新ES Api,比如Promise
  • 最新ES实例/静态方法,比如String.prototype.include

babel-prest-env仅仅只会转化最新的es语法,并不会转化对应的Api和实例方法,比如说ES 6中的Array.from静态方法。

一些内置方法模块,仅仅通过preset-env的语法转化是无法进行识别转化的,所以就需要一系列类似”垫片“的工具进行补充实现这部分内容的低版本代码实现。这就是所谓的polyfill的作用。

其他插件案例

  • 箭头函数转换插件
const babelTypes = require('@babel/types');

function ArrowFunctionExpression(path) {
  const node = path.node;
  hoistFunctionEnvironment(path);
  node.type = 'FunctionDeclaration';
}

/**
 * @param {*} nodePath 当前节点路径
 */
function hoistFunctionEnvironment(nodePath) {
  // 往上查找 直到找到最近顶部非箭头函数的this p.isFunction() && !p.isArrowFunctionExpression()
  // 或者找到跟节点 p.isProgram()
  const thisEnvFn = nodePath.findParent((p) => {
    return (p.isFunction() && !p.isArrowFunctionExpression()) || p.isProgram();
  });
  // 接下来查找当前作用域中那些地方用到了this的节点路径
  const thisPaths = getScopeInfoInformation(thisEnvFn);
  const thisBindingsName = generateBindName(thisEnvFn);
  // thisEnvFn中添加一个变量 变量名为 thisBindingsName 变量值为 this
  // 相当于 const _this = this
  thisEnvFn.scope.push({
    // 调用babelTypes中生成对应节点
    // 详细你可以在这里查阅到 https://babeljs.io/docs/en/babel-types
    id: babelTypes.Identifier(thisBindingsName),
    init: babelTypes.thisExpression(),
  });
  thisPaths.forEach((thisPath) => {
    // 将this替换称为_this
    const replaceNode = babelTypes.Identifier(thisBindingsName);
    thisPath.replaceWith(replaceNode);
  });
}

/**
 * 查找当前作用域内this使用的地方
 * @param {*} nodePath 节点路径
 */
function getScopeInfoInformation(nodePath) {
  const thisPaths = [];
  // 调用nodePath中的traverse方法进行便利
  // 你可以在这里查阅到  https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md
  nodePath.traverse({
    // 深度遍历节点路径 找到内部this语句
    ThisExpression(thisPath) {
      thisPaths.push(thisPath);
    },
  });
  return thisPaths;
}

/**
 * 判断之前是否存在 _this 这里简单处理下
 * 直接返回固定的值
 * @param {*} path 节点路径
 * @returns
 */
function generateBindName(path, name = '_this', n = '') {
  if (path.scope.hasBinding(name)) {
    generateBindName(path, '_this' + n, parseInt(n) + 1);
  }
  return name;
}

module.exports = {
  hoistFunctionEnvironment,
  arrowFunctionPlugin: {
    visitor: {
      ArrowFunctionExpression,
    },
  },
};

附录

类型原名称中文名称描述
Program程序主体整段代码的主体
VariableDeclaration变量声明声明一个变量,例如 var let const
FunctionDeclaration函数声明声明一个函数,例如 function
ExpressionStatement表达式语句通常是调用一个函数,例如 console.log()
BlockStatement块语句包裹在 {} 块内的代码,例如 if (condition){var a = 1;}
BreakStatement中断语句通常指 break
ContinueStatement持续语句通常指 continue
ReturnStatement返回语句通常指 return
SwitchStatementSwitch 语句通常指 Switch Case 语句中的 Switch
IfStatementIf 控制流语句控制流语句,通常指 if(condition){}else{}
Identifier标识符标识,例如声明变量时 var identi = 5 中的 identi
CallExpression调用表达式通常指调用一个函数,例如 console.log()
BinaryExpression二进制表达式通常指运算,例如 1+2
MemberExpression成员表达式通常指调用对象的成员,例如 console 对象的 log 成员
ArrayExpression数组表达式通常指一个数组,例如 [1, 3, 5]
FunctionExpression函数表达式例如const func = function () {}
ArrowFunctionExpression箭头函数表达式例如const func = ()=> {}
AwaitExpressionawait表达式例如let val = await f()
ObjectMethod对象中定义的方法例如 let obj = { fn () {} }
NewExpressionNew 表达式通常指使用 New 关键词
AssignmentExpression赋值表达式通常指将函数的返回值赋值给变量
UpdateExpression更新表达式通常指更新成员值,例如 i++
Literal字面量字面量
BooleanLiteral布尔型字面量布尔值,例如 true false
NumericLiteral数字型字面量数字,例如 100
StringLiteral字符型字面量字符串,例如 vansenb
SwitchCaseCase 语句通常指 Switch 语句中的 Case
  • babel-type api:https://www.npmjs.com/package/babel-types

参考资料地址

在线js转换到AST工具:https://astexplorer.net/#/

babel官网:https://www.babeljs.cn/docs/

AST说明:https://zhaomenghuan.js.org/blog/js-ast-principle-reveals.html

babel指南手册:https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-introduction

babel插件开发相关文章:https://juejin.cn/post/7155434131831128094#heading-7

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值