babel7入门 —— 超详细学习笔记

5 篇文章 0 订阅

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

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

(截止笔者转载这篇文章时,babel-cli官方最新版本为7.8.4,babel-core的最新版本为7.9.0,以下简称babel7,demo均以最新版本为例)

Babel的安装

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

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

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

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

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

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

现在在项目根目录下执行(不了解npx命令的,可以看这篇文章:npx 使用教程

npx babel src/index.js -d dist/

即可在dist目录下生成同名文件index.js,而里面代码与src/index.js中的代码完全一样,生成的dist/index.js内容如下:

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

之所以代码完全一样,其实就是上面所说的,babel在没有使用任何插件时,就是把代码变成抽象语法树,再把抽象语法树原封不动的变成代码,中间没有做任何处理,当然代码也就原样还原回来了。下面,让我们来给babel加点儿料~

Plugins

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

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

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

分别查看dist/index.js确认下转换后的结果,相信立刻就能明白每个插件都干了些啥了~两个插件一起用的话,用" , "隔开:

npx babel src/index.js -d dist/ --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

babel所有的语法转换,就是靠一个个plugins完成的。完整的plugins列表可以查看https://babel.docschina.org/docs/en/plugins

扫完一眼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.js文件如下:

"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
  };
}
  1. .babelrc
    适用场景:适用于简单的静态配置
// JSON
{
  "presets": [...],
  "plugins": [...]
}
  1. package.json
    也可以将.babelrc中的配置项移至package.json配置文件中
// JSON
{
  "name": "my-package",
  "version": "1.0.0",
  "babel": {
    "presets": [ ... ],
    "plugins": [ ... ],
  }
}
  1. .babelrc.js
    和babel.config.js类似,可以使用编程方式创建配置
// Javascript
const presets = [ ... ];
const plugins = [ ... ];

module.exports = { presets, plugins };

有了配置文件,上述既需要@babel/preset-env又需要@babel/plugin-proposal-decorators(测试之前,先通过npm安装好这个插件)的情况,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
	};
};

根目录有了这个配置文件,执行babel指令就不需要带上preset或者plugin参数了:

npx babel src/index.js -d dist/

这边自己试了下,如果不写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写一小段包含装饰器的代码:

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

(不了解polyfill含义的,可以看这篇文章:你不得不知道的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开始时引入一下

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

我们再执行下

npx babel src/ -d dist/

生成的dist/index.js如下:

"use strict";

require("@babel/polyfill");

var fn = function 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]);
};

生成的dist/index.js如下:

"use strict";

require("core-js/modules/es6.array.is-array");

var fn = function 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
"use strict";
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
var a = new Promise();

// b.js
"use strict";
require("core-js/modules/web.dom.iterable");
require("core-js/modules/es6.array.iterator");
require("core-js/modules/es6.object.to-string");
require("core-js/modules/es6.string.iterator");
require("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的运行结果,会发现转换后的代码比之前require的少了些东西。也就是说,设置完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配置文件,两种方式选其一就可以了。

关于webpack4和babel7的配置,可以参考文章:webpack@4中babel7配置

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值