乐趣区

关于babel:一口气了解babel

最近几年,如果你是一名前端开发者,如果你没有应用甚至据说过 babel,可能会被当做穿梭者吧?

说到 babel,一连串名词会蹦出来:

  • babel-cli
  • babel-core
  • babel-runtime
  • babel-node
  • babel-polyfill

这些都是 babel 吗?他们别离是做什么的?有区别吗?

babel 到底做了什么?怎么做的?

简略来说把 JavaScript 中 es2015/2016/2017/2046 的新语法转化为 es5,让低端运行环境 (如浏览器和 node) 可能意识并执行。本文以 babel 6.x 为基准进行探讨。最近 babel 出了 7.x,放在最初聊。

严格来说,babel 也能够转化为更低的标准。但以目前状况来说,es5 标准曾经足以笼罩绝大部分浏览器,因而惯例来说转到 es5 是一个平安且风行的做法。

如果你对 es5/es2015 等等也不理解的话,那你可能真的须要先补补课了。

应用办法

总共存在三种形式:

  1. 应用单体文件 (standalone script)
  2. 命令行 (cli)
  3. 构建工具的插件 (webpack 的 babel-loader, rollup 的 rollup-plugin-babel)。

其中前面两种比拟常见。第二种多见于 package.json 中的 scripts 段落中的某条命令;第三种就间接集成到构建工具中。

这三种形式只有入口不同而已,调用的 babel 内核,解决形式都是一样的,所以咱们先不纠结入口的问题。

运行形式和插件

babel 总共分为三个阶段:解析,转换,生成。

babel 自身不具备任何转化性能,它把转化的性能都合成到一个个 plugin 外面。因而当咱们不配置任何插件时,通过 babel 的代码和输出是雷同的。

插件总共分为两种:

  • 当咱们增加 语法插件 之后,在解析这一步就使得 babel 可能解析更多的语法。(顺带一提,babel 外部应用的解析类库叫做 babylon,并非 babel 自行开发)

举个简略的例子,当咱们定义或者调用办法时,最初一个参数之后是不容许减少逗号的,如 callFoo(param1, param2,) 就是非法的。如果源码是这种写法,通过 babel 之后就会提醒语法错误。

但最近的 JS 提案中曾经容许了这种新的写法(让代码 diff 更加清晰)。为了防止 babel 报错,就须要减少语法插件 babel-plugin-syntax-trailing-function-commas

  • 当咱们增加 转译插件 之后,在转换这一步把源码转换并输入。这也是咱们应用 babel 最实质的需要。

比起语法插件,转译插件其实更好了解,比方箭头函数 (a) => a 就会转化为 function (a) {return a}。实现这个工作的插件叫做 babel-plugin-transform-es2015-arrow-functions

同一类语法可能同时存在语法插件版本和转译插件版本。如果咱们应用了转译插件,就不必再应用语法插件了。

配置文件

既然插件是 babel 的基本,那如何应用呢?总共分为 2 个步骤:

  1. 将插件的名字减少到配置文件中 (根目录下创立 .babelrc 或者 package.json 的 babel 外面,格局雷同)
  2. 应用 npm install babel-plugin-xxx 进行装置

具体书写格局就不详述了。

preset

比方 es2015 是一套标准,蕴含大略十几二十个转译插件。如果每次要开发者一个个增加并装置,配置文件很长不说,npm install 的工夫也会很长,更不谈咱们可能还要同时应用其余标准呢。

为了解决这个问题,babel 还提供了一组插件的汇合。因为罕用,所以不用反复定义 & 装置。(单点和套餐的差异,套餐省下了巨多的工夫和配置的精力)

preset 分为以下几种:

  • 官网内容,目前包含 env, react, flow, minify 等。这里最重要的是 env,前面会具体介绍。
  • stage-x,这外面蕴含的都是当年最新标准的草案,每年更新。
    这外面还细分为
    • Stage 0 – 稻草人: 只是一个想法,通过 TC39 成员提出即可。
    • Stage 1 – 提案: 初步尝试。
    • Stage 2 – 初稿: 实现初步标准。
    • Stage 3 – 候选: 实现标准和浏览器初步实现。
    • Stage 4 – 实现: 将被增加到下一年度公布。

例如 syntax-dynamic-import 就是 stage-2 的内容,transform-object-rest-spread 就是 stage-3 的内容。
此外,低一级的 stage 会蕴含所有高级 stage 的内容,例如 stage-1 会蕴含 stage-2, stage-3 的所有内容。
stage-4 在下一年更新会间接放到 env 中,所以没有独自的 stage-4 可供使用。

  • es201x, latest
    这些是曾经纳入到标准规范的语法。例如 es2015 蕴含 arrow-functions,es2017 蕴含 syntax-trailing-function-commas。但因为 env 的呈现,使得 es2016 和 es2017 都曾经废除。所以咱们常常能够看到 es2015 被独自列出来,但极少看到其余两个。
    latest 是 env 的雏形,它是一个每年更新的 preset,目标是蕴含所有 es201x。但也是因为更加灵便的 env 的呈现,曾经废除。

执行程序

很简略的几条准则:

  • Plugin 会运行在 Preset 之前。
  • Plugin 会从前到后程序执行。
  • Preset 的程序则 刚好相同(从后向前)。

preset 的逆向程序次要是为了保障向后兼容,因为大多数用户的编写程序是 ['es2015', 'stage-0']。这样必须先执行 stage-0 能力确保 babel 不报错。因而咱们编排 preset 的时候,也要留神程序,其实只有依照标准的工夫程序列出即可。

插件和 preset 的配置项

简略状况下,插件和 preset 只有列出字符串格局的名字即可。但如果某个 preset 或者插件须要一些配置项(或者说参数),就须要把本人先变成数组。第一个元素仍然是字符串,示意本人的名字;第二个元素是一个对象,即配置对象。

最须要配置的当属 env,如下:

"presets": [
    // 带了配置项,本人变成数组
    [
        // 第一个元素仍然是名字
        "env",
        // 第二个元素是对象,列出配置项
        {"module": false}
    ],

    // 不带配置项,间接列出名字
    "stage-2"
]

env (重点)

因为 env 最为罕用也最重要,所以咱们有必要重点关注。

env 的外围目标是通过配置得悉指标环境的特点,而后只做必要的转换。例如指标浏览器反对 es2015,那么 es2015 这个 preset 其实是不须要的,于是代码就能够小一点(个别转化后的代码总是更长),构建工夫也能够缩短一些。

如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不蕴含 stage-x 中的插件)。env 蕴含的插件列表保护在这里

上面列出几种比拟罕用的配置办法:

{
  "presets": [
    ["env", {
      "targets": {"browsers": ["last 2 versions", "safari >= 7"]
      }
    }]
  ]
}

如上配置将思考所有浏览器的最新 2 个版本 (safari 大于等于 7.0 的版本) 的个性,将必要的代码进行转换。而这些版本已有的性能就不进行转化了。这里的语法能够参考 browserslist

{
  "presets": [
    ["env", {
      "targets": {"node": "6.10"}
    }]
  ]
}

如上配置将指标设置为 nodejs,并且反对 6.10 及以上的版本。也能够应用 node: 'current' 来反对最新稳固版本。例如箭头函数在 nodejs 6 及以上将不被转化,但如果是 nodejs 0.12 就会被转化了。

另外一个有用的配置项是 modules。它的取值能够是 amd, umd, systemjs, commonjsfalse。这能够让 babel 以特定的模块化格局来输入代码。如果抉择 false 就不进行模块化解决。

其余配套工具

以上探讨了 babel 的外围解决机制和配置办法等,不管任何入口调用 babel 都走这一套。但文章结尾提的那一堆 babel-* 还是让人一头雾水。实际上这些 babel-* 大多是不同的入口 (形式) 来应用 babel,上面来简略介绍一下。

babel-cli

顾名思义,cli 就是命令行工具。装置了 babel-cli 就可能在命令行中应用 babel 命令来编译文件。

在开发 npm package 时常常会应用如下模式:

  • babel-cli 装置为 devDependencies
  • 在 package.json 中增加 scripts (比方 prepublish),应用 babel 命令编译文件
  • npm publish

这样既能够应用较新标准的 JS 语法编写源码,同时又能反对旧版环境。因为我的项目可能不太大,用不到构建工具 (webpack 或者 rollup),于是在公布之前用 babel-cli 进行解决。

babel-node

babel-nodebabel-cli 的一部分,它不须要独自装置。

它的作用是在 node 环境中,间接运行 es2015 的代码,而不须要额定进行转码。例如咱们有一个 js 文件以 es2015 的语法进行编写(如应用了箭头函数)。咱们能够间接应用 babel-node es2015.js 进行执行,而不必再进行转码了。

能够说:babel-node = babel-polyfill + babel-register。那这两位又是谁呢?

babel-register

babel-register 模块改写 require 命令,为它加上一个钩子。尔后,每当应用 require 加载 .js.jsx.es.es6 后缀名的文件,就会先用 babel 进行转码。

应用时,必须首先加载 require('babel-register')

须要留神的是,babel-register 只会对 require 命令加载的文件转码,而 不会对以后文件转码

另外,因为它是实时转码,所以 只适宜在开发环境应用

babel-polyfill

babel 默认只转换 js 语法,而不转换新的 API,比方 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的办法 (比方 Object.assign) 都不会转码。

举例来说,es2015 在 Array 对象上新增了 Array.from 办法。babel 就不会转码这个办法。如果想让这个办法运行,必须应用 babel-polyfill。(外部集成了 core-jsregenerator)

应用时,在所有代码运行之前减少 require('babel-polyfill')。或者更惯例的操作是在 webpack.config.js 中将 babel-polyfill 作为第一个 entry。因而必须把 babel-polyfill 作为 dependencies 而不是 devDependencies

babel-polyfill 次要有两个毛病:

  1. 应用 babel-polyfill 会导致打进去的包十分大,因为 babel-polyfill 是一个整体,把所有办法都加到原型链上。比方咱们只应用了 Array.from,但它把 Object.defineProperty 也给加上了,这就是一种节约了。这个问题能够通过独自应用 core-js 的某个类库来解决,core-js 都是离开的。
  2. babel-polyfill 会净化全局变量,给很多类的原型链上都作了批改,如果咱们开发的也是一个类库供其余开发者应用,这种状况就会变得十分不可控。

因而在理论应用中,如果咱们无法忍受这两个毛病(尤其是第二个),通常咱们会偏向于应用 babel-plugin-transform-runtime

但如果代码中蕴含高版本 js 中类型的实例办法 (例如 [1,2,3].includes(1)),这还是要应用 polyfill。

babel-runtime 和 babel-plugin-transform-runtime (重点)

咱们时常在我的项目中看到 .babelrc 中应用 babel-plugin-transform-runtime,而 package.json 中的 dependencies (留神不是 devDependencies) 又蕴含了 babel-runtime,那这两个是不是成套应用的呢?他们又起什么作用呢?

先说 babel-plugin-transform-runtime

babel 会转换 js 语法,之前曾经提过了。以 async/await 举例,如果不应用这个 plugin (即默认状况),转换后的代码大略是:

// babel 增加一个办法,把 async 转化为 generator
function _asyncToGenerator(fn) {return function () {....}} // 很长很长一段

// 具体应用处
var _ref = _asyncToGenerator(function* (arg1, arg2) {yield (0, something)(arg1, arg2);
});

不必过于纠结具体的语法,只需看到,这个 _asyncToGenerator 在以后文件被定义,而后被应用了,以替换源代码的 await。但每个被转化的文件都会插入一段 _asyncToGenerator 这就导致反复和节约了。

在应用了 babel-plugin-transform-runtime 了之后,转化后的代码会变成

// 从间接定义改为援用,这样就不会反复定义了。var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);

// 具体应用处是一样的
var _ref = _asyncToGenerator3(function* (arg1, arg2) {yield (0, something)(arg1, arg2);
});

从定义方法改成援用,那反复定义就变成了反复援用,就不存在代码反复的问题了。

但在这里,咱们也发现 babel-runtime 出场了,它就是这些办法的汇合处,也因而,在应用 babel-plugin-transform-runtime 的时候必须把 babel-runtime 当做依赖。

再说 babel-runtime,它外部集成了

  1. core-js: 转换一些内置类 (Promise, Symbols等等) 和静态方法 (Array.from 等)。绝大部分转换是这里做的。主动引入。
  2. regenerator: 作为 core-js 的拾遗补漏,次要是 generator/yieldasync/await 两组的反对。当代码中有应用 generators/async 时主动引入。
  3. helpers, 如下面的 asyncToGenerator 就是其中之一,其余还有如 jsx, classCallCheck 等等,能够查看 babel-helpers。在代码中有内置的 helpers 应用时 (如下面的第一段代码) 移除定义,并插入援用(于是就变成了第二段代码)。

babel-plugin-transform-runtime 不反对 实例办法 (例如 [1,2,3].includes(1))

此外补充一点,把 helpers 抽离并对立起来,防止反复代码的工作还有一个 plugin 也能做,叫做 babel-plugin-external-helpers。但因为咱们应用的 transform-runtime 曾经蕴含了这个性能,因而不用重复使用。而且 babel 的作者们也曾经开始探讨这两个插件过于相似,正在探讨在 babel 7 中把 external-helpers 删除,探讨在 issue#5699 中。

babel-loader

后面提过 babel 的三种应用办法,并且曾经介绍过了 babel-cli。但一些大型的我的项目都会有构建工具 (如 webpack 或 rollup) 来进行代码构建和压缩 (uglify)。实践上来说,咱们也能够对压缩后的代码进行 babel 解决,但那会十分慢。因而如果在 uglify 之前就退出 babel 解决,岂不完满?

所以就有了 babel 插入到构建工具外部这样的需要。以(我还算相熟的) webpack 为例,webpack 有 loader 的概念,因而就呈现了 babel-loader

babel-cli 一样,babel-loader 也会读取 .babelrc 或者 package.json 中的 babel 段作为本人的配置,之后的内核解决也是雷同。惟一比 babel-cli 简单的是,它须要和 webpack 交互,因而须要在 webpack 这边进行配置。比拟常见的如下:

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      loader: 'babel-loader'
    }
  ]
}

如果想在这里传入 babel 的配置项,也能够把改成:

// loader: 'babel-loader' 改成如下:use: {
  loader: 'babel-loader',
  options: {// 配置项在这里}
}

这里的配置项优先级是最高的。但我认为放到独自的配置文件中更加清晰正当,可读性强一些。

小结一下

Babel 7.x

最近 babel 公布了 7.0。因为下面局部都是针对 6.x 编写的,所以咱们关注一下 7.0 带来的变动(外围机制方面没有变动,插件,preset,解析转译生成这些都没有变动)

我只筛选一些和开发者关系比拟大的列在这里,省略的少数是针对某一个 plugin 的改变。残缺的列表能够参考官网。

preset 的变更:淘汰 es201x,删除 stage-x,强推 env (重点)

淘汰 es201x 的目标是把抉择环境的工作交给 env 主动进行,而不须要开发者投入精力。但凡应用 es201x 的开发者,都该当应用 env 进行替换。但这里的淘汰 (原文 deprecated) 并不是删除,只是不举荐应用了,不好说 babel 8 就真的删了。

与之相比,stage-x 就没那么好运了,它们间接被删了。这是因为 babel 团队认为为这些“不稳固的草案”破费精力去更新 preset 相当节约。stage-x 尽管删除了,但它蕴含的插件并没有删除(只是被更名了,能够看上面一节),咱们仍然能够显式地申明这些插件来取得等价的成果。残缺列表

为了缩小开发者替换配置文件的机械工作,babel 开发了一款 babel-upgrade 的工具,它会检测 babel 配置中的 stage-x 并且替换成对应的 plugins。除此之外它还有其余性能,咱们一会儿再具体看。(总之目标就是让你更加平滑地迁徙到 babel 7)

npm package 名称的变动 (重点)

这是 babel 7 的一个重大变动,把所有 babel-* 重命名为 @babel/*,例如:

  1. babel-cli 变成了 @babel/cli
  2. babel-preset-env 变成了 @babel/preset-env。进一步,还能够省略 preset 而简写为 @babel/env
  3. babel-plugin-transform-arrow-functions 变成了 @babel/plugin-transform-arrow-functions。和 preset 一样,plugin 也能够省略,于是简写为 @babel/transform-arrow-functions

这个变动不单单利用于 package.json 的依赖中,包含 .babelrc 的配置 (plugins, presets) 也要这么写,为了保持一致。例如

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

顺带提一句,下面提过的 babel 解析语法的内核 babylon 当初重命名为 @babel/parser,看起来是被收编了。

上文提过的 stage-x 被删除了,它蕴含的插件尽管保留,但也被重命名了。babel 团队心愿更显著地区分曾经位于标准中的插件 (如 es2015 的 babel-plugin-transform-arrow-functions) 和仅仅位于草案中的插件 (如 stage-0 的 @babel/plugin-proposal-function-bind)。形式就是在名字中减少 proposal,所有蕴含在 stage-x 的转译插件都应用了这个前缀,语法插件不在其列。

最初,如果插件名称中蕴含了标准名称 (-es2015-, -es3- 之类的),一律删除。例如 babel-plugin-transform-es2015-classes 变成了 @babel/plugin-transform-classes。(这个插件我本人没有独自用过,羞愧)

不再反对低版本 node

babel 7.0 开始不再反对 nodejs 0.10, 0.12, 4, 5 这四个版本,相当于要求 nodejs >= 6 (以后 nodejs LTS 是 8,要求也不算太过分吧)。

这里的不再反对,指的是在这些低版本 node 环境中不能应用 babel 转译代码,但 babel 转译后的代码仍然能在这些环境上运行,这点不要混同。

only 和 ignore 匹配规定的变动

在 babel 6 时,ignore 选项如果蕴含 *.foo.js,实际上的含意 (转化为 glob) 是 ./**/*.foo.js,也就是当前目录 包含子目录 的所有 foo.js 结尾的文件。这可能和开发者惯例的意识有悖。

于是在 babel 7,雷同的表达式 *.foo.js 只作用于当前目录,不作用于子目录。如果仍然想作用于子目录的,就要依照 glob 的残缺标准书写为 ./**/*.foo.js 才能够。only 也是雷同。

这个规定变动只作用于通配符,不作用于门路。所以 node_modules 仍然蕴含所有它的子目录,而不单单只有一层。(否则全世界开发者都要爆炸)

@babel/node 从 @babel/cli 中独立了

和 babel 6 不同,如果要应用 @babel/node,就必须独自装置,并增加到依赖中。

babel-upgrade

在提到删除 stage-x 时候提过这个工具,它的目标是帮忙用户自动化地从 babel 6 降级到 7。

这款降级工具的性能包含:(这里并不列出残缺列表,只列出比拟重要和罕用的内容)

  1. package.json
  2. 把依赖 (和开发依赖) 中所有的 babel-* 替换为 @babel/*
  3. 把这些 @babel/* 依赖的版本更新为最新版 (例如 ^7.0.0)
  4. 如果 scripts 中有应用 babel-node,主动增加 @babel/node 为开发依赖
  5. 如果有 babel 配置项,查看其中的 pluginspresets,把短名 (env) 替换为残缺的名字 (@babel/preset-env)
  6. .babelrc
  7. 查看其中的 pluginspresets,把短名 (env) 替换为残缺的名字 (@babel/preset-env)
  8. 查看是否蕴含 preset-stage-x,如有替换为对应的插件并增加到 plugins

应用形式如下:

# 不装置到本地而是间接运行命令,npm 的新性能
npx babel-upgrade --write

# 或者惯例形式
npm i babel-upgrade -g
babel-upgrade --write

babel-upgrade 工具自身也还在开发中,还列出了许多 TODO 没有实现,因而之后的性能可能会更加丰盛,例如下面提过的 ignore 的通配符转化等等。

退出移动版