18

babel这东西,需要配置时搞不清楚怎么弄,弄清楚后配置好了就很长时间不会去触碰。等再需要配置时又忘了当初怎么弄的了。
回头看之前写的babel学习笔记,发现自己还是有地方没搞清楚,有必要系统性的对babel进行学习和整理。

babel主要作用就是将某些低版本容器(主要是浏览器,主要是IE...)不支持的js语法或api,用该容器支持的语法或api重写,使开发者可以使用更前沿的方式愉快的编写代码。

但实际上更准确点说,是一堆插件在做代码的转换,babel本身是个容器,负责代码解析、转换抽象语法树,然后通过各种插件做代码转换,最后根据转换后的抽象语法树生成最终的代码。这个过程以后再细说,这里想说的就是插件对于babel的作用,而我们使用者可能比较关心的,也就是在做代码转换时,会用到哪些插件。

(截止写这篇时,babel官方最新版本为7.1.0,以下简称babel7,demo均以7.1.0版本为例)

Babel的安装

babel7主要就是两个包,@babel/cli和@babel/core,cli用于执行命令行,core则是babel用于解析、转换、代码生成的核心包。在项目下执行以下命令即可完成安装。

npm i --save-dev @babel/cli @babel/core

有了这两个包以后,就可以对指定文件执行babel命令了。
比如项目目录结构如下:

├── node_modules                  
├── src     
│   └── index.js       // 源文件
├── dist               // 转换后文件放置路径
└── package.json        

src/目录文件index.js代码如下:

// src/index.js

const fn = () => {
  Array.isArray([1, 2, 3]);
};

现在在项目根目录下执行

npx babel src/index -d dist/

即可在dist目录下生成同名文件index.js,而里面代码与src/index.js中的代码完全一样。
之所以代码完全一样,其实就是上面所说的,babel在没有使用任何插件时,就是把代码变成抽象语法树,再把抽象语法树原封不动的变成代码,中间没有做任何处理,当然代码也就原样还原回来了。下面,让我们来给babel加点儿料~

Plugins

const和箭头函数是es6的语法,相应的,@babel/plugin-transform-block-scoping插件用于转换const和let,@babel/plugin-transform-arrow-functions插件用于转换箭头函数。安装完这两个插件之后,分别执行

npx babel src/index -d dist/ --plugins=@babel/plugin-transform-block-scoping

npx babel src/index -d dist/ --plugins=@babel/plugin-transform-arrow-functions

分别查看dist/index.js确认下转换后的结果,相信立刻就能明白每个插件都干了些啥了~(两个插件一起用的话,用“,”隔开:npx babel src/index -d dist/ --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions)
babel所有的语法转换,就是靠一个个plugins完成的。完整的plugins列表可以查看https://babeljs.io/docs/en/pl...

扫完一眼plugins列表,估计和我一样一脸懵逼,这么多插件谁能记得住用得到哪些啊。babel能帮忙整理下打个包给我用么?当然可以,presets就是用来干这事儿的。

Presets

一个特定的preset可以简单理解为是一组特定的plugins的集合。不同的presets包含着不同的plugins,当然适用的场景也就各不相同了。比如@babel/preset-react包含了写react需要用到的@babel/plugin-syntax-jsx,@babel/plugin-transform-react-jsx,@babel/plugin-transform-react-display-name等插件;@babel/preset-es2017包含了@babel/plugin-transform-async-to-generator插件。
而最为常用,也是被官网推荐的,是@babel/preset-env。默认情况下,所有已被纳入规范的语法(ES2015, ES2016, ES2017, ES2018, Modules)所需要使用的plugins都包含在env这个preset中。

还是以上面例子来说
先安装@babel/preset-env

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

执行

npx babel src/index.js -d dist/ --presets=@babel/preset-env

生成的dist/index文件如下:

"use strict";

var fn = function fn() {
  Array.isArray([1, 2, 3]);
};

主体部分与同时使用两个plugins是完全一样的。实际上,presets可以理解为就是把其包含的plugins依次执行一遍。
当然env这个presets不是万能的,其只包含了规范中的语法转换,尚未被纳入规范的处于各个阶段的提案,比如目前处于stage-2(draft)阶段的装饰器语法,光是用presets是不会帮我们转好的,还得单独再使用@babel/plugin-proposal-decorators这个专门用于转换装饰器代码的插件。

值得一提的是,babel7明确指出用stage-x命名的presets已被弃用。具体原因见https://babeljs.io/blog/2018/...
如果希望和之前一样使用处于各阶段的提案功能,建议直接通过引入相应的plugins:

{
  plugins: [
    // Stage 0
    "@babel/plugin-proposal-function-bind",

    // Stage 1
    "@babel/plugin-proposal-export-default-from",
    "@babel/plugin-proposal-logical-assignment-operators",
    ["@babel/plugin-proposal-optional-chaining", { loose: false }],
    ["@babel/plugin-proposal-pipeline-operator", { proposal: "minimal" }],
    ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: false }],
    "@babel/plugin-proposal-do-expressions",

    // Stage 2
    ["@babel/plugin-proposal-decorators", { legacy: true }],
    "@babel/plugin-proposal-function-sent",
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-proposal-numeric-separator",
    "@babel/plugin-proposal-throw-expressions",

    // Stage 3
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-syntax-import-meta",
    ["@babel/plugin-proposal-class-properties", { loose: false }],
    "@babel/plugin-proposal-json-strings",
  ],
}

关于stage-x各代表什么含义,见The TC39 Process

Configure Babel

另一个问题,又是presets,又是plugins,--plugins和--presets后面要跟一堆东西,用命令执行babel未免也太费劲了。
babel官网提供了四种方式通过文件维护配置项,实际工作中,根据情况选择其一。

  1. babel.config.js
    适用场景:以编程方式创建配置;需要编译编译node_modules。

    // Javascript
    module.exports = function () {
      const presets = [ ... ];
      const plugins = [ ... ];
    
      return {
        presets,
        plugins
      };
    }
  2. .babelrc
    适用场景:适用于简单的静态配置

    // JSON
    {
      "presets": [...],
      "plugins": [...]
    }
  3. package.json
    也可以将.babelrc中的配置项移至package.json配置文件中

    // JSON
    {
      "name": "my-package",
      "version": "1.0.0",
      "babel": {
        "presets": [ ... ],
        "plugins": [ ... ],
      }
    }
  4. .babelrc.js
    和babel.config.js类似,可以使用编程方式创建配置

    // Javascript
    const presets = [ ... ];
    const plugins = [ ... ];
    
    module.exports = { presets, plugins };

有了配置文件,上述既需要@babel/preset-env又需要@babel/plugin-proposal-decorators的情况,babel.config.js文件配置如下:

// Javascript
    module.exports = function(api) {
      api.cache(true);
      const presets = ['@babel/env'];
      const plugins = [['@babel/proposal-decorators', { legacy: true }]];
      return {
        presets,
        plugins
      };
    };

这边自己试了下,如果不写api.cache(true),会报一个很奇怪的错:

Error: Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured for various types of caching, using the first param of their handler functions

官网上暂时没有找到为什么一定要执行下api的方法,甚至把

api.cache(true);

改成

const env = api.env();

都可以避免上述报错,无法理解。还是换成.babelrc方式写配置吧。。。

// JSON
{
  "presets": ["@babel/env"],
  "plugins": [["@babel/proposal-decorators", { "legacy": true }]]
}

验证一下,在src/decorator.js写一小段包含装饰器的代码:

// src/decorator.js
const decro = (val) => (_class) => new _class(val);

@decro("abc")
class Test {
  constructor(val){
    this.val = val
  }
  log() {
    console.log(this.val);
  }
}

Test.log(); // "abc"

完了在项目根目录执行中执行一下

npx babel src/ -d dist/

在dist/下就可以看到转换后的decorator.js文件。
执行

node dist/decorator.js

即可看到结果"abc"啦~

注意到一件事情,plugins是个二维数组,而presets是个一位数组。这是babel配置文件中对指定preset或者plugin添加参数的方式。

Plugin & Preset Options

babel提供的plugin和preset都允许传入一些参数来达到不同的目的。
以下是引用presetA和pluginA的三种形式,结果完全一样。

{
  "presets": [
    "presetA",
    ["presetA"],
    ["presetA", {}]
  ],
  "plugins": [
    "pluginA",
    ["pluginA"],
    ["pluginA", {}],
  ]
}

意思就是说,单个preset或者plugin如果不需要添加参数,那么直接用string就可以了;如果需要添加参数,那么需要将单个preset或者plugin放入数组中,第一项为string表示preset或者plugin的名字,第二项为object用于指定参数。

先举个后面会用到的例子,具体在写@babel/preset-env的时候细说

{
  "presets": [
    [
      "@babel/env", {
        "targets": {
          "ie": "9"
        },
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Polyfill

现在,回过头来再看下src/index.js中的例子。最后转换出来的代码中,Array.isArray这个静态方法在低版本IE浏览器中,依然是无法执行的。
我理解为,babel的插件专注于对语法做转换,而API的调用并非什么新鲜的语法,这部分并不属于babel插件的管辖范围。正常来说,让不识别Array.isArray的浏览器运行这个方法,最简单的方法就是用浏览器能识别的方式为Array写一个静态方法isArray。

Array.isArray = function(arg) {
  var toString = {}.toString;
  return toString.call(arg).slice(8, -1) == 'Array';
}

@babel/polyfill就是干这活儿的。

首先安装@babel/polyfill

npm i -S @babel/polyfill

然后在项目入口src/index.js开始时引入一下

// src/index.js

import "@babel/polyfill";
const fn = () => {
  Array.isArray([1, 2, 3]);
};

简单粗暴,搞定收工。
等下等下,还没完呢,不想知道@babel/polyfill这里面都有些啥东西么?如果你会使用webpack-bundle-analyzer做打包分析,会发现多出的core-js这个包有200多kb,这个就是@babel/polyfill的依赖包,(除了core-js外其实还有个regenerator-runtime用来处理async function的)。为了实现Array.isArray,要增加这么大体积的包,有没有问题呢?
当然有问题,对于我们这种有追求的程序员来说,能把包缩小一点是一点,关乎用户体验的事情能优化一点儿是一点儿~

好,怎么办?
找到@babel/polyfill中处理Array.isArray的包,单独引用就好了呗~

import "core-js/modules/es6.array.is-array";
const fn = () => {
  Array.isArray([1, 2, 3]);
};

搞定,包瞬间缩小到几k。可问题又来了,这只是一个Array.isArray,那么多新的String的API,Object的API等等,难道需要自己一个个把需要的包单独引用进自己的项目里去吗?
No~ 见识一下@babel/preset-env的强大之处吧~

@babel/preset-env

@babel/preset-env这个preset比较特殊,他不仅仅是包含了众多plugins,而且还提供了一些有用的配置项,动态引入依赖包,只为跟小体积的包~ 来看下主要的两个配置项 useBuiltIns 和 targets

  • useBuiltIns

    该配置项有三个属性: "usage", "entry", "false"。默认为false。
    需要事先说明一下,这个属性如果是"usage"或"entry"时,必须要安装@babel/polyfill,因为在转换出来的代码中,会引入core-js下面的包。

    • entry

      要使用entry属性,必须在项目入口处引入一次@babel/polyfill。然后,babel在做代码转换的时候,会把

      import @babel/polyfill

      转成

      require("core-js/modules/es6.array.copy-within");
      require("core-js/modules/es6.array.every");
      require("core-js/modules/es6.array.fill");
      ...

      问题来了,只是把一个大包拆成一个个小包,并不会减小体积啊。嗯,单独使用"useBuiltIns": "entry"好像是没什么用,但结合后面要说的targets配置项就有用了,后面再说。

    • usage(experimental)

      虽然官网标注了是个处于实验阶段的功能,但亲测很强大。他能通过识别所有代码中使用到的高级API,自动在该文件头部注入相应的polyfill包。
      使用这个属性的时候,是不需要在项目中手动引入@babel/polyfill的。babel自动检测哪个文件要用到哪些包,就在那个文件头部引入那些包。

      搬个官网的例子:

      // a.js
      var a = new Promise();
      
      // b.js
      var b = new Map();
      
      // .babelrc
      {
        "presets": [["@babel/env", {"useBuiltIns": "usage"}]]
      }

      转化后

      // a.js
      import "core-js/modules/es6.promise";
      var a = new Promise();
      
      // b.js
      import "core-js/modules/es6.map";
      var b = new Map();

      一点点冗余代码都没有了~

  • targets

    targets配置项用于指定需要支持的环境,对于前端开发来说,主要指的就是浏览器版本。(targets还可以指定node、android、ios等其他环境)
    经常使用的方式也很简单,比如老板说,我们只需要支持chrome64,上例中所需的完整配置如下:

    {
      "presets": [
        [
          "@babel/env",
          {
            "useBuiltIns": "usage",
            "targets": {
              "chrome": "64"
            }
          }
        ]
      ],
      "plugins": [['@babel/proposal-decorators', { "legacy": true }]]
    }

    直接看执行babel的运行结果,会发现转换后的代码基本和原来的一样。也就是说,设置完targets后,babel会先判断一下指定的环境已经支持了多少种新语法和API,对于已经支持的部分,就不会再转换代码或者引入相应的包了。
    对于只要求在较高版本的浏览器运行的项目,targets + useBuiltIns两个配置项就能将转换后的代码体积缩减到最小。不过如果要求支持IE9,那设不设置targets影响就不大了~
    关于@babel/preset-env更为详细的文档,当然是官网啦~https://babeljs.io/docs/en/ba...

Plugin ordering

补充一点,配置文件中,presets和plugins都是允许设置多个的,某些plugin对执行顺序很敏感,这也就对配置中设置presets和plugins的顺序有要求了。

babel执行presets和plugins的顺序如下:

  • Plugins先于Presets执行。
  • Plugins由数组中的第一个plugin开始依次执行。
  • Presets与Plugins执行顺序相反,由数组中最后一个preset开始执行。

到这儿,babel7的基本使用方法就介绍完了。
不过在实际项目中,我们现在一般不直接通过babel命令对代码做转换,webpack的babel-loader会在打包时帮助我们做这件事情。

Babel-loader

babel 7.x对应的babel-loader版本为8.x。之前的babel 6.x对应的babel-loader版本为7.x。
关于webpack的loader超出了本篇的范围,这里就不多加赘述了。有需要进一步了解的可以看https://webpack.js.org/concep...
这里只简单介绍一下babel-loader的配置。在webpack配置中可能经常会见到类似下面这段:

module: {
  rules: [
    {
      test: /\.js|jsx$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

如果你已经清楚.babelrc文件是如何配置的,那么对上面的options一定不会陌生了。
webpack执行打包时,优先读取options中的配置,如果没有设置options属性,再从package.json同级目录中找babel配置文件。通过配置options,或者通过babel配置文件,两种方式选其一就可以了。


梦梦她爹
1.8k 声望122 粉丝

引用和评论

0 条评论