babel实战--如何在项目中使用babel

概要

目前在使用的三大框架(vue.js, react.js, angular.js)都有相应的脚手架工具已经贴心的帮我们集成了babel的各种配置,因此我们少了很多配置的工作量,一条命令就可以开始开发业务代码了,觉得自己又牛逼闪闪了。显示那是我们的错觉,与大佬之间的距离也许就差一个对babel深度的学习,接下来我们来详细了解一下babel的配置,让你见到这些配置时不再一脸懵逼。

餐前小菜

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

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

准备工作

以windows系统为例:
在你的项目目录下新建文件夹babel-demo, 执行cd babel-demo进入文件夹,执行npm init -y 初始化项目。
执行 npm install --save-dev @babel/core @babel/cli 安装babel必要的依赖。

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

新建目录src, 在目录下新建index.js, 并写下如下测试代码:
babel实战--如何在项目中使用babel

在package.json中配置一条命令--compiler,用于执行babel

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

然后执行 npm run compiler理论上babel会将let都转化成var,但是你会发现这时候转译出来的代码跟原来一样,这是为啥呢?
Babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 里面。因此当我们不配置任何插件时,经过 babel 处理的代码和输入是相同的。所以我们需要安装插件。

插件(用于处理语法层)

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

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

补充说明:如果你用 Vue ,presets 一般是 @vue/app,这个是把 在@babel/preset-env 包含的 plugins 上又加了很多自己定义的 plugins。
所有的 Vue CLI 应用都使用 @vue/babel-preset-app,它包含了 babel-preset-envJSX 支持以及为最小化包体积优化过的配置。@vue/babel-preset-app已经默认配置了@babel/plugin-transform-runtime。通过它的文档可以查阅到更多细节和 preset 选项。

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

"use strict";

var a = 1;
var b = 2;
var c = a + b;

babel还可配置taget或者提供.browserlist文件,用于指定目标环境,这样能使你的代码体积保持更小。

//.browserslistrc
> 0.25%
not dead

polyfill(用于处理Api层)

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

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

执行npm run compiler命令,编译结果如下:

"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的定义,所以这时候代码便可以在低版本浏览器中运行了。

useBuiltIns属性

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

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

然后我们修改配置如下:

执行npm run compiler命令,这是命令行中会出现如下警告:
babel实战--如何在项目中使用babel

意思是需要我们指定corejs的版本。修改.babelrc如下:

{
  "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);
});

@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 @babel/runtime
然后修改配置:

{
    "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);
});

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

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

但是transform-runtime也有缺点:
每个特性都会经历检测和替换,随着应用增大,可能会造成转译效率不高

webpack中使用babel

安装babel-loader, npm install --save-dev babel-loader
webpack.config.js配置如下:

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就会 显式转译这个依赖。

文章引用

关于babel的详细解读(精华又通俗)

上一篇:AgileEAS.NET SOA中间件平台更新日志 2015-04-28


下一篇:SEV/WFE/WFI