Babel介绍
主要介绍babel以及babel插件相关内容,参考文章,涵盖AST(抽象语法树)、Babel处理步骤、解析、转化以及生成等内容。主要是参考了下面的这两篇文档来理解抽象语法树和操作抽象语法树的方式,如有需要也可以去查看,有的链接打不开需要翻墙。
介绍
什么是babel?在进入官网就能看到说明,Babel是Js的编译器。其本质就是一种源码到源码之间的转化编译器,拥有众多模块可用于不同形式的静态分析。其作用就是用于js语法的转化,为了去兼容更低更老的环境,因为一些新的js语法对于低版本的一些环境不能兼容。
抽象语法树(AST)
抽象语法树:个人理解就是将代码描述为一颗树状结构的数据类型。具体可以参考:
对于抽象语法树生成的结构可以通过AST生成器去看一段js代码转化为AST之后的结构,对于AST结构可以看到其每一层都有如下所示的结构
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
这样的每一层结构又被叫做节点,一颗AST就是由成千上百个节点组成,因此对于AST理解这个点就可以了。
Babel的处理步骤
对于babel的处理主要分为三个步骤:解析(parse)、转化(transform)、生成(generator)
解析
解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:**词法分析(Lexical Analysis) **和 语法分析(Syntactic Analysis)。.
词法分析:
含义:字符串形式的代码转换为 令牌(tokens) 流。如:n * n,其令牌流如下所示
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
对于上面展示的type中的value值,其内部的值又如下:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
语法分析
含义:语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。
解析概述
对于这一层,我们仅仅需要做的就是关注和了解,因为我们在实际操作过程中不会对其有什么过多的操作,在实操过程能用到的就只是下面的两种方式:
// 第一种方式
const { parse } = require('@babel/parser');
const code = 'const n = 1';
// 解析为AST
const ast = parse(code);
//第二种方式
const babel = require('@babel/core');
const code = 'function greet(name) { return "Hello" + name} console.log(greet("tanhahahaha"))';
// 这里的babel.transformSync就是将code转化为AST
const output = babel.transformSync(code, {
})
转换
转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程 同时也是插件将要介入工作的部分。转化的过程才是我们应该主要关注的部分,这部分才和我们的实际操纵息息相关,因为在这里我们将直接操作AST对其进行转化,以便于生成我们想要的代码结果
生成
代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
Babel案例分析
下面有一个demo,这个demo的主要作用就是将变量中的名词替换为另外一个,实现这个demo采用了两种方式来写:
- 第一种是采用各种@babel/包,其实主要有三个,一个是@babel/parse、@babel/generator 以及@babel/traverse,由于我采用的是commonJs的方式引入,在最新的引入方式中有点问题,因此我的traverse包是直接从@babel/core中获取的。具体如下:
// code -> AST -> transformed AST -> transformed code
// 第一步:npm install @babel/parser @babel/core @babel/generator -D
// 第二步: 创建一个index.js文件,内容就是下面这段代码
const { parse } = require('@babel/parser');
const { traverse } = require('@babel/core');
const generator = require('@babel/generator').default;
const code = 'const n = 1';
// 转化为AST
const ast = parse(code);
traverse(ast, {
enter(path) {
// 找到id是identifier,并且name是n,然后进行替换
if (path.isIdentifier({ name: 'n' })) {
path.node.name = 'x';
}
}
})
console.log(generator(ast, code).code)
// 最后终端使用node index.js
- 第二种则是直接使用的@babel/core来写
// use @babel/core
const babel = require('@babel/core');
const code = 'const n = 1';
const output = babel.transformSync(code, {
plugins: [
function myCustomPlugin() {
return {
visitor: {
Identifier(path) {
if (path.isIdentifier({ name: 'n' })) {
path.node.name = 'x';
}
}
}
}
}
]
})
console.log(output.code);
这两种实现转化的方式都是babel实现的,其实两种都差不多,主要是语法上的差别,第一种方法的包其实就是从@babel/core这个包里单独分离出去的。当实现完这两个案例之后,来了解下面的包,就更容易明白:
Visitors(访问者)
为什么首先了解这个呢?其实从第二个案例来看就容易明白了,在第二个例子中plugins中的方法返回了一个对象,就是visitor,他在这里表示的是一个访问者的含义。Vistors这是设计模式中的一种访问者模式,其在babel中的作用就是用于来遍历,是一个用于 AST 遍历的跨语言的模式。
Paths(路径)
AST 通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,或者也可以用Paths(路径)来简化这件事情。Path 是表示两个节点之间连接的对象。在某种意义上,路径是一个节点在树中的位置以及关于该节点各种信息的响应式 Reactive 表示。 当你调用一个修改树的方法后,路径信息也会被更新。 Babel 帮你管理这一切,从而使得节点操作简单,尽可能做到无状态。
Paths in Visitors(存在于访问者中的路径)
当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。
const MyVisitor = {
Identifier(path) {
console.log("Visiting: " + path.node.name);
}
};
a + b + c;
path.traverse(MyVisitor);
Visiting: a
Visiting: b
Visiting: c
State(状态)
状态是抽象语法树AST转换的敌人,状态管理会不断牵扯你的精力,而且几乎所有你对状态的假设,总是会有一些未考虑到的语法最终证明你的假设是错误的。
Scopes(作用域)
JavaScript 支持词法作用域,在树状嵌套结构中代码块创建出新的作用域。在 JavaScript 中,每当你创建了一个引用,不管是通过变量(variable)、函数(function)、类型(class)、参数(params)、模块导入(import)还是标签(label)等,它都属于当前作用域。
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
}
}
当编写一个转换时,必须小心作用域。我们得确保在改变代码的各个部分时不会破坏已经存在的代码。我们在添加一个新的引用时需要确保新增加的引用名字和已有的所有引用不冲突。 或者我们仅仅想找出使用一个变量的所有引用, 我们只想在给定的作用域(Scope)中找出这些引用。
Bindings(绑定)
所有引用属于特定的作用域,引用和作用域的这种关系被称作:绑定(binding)
function scopeOnce() {
var ref = "This is a binding";
ref; // This is a reference to a binding
function scopeTwo() {
ref; // This is a reference to a binding from a lower scope
}
}
// Text for Translation
{
identifier: node,
scope: scope,
path: path,
kind: 'var',
referenced: true,
references: 3,
referencePaths: [path, path, path],
constant: false,
constantViolations: [path]
}
Babel插件
babel-traverse
作用: Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
babel-types
作用:Babel Types模块是一个用于 AST 节点的 Lodash 式工具库(译注:Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程风格的众多工具函数), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。
babel-generator
作用:Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)
babel-template
作用:babel-template 是另一个虽然很小但却非常有用的模块。 它能让你编写字符串形式且带有占位符的代码来代替手动编码, 尤其是生成的大规模 AST的时候。 在计算机科学中,这种能力被称为准引用(quasiquotes)。
结语
// use babel 实现
const babel = require('@babel/core');
// console.log(babel)
const code = 'function greet(name) { return "Hello" + name} console.log(greet("tanhahahaha"))';
const output = babel.transformSync(code, {
plugins: [
function myCustomPlugin() {
return {
visitor: {
Identifier(path) {
// if (path.isIdentifier({ name: 'n' })) {
// path.node.name = 'x';
if (!(
path.parentPath.isMemberExpression() &&
path.parentPath
.get('object')
.isIdentifier({ name: 'console' }) &&
path.parentPath.get('property').isIdentifier({ name: 'log' })
)) {
path.node.name = path.node.name.split('').reverse().join('');
}
// console.log('identifier');
},
StringLiteral(path) {
// console.log('string literal');
const newNode = path.node.value.split('').map(c => babel.types.stringLiteral(c)).reduce((prev, cur) => {
return babel.types.binaryExpression('+', prev, cur);
})
path.replaceWith(newNode);
path.skip();
}
}
}
}
]
})
console.log(output.code);
内容太多了,不想写,其实就是文章中的,这里就给两个例子,自己跑下,然后看看文章实际操作下就可以了。