Babel是一个JavaScript编译器,不仅能将当前运行环境不支持的JavaScript语法(例如ES6、ES7等)编译成向下兼容的可用语法(例如ES3或ES5),这其中会涉及新语法的转换和缺失特性的修补;还支持语法扩展,从而能随时随地的使用JSX、TypeScript等语法。目前最新版本是7.4,自从6.0以来,Babel被分解的更加模块化,各种转译功能都以插件的形式分离出来,可按自己的需求,灵活配置。

  在7.0版本中,对Babel的包做了一次大调整,统一改成域级包,将原先以“babel-”为前缀的包迁移到@babel的命名空间,例如@babel/core、@babel/cli等。这种模块化的设计,既能区分是否是官方发布的,也能避免命名冲突。

一、@babel/core

  如果要以编程的方式使用Babel,那么可以通过@babel/core实现,安装命令如下所示。

npm install --save-dev @babel/core

  在引入该包后,就能在JavaScript文件中直接编译代码、文件或AST。以编译代码为例,可选择的方法有三个,如下所示。

var babel = require("@babel/core");
babel.transform(code, options, function(err, result) {
  console.log(result);                   // => { code, map, ast }
});
babel.transformSync(code, options)        // => { code, map, ast }
babel.transformAsync(code, options)       // => Promise<{ code, map, ast }>

  虽然transform()用于异步编译,transformSync()用于同步编译,但是它们得到的结果相同,都是一个由转换后的代码(code)、源码映射信息(map)和抽象语法树(ast)组成的对象。而transformAsync()得到的结果与前面两个方法不同,它返回一个包含code、map和ast的Promise对象。这些方法的第二个options参数,可用于传递配置信息,具体可参考官方文档

  如果要编译文件或AST,那么也有类似的三个方法可供选择。除了能编译之外,利用这个包还能把代码解析成AST(即传入一段代码,得到一个AST)以便其它插件对其进行语法分析,可使用的方法有parse()、parseSync()和parseAsync()。

二、@babel/cli

  @babel/cli是一个Babel内置的命令行工具,可通过命令来编译文件,其安装如下所示。

npm install --save-dev @babel/cli

  假设有一个src.js文件,其内容如下代码所示,用到了ES6中的箭头函数。

let fn = () => true;

  在运行下面的命令后,可以看到输出和输入的代码相同,这是因为此时还未指定任何转译插件。

./node_modules/.bin/babel src.js

  如果当前的npm版本是5.2以上,那么可将./node_modules/.bin缩短为npx,如下所示。

npx babel src.js

  在babel中,有多个可用的参数,下面只列举其中的四个,其余参数的用法可参考官方文档

npx babel src --out-dir dist
npx babel src.js --out-file dist.js
npx babel src.js --out-file dist.js --plugins=@babel/plugin-transform-classes,@babel/transform-modules-amd
npx babel src.js --out-file dist.js --presets=@babel/preset-env,@babel/flow

  (1)“--out-dir”参数可编译整个src目录下的文件并输出到dist目录中。

  (2)“--out-file”参数可编译src.js文件并输出到dist.js文件中。

  (3)“--plugins”参数可指定编译过程中要使用的插件,多个插件用逗号隔开。

  (4)“--presets”参数可指定编译过程中要使用的预设,多个预设用逗号隔开。

  在package.json文件中,可以通过scripts字段声明脚本命令。如果想要简化babel命令,那么可以将它们迁移到scripts字段中,如下所示。

{
  "scripts": {
    "compile": "npx babel src.js --out-file dist.js"
  }
}

  现在要编译目录的话,只要执行下面的这条npm命令即可。

npm run compile

三、配置

  在Babel中,可以将各种命令的参数集中到一个配置文件中,而可配置的文件包括babel.config.js、.babelrc和package.json。

1)babel.config.js

  这是Babel 7最新引入的配置文件,存在于根目录中(即package.json文件所在的目录)。它不仅能以编程的方式声明全局生效的配置参数,还能利用overrides字段对不同的子目录进行针对性的配置,从而就能避免为相关目录创建一个.babelrc文件了。在下面的示例中,为test目录单独配置了预设(presets)。

module.exports = {
  presets: [...],
  overrides: [{
    test: ["./test"],
    presets: [...]
  }]
};

  当运行下面两条命令进行编译时,第一条读取的是最外层的预设,第二条读取的是overrides中的预设。

npx babel src.js
npx babel ./test/src.js

2).babelrc

  babel.config.js并不是.babelrc的替代品,.babelrc文件用于局部配置(如下代码所示),可放置于所有目录中。如果当前目录没有.babelrc文件,那么就会往上查找直至找到为止。

{
  "presets": [...]
}

  注意,如果.babelrc的后缀是“.js”(即.babelrc.js),那么在文件中可以通过JavaScript配置参数。

3)package.json

  在package.json文件中,可以声明一个babel字段,其值就是.babelrc文件中的配置参数,如下所示。

{
  "babel": {
    "presets": [...]
  }
}

  注意,不能让.babelrc和声明过babel字段的package.json处在相同的目录中。

4)配置函数或方法

  前面三种都是用单独的文件来配置Babel的参数,其实还可以通过相关的函数或方法来达到相同地目的。在下面的示例中,引入gulp-babel包后就能通过得到的babel()函数来配置Babel的参数。

let babel = require("gulp-babel");
babel({
  presets: [...]
});

四、插件

  Babel的编译可分为三个阶段:解析、转换和生成,而插件(Plugin)在转换过程中起到了至关重要的作用。借助Babel的插件可将解析完成的AST按照特定的要求进行处理,然后再输出生成的代码。

1)插件类型

  Babel中的插件分为两种类型:语法和转换。语法插件只允许Babel解析成特定类型的语法,例如JSX、Flow等。转换插件常用来编译ES6、ES7、React和Modules等,由于能自动开启相应的语法插件,因此不用显式的指定两种插件。以之前的箭头函数为例,如果要将其编译成所有浏览器都能识别的形式,那么就得安装相应的插件,如下所示。

npm install --save-dev @babel/plugin-transform-arrow-functions

  在.babelrc文件中的配置如下所示。

{
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

  在配置文件中,不仅能指定插件的相对或绝对路径,还可省略插件名称中的“babel-plugin-”前缀。假设有个插件名叫@org/babel-plugin-name,那么它的相对路径和简写名称如下所示。

{
  "plugins": [
    "../@org/babel-plugin-name",
    "@org/name"
  ]
}

2)执行顺序

  插件的执行顺序会受配置时所处的位置的影响,具体规则如下所列,其中预设是指官方预先设计的一组插件集,本质上仍然是插件。

(1)插件执行在预设之前。

(2)插件会按顺序从前往后执行。

(3)预设与插件相反,从后往前执行。

  以下面的配置信息为例,在插件中,先执行@babel/plugin-transform-arrow-functions,后执行@babel/plugin-transform-classes。而在预设中,先执行@babel/preset-react,后执行@babel/preset-env。

{
  "plugins": ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-classes"],
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

3)插件参数

  如果要配置插件的参数,那么得在插件名称后增加一个参数对象,并且写成数组的形式,如下所示。

{
  "plugins": [
    ["@babel/plugin-transform-arrow-functions", { "spec": true }], 
    ["@babel/plugin-transform-classes", { "loose": true }]
  ]
}

  预设也能接收参数,其写法与之类似。

五、预设

  如果在配置文件中一个一个的声明插件,那么不仅会让该文件变得巨大,而且还难免会有所遗漏。官方为了避免此类问题,引入了预设(Preset)的概念。预设类似于生活中的套餐,每个套餐会集合不同的插件,从而能够一次性安装各类插件,并且还可共享配置的参数。

1)@babel/preset-env

  此预设不仅集合了最新的ES语法(即编译指定的ES标准),还能配置所要支持的平台(例如Node、Chrome等)和版本,以及按需加载插件,其安装命令如下所示。

npm install --save-dev @babel/preset-env

  下面是一个预设的配置示例,支持Chrome 58和IE 11,以及超过市场份额5%的浏览器。还有许多其它可供选择的配置参数,具体可翻阅官方文档

{
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          chrome: "58",
          ie: "11",
          browsers: "> 5%"
        }
      }
    ]
  ]
}

  除了@babel/preset-env之外,官方还给出了其它实用的预设,例如@babel/preset-flow、@babel/preset-react和@babel/preset-typescript等。

  与插件类似,预设也能指定相对或绝对路径,只不过它省略的前缀是“babel-preset-”。假设有个预设名叫@org/babel-preset-name,那么它的相对路径和简写名称如下所示。

{
  "presets": [
    "../@org/babel-preset-name",
    "@org/name"
  ]
}

2)stage-x

  这是一组实验性质的预设,囊括了处于提案阶段的标准,TC39委员会将提案分为5个阶段,如表2所示。

表2  提案的五个阶段

阶段 描述 预设
Stage 0 设想(Strawman),由TC39的成员或已经注册成TC39贡献者的会员提出的想法 @babel/preset-stage-0
Stage 1 建议(Proposal),产生一个正式的提案,确定提案负责人 @babel/preset-stage-1
Stage 2 草案(Draft),规范的第一个版本,包含新特性的语法和语义,接下来只接受增量修改 @babel/preset-stage-2
Stage 3 候选(Candidate),完成规范,获得用户反馈,接下来只有出现重大问题才做修改 @babel/preset-stage-3
Stage 4 完成(Finished),将新规范纳入到标准中,准备发布 不存在,因为它就是已发布的标准预设,例如ES6、ES7等

  以上4个阶段的预设存在着依赖关系,阶段靠前的依赖阶段靠后的(即数字小的包含数字大的),例如@babel/preset-stage-0依赖@babel/preset-stage-1。

  由于这些提案阶段的预设不太稳定,很有可能会被TC39委员会除名或变更,并且混合使用在配置上容易出错,因此从Babel 7开始,它们都将被废弃。

六、@babel/polyfill

  虽然env预设(@babel/preset-env)能统一JavaScript的新语法(即高版本编译成低版本),但是无法支持内置的新方法或新对象,例如Promise、Array.of()等。为此,Babel引入了的Polyfill技术(全部打包在@babel/polyfill中),将所缺的特性添加到全局对象中或内置对象的原型上,弥补env预设的不足,从而模拟出完整的ES6+语法和特性。

  @babel/polyfill包含两个模块:regenerator-runtime和core-js,前者用于编译生成器与异步函数(async和await),后者用于处理其它兼容性问题。@babel/polyfill的安装命令如下所示,注意,使用的参数是--save而不是--save-dev,因为需要在源码之前先执行Polyfill。

npm install --save @babel/polyfill

1)useBuiltIns

  在env预设中,存在一个与@babel/polyfill有紧密联系的useBuiltIns参数,它有三个关键字可供选择,分别是false、entry和usage,具体说明如下所列。

  (1)false:默认值,不开启Polyfill,显式的配置如下所示。

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

  (2)entry:加载运行环境(可在targets参数中声明)所需的Polyfill,下面是一个使用Promise的例子。

require("@babel/polyfill");
new Promise();

  当useBuiltIns参数的值为entry时,这段代码在编译后,就会引入许多不相干的JavaScript文件,造成资源的浪费,如下所示。

require("core-js/modules/es6.promise");
require("core-js/modules/es6.array.fill");
require("core-js/modules/es6.math.trunc");
require("core-js/modules/es6.string.fixed");
......
new Promise();

  (3)usage:自动加载源码所需的Polyfill,仍然以Promise为例,如下代码所示,不用再显式的引入@babel/polyfill。

new Promise();

  当useBuiltIns参数的值为usage时,这段代码在编译后,就会只引入需要的JavaScript文件,如下所示。

require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
new Promise();

七、@babel/runtime

  @babel/runtime与@babel/polyfill类似,也是用来增强env预设的,只是它以非侵入式的辅助函数来填补平台所没有的特性,其安装命令如下所示。

npm install --save @babel/runtime

  在Babel 7中还引入了一个@babel/runtime-corejs2,它比@babel/runtime多包含一个core-js模块,即能够编译Promise、Symbol等。接下来以ES6的类为例,如下所示。

class People {
  name() {}
}

  在将People类编译后,得到的结果如下所示,省略了三个函数中的逻辑代码。

"use strict";
function _classCallCheck(instance, Constructor) { }
function _defineProperties(target, props) { }
function _createClass(Constructor, protoProps, staticProps) { }

var People =
/*#__PURE__*/
function () {
  function People() {
    _classCallCheck(this, People);
  }
  _createClass(People, [{
    key: "name",
    value: function name() {}
  }]);
  return People;
}();

1)@babel/plugin-transform-runtime

  由于编译生成的辅助函数会滞留在所使用的文件中,因此文件越多冗余的函数就越多。如果人工分离,那工作量将巨大,因此Babel提供了能自动将它们分离的@babel/plugin-transform-runtime插件,其安装命令如下所示。

npm install --save-dev @babel/plugin-transform-runtime

  仍然以ES6的People类为例,先在配置文件中将其声明,如下所示。

{
  plugins: ["@babel/plugin-transform-runtime"]
}

  然后再将类编译,三个辅助函数就能通过引入的方式得到,如下所示。

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

八、动态编译

  在Babel中有两种方式实现动态编译,分别是@babel/register和@babel/node,它们的安装命令如下所示。

npm install --save-dev @babel/register
npm install --save-dev @babel/node

  虽然动态编译省去了手动操作,但是会损耗程序的速度和性能,因此不适合生产环境。

1)@babel/register

  在将其安装后,就会为Node.js的require()函数增加一个钩子。每当通过require()加载后缀为.es6、.es、.jsx、.mjs或.js的文件时,就能自动对其进行Babel编译。接下来用一个例子来演示@babel/register的用法,首先创建一个src.js的文件,其代码如下所示。

module.exports = () => true;

  然后创建一个default.js文件,引入@babel/register和之前的src.js,如下所示。

require("@babel/register");
var code = require("./src.js");
console.log(code.toString());

  最后在命令行工具中输入“node default.js”,打印出编译后的匿名函数,如下所示。

function () {
  return true;
}

  在@babel/register中,如果需要使用Polyfill,那么得逐个引入。还要注意,它默认不会编译node_modules目录中的模块,但是可以通过ignore参数修改忽略规则来覆盖其行为。

  ignore参数是一个由正则表达式和函数组成的数组(如下代码所示),如果要让文件不被编译,那么得将其路径与正则表达式匹配或函数返回true,其中函数的参数就是文件路径。

require("babel-register")({
  ignore: [
    /regex/,
    function(filepath) {
      return true;
    }
  ]
});

  另外还有一个only参数,其值也是一个数组,同样也用来设置文件不被编译的条件。只是其规则与ignore参数正好相反,即路径与正则表达式不匹配或函数返回false时,才不编译。

  @babel/register还可以接收其它参数,例如extensions、cache等,并且会合并配置文件中的信息,作为其参数传递进来。

2)@babel/node

  在Babel 7之前,@babel/cli会自带babel-node命令,但之后,官方将其拆成一个单独的包:@babel/node。与@babel/register不同,@babel/node不需要调整源码,直接一个命令就能实现动态编译,如下代码所示,其中src就是之前的src.js文件。

npx babel-node src

 

 posted on 2019-08-26 09:33  咖啡机(K.F.J)  阅读(739)  评论(0编辑  收藏  举报