美文网首页JSJavaScript 进阶营让前端飞
babel插件实践(一)babel编译原理分析

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

作者: 小猿_Luck_Boy | 来源:发表于2021-08-26 16:17 被阅读0次

    前言

    我们都知道在前端编译构建工具出现之前,前端项目基本都是用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插件

    相关文章

      网友评论

        本文标题:babel插件实践(一)babel编译原理分析

        本文链接:https://www.haomeiwen.com/subject/picliltx.html