babel-从入门到上手

a33a9f04d4747da65365494735293080.png

本文将引导你一步一步的学会babel,在学习的过程将着重介绍以下几点:

  • babel转译的过程

  • AST介绍

  • babel常用的api

  • @babel/preset-env

  • @babel/plugin-transform-runtime

1.babel的作用

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

2.babel转译的三个阶段

  1. 源码 parse 生成 AST(parse)

  2. 遍历 AST 并进行各种增删改(核心)(transform)

  3. 转换完 AST 之后再打印成目标代码字符串(generate)27b4b4502bbe2a8a1f8dbe613dc42669.png

3. AST 如何生成

在学习AST和babel转换时,可以借助下面两个网站辅助查看代码转换成AST之后的结果。

https://esprima.org/demo/parse.html

https://astexplorer.net/

整个解析过程主要分为以下两个步骤:词法分析语法分析

3.1 词法分析

词法分析,这一步主要是将字符流(char stream)转换为令牌流(token stream),又称为分词。其中拆分出来的各个部分又被称为 词法单元 (Token)

可以这么理解,词法分析就是把你的代码从 string 类型转换成了数组,数组的元素就是代码里的单词(词法单元), 并且标记了每个单词的类型。

比如:

const a = 1

生成的tokenList

[
    { type: 'Keyword', value: 'const' },
    { type: 'Identifier', value: 'a' },
    { type: 'Punctuator',value: '=' },
    { type: 'Numeric', value: '1' },
    { type: 'Punctuator', value: ';' },
];

词法分析结果,缺少一些比较关键的信息:需要进一步进行 语法分析 。

3.2 语法分析

语法分析会将词法分析出来的 词法单元 转化成有语法含义的 抽象语法树结构(AST),同时,验证语法,语法如果有错,会抛出语法错误。

这里截取AST树上的program的body部分(采用@babel/parser进行转化)

"body": [
    {
       "type": "VariableDeclaration",
       "start": 0,
       "end": 11,
       "loc": {},
       "declarations": [
        {
           "type": "VariableDeclarator",
           "start": 6,
           "end": 11,
           "loc": {},
           "id": {
             "type": "Identifier",
             "start": 6,
             "end": 7,
             "loc": {},
             "name": "a"
          },
           "init": {
             "type": "NumericLiteral",
             "start": 10,
             "end": 11,
             "loc": {},
             "extra": {},
             "value": 1
          }
        }
      ],
       "kind": "const"
    }
  ]

可以看到,经过语法分析阶段转换后生成的AST,通过树形的层属关系建立了语法单元之间的联系。

4. AST节点

转换后的AST是由很多AST节点组成,主要有以下几种类型:字面量(Literal)、标志符(Identifer)、语句(statement)、声明(Declaration)、表达式(Expression)、注释(Comment)、程序(Program)、文件(File)

每种 AST节点都有自己的属性,但是它们也有一些公共属性:

  • 结点类型(type):AST 节点的类型。

  • 位置信息(loc):包括三个属性start、 end、 loc。其中start 和 end 代表该节点对应的源码字符串的开始和结束下标,不区分行列。loc 属性是一个对象,有 line 和 column 属性分别记录开始和结束行列号。

  • 注释(comments):主要分为三种leadingComments、innerComments、trailingComments ,分别表示开始的注释、中间的注释、结尾的注释。

5. babel 常用的api

babel中有五个常用的api:

  1. 针对parse阶段有@babel/parser,功能是把源码转成 AST

  2. 针对transform 阶段有 @babel/traverse,用于增删改查AST

  3. 针对generate 阶段有@babel/generate,会把 AST 打印为目标代码字符串,同时生成 sourcemap

  4. 在transform阶段,当需要判断和生成结点时,需要@babel/types,

  5. 当需要批量创建 AST 的时候可以使用 @babel/template 来简化 AST 创建逻辑。

我们可以通过这些常用的api来自己实现一个plugin,对代码进行转换。接下来就介绍一下几个常见的api。

5.1 @babel/parser

babelParser.parse(code, [options])--- 返回的 AST 根节点是File节点babelParser.parseExpression(code, [options])---返回的AST根结点是Expression

第一个参数是源代码,第二个参数是options,其中最常用的就是 plugins、sourceType 这两个:

  • sourceType: 指示分析代码的模式,主要有三个值:script、module、unambiguous。

  • plugins:指定要使用插件数组。

5.2 @babel/traverse(核心)

function traverse(ast, opts)

ast:经过parse之后的生成的ast

opts :指定 visitor 函数--用于遍历节点时调用(核心)

方法的第二参数中的visitor是我们自定义插件时经常用到的地方,你可以通过两种方式来定义这个参数

第一种是以方法的形式声明visitor

traverse(ast, {
   BlockStatement(path, state) {
       console.log('BlockStatement>>>>>>')
  }
});

第二种是以对象的形式声明visitor

traverse(ast, {
   BlockStatement: {
       enter(path, state) {
           console.log('enter>>>', path, state)
      },
       exit(path, state) {
           console.log('exit>>>', path, state)
      }
  }
});

每一个visitor函数会接收两个参数 pathstate,path用来操作节点、遍历节点和判断节点,而state则是遍历过程中在不同节点之间传递数据的机制, 我们也可以通过 state 存储一些遍历过程中的共享数据。

5.3. @babel/generator

转换完AST之后,就要打印目标代码字符串,这里通过@babel/generator来实现,其方法常用的参数有两个:

  • 要打印的 AST

  • options--指定打印的一些细节,比如comments指定是否包含注释

6. babel的内置功能

上面我们介绍了几个用于实现插件的api,而babel本身为了实现对语法特性的转换以及对api的支持(polyfill),也内置了很多的插件(plugin)预设(preset)

其插件主要分为三类:

  • syntax plugin:只是在parse阶段使用,可以让 parser 能够正确的解析对应的语法成 AST

  • transform plugin:是对 AST 的转换,针对es20xx 中的语言特性、typescript、jsx 等的转换都是在这部分实现的

  • proposal plugin:未加入语言标准的特性的 AST 转换插件

那么预设是什么呢?预设其实就是对于插件的一层封装,通过配置预设,使用者可以不用关心具体引用了什么插件,从而减轻使用者的负担。

而根据上面不同类型的插件又产生了如下几种预设:

  • 专门根据es标准处理语言特性的预设 -- babel-preset-es20xx

  • 对其react、ts兼容的预设 -- preset-react  preset-typescript

我们目前最常使用的便是 @babel/preset-env这个预设,下文将会通过一个例子来介绍它的使用。

7. 案例1--自定义插件

需求

如果有一行代码

const a = 1

我需要通过babel自定义插件来给标识符增加类型定义,让它成为符合ts规范的语句,结果:const a: number = 1。

实现

通过babel处理代码,其实就是在对AST节点进行处理。

我们先搭起一个架子

// 源代码
const sourceCode = `
 const a = 1
`;
// 调用parse,生成ast
const ast = parser.parse(sourceCode, {})

// 调用traverse执行自定义的逻辑,处理ast节点
traverse(ast, {})

// 生成目标代码
const { code } = generate(ast, {});

console.log('result after deal with》〉》〉》', code)

在引入对应的包后,我们的架子主要分为三部分,我们首先需要知道这句话转换完之后的AST节点类型

"sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 0,
        "end": 11,
        "loc": {...},
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 6,
            "end": 11,
            "loc": {...},
            "id": {...},
            "init": {...}
          }
        ],
        "kind": "const"
      }
    ]

上图可以看出这句话的类型是VariableDeclaration,所以我们要写一个可以遍历VariableDeclaration节点的visitor。

// 调用traverse执行自定义的逻辑,处理ast节点
traverse(ast, {
     VariableDeclaration(path, state) {
       console.log('VariableDeclaration>>>>>>', path.node.type)
    }
})

继续观察结构,该节点下面有declarations属性,其包括所有的声明,declarations[0]就是我们想要的节点。

traverse(ast, {
    VariableDeclaration(path, state) {
       console.log('VariableDeclaration>>>>>>', path.node.type)
       const tarNode = path.node.declarations[0]
       console.log('tarNode>>>>>>', tarNode)
    }
})

每一个声明节点类型为VariableDeclarator,该节点下有两个重要的节点,id(变量名的标识符)和 init(变量的值)。这里我们需要找到变量名为 a 的标识符,且他的值类型为number(对应的节点类型为NumericLiteral)。

"declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "loc": {...},
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "loc": {...},
            "name": "a"
          },
          "init": {
            "type": "NumericLiteral",
            "start": 10,
            "end": 11,
            "loc": {...},
            "extra": {...},
            "value": 1
          }
        }
      ]

这时候就需要我们使用一个新的包 @babel/types 来判断类型。判断类型时只需调用该包中对应的判断方法即可,方法名都是以isXxx或者assertXxx来命名的(Xxx代表节点类型),需要传入对应的节点才能判断该节点的类型。

traverse(ast, {
  VariableDeclaration(path, state) {
    const tarNode = path.node.declarations[0]
    if(types.isIdentifier(tarNode.id) && types.isNumericLiteral(tarNode.init))       
     {
       console.log('inside>>>>>>')
     }
  }
})

锁定了节点后,我们需要更改id节点的name内容, 就可以实现需求了。

traverse(ast, {
  VariableDeclaration(path, state) {
  const tarNode = path.node.declarations[0]
    if(types.isIdentifier(tarNode.id)&&types.isNumericLiteral(tarNode.init))       
    {
       console.log('inside>>>>>>')
       tarNode.id.name = `${tarNode.id.name}: number`
    }
  }
})

8. 案例2--工程化使用

8.1 准备工作

@babel/core是babel的核心库,@babel/cli是babel的命令行工具。如果要使用babel,首先要安装 @babel/core@babel/cli

源代码为:

const fn = () => 1 ;

位置放在src下的test.js文件。

8.2 通过配置文件使用

根据官方文档的说法,目前有两类配置文件:项目范围配置 和 文件相对配置。

1.项目范围配置(全局配置) -- babel.config.json
2.文件相对配置(局部配置) -- .babelrc.json、package.json

区别:第一种配置作用整个项目,如果 babel 决定应用这个配置文件,则一定会应用到所有文件的转换。而第二种配置文件只能应用到“当前目录”下的文件中。

babel 在决定一个 js 文件应用哪些配置文件时,会执行如下策略: 如果这个 js 文件在当前项目内,则会递归向上搜索最近的一个 .babelrc 文件(直到遇到package.json),将其与全局配置合并。

这里我们只需使用babel.config.json的形式进行配置

配置文件:

{
     "presets": [
           [
                 "@babel/preset-env"
           ]
      ]
}

再在package.json里配置一下执行的脚本

"dev": "./node_modules/.bin/babel src --out-dir lib"
8.4 常用的包
我们在工程里常用包主要有两个:
  • @babel/preset-env
  • @babel/plugin-transform-runtime
8.4.1 @babel/preset-env

@babel/preset-env是一个智能的预设,它允许你使用最新的JavaScript,而不需要微管理你的目标环境需要哪些语法转换,根据babel官网上的描述,它是通过browsersList、compat-table相结合来实现智能的引入语法转换工具。db9a4bee88d6136127ed2d7d79f82fe3.png

compat-data形如如下,其注明了什么特性,在什么环境下支持,再结合通过browsersList查询出的环境版本号,就可以确定需要引入哪些plugin或者preset。

"es6.array.fill": {
   "chrome": "45",
   "opera": "32",
   "edge": "12",
   "firefox": "31",
   "safari": "7.1",
   "node": "4",
   "ios": "8",
   "samsung": "5",
   "rhino": "1.7.13",
   "electron": "0.31"
},

@babel/preset-env有三个常用的关键可选项:

  • targets

  • useBuiltIns

  • corejs

targets

描述项目支持的环境/目标环境,支持browserslist查询写法

{  "targets": "> 0.25%, not dead" }  // 全球使用人数大于0.25%且还没有废弃的版本

支持最小环境版本构成的对象

{  "targets": { "chrome": "58",  "ie": "11" } }

如果没配置targets, Babel会假设你的目标是最老的浏览器 @babel/preset-env将转换所有ES2015-ES2020代码为ES5兼容

useBuiltIns

可以使用三个值:"usage" 、"entry" 、 false,默认使用false

false

当使用false时:在不主动import的情况下不使用preset-env来处理polyfills

entry

babel将会根据浏览器目标环境(targets)的配置,引入全部浏览器暂未支持的polyfill模块,只要我们在打包配置入口 或者 文件入口写入 import "core-js" 这样的引入, babel 就会根据当前所配置的目标浏览器(browserslist)来引入所需要的polyfill 。

usage

设置useBuiltIns的值为usage时,babel将会根据我们的代码使用情况自动注入polyfill。

8.4.2 entry与usage的区别

在上文所示例子的基础上,我们修改一下源代码

function test() {
    new Promise()
}
test()
const arr = [ 1 , 2 , 3 , 4 ].map(item => item * item)
console.log(arr)

我们没有配置useBuiltIns时,preset-env只对代码的语法进行了处理,对于新增的api并没有引入对应的polyfill。下图是转换结果:

"use strict";

function test() {
    new Promise();
}

test();
var arr = [1, 2, 3, 4].map(function (item) {
    return item * item;
});
console.log(arr);

当我们使用useBuiltIns:“entry”时(入口文件需要引入core-js),由于我们没有指定targets,结果当然是引入了一堆包。

4192d31fc3d3b63bcb3716efe49b322b.png

加入"targets": "> 0.25%, not dead"后,很明显少了很多的引入(如下图所示),这也印证了上面所说的, 当 useBuiltIns 的值为 entry 时,@babel/preset-env 会按照你所设置的targets来引入所需的polyfill。

69ab66227a20bbbf4d9ec30a5ab910f3.png

当我们使用useBuiltIns:“usage”时,这时就无须在入口文件引入core-js了。

5b9d005b1d6730708fd6ad3a69750d69.png

可以看出引入的包非常精准,需要哪些就引入哪些polyfill。当然你也可以配置targets,这样的话targets会辅助preset-env引入,从而进一步控制引入包数量。

corejs

corejs是JavaScript的模块化标准库,其中包括各种ECMAScript特性的polyfill。上面我们转换后的代码中引入的polyfill都是来源于corejs。它现有2和3两个版本,目前2版本已经进入功能冻结阶段了,新的功能会添加到3版本中。

具体的变化可以查看corejs的github的说明文档:core-js@3, babel and a look into the future

这个选项只有在和 useBuiltIns: "usage" 或 useBuiltIns:"entry" 一起使用时才有效果,该属性默认为"2.0"。其作用是进一步约束引入的polyfill的数量。

8.4.3 @babel/plugin-transform-runtime

虽然经过了preset-env的转换,代码已经可以实现不同版本的特性兼容了。但是会产生两个问题:

1.preset-env转换后引入的polyfill,是通过require进行引入的,这就意味着,对于Array.from 等静态方法,以及 includes 等实例方法,会直接在 global 上添加。这就导致引入的polyfill方法可能和其他库发生冲突。

2.babel转换代码的时候,可能会使用一些帮助函数来协助转换,比如class

class a {}

转换之后:

600e5f6e6c9afa1044f9b5d065e352a3.png

这里就使用了_classCallCheck这样的辅助函数,如果有多个文件声明class的话,就会重复创建这样的方法。

@babel/plugin-transform-runtime这个插件的作用就是为了处理这样的问题。该插件也有一个corejs的配置,这里配置的是runtime-corejs 的版本,目前有 2、3 两个版本。

{
 "presets": [
  [
     "@babel/preset-env"
  ]
],
 "plugins": [
  [
     "@babel/plugin-transform-runtime",
    {
       "corejs": "3.0"
    }
  ]
]
}

转换结果:

57ede693719191a22255d1f5247f7ca2.png

这里由于babel是先执行plugins后执行presets的内容,@babel/plugin-transform-runtime插件先于preset-env将polyfill引入了,并且做了一层包装,所以就无须再通过@babel/preset-env来引入polyfill了。

可以看到,转换之后的_classCallCheck的方法定义全部改为了从runtime-corejs中引入,对于新特性的polyfill也不再挂载在全局了。这样的方法适合定义类库时使用,可以防止变量污染全局。

结束语

相信通过这么一篇文章,大家基本都了解了babel的基础原理,以及它是如何实现对代码的转换的,并可以自己实现简单的一个babel的插件。当然,本文中所述之内容只是babel全部内容的十之一二,只是作一个学习babel的引路石,如果有较强烈的需求,还是要常翻阅官方文档。最后希望大家可以将bebel学的更透彻。

参考文章:

  1. https://juejin.cn/post/6844903797571977223

  2. https://juejin.cn/post/6844904013033373704

  3. https://www.cnblogs.com/zhishaofei/p/13896056.html

  4. https://zhuanlan.zhihu.com/p/367724302

  5. https://www.babeljs.cn/

  6. https://juejin.cn/book/6946117847848321055

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值