关于babel(精华又通俗)

8,435 阅读12分钟

image.png 这次详细的讲一下babel(重点是应用),如有错误,欢迎指出

babel是什么?

浏览器的发展永远跟不上语言的发展,es6+虽然很普及了,但也不是所有浏览器都可以支持es6+语法。babel的诞生就源于此。
官方定义:
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

关于babel这个名字

babel译作巴别塔 也译作巴贝尔塔巴比伦塔,或意译为通天塔),本是犹太教塔纳赫·创世纪篇》(该书又被称作《希伯来圣经》或者《旧约全书》)中的一个故事:
以下内容摘自维基百科

创世记第11章1-9句记录了“巴别城”的故事。当时地上的人们都说同一种语言,当人们离开东方之后,他们来到了示拿之地。在那里,人们想方设法烧砖好让他们能够造出一座城和一座高耸入云的塔来传播自己的名声以免他们分散到世界各地。上帝来到人间后看到了这座城和这座塔,说一群只说一种语言的人以后便没有他们做不成的事了;于是上帝将他们的语言打乱,这样他们就不能听懂对方说什么了,还把他们分散到了世界各地,这座城市也停止了修建。这座城市就被称为“巴别城”,那座塔就叫“巴别塔”。

猜想babel这个名字也许跟这个是有关系的:象征人们语言相通,齐心协力而可与天公一比高低。

babel的工作原理

babel是一个转译器,感觉相对于编译器compiler,叫转译器transpiler更准确,因为它只是把同种语言的高版本规则翻译成低版本规则,而不像编译器那样,输出的是另一种更低级的语言代码。
但是和编译器类似,babel的转译过程也分为三个阶段:parsing(解析)、transforming(转化)、generating(生成),以ES6代码转译为ES5代码为例,babel转译的具体过程如下:
image.png
ES6代码输入 ==》 babylon进行解析 ==》 得到AST
==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树
==》 用babel-generator通过AST树生成ES5代码

更详细的没有深究,有兴趣的大佬可以参考这篇文章《剖析 Babel——Babel 总览

babel在项目中的使用(重点)

目前在使用的三大框架都有相应的脚手架工具已经贴心的帮我们集成了babel的各种配置,那是不是就没有必要学了。你与大佬之间的距离也许就差一个对babel的学习~
接下来我们来详细了解一下babel的配置,让你见到这些配置时不再一脸懵逼。

理论知识前提

babel将es6+(指es6及以上版本)分为

  • 语法层: let、const、class、箭头函数等,这些需要在构建时进行转译,是指在语法层面上的转译,(比如class...将来会被转译成var function...)
  • api层:Promise、includes、map等,这些是在全局或者Object、Array等的原型上新增的方法,它们可以由相应es5的方式重新定义
    babel对这两个分类的转译的做法肯定是不一样的,我们也需要给出相应的配置

安装

了解了基本的理论只是之后。接下来就可以真正的使用babel了。
在使用babel之前首先要先安装相应的依赖。至少需要的是:

  • babel-core:Babel 的核心,包含各个核心的 API,供 Babel 插件和打包工具使用
  • babel-cli:命令行对 js 文件进行换码的工具

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

test项目初始化

为了更清晰的讲清楚babel,我们首先可以建一个测试项目
首先建立文件夹 babel-study,执行npm init -y进行初始化。建立项目src/index.js,并写下以下es6代码:

let a = 1
let b = 2
let c = a+b

注: 这里只有 let 属于es6,并且只是语法层面的。我们先看语法层面的,从最简单的开始,然后在看api层面的
package.json中添加一个命令--compiler,用于执行babel

//..."scripts": {    "compiler": "babel src --out-dir lib --watch"}

然后执行 npm run compiler
理论上babel会将let都转化成var,但是你会发现这时候转译出来的代码跟原来一样,这是为啥呢?

Babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 里面。因此当我们不配置任何插件时,经过 babel 的代码和输入是相同的。

所以我们需要安装插件

插件(用于处理语法层)

两种方式使用插件,一种是一个个的安装(比较麻烦),另一种是以preset的方式安装一组插件,我们当然要选省事的preset了。

  • 首先安装preset
    npm i @babel/preset-env -D @babel/preset-env 所包含的插件将支持所有最新的JS特性(ES2015,ES2016等,不包含 stage 阶段)
  • 配置:
    建立 .babelrc文件或者babelconfig.js文件,添加以下代码,babel会自动寻找这个文件
//.babelrc
{    
	"presets": ["@babel/preset-env"]
}

补充说明:如果你用 Vue ,presets 一般是 @vue/app,这个是把 在@babel/preset-env 包含的 plugins 上又加了很多自己定义的 plugins

所有的 Vue CLI 应用都使用 @vue/babel-preset-app,它包含了 babel-preset-env、JSX 支持以及为最小化包体积优化过的配置。通过它的文档可以查阅到更多细节和 preset 选项。 --- 摘自vue-cli官方文档

通过查看文档,@vue/babel-preset-app已经默认配置了@babel/plugin-transform-runtime

执行命令,这时候就转译成功了,转译结果如下:

"use strict";

var a = 1;
var b = 2;
var c = a + b;
  • target属性
    babel还可配置taget或者提供.browserlist文件,用于指定目标环境,这样能使你的代码体积保持更小
//.browserslistrc0.25%
not dead

polyfill(用于处理api层)

上边只验证了语法层面的转译,接下来我们试试api层面的转译是怎样的
我们修改index.js如下:

// Promise是api层面的,是一个es6中的全局对象
const p = new Promise((resolve, reject) => {    
  resolve(100);
});

执行complier命令,编译结果如下:

"use strict";

var p = new Promise(function (resolve, reject) {
  resolve(100);
});

const-->var没有问题,但是Promise并没有任何变化,这肯定是不对的。
这时候就需要Polyfill这个东西了。

polyfill的中文意思是垫片,顾名思义就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用。
如何使用:
首先,安装 @babel/polyfill 依赖。注意这是一个运行时依赖。所以不要加-dev
npm install --save @babel/polyfill
@babel/polyfill 模块包括 core-js 和一个自定义的 regenerator runtime 模块,可以模拟完整的 ES2015+ 环境。
然后在index.js中引入

import '@babel/polyfill';
const p = new Promise((resolve, reject) => {    
  resolve(100);
});

转译结果:

"use strict";

require("@babel/polyfill");

var p = new Promise(function (resolve, reject) {
  resolve(100);
});

虽然看起来Promise还是没有转译,但是我们引入的 polyfill 中已经包含了对Promise的es5的定义,所以这时候代码便可以在低版本浏览器中运行了。

useBuiltIns属性

不知道大家又没有想到这样一个问题,我代码里边只用到了几个es6,却需要引入所有的垫片,这不合情理啊。要优化这一点,就需要用到useBuiltIns这个属性了。
useBuiltIns这一配置项,它的值有三种:

  • false: 不对polyfills做任何操作
  • entry: 根据target中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfill
  • usage(新):检测代码中ES6/7/8等的使用情况,仅仅加载代码中用到的polyfills

然后我们修改配置如下:

//.babelrc
{
   "presets": [
       ["@babel/preset-env",{
           "useBuiltIns""usage"
       }]
   ]
}

执行compiler命令,这是命令行中会出现警告
image.png
意思是需要我们指定corejs的版本。那我们就指定呗,指定的方法也很简单:

{
   "presets": [
       ["@babel/preset-env",{
           "useBuiltIns""usage",
        	 "corejs":3
       }]
   ]
}

目前推荐使用的corejs版本是3版本,新的特性也只会添加在这个版本。
此时,转译结果如下:

"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

var p = new Promise(function (resolve, reject) {
  resolve(100);
});

到这里,好像已经完美了呢!
                                              image.png

然鹅我们离完美还差一个@babel/plugin-transform-runtime。

@babel/plugin-transform-runtime

解决代码冗余

代码冗余是出现在转译语法层时出现的问题。
首先依然修改下我们的index.js,这次我们再写一个语法层面的es6-->class(let,const这些对比不出问题,所以用class),看看babel会把class转化成什么

//index.js
class Student {
    constructor(name,age){
        this.name = name;
        this.age = age
    }
}

转化结果

"use strict";

require("core-js/modules/es.function.name");

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Student = function Student(name, age) {
  _classCallCheck(this, Student);

  this.name = name;
  this.age = age;
};

这种结果看起来没什么毛病,但事实上,如果我们其他文件中也使用了class,你会发现_classCallCheck在每个文件中都会出现,这就造成了代码冗余(我们示例中只使用了class,可能冗余的不明显,实际项目中这些函数可能是很长的)。
这时候需要用到另外一个插件了@babel/plugin-transform-runtime
该插件会开启对 Babel 注入的辅助函数(比如上边的_classCallCheck)的复用,以节省代码体积,这些辅助函数在@babel/runtime中,所以需要安装@babel/runtime,当然@babel/runtime也是运行时依赖。(在对一些语法进行编译的时候,babel需要借助一些辅助函数)
安装@babel/plugin-transform-runtime@babel/runtime
npm i --save-dev @babel/plugin-transform-runtime
npm i @babel/runtime --save
然后修改配置

{
    "presets": [
        ["@babel/preset-env",{
            "useBuiltIns":"usage",
            "corejs":3
        }]
    ],
    "plugins": [
        ["@babel/plugin-transform-runtime"]
    ]
}

编译结果

"use strict";

require("core-js/modules/es.function.name");

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

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

var Student = function Student(name, age) {
  (0, _classCallCheck2["default"])(this, Student);
  this.name = name;
  this.age = age;
};

可以发现,相关的辅助函数是以require的方式引入而不是被直接插入进来的,这样就不会冗余了。
除了解决代码冗余,@babel/plugin-transform-runtime还有另外一个重要的能力——解决全局污染。

解决全局污染

全局污染是出现在转译api层出现的问题
我们这次依然用Promise来做实验
修改index.js

new Promise(function (resolve, reject) {
    resolve(100);
});

转译结果

"use strict";

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

new Promise(function (resolve, reject) {
  resolve(100);
});

可以看出preset-env在处理例如Promise这种的api时,只是引入了core-js中的相关的js库,这些库重新定义了Promise,然后将其挂载到了全局。
这里的问题就是:必然会造成全局变量污染,同理其他的例如Array.from等会修改这些全局对象的原型prototype,这也会造成全局对象的污染。
解决方式就是:将core-js交给transform-runtime处理。添加一个配置即可,非常简单

{
    "presets": [
        ["@babel/preset-env"]
    ],
    "plugins": [
        ["@babel/plugin-transform-runtime",{
            "corejs":3
        }]
    ]
}

可以看出,我们是将core-js这个属性添加到@babel/plugin-transform-runtime这个插件的配置下,让这个插件处理,同时也不需要配置useBuiltIns了,因为在babel7中已经将其设置为默认值 (transform-runtime是利用plugin自动识别并替换代码中的新特性,检测到需要哪个就用哪个) 这里注意下:有的博客上说无法转译includes等实例方法,其实说错了,官方文档是这样说的:
corejs: 2仅支持全局变量(例如Promise)和静态属性(例如Array.from),corejs: 3还支持实例属性(例如[].includes)。
转译结果:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

new _promise["default"](function (resolve, reject) {
  resolve(100);
});

可以看出,这时的代码并没有在全局直接添加一个Promse,而是定义了一个_promise["default"]方法,这样便不会出现全局变量污染的情况
所以综上可得出@babel/plugin-transform-runtime这个插件的强大之处有以下几点:

  • 实现对辅助函数的复用,解决转译语法层时出现的代码冗余
  • 解决转译api层出现的全局变量污染

但是transform-runtime也有缺点:

  • 每个特性都会经历检测和替换,随着应用增大,可能会造成转译效率不高 更多关于这个插件的配置可参考官方文档

webpack中使用babel

首先安转babel-loader

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          "plugins": [
              ["@babel/plugin-transform-runtime",{
                  "corejs":3
              }]
          ]
        }
      }
    }
  ]
}


使用vue-cli的同学需要注意
因为babel-loader很慢,所以webpack官方推荐转译尽可能少的文件(参考),所以vue-cli配置了该loader的exclude选项,将node_moduls中的文件排除了,但是这样可能会造成某个依赖出现兼容性问题。所以,如果你的项目中某个依赖出现了兼容性问题,这可能就是原因。解决办法就是在vue.config.js中配置transpileDependencies这个选项,Babel就会 显式转译这个依赖。

参考

感谢以下各位大佬的博客
《不容错过的 Babel7 知识》
《babel中文网》
《babel学习系列》
《一口(很长的)气了解babel》
《风动之石的博客--babel》