babel主要作用就是将某些低版本容器(主要是浏览器,主要是IE…)不支持的js语法或api,用该容器支持的语法或api重写,使开发者可以使用更前沿的方式愉快的编写代码。
但实际上更准确点说,是一堆插件在做代码的转换,babel本身是个容器,负责代码解析、转换抽象语法树,然后通过各种插件做代码转换,最后根据转换后的抽象语法树生成最终的代码。这个过程以后再细说,这里想说的就是插件对于babel的作用,而我们使用者可能比较关心的,也就是在做代码转换时,会用到哪些插件。
(截止笔者转载这篇文章时,babel-cli官方最新版本为7.8.4,babel-core的最新版本为7.9.0,以下简称babel7,demo均以最新版本为例)
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加点儿料~
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就是用来干这事儿的。
一个特定的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
另一个问题,又是presets,又是plugins,–plugins和–presets后面要跟一堆东西,用命令执行babel未免也太费劲了。
babel官网提供了四种方式通过文件维护配置项,实际工作中,根据情况选择其一。
// Javascript
module.exports = function () {
const presets = [ ... ];
const plugins = [ ... ];
return {
presets,
plugins
};
}
// JSON
{
"presets": [...],
"plugins": [...]
}
// JSON
{
"name": "my-package",
"version": "1.0.0",
"babel": {
"presets": [ ... ],
"plugins": [ ... ],
}
}
// 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添加参数的方式。
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)
现在,回过头来再看下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
这个preset比较特殊,他不仅仅是包含了众多plugins,而且还提供了一些有用的配置项,动态引入依赖包,只为更小体积的包~ 来看下主要的两个配置项 useBuiltIns 和 targets
该配置项有三个属性: “usage”, “entry”, “false”。默认为false。
需要事先说明一下,这个属性如果是"usage"或"entry"时,必须要安装@babel/polyfill,因为在转换出来的代码中,会引入core-js下面的包。
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配置项就有用了,后面再说。
// 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还可以指定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…
补充一点,配置文件中,presets和plugins都是允许设置多个的,某些plugin对执行顺序很敏感,这也就对配置中设置presets和plugins的顺序有要求了。
babel执行presets和plugins的顺序规则如下:
到这儿,babel7的基本使用方法就介绍完了。
不过在实际项目中,我们现在一般不直接通过babel命令对代码做转换,webpack的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配置