Babel插件指南
创始人
2024-01-28 14:41:29

Babel插件指南

文章目录

    • Babel插件指南
      • Babel简介
      • AST(Abstract syntax tree)简介
      • ESTree AST Node
      • Visitors(访问者)
      • Babel API
        • babylon
        • babel-traverse
        • babel generator
      • 项目中实践
        • 引用自定义的babel插件
        • 新增插件js文件
        • 确定要实现的功能,编译成AST进行分析
        • 编写和调试插件
        • 注意
      • 额外说明polyfill
      • 其他插件案例
      • 附录
      • 参考资料地址

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 = thisthisEnvFn.scope.push({// 调用babelTypes中生成对应节点// 详细你可以在这里查阅到 https://babeljs.io/docs/en/babel-typesid: babelTypes.Identifier(thisBindingsName),init: babelTypes.thisExpression(),});thisPaths.forEach((thisPath) => {// 将this替换称为_thisconst 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.mdnodePath.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

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
不用电脑怎么设置苹果铃声 ip... 需要一个app,加上苹果自带的app即可1、手机下载爱思极速版。因为之前换手机搬移数据时店员帮忙整理...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
才高八斗的主人公 三顾茅庐的主... 古代君王在建立自己的政治体系之时,大多会采取一些礼贤下士的手段,对于一些有卓越才能的人放低姿态。因为...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...