Babel 7 初探

  Babel有两大功能,转译和polyfill。转译就是把新的JS的语法,转化成旧的JS的语法。polyfill则是针对JS中新增的一些对象(Map, Set)和实例方法,这些对象和方法,在旧的浏览器中肯定没有,如果使用它们,肯定也会报错,但你又想使用它们,那怎么办? 很简单,既然没有,就手动实现它们,用旧的浏览器支持的语法实现它们,只要把实现引进到我们写的程序中,就相当旧的浏览器也拥有了这些新的对象和方法,在程序中就可以大胆地使用它们了,这些用旧的浏览器支持的语法实现新的对象和方法的实现或模式就是polyfill. 举一个例子,promise对象在ES2019中添加了一个finally()方法,旧的浏览器中肯定没有,如果在旧的浏览器中使用它,就会报错,因此需要为旧的浏览器手动实现这个方法。

if (!Promise.prototype.finally) {
    Promise.prototype.finally = function f(fn){
        return this.then(
            function t(v){
                return Promise.resolve( fn() )
                    .then(function t(){
                        return v;
                    });
            },
            function c(e){
                return Promise.resolve( fn() )
                    .then(function t(){
                        throw e;
                    });
            }
        );
    };
}

  这就是finally()方法的polyfill,如果我们在代码中引用这个polyfill,  就相当于旧的浏览器中有了finally()方法,程序中再使用finally()方法,就不会报错了,程序就可以运行中各个浏览器上了。你也注意到了,polyfill 有一个if 条件判断,这个很好理解,如果浏览器中实现了这个方法,我们就没有必要再手动实现了。if 条件判断,保证了,只为旧的浏览器提供polyfiil, 新的浏览器会自动忽略这个polyfill.

  通过以上分析,可以发现polyfill的重要性,实际上,转译和polyfill已经成了现代JS 开发的一部分,ES标准在发展,而旧的浏览器也会一直存在,为了使用新的语法,但又想让它在旧的浏览器中运行,转译和polyfill 正是有效解决这个问题的方法。Babel则是转译和polyfill的工具,学习Babel 对现代JS开发非常重要。

  Babel转译

  Babel转译JS使用的是插件机制,提供给Babel什么插件,Babel 就会转译什么语法。提供转化箭头函数的插件,Babel就会把遇到的箭头函数全部转化成普通函数。新建一个项目尝试一下,建一个文件夹babel-learning,在其中建一个src目录,在src 下建index.js, (mkdir babel-learning && cd babel-learning && mkdir src && cd src && touch index.js),  最好再建一个package.json 文件(cd ..  && npm init -y),管理项目依赖。提供了插件,怎么调用呢? 还要安装两个包,@babel/cli和 @babel/core

  @babel/cli:  babel 命令集合,在命令行中直接调用babel,对文件进行编译。

  @babel/core: babel的核心,调用插件,转译js 语法,要注意的是,单独使用它,不起作用。

  npm i @babel/core @babel/cli @babel/plugin-transform-arrow-functions --save-dev,这里要注意,babel 7 把babel的包名重写了,以前是 babel-,现在是@babel/,安装完成后,在index.js 文件中写一个箭头函数,

const sum = (a, b ) => a + b;

  然后在命令行中,npx babel src  --out-dir dist --plugins=@babel/plugin-transform-arrow-functions,npx可以直接调用node_modules 中的命令,--out-dir 表示转译后的文件输出到什么地方,  --plugins表示使用哪些插件进行转译。看一下转译后dist目录中的index.js ,箭头函数转化成了普通函数。

  这时你再想,转译一下const ,那就需要提供另外一个babel插件了,随着转译的语法越来越多,需要提供的插件也就越来越多,如果一个一个手动添加,那就有点麻烦了,管理起来也不方便。Babel 提供了一个转译语法的插件集合@babel/preset-env,那它包含哪些插件呢?它能转译哪些语法呢?ECMAScript 官方发布的正式版本,如ES2015 ~ES2020 中的新语法和ECMAScript proposals 中stage 4  中的新语法,这也就是下一年要发布的ECMAScript 版本中的新语法。只要官方定稿的语法,@babel/preset-env 都可以进行转译,那就方便多了,只要安装它这一个,你就可以转译一堆新语法。插件集合称为预设(presets)。从管理一个一个的插件,变成了管理一个预设,我们要做的就是跟踪这个预设,而不是一个个的插件,版本管理方便多了。npm i @babel/preset-env  -- save-dev,此时命令行中,就不要使用--plugins了,要用--presets,你会发现,这么调用babel也有点麻烦。为此,Babel提供了配置文件,当在命令行中调用babel时,它会读取配置文件的内容,可以把plugins和presets都放到配置文件中,而不用放到命令行中。现在官方建议配置文件的命名是babel.config.json,而不是原来的 .babelrc, touch babel.config.json

{
    "presets": [
        "@babel/preset-env"
    ]
}
  npx babel src  --out-dir dist,const 和箭头函数都转译了。如果新语法实在太新了,比如 decorator,  官方没有定稿,还在stage-2 阶段,@babel/preset-dev 并没有包含它们,还能不能用?能用,不过要单独为这个新语法(decorator)安装一个插件。npm install --save-dev @babel/plugin-proposal-decorators,  然后在babel.config.json 中配置plugins,在stage <=3 阶段的语法,转译插件名称变成了proposal.
{
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [
        "@babel/plugin-proposal-decorators"
    ]
}
  如果安装的presets 和 plugins 越来越多,最好注意一下babel 的执行顺序。先执行plugins, 再执行presets, 如果plugins有多个,按照书写顺序从上到下(或从左到右) 依次执行每一个plugin.  但presets 确相反,如果有多个pesets, 按照书写顺序从下到上或从右到左依次执行,一般来说,顺序不会引起问题。
  说完@babel/preset-env的基本功能,再说一下它的几个配置项。
  modules: 要不要把ES module 转化成CommonJS,默认是true, 也就是说,在默认情况下,Babel会把ES模块转化成CommonJS模块,import/export 语句全都变成require 和 exports。 index.js 修改成
export const sum = (a, b) => a + b;

  npx babel src  --out-dir dist,如果觉得npx命令比较麻烦,可以package.json 的script 中写入"babel": "babel src --out-dir dist" ,在命令行中npm run babel。

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.sum = void 0;

var sum = function sum(a, b) {
  return a + b;
};

exports.sum = sum;

  所有ES module的语法,都变成了CommonJS 语法。把module设为false, 则告诉Babel,  不要把ES module 转化成commonJS module了,import/export语句保留,不要动,转化其它ES语法就好了。With  modules  set  to  false  Babel  will  output  the  transpiled  files  with  their  ESModule  syntax  untouched.  The  rest  of  the  code will be transformed, just the module related statements stay the same.  Babel config 文件改为

presets: [
        [
            "@babel/preset-env",
            {
                "modules": false
            }
        ]
    ]

  npm run babel

export var sum = function sum(a, b) {
  return a + b;
};

  ES module 语法没有变化,只把箭头函数的语法转化了。为什么要把module设为false 呢? tree shaking,webpack等打包工具都支持ES module, 并且进行tree shaking。那为什么会把箭头函数进行转化呢?这就是target 配置项

  target:默认情况下,babel会把所有语法都转化成ES5 语法。如果你的JS是运行在新的浏览器中, 那就没有必要全部转译了,只转译那些没有支持的语法就好了。那就要设置target, 告诉babel,你支持哪些浏览器,因此它的值是browserlist, bable能够根据指定的浏览器决定要不要编译最新的语法到旧的语法。
"presets": [
    [
        "@babel/preset-env",
        {
            "modules": false,
            "targets": {
                "chrome": "58"
            }
        }
    ]
]
  npm run babel ,任何语法都没有进行转译,因为chrome 58 已经支持const 和箭头函数了。如果把IE 11 加上
"targets": {
    "chrome": "58",
    "ie": "11"
}
  npm run  babel, 所有的语法都编译成旧的语法。
  但是一般不在@babel/preset-env这里设置,因为,项目中其它配置工具也需要指定一下browserlist,  可以在.browserslistrc 文件或package.json中配置
"browserslist": [
  "Chrome 58",
  "IE 11"
]
  Polyfill
  原来有@babel/polyfill, 但现在已经被废弃了,它被拆分成两个库,core-js 和 regenerator-runtime. 现在实现polyfiil有两种方式,一种是配置@babel/preset-env 中的useBuiltIns 和 corejs , 一种是配置@babel/plugin-transform-runtime
  配置@babel/preset-env 中的useBuiltIns 和 corejs。useBuiltIns有三个取值
  1, false, 默认值,也就是说,如果不配置useBuiltIns,  @babel/preset-env 是不会进行polyfill的。
  2,   entry:   在项目的入口文件引入 core-js或 regenerator-runtime/runtime, babel 就会把整个的引入变成一个一个小polyfill的引入,引入哪些小polyfill 是根据browserlist 定义的浏览器目标决定的,这就是按需加载polyfill
  3,usage:  它是按文件进行polyfill,  如果一个文件中用到了一个对象,如promise对象,而browserlist 中定义的浏览器又不支持这个对象,它就会在这个文件的顶部引入polyfill.  也是按需加载polyfill
  corejs 的取值有2, 3等,就是指定core-js 的版本。默认是取值2,现在core-js 支持3,项目中最好配置3,使用core-js的3版本,就要安装它,npm i core-js@3 --save。 如果项目中用到了async/await, 还要安装regenerator-runtime,npm i --save regenerator-runtime
"presets": [
        [
            "@babel/preset-env",
            {
          "modules": false,
"useBuiltIns": "entry", "corejs": "3.0" } ] ]

  使用entry,那就在项目的入口文件index.js 中 import 'core-js' 和 import "regenerator-runtime/runtime"

import 'core-js';
import "regenerator-runtime/runtime";
const promise = new Promise();
async function request(){}

  npm run babel, 转译后的index.js引入了200多个的polyfill .

import "core-js/modules/es.symbol.js";
import "core-js/modules/es.symbol.description.js";
import "core-js/modules/es.symbol.async-iterator.js";

  这时把package.json中的 browserslist 中 IE: 11 去掉, 重新npm run babel,  index.js只引入了100多个polyfill, 按需加载polyfill。

  把entry 改成 usage, 并且把index.js 中的import 'core-js'  import "regenerator-runtime/runtime"去掉

{
  "useBuiltIns": "usage",
  "corejs": "3.0"
}

  npm run build, 在编译后的index.js 中开头部分只引入了3个polyfill。再ie:11 加上,那就只引入了4个polyfill,也是按需加载polyfill.

  Babel现在也可以在对proposals 阶段中的方法进行polyfill, 如果useBuiltIns: "entry", 直接可以在入口文件中引入建议的方法。

import "core-js/proposals/string-replace-all";

  如果使用的是useBuiltIns: "usage"只要corejs 选项配置proposals

{
    modules: false,
    "useBuiltIns": "usage",
    "corejs": { version: "3.0", proposals: true }
}

  @babel/preset-env 中配置core-js,会造成全局变量的污染,core-js 下面定义的都是global polyfill.  require("core-js/modules/es.promise"); 最终的结果是一个globle.Promise对象的存在,在浏览器中就是window.Promise. 如果不想全局变量的污染,@babel/plugin-transform-runtime. npm install --save-dev @babel/plugin-transform-runtime 和npm install --save @babel/runtime, babel配置如下

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

  同时把index.js 改成

const promise = new Promise();

  npm run babel, dist/index.js

var promise = new Promise();

  发现并没有polyfill ,Babel 7.4 又后,@babe/runtime没有polyfill 的功能了 。babel 重新提供了两个包@babel/runtime-core2, @babel/runtime-corejs3进行polyfill, 它们分别对应 core-js@ 2和core-js@3, 直接使用3 就可以了。 npm install --save @babel/runtime-corejs3, 同时babel 的配置改一下

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

  npm run babel, dist/index.js

"use strict";

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

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

var promise = new _promise.default();

  可以看到polyfill, 注意,它的polyfill来自runtime-corejs3, 看一下@babel/runtime-corejs3,在node_modules下找到它,这个包其实不包含任何代码,它只是把core-js 和regenator-runtime 列为了依赖项(package.json中)。再看一下@babel/runtime-corejs3下面的core-js-stable 目录下的 promise.js , module.exports = require("core-js-pure/stable/promise"); 它引用的core-js-pure 下的文件。core-js-pure 是core-js 不污染全局变量的版本,我们只是在index.js引用了这个promise. 如果使用打包工具webpack的话,这个promise 的实现最终会打包到 最终的bundle文件中。简单看一下这个polyfill 的过程,真正起作用的是 @babel/plugin-transform-runtime, 它把@babel/runtime-core3 node_modules中的 polyfill 插入到要需要polyfill的文件。这也就是index.js 中为什么会有 @babel/runtime-corejs3/core-js-stable。现在@babel/plugin-tranform-runtime也可以对proposal阶段的建议进行polyfill, 当然,默认情况下,它是不会开启,需要进行配置proposal: true

["@babel/plugin-transform-runtime",
    { corejs: 3, proposals: true }
]

  但@babel/plugin-transform-runtime 有一个问题,那就是它不能按需polyfill, 它不管你的target 浏览器,它觉得polyfill, 就会进行polyfill,这就会导致你的polyfiil 后的文件代码增大。@babel/runtime还有没有用,去掉了polyfill的功能,它还剩什么功能?有用,剩下了helper函数。什么是helper 函数?举一个例子就知道了,在src 下建立 main.js, index.js 和main.js 都写一个class 类, 

class A {
    constructor() {
        this.a = 1;
    }
}

  把babel.config.json 中的plugins删除一下。npm run babel,转译后的代码中都有

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

  一模一样的函数__classCallCheck, 如果代码中使用大量的类,就会存在大量重复的代码,最终会影响文件的体积, 其实这个函数完全可以抽成一个共用的函数, babel中配置 @babel/runtime,看看发生什么?

"plugins": [["@babel/plugin-transform-runtime"]]

  去掉了core: 3的配置,@babel/plugin-transform-runtime 就是去找@babel/runtime, npm run babel

"use strict";

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

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

var fn = function fn() {
  (0, _classCallCheck2.default)(this, fn);
};

   转译后的文件中并没有定义__classCallCheck, 而是require("@babel/runtime/helpers/classCallCheck"), babel 把转译过程中需要的函数都抽成公用的,这些公用的函数都放到了@babel/runtime/ helpers 中.所以称之为helper 函数。 

   作为一个应用开发者,我们并不关心全局变量污染的问题,可以使用@babel/preset-env 加上 core-js@3, 同时使用@babel/runtime 中的helper 函数。npm i @babel/preset-env @babel/plugin-transform-runtime  -- save-dev,  npm i core-js@3 @babel/runtime --save, 如果使用async/await, 再npm i regenerator-runtime 

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "chrome": "60",
                    "ie": 11
                },
                "useBuiltIns": "entry",
                "corejs": 3
            }
        ]
    ],
    "plugins": ["@babel/plugin-transform-runtime"]
}

  useBuiltIns: "entry",  要在项目的入口文件 import 'core-js'  和 import "regenerator-runtime/runtime";(如果项目中使用async/await).

  如果你是一个库的作者,最好不要污染使用者的全局变量,那就用@babel/plugin-transform-runtime 和 @babel/runtime-core@3 进行polyfill ,使用@babel/preset-env 进行语法转译。 npm i @babel/preset-env @babel/plugin-transform-runtime  -- save-dev,  npm i @babel/runtime-corejs3 --save, 

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

  对于应用开发者来说,@babel/preset-env的useBuiltIns可不可以设置成"usage", 

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

  绝大数情情况下是可以的,但这里有一种情况需要考虑,把index.js 改成

async function f() {}

  npm run babel, 转译后的文件如下

"use strict";

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

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

require("regenerator-runtime/runtime");

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

function f() {
  return _f.apply(this, arguments);
}

function _f() {
  _f = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
    return _regenerator.default.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _f.apply(this, arguments);
}

  可以看到有

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

  在node_modules 中找到这个文件,可以发现它依赖Promise,  如果像我们现在这样,Promise 并没有polyfill,  所以对于不支持promise 的浏览器来说,项目运行就会报错。useBuiltIns 设置为"usage",有的时候,不太好配置,但是绝大部分情况下没有问题。

 Babel includes helpers from @babel/runtime! These helpers can depend on some global features to be available. In this case, that feature is Promise. The code in @babel/runtime/helpers/asyncToGenerator uses Promises!. 
posted @ 2020-05-30 18:08  SamWeb  阅读(562)  评论(0编辑  收藏  举报