Babel 7 初探

  最近在做项目的依赖的清理,看到babel 已经升级7.9了,而我对babel的认知还停留在6时代,觉得需要重新学习一下了。

  我们都知道, Babel是转译工具,它能把最新的javaScript的语法,转化成旧的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 进行转译,要安装哪些依赖呢?

  @babel/cli  babel 命令集合,在命令行中直接调用babel 命令,对文件进行编译,如果使用webpack,那就不用它,要用babel-loader. 

  @babel/core, babel的核心,它负责转译js 语法,但单独使用它,却起不到转译的作用,需要你给它提供插件。也就是说,如果只安装@babel/core, 调用babel 命令进行转译,文件会原模原样地进行输出,转译后的文件和原文件一模一样。

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

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

  然后在命令行中调用babel 命令 npx babel src  --out-dir dist,npx可以直接调用node_modules 中的命令,--out-dir 表示转译后的文件输出到什么地方。也可以在package.json 的script 中写入"babel": "babel src --out-dir dist" ,再npm run babel 调用babel 命令。看一下转译后dist目录中的index.js ,没有任何变化。

  提供转译箭头函数的插件, npm i  @babel/plugin-transform-arrow-functions --save-dev, 调用babel 命令的时候,怎么让babel 使用这个插件呢?有两种方式,一种是在命令行后面加--plugins, 一种是配置文件。最好使用配置文件吧,因为添加和修改插件都比较方便。配置文件的命名也改了,原来是.babelrc, 现在官方建议是babel.config.json. touch babel.config.json

{
    "plugins": ["@babel/plugin-transform-arrow-functions"]
}
  npm run babel,箭头函数已经转译了,这时你再想,要不要把const 再转译一下,那就需要提供另外一下babel插件了,随着转译的语法越来越多,你要在babel 的config 文件中添加的插件也就越来越多,如果一个一个手动添加进去,那就有点麻烦了。有一个插件集合就好了,正好 babel 提供了一个@babel/preset-env。 @babel/preset-env,它是一个插件集合,所以它的作用也是转译新的js 语法到旧的js语法。那么它能转译哪些语法呢?ECMAScript 官方发布的正式版本,如ES2015 ~ES2020 中的新语法和ECMAScript proposals 中stage 4  中的新语法,这也就是下一年要发布的ECMAScript 版本中的新语法。只要官方定稿的语法,@babel/preset-env 都可以进行转译,那这就方便多了,只要安装它这一个,你就可以写一堆的新语法。插件集合称为预设(presets)。npm i @babel/preset-env  -- save-dev,相应的babel.config.json 文件就要改成
{
    "presets": [
        "@babel/preset-env"
    ]
}
  npm run babel, const 和箭头函数都转译了。当然有的时候,确实想用最新的语法,比如 decorator,  官方没有定稿,还在stage-2 阶段,那就单独为这个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, 除了它是一个插件集合,能够转译最新语法外,它还集成了browserlist, 能够根据指定的浏览器决定要不要编译最新的语法到旧的语法。默认情况下,如果没有指定browserlist, 它会全部进行转译,就像现在的配置一样。指定一下browserlist,  有三种方式,一个是 .browserslistrc 文件,一个是package.json,一个是@babel/preset-env options,如果使用options, presets中的每一项就要变成数组,数组的第一项是使用的preset, 第二项是该preset的参数配置
"presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "chrome": "58"
                }
            }
        ]
    ]
  npm run babel 可以发现,任何语法都没有进行转译,因为chrome 58 已经支持const 和箭头函数了。再把IE 11 加上
"targets": {
    "chrome": "58",
    "ie": "11"
}
  npm run  babel, 所有的语法都重新编译到旧的语法。
  JavaScript的世界不止新的语法,还有新的对象和方法,如Promise 对象,Map 对象,这些对象和方法是无法进行编译的,把index.js 改成如下代码
const promise = new Promise()
  npm run babel,只有const 转化成了var, promise对象没有处理,这需要用到polyfill,  就是把新的对象和方法用已经实现了的对象和方法实现。 原来实现polyfill 的方式有两种,一种是@babel/polyfill,  一种是@babel/plugin-transform-runtime 和@babel/runtime,为什么有两种呢?不同的使用场景而已,@babel/polyfill会造成全局变量的污染,所以适用于应用开发,@babel/runtime 不会造成全局变量的污染,适用于库的开发。现在的实现方式有了很大的区别。@babel/polyfill 在7.4 以后被废弃了。@babel/runtime 现在仅仅剩下helper 函数了,polyfill 的功能被移除了。
  @babel/polyfill  被废弃的原因是babel直接支持core-js, 更准确的说是支持core-js@3 三版本。 core-js@3 不仅包含了core-js@2,core-js@3 中的stable 就是对应的core-js@2,  且比它更加强大, babel 通过直接支持core-js还可以实现按需polyfill.  而babel/polyfill 正好使用的是core-js@2, 并且无法平滑地升级到core-js@3,而且使用@babel/polyfill 无法实现按需polyfill, 所以直接废弃了。babel 直接支持core-js@3 是通过@babel/preset-env 中的useBuiltIns 和 corejs 实现的。useBuiltIns有三个取值
  1, false, 默认值,表示 @babel/preset-env 不会进行polyfill.
  2,   entry:   我们在项目的最开始部分引入 core-js; 或 regenerator-runtime/runtime, 和@babel/polyfill 用法一致,babel 就会把整个的引入变成一个一个小polyfill的引入,引入哪些小polyfill 是根据browserlist 定义的浏览器目标决定的,这就是按需加载polyfill
  3,usage:  它是按文件进行polyfill,  如果一个文件中 用到了一个对象如promise 对象,而browserlist 中定义的浏览器又不支持这个对象,它就会在这个文件的顶部引入polyfill.  也是按需加载polyfill
  corejs 有2, 3等, 就是指定core-js 的版本。npm i core-js@3 --save
"presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "chrome": "58",
                    "ie": 11
                },
                "useBuiltIns": "entry",
                "corejs": 3
            }
        ]
    ]

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

import 'core-js';

const promise = new Promise();

  npm run babel, 编译后的index.js引入了450个小的polyfill . 这时把ie: 11 去掉, 重新npm run babel,  index.js只引入了100多个polyfill, 确实是按需要加载。

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

            {
                "targets": {
                    "chrome": "58",
                    "ie": 11
                },
                "useBuiltIns": "usage",
                "corejs": 3
            }    

  npm run build, 在编译后的index.js 中开头部分只引入了2个关于promise 的polyfill。当你再把ie:11 去掉,那就只引入了一个polyfill

"use strict";

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

const promise = new Promise();

  @babel/preset-env 中配置core-js和使用@babel/polyfill一样,会造成全局变量的污染,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",
            {
                "targets": {
                    "chrome": "60",
            "ie": 11
} } ] ], "plugins": ["@babel/plugin-transform-runtime"] }

  npm run babel, 发现并没有polyfill ,因为@babe/runtime 现在只剩下helpers, 没有polyfill 了。什么是helper ,就是一些辅助函数,为了避免重复的代码,主要是在编译过程中出现的重复代码。简单看一个例子,再在src 下建立 main.js, index.js 和main.js 都写一个class 类, 把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"); } }

var fn = function fn() {
  _classCallCheck(this, fn);
};

  都有一个一模一样的函数__classCallCheck, 如果代码中使用大量的类,就会存在大量重复的代码,最终会影响文件的体积。其实这个函数完全可以抽成一个共用的函数,这些函数是helper. babel 配置文件中的plugins 加回去, 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);
};

   require("@babel/runtime/helpers/classCallCheck"), 这个函数就是__classCallCheck了, babel 把编译过程中需要的函数都抽成公用的,这些公用的函数都放到了@babel/runtime/ helpers 中.所以称之为helper 函数。  @babel/runtime 不提供polyfill 之后,babel 重新提供了两个包@babel/runtime-core2, @babel/runtime-corejs3, 它们分别对应 core-js@ 和core-js@3, 直接使用3 就可以了 npm install --save @babel/runtime-corejs3, 同时babel 的配置改一下

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

   把index.js 改回promise, 可以看到成功编译了

"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();

  @babel/runtime-corejs3/core-js-stable/promise,在vscode,安装ctrl,点击它,找到这个文件,module.exports = require("core-js-pure/stable/promise"); 它引用的core-js-pure 下的文件。core-js-pure 是core-js 不污染全局变量的版本,我们只是在index.js引用了这个promise. 如果使用打包工具webpack的话,这个promise 的实现最终会打包到 最终的bundle文件中。简单看一下这个polyfill 的过程,@babel/runtime-corejs3, 这个包其实不包含任何代码,它只是把core-js 和regenator-runtime 列为了依赖项(githup仓库可以看到)。真正起作用的是 @babel/plugin-transform-runtime, 它把@babel/runtime-core3 node_modules中的 polyfill 插入到要polyfill的文件。这也就是index.js 中为什么会有 @babel/runtime-corejs3/helpers or @babel/runtime-corejs3/core-js-stable。 @babel/plugin-transform-runtime 同样也有一个问题,那就是它不能按需polyfill, 可能会使你的polyfiil 后的文件代码增大。

   作为一个应用开发者,我们并不关心全局变量污染的问题,可以使用@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, 

{
    "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).

  useBuiltIns 也可以设置成"usage", 

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

  但这里有一种case 需要考虑,把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/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  进行设置 

 
posted @ 2020-05-30 18:08  SamWeb  阅读(249)  评论(0编辑  收藏