zoukankan      html  css  js  c++  java
  • babel深入教程(babel7版本)

    最近在一些项目编译系统的工作中涉及到了很多关于babel插件的开发,关于babel大多数人的感受可能是既陌生又熟悉,可能大多数人对于babel的应用场景的认识就是在webpack中使用一个babel-loader,但当你真正了解他掌握它的时候,会发现他其实还有些更强的用法。。。

    基本概念

    babel是什么?

    Babel 是一个编译器(输入源码 => 输出编译后的代码)。就像其他编译器一样,编译过程分为三个阶段:解析、转换和打印输出。(官网的解释)。

    babel plugin和babel preset是什么?

    babel中有很多概念,比如:插件(plugin),预设(preset)和一些比较基础的工具(例如@babel/parser,@babel/traverse等等)。关于他们的关系,可以理解为babel的plugin构建在基础工具之上,而babel的preset是多个babel plugin的打包集合,例如我们所熟悉的@babel/preset-env,@babel/preset-react。

    babel深入

    本篇文章不对babel官方的plugin,preset库做过多阐述,毕竟这是一篇深入教程。我们要提的是一个更本质的问题:babel是如何转译代码的?

    我们大体上把这个转译代码的过程分为三步:

    • 第一步(parse):code=>ast
    • 第二步(transform):ast=>修改过的ast
    • 第三步(generate):修改过的ast=>编译后的code

    这三步分别对应babel的三个基本工具,第一步对应@babel/parser,第二步对应@babel/traverse,第三步对应@babel/generator。下面就来详述一下这三个过程。

    parse(@babel/parser)

    这一步是babel将code转化为ast。ast是Abstract syntax tree的缩写,即抽象语法树,单说抽象语法树可能不太好理解,我们可以先来看一下一个具体的例子,你可以使用来帮你运行@babel/parser:

    function mirror(something) {
      return something
    }

    被转译成ast:

    {
      "type": "File",
      "start": 0,
      "end": 49,
      "loc": {
        "start": {
          "line": 1,
          "column": 0
        },
        "end": {
          "line": 3,
          "column": 1
        }
      },
      "program": {
        "type": "Program",
        "start": 0,
        "end": 49,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 3,
            "column": 1
          }
        },
        "sourceType": "module",
        "interpreter": null,
        "body": [
          {
            "type": "FunctionDeclaration",
            "start": 0,
            "end": 49,
            "loc": {
              "start": {
                "line": 1,
                "column": 0
              },
              "end": {
                "line": 3,
                "column": 1
              }
            },
            "id": {
              "type": "Identifier",
              "start": 9,
              "end": 15,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 9
                },
                "end": {
                  "line": 1,
                  "column": 15
                },
                "identifierName": "mirror"
              },
              "name": "mirror"
            },
            "generator": false,
            "async": false,
            "params": [
              {
                "type": "Identifier",
                "start": 16,
                "end": 25,
                "loc": {
                  "start": {
                    "line": 1,
                    "column": 16
                  },
                  "end": {
                    "line": 1,
                    "column": 25
                  },
                  "identifierName": "something"
                },
                "name": "something"
              }
            ],
            "body": {
              "type": "BlockStatement",
              "start": 27,
              "end": 49,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 27
                },
                "end": {
                  "line": 3,
                  "column": 1
                }
              },
              "body": [
                {
                  "type": "ReturnStatement",
                  "start": 31,
                  "end": 47,
                  "loc": {
                    "start": {
                      "line": 2,
                      "column": 2
                    },
                    "end": {
                      "line": 2,
                      "column": 18
                    }
                  },
                  "argument": {
                    "type": "Identifier",
                    "start": 38,
                    "end": 47,
                    "loc": {
                      "start": {
                        "line": 2,
                        "column": 9
                      },
                      "end": {
                        "line": 2,
                        "column": 18
                      },
                      "identifierName": "something"
                    },
                    "name": "something"
                  }
                }
              ],
              "directives": []
            }
          }
        ],
        "directives": []
      },
      "comments": []
    }

    乍一看似乎很复杂,但是你要做的是从中找到关键信息,我们将当中影响阅读的字段去除(去除loc,start,end,以及函数体外层的嵌套):

    {
      "type": "FunctionDeclaration",
      "id": {
        "type": "Identifier",
        "name": "mirror"
      },
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "name": "something"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ReturnStatement",
            "argument": {
              "type": "Identifier",
              "name": "something"
            }
          }
        ],
        "directives": []
      }
    }

    这样是不是简单很多!我们看一下这个json描述了什么:外层是一个叫mirror的函数声明,他的传参有一个,叫something,函数体内部return了一个叫something的变量。我们把这个描述与上边的js代码对照着看,竟然不谋而合(其实从这一点也能看出code<=>ast这个过程是可逆的)。对于初学者而言,上边的抽象语法树难以理解的可能是这些名字冗长的节点type,下边简单列举一下js中的常见的节点名称(慎看,可以选择性跳过,但是了解这些节点名称可以加深你对babel甚至js语言本身的理解)。详见

    FunctionDeclaration(函数声明)
    
    function a() {}
    
    FunctionExpression(函数表达式)
    
    var a = function() {}
    
    ArrowFunctionExpression(箭头函数表达式)
    
    ()=>{}(此处可以思考:为什么没有箭头函数声明,以及Declaration和Expression的区别)
    
    AwaitExpression(await表达式)
    
    async function a () { await b() }
    
    CallExpression(调用表达式)
    
    a()
    
    MemberExpression(成员表达式)
    
    a.b
    
    VariableDeclarator(变量声明)
    
    var,const,let(var,const,let用Node中的kind区分)
    
    Identifier(变量标识符)
    
    var a(这里a是一个Identifier)
    
    NumericLiteral(数字字面量)
    
    var a = 1
    
    StringLiteral(字符串字面量)
    
    var a = 'a'
    
    BooleanLiteral(布尔值字面量)
    
    var a = true
    
    NullLiteral(null字面量)
    
    var a = null(此处可以思考:为什么没有undefined字面量)
    
    BlockStatement(块)
    
    {}
    
    ArrayExpression(数组表达式)
    
    []
    
    ObjectExpression(对象表达式)
    
    var a = {}
    
    SpreadElement(扩展运算符)
    
    {...a},[...a]
    
    ObjectProperty(对象属性)
    
    {a:1}(这里的a:1是一个ObjectProperty)
    
    ObjectMethod(函数属性)
    
    {a(){}}
    
    ExpressionStatement(表达式语句)
    
    a()
    
    IfStatement(ifif () {}
    
    ForStatement(forfor (;;){}
    
    ForInStatement(for infor (a in b) {}
    
    ForOfStatement(for of)
    
    for (a of b) {}
    
    ImportDeclaration(import声明)
    
    import 'a'
    
    ImportDefaultSpecifier(import default说明符)
    
    import a from 'a'
    
    ImportSpecifier(import说明符)
    
    import {a} from 'a'
    
    NewExpression(new表达式)
    
    new A()
    
    ClassDeclaration(class声明)
    
    class A {}
    
    ClassBody(class body)
    
    class A {}(类的内部)

    常见的列举的差不多了。。。就先写到这吧。

    generate(@babel/generator)

    generate本来应该是第三步,为什么将第三步放到这里呢?因为他比较简单,而且当我们使用traverse时,需要用到它。在这里我们简单的把一段code转换为ast,再转换为code:

    先安装好依赖。这一点以后不再赘述

    yarn add @babel/parser @babel/generator
    const parser = require('@babel/parser')
    const generate = require('@babel/generator').default
    
    const code = `function mirror(something) {
      return something
    }`
    const ast = parser.parse(code, {
      sourceType: 'module',
    })
    const transformedCode = generate(ast).code
    console.log(transformedCode)
    • 结果:
    function mirror(something) {
      return something;
    }

    这就是generator的基本用法,详细参照

    transform(@babel/traverse,@babel/types,@babel/template)

    到了最为关键的transform步骤了,这里的主角是@babel/traverse,@babel/types和@babel/template是辅助工具。我们首先来谈一下visitor这个概念。

    visitor

    1. visitor是什么
    访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。

    假如你这样写了一个visitor传递给babel:

    const visitor = {
      Identifier () {
        enter () {
          console.log('Hello Identifier!')
        },
        exit () {
          console.log('Bye Identifier!')
        }
      }
    }

    那么babel会使用他的递归遍历器去遍历整棵ast,在进入和退出Identifier节点时,会执行我们定义的函数。

    2.在一般情况下exit较少使用,所以可以简写成:

    const visitor = {
      Identifier () {
        console.log('Hello Identifier!')
      }
    }

    3.如有必要,你还可以把方法名用|分割成a节点类型|b节点类型形式的字符串,把同一个函数应用到多种访问节点。

    const visitor = {
      'FunctionExpression|ArrowFunctionExpression' () {
        console.log('A function expression or a arrow function expression!')
      }
    }

    好了,现在以上边的mirror函数为例,来动手写一个traverse的简单示例吧:

    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    
    const code = `function mirror(something) {
      return something
    }`
    const ast = parser.parse(code, {
      sourceType: 'module',
    })
    const visitor = {
      Identifier (path) {
        console.log(path.node.name)
      }
    }
    traverse(ast, visitor)
    • 结果:mirror,something,something

    与你的预估是否一致呢?如果一致,那我们可以继续往下。此处你可能提出疑问:这个path是什么?

    path

    可以简单地认为path是对当前访问的node的一层包装。例如使用path.node可以访问到当前的节点,使用path.parent可以访问到父节点,这里列出了path所包含的内容(尚未列出path中所包含的一些方法)。

    {
      "parent": {...},
      "node": {...},
      "hub": {...},
      "contexts": [],
      "data": {},
      "shouldSkip": false,
      "shouldStop": false,
      "removed": false,
      "state": null,
      "opts": null,
      "skipKeys": null,
      "parentPath": null,
      "context": null,
      "container": null,
      "listKey": null,
      "inList": false,
      "parentKey": null,
      "key": null,
      "scope": null,
      "type": null,
      "typeAnnotation": null
    }
    当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。babel handbook

    path中还提供了一系列的工具函数,例如traverse(在当前path下执行递归),remove(删除当前节点),replaceWith(替换当前节点)等等。

    解释完了path之后,我们试着真正的来转换一下代码吧,在这里使用了@babel/generator来将ast转换为code

    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const generate = require('@babel/generator').default
    
    const code = `function mirror(something) {
      return something
    }`
    const ast = parser.parse(code, {
      sourceType: 'module',
    })
    const visitor = {
      Identifier (path) {
        path.node.name = path.node.name.split('').reverse().join('')
      }
    }
    traverse(ast, visitor)
    const transformedCode = generate(ast).code
    console.log(transformedCode)
    • 结果:
    function rorrim(gnihtemos) {
      return gnihtemos;
    }

    这段代码应该不难理解,就是将所有的变量做了个字符串翻转。是不是事情已经变得有趣起来了?

    @babel/types

    Babel Types模块是一个用于 AST 节点的 Lodash 式工具库(译注:Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程风格的众多工具函数), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。(依然是handbook原话)

    展示一下最常用的使用方式,用来判断节点的类型

    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const t = require('@babel/types')
    
    const code = `function mirror(something) {
      return something
    }`
    const ast = parser.parse(code, {
      sourceType: 'module',
    })
    const visitor = {
      enter(path) {
        if (t.isIdentifier(path.node)) {
          console.log('Identifier!')
        }
      }
    }
    traverse(ast, visitor)
    • 结果:Identifier! Identifier! Identifier!

    @babel/types还可以用来生成节点,结合上边的知识,我们试着改动mirror函数的返回值

    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const generate = require('@babel/generator').default
    const t = require('@babel/types')
    
    const code = `function mirror(something) {
      return something
    }`
    const ast = parser.parse(code, {
      sourceType: 'module',
    })
    const strNode = t.stringLiteral('mirror')
    const visitor = {
      ReturnStatement (path) {
        path.traverse({
          Identifier(cpath){
            cpath.replaceWith(strNode)
          }
        })
      }
    }
    traverse(ast, visitor)
    const transformedCode = generate(ast).code
    console.log(transformedCode)
    • 结果:
    function mirror(something) {
      return "mirror";
    }

    在这里我们用到了t.stringLiteral('mirror')去创建一个字符串字面量节点,然后递归遍历ReturnStatement下的Identifier,并将其替换成我们所创建的字符串字面量节点(注意此处我们已经开始使用了一些path下的公共方法)。

    @babel/template

    使用@babel/type创建一些简单节点会很容易,但是如果是大段代码的话就会变得困难了,这个时候我们可以使用@babel/template。下面写了一个简单示例,为mirror函数内部写了一些逻辑判断。

    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const generate = require('@babel/generator').default
    const template = require('@babel/template').default
    const t = require('@babel/types')
    
    const code = `function mirror(something) {
      return something
    }`
    const ast = parser.parse(code, {
      sourceType: 'module',
    })
    const visitor = {
      FunctionDeclaration(path) {
        // 在这里声明了一个模板,比用@babel/types去生成方便很多
        const temp = template(`
          if(something) {
            NORMAL_RETURN
          } else {
            return 'nothing'
          }
        `)
        const returnNode = path.node.body.body[0]
        const tempAst = temp({
          NORMAL_RETURN: returnNode
        })
        path.node.body.body[0] = tempAst
      }
    }
    traverse(ast, visitor)
    const transformedCode = generate(ast).code
    console.log(transformedCode)
    • 结果:
    function mirror(something) {
      if (something) {
        return something;
      } else {
        return 'nothing';
      }
    }

    完美!以上,babel基本的工具使用方式就介绍的差不多了,下边步入正题:尝试写一个babel插件。

    写一个babel插件

    其实到这里,编写一个babel插件已经非常简单了,我们尝试直接将上边的代码移植成为一个babel插件

    module.exports = function (babel) {
      const {
        types: t,
        template
      } = babel
      const visitor = {
        FunctionDeclaration(path) {
          const temp = template(`
            if(something) {
              NORMAL_RETURN
            } else {
              return 'nothing'
            }
          `)
          const returnNode = path.node.body.body[0]
          const tempAst = temp({
            NORMAL_RETURN: returnNode
          })
          path.node.body.body[0] = tempAst
        }
      }
      return {
        name: 'my-plugin',
        visitor
      }
    }

    babel插件暴露了一个函数,函数的传参是babel,你可以使用解构赋值获取到types,template这些工具。函数返回值中包含一个name和一个visitor,name是插件的名称,visitor就是我们上边多次编写的visitor。

    你可能注意到了一些babel插件是可以传参的,那我们如何在babel插件中接收参数呢

    module.exports = function (babel) {
      const {
        types: t,
        template
      } = babel
      const visitor = {
        FunctionDeclaration(path, state) {
          const temp = template(`
            if(something) {
              NORMAL_RETURN
            } else {
              return '${state.opts.whenFalsy}'
            }
          `)
          const returnNode = path.node.body.body[0]
          const tempAst = temp({
            NORMAL_RETURN: returnNode
          })
          path.node.body.body[0] = tempAst
        }
      }
      return {
        name: 'my-plugin',
        visitor
      }
    }

    在上边的例子中我们看到在visitor中可以传入第二个参数state,在这个state中,使用state.opts[配置名]就可访问到用户所传递的对应配置名的值

    如何测试你所编写的babel插件是可以使用的呢?引用你所编写的插件并测试一下:

    const babel = require("@babel/core")
    
    const code = `function mirror(something) {
      return something
    }`
    const res = babel.transformSync(code, {
      plugins: [
        [require('你编写的插件地址'), {
          whenFalsy: 'Nothing really.'
        }]
      ]
    })
    
    console.log(res.code)
    • 结果:
    function mirror(something) {
      if (something) {
        return something;
      } else {
        return 'Nothing really.';
      }
    }

    以上,我们基本对babel的原理有了一个基本的认识,并且可以自己写出一个babel插件了。至于如何将babel的威力发挥在日常的工作中呢?就需要各位去自行探索了。

     

    喜欢这篇文章?欢迎打赏~~

  • 相关阅读:
    转:【More Effective C#】Lambda表达式优化
    转:Highcharts图表控件的使用
    C# subString的理解
    转:TimeSpan的用法
    Android学习笔记一:Android基本组件和Activity生命周期
    IIS 反向代理设置
    WebApi 身份认证解决方案:Basic基础认证
    Calling async method synchronously
    C# 公共类
    aspnet-api-versioning
  • 原文地址:https://www.cnblogs.com/cangqinglang/p/14204957.html
Copyright © 2011-2022 走看看