babel

526 阅读5分钟

本文为了讲解方便,都是用 Babel 原生的 @babel/cli 来编译文件,实际使用中,更多的是结合 webpack、rollup 这样第三方的工具来使用的。

babel是什么

babel是JS的编译器,把 ES6 的代码转化为浏览器或者其它环境支持的代码

syntax和built-in转译

Babel 把 ES6 的标准分为 syntax 和 built-in 两种类型。syntax 就是语法,像 const、=> 这些默认被 Babel 转译的就是 syntax 的类型。而对于那些可以通过改写覆盖的语法就认为是 built-in,像 includes 和 Promise 这些都属于 built-in。

Babel 默认只转译 syntax 类型的。

对于 built-in 类型的就需要就需要进行polyfill,比如这样。过去依赖 @babel-polyfill ,现在依赖 core-js。

Object.defineProperty(Array.prototype, 'includes',function(){  
...
})

built-in 类型语法 的 polyfill 实现方式

截至目前为止,对于 built-in 类型的语法的 polyfill,一共有三种方式:

  • 使用 @babel/preset-env ,useBuiltIns 设置为 'entry'

  • 使用 @babel/preset-env ,useBuiltIns 设置为 'usage'

  • 使用 @babel/plugin-transform-runtime

前两种方式支持设置 targets ,可以根据目标环境来适配。useBuiltIns 设置为 'entry' 会注入目标环境不支持的所有 built-in 类型语法,useBuiltIns 设置为 'usage' 会注入目标环境不支持的所有被用到的 built-in 类型语法。注入的 built-in 类型的语法会污染全局(比如修改Array.prototype)。

第三种方式目前不支持设置 targets,所以不会考虑目标环境是否已经支持,所有环境下都会做转译。它是通过局部变量的方式实现了所有被用到的 built-in 类型语法,不会污染全局

未来方案:Polyfill provider

Babel正在尝试既能配置targets,又不污染全局的方案

大概写法是, targets, presets, plugins, polyfills都是平级的

// babel.config.js
const targets = [
  '>1%'
]
const presets = [
  [
    '@babel/env',
    {
      debug: true
    }
  ]
]
const plugins = [
  '@babel/plugin-proposal-class-properties'
]
const polyfills = [
  [
    'corejs3',
    {
      method: 'usage-pure'
    }
  ]
]
 
module.exports = { targets, presets, plugins, polyfills }

配置中的 method 值有 'entry-global'、'usage-global'、'usage-pure' 三种。 

第一个单词控制是否按需引入polyfill,第二个参数控制该polyfill是否是局部变量

  •  'entry-global' 等价于 @babel/preset-env 中的 useBuiltIns: 'entry' 
  •  'usage-global' 等价于 @babel/preset-env 中的 useBuiltIns: 'usage' 
  •  'usage-pure' 等价于 @babel/plugin-transform-runtime 中的 corejs

设置 method : 'usage-pure',即可做到:既能配置targets,又能注入目标环境不支持的被用到的built-in类型语法,又不污染全局的方案

配置targets

@babel/preset-env 中还有一个非常重要的参数 targets,最早的时候我们就提过,Babel 转译是按需的,对于环境支持的语法可以不做转换的。就是通过配置 targets 属性,让 Babel 知道目标环境,从而只转译环境不支持的语法。如果没有配置会默认转译所有 ES6 的语法。

// babel.config.js
const presets = [
  [
    '@babel/env',
    {
      debug: true,
      targets: {
        chrome: '58'
      }
    }
  ]
]
const plugins = ['@babel/plugin-proposal-class-properties']
 
module.exports = { presets, plugins }

babel工作流程

前面提到 Babel 其实就是一个纯粹的 JavaScript 的编译器,任何一个编译器工作流程大致都可以分为如下三步:

  • Parser 解析源文件

  • Transfrom 转换

  • Generator 生成新文件

Babel 也不例外,如下图所示:

简单的说,就是这个流程:

  1. 这个库会先将源码转化为抽象语法树 (AST);
  2. 再对 AST 作转换:对syntax进行转译,对built-in进行polyfill;
  3. 最后将转化后的 AST 生成最终代码,便得到了被 Babel 编译后的文件。

那 Babel 是如何知道该怎么转化的呢?答案是通过插件,Babel 为每一个新的语法提供了一个插件,在 Babel 的配置中配置了哪些插件,就会把插件对应的语法给转化掉。插件被命名为 @babel/plugin-xxx 的格式。

更具体的流程如下:

babel原理浅析

  • 解析
    将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过Babylon(github.com/babel/babyl…)实现的。解析过程有两个阶段:词法分析语法分析,词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成 AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。

  • 转换
    转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 Babel通过babel-traverse对其进行深度优先遍历,维护AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。

  • 生成
    代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)(www.html5rocks.com/en/tutorial…)。.
    代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
    Babel通过
    babel-generator
    再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。

关于AST,详见这里

AST详解与运用

参考

前端科普系列(4):Babel —— 把 ES6 送上天的通天塔

AST详解与运用