什么是 babel
个人觉得它就是一个通用的多用途 JavaScript 编译器,引用一下官方的说明:Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情
- 语法转换
- 换通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换(codemods)
- 代码的静态分析
如何使用 babel
简单介绍一下:
-
配置文件
-
预设(preset):预设可以作为可共享的 Babel 插件集和/或配置 options,预设是一系列插件的集合 ,主要用于支持特定的语言功能。通常情况下,预设中已经预先安装了一些常用的插件和设置,以便在编译JavaScript代码时能够自动应用这些插件和设置,所以说,预设主要是为了封装特定的转换器集合以针对特定的用例
-
插件:插件是单独的转换器,可以被单独添加或移除以适应特定需求。插件是一个函数,可以根据需要进行自定义配置。插件的配置类似于预设,通常在plugins中配置
-
预设和plugins的区别:显而易见,预设是一堆插件,而插件就单独的一个插件,所以在实际使用中,为了兼顾预设和个性化需求,通常需要同时配置预设和插件。通过使用预设,可以快速地构建一个基本的Babel配置,然后根据具体需求添加插件进行自定义配置。但预设更侧重于封装一组常用的插件以简化配置,而插件则更灵活,可以根据具体需求进行自定义配置
- Plugin 会运行在 Preset 之前
- Plugin 会从前到后顺序执行
- Preset 的顺序则 刚好相反(从后向前)
-
预设和插件都是数组。如下:
js
{
"plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]
}
// 如果有配置项
{
"plugins": [
[
"transform-async-to-module-method",
{
"module": "bluebird",
"method": "coroutine"
}
]
]
}
-
简略情况下,plugins 和 preset 只要列出字符串格式的名字即可。但如果某个 preset 或者 plugins 需要一些配置项(或者说参数),就需要把自己先变成数组。第一个元素依然是字符串,表示自己的名字;第二个元素是一个对象,即配置对象。如下:
js
{
"presets": [
"presetA", // bare string
["presetA"], // wrapped in array
["presetA", {}] // 2nd argument is an empty options object
]
}
-
一些集成包:
- babel-cli:cli 就是命令行工具。安装了 babel-cli 就能够在命令行中使用 babel 命令来编译文件。在 package.json 中添加 scripts ,使用 babel 命令编译文件
- babel-register:babel-register 模块改写 require 命令,为它加上一个钩子。此后,每当使用 require 加载 .js、.jsx、.es 和 .es6 后缀名的文件,就会先用 babel 进行转码。使用时,必须首先加载 require('babel-register')。需要注意的是,babel-register 只会对 require 命令加载的文件转码,而 不会对当前文件转码。另外,由于它是实时转码,所以 只适合在开发环境使用
-
一些工具包:
-
@babel/parser:一个解析器,可以把源码转换成AST
-
@babel/traverse:是一个用于遍历和操作抽象语法树(ast)的Babel模块。它接受一个ast作为输入,并对其进行深度优先遍历。在遍历过程中,它可以访问和操作ast的每个节点,包括替换、删除和添加节点等操作。这使得开发者可以在不改变源代码的情况下对代码进行各种转换和优化
-
@babel/core:babel的核心包,它提供了一些基础的配置和功能
- 配置Babel:通过@babel/core,开发者可以配置Babel的各种选项和插件,从而定制化代码转换的过程。
- 运行Babel:@babel/core提供了运行Babel的入口函数,可以用来执行Babel的编译任务
- 集成其他Babel包:@babel/core作为Babel的核心包,可以与其他Babel包(如@babel/parser、@babel/traverse等)一起使用,共同完成JavaScript代码的转换和编译
-
@babel/generator:将 AST 转换为代码。
-
@babel/code-frame:Babel的一个工具包,主要用于生成错误信息并打印出错误原因和错误行数。在开发过程中,当代码出现错误时,这个包可以帮助开发者快速定位到错误的源头,从而快速解决问题
-
@babel/runtime:Babel提供的一种运行时解决方案,用于解决Babel编译时生成代码中全局变量的污染问题
-
@babel/template:Babel的一个工具包,主要用于生成AST(抽象语法树)。它提供了一种更简单、更直观的方式来构建AST,使得开发者可以更加方便地进行代码转换和编译。@babel/template提供了一个模板引擎,可以将字符串代码片段转换为AST。它支持在字符串中嵌入变量,可以直接将变量嵌入到生成的AST中,使得代码更加简洁和易于维护(简化AST的创建逻辑)
-
@babel/types:主要用于操作和转换AST(抽象语法树)。它提供了大量与AST相关的方法,可以用于生成、验证和修改AST节点,对编写处理 AST 逻辑非常有用
- 验证AST节点类型:可以使用@babel/types中的方法来判断AST节点是否符合预期的格式或类型。例如,可以使用isClassMethod或assertClassMethod方法来判断一个AST节点是否为类中的一个方法。
- 构建AST节点:可以使用@babel/types中的方法来生成新的AST节点。例如,classMethod方法可以生成一个新的classMethod类型的AST节点。
- 转换AST节点:可以使用@babel/types中的方法来修改AST节点,从而实现代码的转换和优化。例如,可以使用replaceWith方法来替换AST中的节点,或使用remove方法来删除AST中的节点。
-
@babel/types主要用于在创建AST的过程中进行各种操作,@babel/template主要用于快速构建AST,而@babel/parser则主要用于将源代码转化为AST。
-
-
一些辅助包:
- @babel/helper-compilation-targets:适用于编译目标(浏览器或其他环境,如节点)和兼容表(知道哪个版本支持特定语法)。 @babel/preset-env 使用它来根据 targets 选项确定应该启用哪个插件
- @babel/helper-module-imports:用于帮助生成模块导入的代码。在Babel的转换过程中,有时需要动态地插入或修改模块的导入语句。这个辅助模块提供了一些工具函数,使得这个过程更加便捷和标准化。
- @babel/helper-validator-identifier:是一个用于解析 JavaScript 关键字和标识符的实用程序包。 它提供了几个辅助函数来识别有效的标识符名称和检测保留字和关键字
- @babel/helper-environment-visitor:主要用于创建一个访问者(visitor)对象,该对象能够遍历抽象语法树(AST)并根据不同的环境(例如不同的 JavaScript 版本或特定的浏览器环境)应用不同的转换。在 Babel 的工作流中,它将源代码解析为 AST,然后通过各种插件来转换这个 AST。每个插件都可以定义一个或多个访问者,这些访问者知道如何修改 AST 的特定部分。@babel/helper-environment-visitor 提供了一种机制,使得这些访问者可以根据当前的目标环境(通过 .babelrc 或其他配置指定)进行定制。这个辅助模块不是直接面向最终用户的,而是被其他 Babel 插件或内部工具使用,以生成针对特定环境的优化代码。例如,某些新的 JavaScript 特性可能在旧版本的浏览器上不受支持,Babel 可以使用这个辅助模块来生成一个访问者,该访问者知道如何将这些新特性转换为旧浏览器可以理解的代码。
开发babel插件相关的知识了解
babel 的 三个主要处理步骤 分别是
- parse:通过
parser
把源码转成抽象语法树(ast
) - transform:遍历
ast
,通过transform
插件对ast
进行操作 - generate:把转换后的
ast
重新生成代码(source maps)
babel其实就是在和 ast
打交道,不清楚 ast
的可以移步 这儿
Visitor
babel会依次遍历ast的每一个节点,也就是递归的树形遍历,遍历节点的时候,可以理解为访问这个节点(Visitor 访问者
),babel插件就和这个 Visitor 有很大关系
js
const babel = require('@babel/core');
let myPlugins = {
// visitor 可以对特定的节点(ast的type)进行处理
visitor: {
BinaryExpression(path) { // 这个path 很重要
// do something
}
}
}
babel.transform(code, {
plugins: [
myPlugins
]
})
- Visitor 的对象定义了用于 AST 中获取具体节点的方法
- Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法
path
ast 通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,这个就是 Path ,简单来说 path 用于表示两个节点之间连接的对象,这是一个可操作和访问的巨大可变对象
path对象会作为入参传递给每个插件(visitor对象下面的方法的实参就是path),插件通过修改 Path 对象达到修改 ast 结构的目的。
js
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}
将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:
js
{
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}
同时它还包含关于该路径的其他元数据
js
// 常用的给了注释
{
"parent": {...}, // 父 AST 节点
"node": {...}, // 当前 AST 节点
"hub": {...}, // 可以通过 path.hub.file 拿到最外层 File 对象, path.hub.getScope 拿到最外层作用域,path.hub.getCode 拿到源码字符串
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null, // 父 AST 节点的 path
"context": null,
"container": null,
"listKey": null, // 当前 AST 节点所在父节点属性的属性值为数组时 listkey 为该属性名,否则为 undefined
"inList": false, // 是否有同级节点
"parentKey": null,
"key": null, // 当前 AST 节点所在父节点属性的属性名或所在数组的下标
"scope": null, // 作用域
"type": null,
"typeAnnotation": null
}
// scope 属性的一些属性:
// scope.bindings 当前作用域内声明的所有变量
// scope.block 作用域的 block,例如FunctionDeclaration, Program等AST节点
// scope.path 生成作用域的节点对应的 path,例如FunctionDeclaration, Program等AST节点的path
// scope.references 所有 binding 的引用对应的 path
// scope.dump() 打印作用域链的所有 binding 到控制台
// scope.parentBlock() 父级作用域的 block
// getAllBindings() 从当前作用域到根作用域的所有 binding 的合并
// getBinding(name) 查找某个 binding,从当前作用域一直查找到根作用域
// getOwnBinding(name) 从当前作用域查找 binding
// parentHasBinding(name, noGlobals) 查找某个 binding,从父作用域查到根作用域,不包括当前作用域。可以通过 noGlobals 参数指定是否算上全局变量(比如console,不需要声明就可用),默认是 false
// removeBinding(name) 删除某个 binding
// hasBinding(name, noGlobals) 从当前作用域查找 binding,可以指定是否算上全局变量,默认是 false
// moveBindingTo(name, scope) 把当前作用域中的某个 binding 移动到其他作用域
// generateUid(name) 生成作用域内唯一的名字,根据 name 添加下划线,比如 name 为 a,会尝试生成 _a,如果被占用就会生成 __a,直到生成没有被使用的名字。
// 通过scope能非常方便的操作作用域中的某个变量,不需要手动去获取对应的AST
当然,path还有很多的方法,比如:
- inList:inList属性主要用于判断是否有同级节点。具体来说,当container容器为数组且只有一个成员的时候,此方法也会返回true
- container:表示当前节点的父节点
- key:key属性表示当前节点在父节点中的位置
- listKey:listKey属性则表示当前节点所在的容器(container)的名称
- get(key) 获取某个属性的 path
- set(key, node) 设置某个属性的值
- getSibling(key) 获取某个下标的兄弟节点
- getNextSibling() 获取下一个兄弟节点
- getPrevSibling() 获取上一个兄弟节点
- getAllPrevSiblings() 获取之前的所有兄弟节点
- getAllNextSiblings() 获取之后的所有兄弟节点
- find(callback) 从当前节点到根节点来查找节点(包括当前节点),调用 callback(传入 path)来决定是否终止查找
- findParent(callback) 从当前节点到根节点来查找节点(不包括当前节点),调用 callback(传入 path)来决定是否终止查找
- isXxx(opts) 判断当前节点是否是某个类型,可以传入属性和属性值进一步判断,比如path.isIdentifier({name: 'b'})
- assertXxx(opts) 同 isXxx,但是不返回布尔值,而是抛出异常
- insertBefore(nodes) 在之前插入节点,可以是单个节点或者节点数组
- insertAfter(nodes) 在之后插入节点,可以是单个节点或者节点数组
- replaceWith(replacement) 用某个节点替换当前节点
- replaceWithMultiple(nodes) 用多个节点替换当前节点
- replaceWithSourceString(replacement) 解析源码成 AST,然后替换当前节点
- remove() 删除当前节点
- traverse(visitor, state) 遍历当前节点的子节点,第1个参数是节点,第2个参数是用来传递数据的状态
- skip() 跳过当前节点的子节点的遍历
- stop() 结束所有遍历
path源码学习: 当前项目有babel:\node_modules@babel\traverse\lib\path,也可以去这儿
具体流程
开发环境搭建
为了方便开发和调试插件,需要搭建一个插件的开发环境, 只需要安装 babel 的核心包,然后在 .babelrc 文件中配置 plugins(配置项是一个数组) 就行了
js
// package.json
{
"scripts": {
"build": "babel src -d dist",
},
"devDependencies": {
"@babel/cli": "^7.23.9",
"@babel/core": "^7.23.7",
"@babel/types": "^7.23.6"
}
}
// .babelrc
// 也可以将插件单独打包,发布到 npm 上
{
"plugins": ["./src/plugin.js"]
}
实现一个简单的转let const 为var的插件
js
// babel 的核心库 用来实现核心的转换引擎
const babel = require('@babel/core');
let code = 'const name = constRen'
// let const = 'const name = constRen'
let myPlugins = {
// visitor 可以对特定的节点(ast的type)进行处理
visitor: {
VariableDeclaration(path) {
const { node } = path;
if (node.kind === "let" || node.kind === "const") {
node.kind = "var";
}
}
}
}
// babel 会先把代码 转为ast 再经行遍历
let res = babel.transform(code, {
plugins: [
myPlugins
]
})
console.log('res', res.code); // var name = constRen;
实现一个简单计算的插件
js
const babel = require('@babel/core');
// 节点类型判断 生成ast的节点
const types = require('@babel/types');
let code = 'let num = 2*6'
let myPlugins = {
visitor: {
BinaryExpression(path) {
const node = path.node;
const leftVal = node.left.value;
const rightVal = node.right.value;
if (!isNaN(leftVal) && !isNaN(rightVal)) {
let res = eval(leftVal + node.operator + rightVal);
res = types.numericLiteral(res);
path.replaceWith(res)
}
}
}
}
let res = babel.transform(code, {
plugins: [
myPlugins
]
})
console.log('res', res.code); // let num = 12;
如果遇到的层级运算呢? let codePro = 'const num = 2*6*8*9*10*11*12'
先来看下 ast 结构
json
{
"type": "Program",
"start": 0,
"end": 29,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 28,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 28,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "num"
},
"init": {
"type": "BinaryExpression",
"start": 12,
"end": 28,
"left": {
"type": "BinaryExpression",
"start": 12,
"end": 25,
"left": {
"type": "BinaryExpression",
"start": 12,
"end": 22,
"left": {
"type": "BinaryExpression",
"start": 12,
"end": 19,
"left": {
"type": "BinaryExpression",
"start": 12,
"end": 17,
"left": {
"type": "BinaryExpression",
"start": 12,
"end": 15,
"left": {
"type": "Literal",
"start": 12,
"end": 13,
"value": 2,
"raw": "2"
},
"operator": "*",
"right": {
"type": "Literal",
"start": 14,
"end": 15,
"value": 6,
"raw": "6"
}
},
"operator": "*",
"right": {
"type": "Literal",
"start": 16,
"end": 17,
"value": 8,
"raw": "8"
}
},
"operator": "*",
"right": {
"type": "Literal",
"start": 18,
"end": 19,
"value": 9,
"raw": "9"
}
},
"operator": "*",
"right": {
"type": "Literal",
"start": 20,
"end": 22,
"value": 10,
"raw": "10"
}
},
"operator": "*",
"right": {
"type": "Literal",
"start": 23,
"end": 25,
"value": 11,
"raw": "11"
}
},
"operator": "*",
"right": {
"type": "Literal",
"start": 26,
"end": 28,
"value": 12,
"raw": "12"
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
根据 ast 树的结构和 ast 遍历的顺序(依次遍历 深度优先) 可以得知 它是从左往右依次计算 26 89 1011 12 ==> ((((((2*6))8)9 10))1112) 所以最先计算的就是 26 ,而且有嵌套层级的 BinaryExpression
所以 我们需要递归计算
js
let myPluginsPro = {
visitor: {
BinaryExpression(path) {
const node = path.node;
const leftVal = node.left.value;
const rightVal = node.right.value;
if (!isNaN(leftVal) && !isNaN(rightVal)) {
let res = eval(leftVal + node.operator + rightVal);
res = types.numericLiteral(res);
path.replaceWith(res);
let parentPath = path.parentPath; // 拿到父路径
// 嵌套的这种 判断 他的父亲也是二进制表达式 那就需要递归处理
if (parentPath.node.type === 'BinaryExpression') {
myPluginsPro.visitor.BinaryExpression.call(null, parentPath)
}
}
}
}
}
console.log('resPro', resPro.code); // const num = 1140480
手写一个将箭头函数转为普通函数的插件
官方插件是babel-plugin-transform-es2015-arrow-functions
,来模仿一个
js
const babel = require('@babel/core');
const types = require('@babel/types');
// let code = 'let add=(a,b)=>a+b'
let code = 'function fn(){const log=()=>console.log(this)}';
// 思路:
// 观察箭头函数和普通函数的ast的异同 他们的参数以及逻辑都是一样的,我们要做的只是把外在形式改变一下(加上{}和里面的return)和处理一下this
let myPlugins = {
visitor: {
// ArrowFunctionExpression(path) { // 方法名必须为ast的type 也就是可以处理的节点
// console.log('path', path);
// }
ArrowFunctionExpression: { // 这种对象的写法也行 上面的函数写法也行
enter(path) {
const { id, params, generator, async } = path.node; //箭头函数这个对象
path.node.type = 'FunctionExpression' // 修改type为普通函数的type
// 处理 箭头函数的this
hoistFunctionEvn(path)
// 判断当前箭头函数有没有BlockStatement 也就是{}代码块 箭头函数也可能是 let add=(a,b)=>{return a+b }
if (!types.isBlockStatement(path.node.body)) {
// 箭头函数没有大括号的代码块(也就是body) 所以需要创建一个 参数是一个表达式 https://babeljs.io/docs/en/babel-types 中查询你想要生成的节点名称然后复制
// body中有return,需要创建一个return的节点,文档上要求是一个数组
// 方法1
let body = types.blockStatement([
// 创建 大括号里面的retuen 参数(也就是要 return 的内容)就是箭头函数里面的内容
types.returnStatement(path.node.body)
]);
// 这儿的 id, params, body, generator, async 就是 ast的 ArrowFunctionExpression 节点里面的数据 但是body需要重新生成为普通函数的body
let fn = types.functionExpression(id, params, body, generator, async);
// https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-replacing-a-node
path.replaceWith(fn)
// 方法2
// 其实也可以不用上面的 types.functionExpression 方法生成 直接替换body也行 因为其他参数都是一样的 只有body不一样
// path.node.body = types.blockStatement([
// // 创建 大括号里面的retuen 参数(也就是要 return 的内容)就是箭头函数里面的内容
// types.returnStatement(path.node.body)
// ]);
}
},
exit() {
console.log('exit');
}
}
}
}
function hoistFunctionEvn(path) {
// 处理this 其实就是在转为普通函数的时候 加一个我们开发中常用的 const _this=this 把上级作用域的 this 存起来
// 1. 查找this父作用域 父作用域是一个函数且不是箭头函数 或者 直接到跟作用域
const fatherScope = path.findParent((parent => (parent.isFunction() && !parent.isArrowFunctionExpression() || parent.isProgram())));
// 2. 绑定this
const thisPointer = '_this' // const _this=this
// 3. 把当前路径(顶级作用域)下的 所有 this 全部替换为 _this
// 3.1 获取所有this路径
const thisPaths = getAllThisPath(path);// 返回this的path节点
if (thisPaths.length > 0) {
// 3.2 修改所有路径中的this为_this
thisPaths.forEach(path => {
// this -> _this types.identifier==>生成标识符
path.replaceWith(types.identifier(thisPointer))
})
// 4. 当前作用域里面加一句 const _this=this 代码
fatherScope.scope.push({
// ast 的内容都有 id 和init
id: types.identifier(thisPointer),
init: types.thisExpression()
})
}
}
function getAllThisPath(path) {
let arr = []
path.traverse({
ThisExpression(path) {
arr.push(path)
}
})
return arr
}
// 开始 转换 babel 会先把代码 转为ast 再经行遍历
let res = babel.transform(code, {
// 转换时需要插件
plugins: [
myPlugins
]
})
普通函数和箭头函数的ast的对比
参考
babel.nodejs.cn/docs/ github.com/jamiebuilds... github.com/jamiebuilds... github.com/jamiebuilds... babel.nodejs.cn/docs/plugin...