babel插件实践(一)babel编译原理分析

前言

我们都知道在前端编译构建工具出现之前,前端项目基本都是用es5浏览器识别的语法来实现的。(jqueryes5...)。随着前端技术的发展(es6甚至更新语法的问世),浏览器是不能识别这些新语法的。那么就出现了编译构建工具,其中babel扮演着举足轻重的角色。那么下边我们来探索一下babel究竟是什么?

babel是什么?

官方介绍

Babel 是一个 JavaScript 编译器。

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

简单的说就是为了保证javascript在浏览器上正常运行,需要把浏览器不识别的语法转换成浏览器识别的预发,其中转换这一步骤就是babel做的事情,在计算机编程中这一步骤也会被叫做编译。

其实编译涉及的东西很多,有兴趣的同学可以了解一下《编译原理》,编译原理主要包括词法分析,语法分析,语义分析,中间代码生成,代码优化,目标代码生成这几大步骤,这里就不做过多介绍了,此处省略一百万字...

其实babel的工作流程和编译原理中的编译流程相对简单。我们可以归纳如下几个步骤:

  • 词法分析
  • 语法分析
  • 代码转换
  • 代码生成

babel的整体工作流程如下图:

未命名文件.png

其中分为词法分析和预发分析两步可以合并成解析(parse)过程

从上图可以看到编译从开始到结束有一个最重要的东西,抽象语法树/AST的知识,以下简称ASTbabel编译代码的整个流程都离不开它。

抽象语法树(AST)

抽象语法树是高级编程语言(JavaJavaScript等)转换成机器语言的桥梁。解析器会根据ECMAScript 标准「JavaScript语言规范」来对代码字符串进行词法分析,拆分成一个个词法单元,再遍历各个词法单元进行语法分析构造出AST。我们通过如下代码来分析原理:

let age = 10;
age = age + 20;

词法分析

词法分析阶段是对源代码进行“分词”,它接收一段源代码,然后执行一段tokenize函数,把代码分割成被称为tokens的东西。tokens是一个数组,由一些代码的碎片组成,比如数字、标点符号、运算符号等等等等,例如这样:

这里我们利用在线工具把上述代码进行词法分析的结果如下:
词法分析工具

[
    { "type": "Keyword", "value": "let"},
    { "type": "Identifier", "value": "age"},
    { "type": "Punctuator", "value": "="},
    { "type": "Numeric", "value": "10"},
    { "type": "Punctuator", "value": ";"},
    { "type": "Identifier", "value": "age"},
    { "type": "Punctuator", "value": "="},
    { "type": "Identifier", "value": "age"},
    { "type": "Punctuator", "value": "+"},
    { "type": "Numeric", "value": "20"},
    { "type": "Punctuator", "value": ";"}
]

从词法分析结果可以看出,最终结果就是把代码解析成各个单词(let,age,+,=等等)
babel-tokenizer方法实现

语法分析

在词法分析之后,语法分析会把词法分析得到的tokens转化为AST,有兴趣的可以阅读一下babel源码babel转化AST源码

AST抽象语法树是babel插件的核心概念,在编写自定义babel插件也会用到,因为在代码转换其实就是针对AST语法树各个节点进行的操作

下边推荐一个在线生成AST语法树工具

生成的AST太长,这里不展示了,有兴趣的可以在线尝试。

AST树,顾名思义数据结构中典型的一种数据类型-树,那么我们也知道,树都有一个根节点,也会有许多子节点。AST语法树是会有一个type值是Program的根节点,如下

{
  "type": "Program",
  "start": 0,
  "end": 29,
  "body": [],
  "sourceType": "module"
}

经过观察子节点,其实子节点(包括根节点)都有相同的数据结构,如下

{
    "type": "VariableDeclaration",
    "start": 0,
    "end": 13,
    "declarations": [...],
    "kind": "let"
}
{
  type: "Identifier",
  name: ...
}
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

以上只是列举了几个不同类型的节点(注意:出于简化的目的移除了某些属性),其实AST语法树就是由这些节点组成的,它们组合在一起可以描述用于静态分析的程序语法。

从上边可以得出结论:每一个节点都有一个type字段代表节点的类型,还定义了一些附加属性用来进一步描述该节点类型。

babel编译

babel编译流程代码演示

上边我们也给出了babel编译代码的流程图,下边我们具体实践一下babel编译流程

这里先简单创建一个空项目,步骤如下:

创建一个文件夹,使用npm init -y创建package.json

然后在项目下创建src/index.js文件

let name = "hello babel";
console.log(name);

package.json中添加执行scripts

"scripts": {
  "build": "node src/index.js"
}

为方便我们后边打断点debug,这里我们利用vscode工具给我们生成一个launch.js文件,添加自己的launch配置

1629953490(1).jpg

我的launch.js内容如下

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Debug",
            "runtimeExecutable": "npm",
            "restart": true,
            "console": "integratedTerminal",
            "runtimeArgs": ["run-script", "build"],
        }
    ]
}

具体配置请小伙伴们搜一下...

然后我们点想要断点的地方打上断点,击上图debug按钮运行即可,如下

image.png

更多关于vscode调试工具请自行学习,这里不做过多讲述

接下来正式回到babel编译正题,我们需要安装3个babel官方提供的插件

npm install -D @babel/parser @babel/generator @babel/traverse

接下来了解一下这3个包的简单用法,修改src/index.js代码如下

const Parser = require("@babel/parser")
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

// 源代码
const compilerCode = `
let age = 10;
age = age + 20;
`
// 源代码经过parse过程(词法分析/语法分析)转换成AST语法树
const ast = Parser.parse(compilerCode, {});

// 对AST语法树上的节点进行操作
traverse(ast);

// ast语法树生成最终代码
const codeObj = generator(ast, {}, compilerCode);

console.log(codeObj.code);

以上只是简单的用代码形式演示了babel是如何编译代码的。

编译生成的代码如下

let age = 10;
age = age + 20;

这里和源代码比较一下发现没有什么差别,因为我们没有使用插件对代码进行操作(压缩,混淆,优化等等)

@babel/parser 包的parse方法传入源代码,进行词法分析合语法分析,最终生成AST抽象语法树
@babel/traversetraverse方法接收AST抽象语法树并对其进行遍历(深度遍历),在此过程中对节点进行添加、更新及移除等操作。 这是Babel或是其他编译器中最复杂的过程,同时也是插件将要介入工作的地方,插件部分我们后边在讲
@babel/generatorgenerator方法接收的AST抽象语法树转换成字符串形式的代码,同时还会创建源码映射(sourceMap,根据传入的参数控制是否生成sourceMap

上边也提到了,@babel/traversetraverse转换过程是深度遍历整颗树对节点进行操作,它会访问树中的所有节点。这时候该方法第二个参数就起到作用了。这个参数是一个对象,对象每个属性是一个钩子函数。这个对象的属性值除了支持AST语法树节点的type值外,还有enterexit;也就是在遍历每个节点的时候会先进入enter钩子函数,如果存在该节点对应的钩子函数,还会执行该钩子函数,最后在访问该节点结束的时候执行exit钩子函数...

修改转换代码如下:

traverse(ast, {
    enter(path){
        console.log(path.type, "-进入")
    },
    exit(path){
        console.log(path.type,"-离开")
    }
});

再次debug运行代码

Program -进入
VariableDeclaration -进入
VariableDeclarator -进入 
Identifier -进入
Identifier -离开
NumericLiteral -进入     
NumericLiteral -离开
VariableDeclarator -离开
VariableDeclaration -离开
ExpressionStatement -进入
AssignmentExpression -进入
Identifier -进入
Identifier -离开
BinaryExpression -进入
Identifier -进入
Identifier -离开
NumericLiteral -进入
NumericLiteral -离开
BinaryExpression -离开
AssignmentExpression -离开
ExpressionStatement -离开
Program -离开

从上边打印结果可以看出,遍历到每个节点时都有执行enterexit函数。合AST抽象语法树对比,也能看出确实属于深度优先递归遍历

接下来我们在添加VariableDeclaration钩子函数代码如下,

traverse(ast, {
    enter(path){
        
    },
    VariableDeclaration(path){
        console.log(path.type)
    },
    exit(path){
        
    }
});

再次debug运行代码,VariableDeclaration函数会执行一次,因为我们这个AST语法树只有一个VariableDeclaration类型的节点。

到这里,相信很多小伙伴注意到了,钩子函数path参数是做什么的?

path代表着在遍历AST的过程中连接两个节点的路径,你可以通过path.node获取当前的节点path.parent.node获得父节点,它也提供了path.replaceWith, path.removeAPI,这样就能通过一定条件来获取特点的节点进行修改了。

到这里可能有的小伙伴还有一个问题,babel可能定义了很多节点类型,我们怎么知道不同类型的节点是什么呢?

官方给出了所有类型点我查看类型,这里类型太多了,现用现查文档吧!!!

@babel/types

这里小编也推荐一个插件@babel/types,该插件包含非常多api官方文档。它的作用是创建、修改、删除、查找ast节点。另外从上边知道AST的节点也是分为多种类型,比如ExpressionStatement是表达式、ClassDeclaration是类声明、VariableDeclaration是变量声明等等,同样的这些类型都对应了其创建方法:t.expressionStatementt.classDeclarationt.variableDeclaration,也对应了判断方法:t.isExpressionStatementt.isClassDeclarationt.isVariableDeclaration。这个插件往往和traverse遍历插件一起使用,因为types只能对单一节点进行操作,一般是在对节点的深度遍历中使用。

相信到这里,小伙伴们对babel编译原理已经有了基本了解,并且对AST抽象语法树也有了了解。下一边文章我们来实践一下怎么编写一个babel插件

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):