还不知道 babel 配置的看这里

一、什么是 Babel?

我们先来看一下官网给出的解释:

image.png 简单来说就是将 JavaScript 中 es5+ 的新语法转化成能够向后兼容的语法,让代码能够在低版本的浏览器或其他环境中执行。但是我觉得这么说似乎不太准确,个人认为 babel 其实就是一个提供代码转化能力的平台,在合适的时机调用我们注册的 plugins 去对目标代码进行转化。

二、Babel 工作原理简介

1、编译器和转译器

编译指的是高级语言到低级语言的转化,我们平时开发都是用的高级开发语言,比如 JavaScript、C++、Java 等,高级语言的目的就是提供上层的语法和 API 便于开发者开发和理解,提高开发效率,但我们的计算机却不认识这些高级开发语言,所以编译器的作用就来了,它会将我们所写的高级开发语言转换成机器能够理解执行的低级语言,比如汇编语言、机器语言等。

转译指的是高级语言到高级语言的转化,比如 TypeScript 转 rust,一般转译器都是由 parsetransformgenerate这三个阶段组成:

image.png

  • parse 包含词法分析、语法分析和语义分析三个阶段,最终生成抽象语法树(AST)来面熟源代码。

  • transform 阶段会调用各种插件对 AST 进行增删改。

  • generate 阶段会吧 AST 转换成最终的目标代码并生成 sourcemap。

2、Babel 工作流程

上面讲完编译器和转译器的区别,我们的 babel 属于哪一类就一目了然了,babel 就是一个转译器,他转译完的 js 代码最终交给 js 引擎去解析。对于 babel 来说最重要的部分在于 transform 阶段,我们在 babel 上注册的 plugins 就是在这里被执行的,如果我们不注册任何插件自然 babel 就不会对我们的代码进行任何转换,这就验证了我开头说的,babel 其实就是一个平台。

三、Babel 的基础配置

1、项目搭建

在学习 babel 前我们先手动搭建一个项目。

mkdir babel-test && cd babel-test
npm init
mkdir src && cd src
touch index.js
复制代码

新建好项目之后我们先装几个 babel 必备的依赖:

  1. @babel/core

这个包是 babel 的核心库,我们的 parsetransform 两个核心方法都是在这个库中,具体怎么使用我们不需要知道,我们现安装它。

npm i --save-dev @babel/core

//使用
// var babel = require("@babel/core");
// import { transform, parse } from "@babel/core";
复制代码
  1. @babel/cli

这是一个终端运行工具,这样我们可以直接使用终端命令对指定的文件进行转译,同样先装它。

npm i --save-dev @babel/cli
复制代码

2、plugins

好了依赖装完之后可以来小试牛刀了,我们在 index.js 中写入一个函数:

const fn = () => {
  console.log("babel");
};
复制代码

如果要运行 babel 命令的话我们还需要在 package.json 中添加运行命令,如下所示:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "babel ./src/index.js --out-file ./src/compliled.js --watch"
  },
复制代码

babel 就是我们的启动命令,./src/index.js 表示待转译的文件,--out-file表示指定我们的输出文件,--watch 表示监听文件的改动并重新编译文件。

下面我们在终端运行 npm run build,查看 complied.js 文件:

// complied.js
const fn = () => {
  console.log("babel");
};
复制代码

我们发现 babel 并没有对代码进行转译,这是符合我们的预期的,因为现在我们并没有注册 plugins。如果想对箭头函数进行转换我们需要安装 @babel/pluign-transform-arrow-function 这个插件。

npm install --save-dev @babel/plugin-transform-arrow-function
复制代码

然后在根目录下新建一个 .babelrc 文件去给 babel 添加配置文件:

// .babelrc
{
  "plugins": [
    "@babel/plugin-transform-arrow-functions"
  ]
}
复制代码

我们重新编译一下:

// complied.js
const fn = function () {
  console.log("babel");
};
复制代码

我们看到 babel 成功对代码进行转译,现在我们想想如果我们要将其他的 es5+ 的语法进行转换是不是需要将所有的 plugin 都引入,答案是肯定的,不过不需要我们手动去引入 babel 已经帮我们把这件事做了,这就是下面要介绍的 @babel/preser-env

3、@babel/preset-env

@babel/preset-env 主要作用是对我们所使用的并且目标浏览器中缺失的功能进行代码转换,那这里有个疑问了,babel 是怎么知道目标浏览器到底支不支持该特性呢?这就需要我们在配置项 targets 中去设置浏览器的版本,下面我们举例说明: 先安装 @babel/preset-env 预设:

npm i --save-dev @babel/preset-env
复制代码

我们更改下 index.js 中的内容:

// index.js
const fn = function () {
  console.log("babel");
};
const num = 2 ** 4;
复制代码

@babel/preset-env 加入配置文件:

// .babelrc
{
  "presets": [
    "@babel/env" // 也可以写 @babel/preset-env 两者是等价的
  ]
}
复制代码

接下来我们再重新执行 npm run build

"use strict";

var fn = function fn() {
  console.log("babel");
};

var num = Math.pow(2, 4);
复制代码

babel 顺利对我们的代码进行转译,如果我们想指定浏览器的版本,我们可以这样写:

// .babelrc
{
  "presets": [
    [
        "@babel/env", // 也可以写 @babel/preset-env 两者是等价的
        {
            "targets": {
                "browsers": "Chrome 99"
            }
        }
    ]
  ]
}
复制代码

此时就会告诉 babel 我们的浏览器的版本是 Chrome 99,而对于这个版本的浏览器他是支持箭头函数的,所以 babel 不会对他进行转译。

4、@babel/polyfill

polyfill 的中文意思是垫片,垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用,也就是说对于像 PromiseclassArray.prototype.includes 这些特性 babel 是无法转换的,此时我们就需要用到 polyfill

我们先安装 @babel/polyfill,然后需要在 index.js 文件中引入:

// index.js
import '@babel/polyfill';
const fn = () => {
  console.log("babel");
};
const p = new Promise((resolve, reject) => {
  resolve("babel");
});
const list = [1, 2, 3, 4].map((item) => item * 2);
复制代码

我们看下转译后的代码:

"use strict";

require("@babel/polyfill");

var fn = function fn() {
  console.log("babel");
};

var p = new Promise(function (resolve, reject) {
  resolve("babel");
});
var list = [1, 2, 3, 4].map(function (item) {
  return item * 2;
});
复制代码

乍一看好像没啥特别的变化,只是多了一行 require("@babel/polyfill"),这个意思就是把这个库中的所有 polyfill 都引入进来,里面就包括了 Promise 和 map 的实现,那我们能不能再优化一下呢?让他支持按需加载。

5、useBuiltIns

上面我们通过 import "@bable/polyfill" 的方式来磨平一些新特性,但是从 babel v7.4.0 开始官方就不建议采取这样的方式了,因为引入这个库就等于引入了下面两个库:

require("core-js"); //这里的corejs版本为2
require("regenerator-runtime/runtime");
复制代码

这样意味着会带来两个问题:

  • 我们不能实现按需加载
  • 污染全局环境:因为像 includes、filter 这样的方法是通过向全局对象和内置对象的 prototype 上添加方法来实现的。

@babel/preset-env 提供了一个 useBuiltIns 参数,设置值为 usage 时,就只会包含代码需要的 polyfill,这里需要注意的是如果我们将 useBuiltIns 设置为 usage 时必须要将 corejs 的版本设置为 3,因为在 corejs@2 中已经不会再添加新的特性了,同时我们需要手动去安装 corejs@3。

此时我们再去修改 .babelrc 中的配置:

{
  "presets": [
    [
      "@babel/env",
      {   
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}
复制代码

修改 index.js 中的代码:

const fn = () => {
  console.log("babel");
};
const p = new Promise((resolve, reject) => {
  resolve("babel");
});
const list = [1, 2, 3, 4].map((item) => item * 2);

class Person {
    constructor(name) {
      this.name = name;
    }
    play() {
      console.log(this.name);
    }
}

const asyncFun = async () => {
  await 1;
}
复制代码

我们看一下最终转译后的代码:

"use strict";

require("core-js/modules/es.object.define-property.js");

require("regenerator-runtime/runtime.js");

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

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

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

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

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

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

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }

var fn = function fn() {
  console.log("babel");
};

var p = new Promise(function (resolve, reject) {
  resolve("babel");
});
var list = [1, 2, 3, 4].map(function (item) {
  return item * 2;
});

var Person = /*#__PURE__*/function () {
  function Person(name) {
    _classCallCheck(this, Person);

    this.name = name;
  }

  _createClass(Person, [{
    key: "play",
    value: function play() {
      console.log(this.name);
    }
  }]);

  return Person;
}();

var asyncFun = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return 1;

          case 2:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));

  return function asyncFun() {
    return _ref.apply(this, arguments);
  };
}();
复制代码

此时我们会发现 babel 并没有把所有的 polyfill 都引入进来而是按需引入,但是从转译后的结果看来,还是存在以下问题:

  • 全局环境污染还是存在

  • 代码的重复引入

    _classCallCheck_defineProperties这样的函数只要用到了 class 就会被引入进来,就会产生重复引入的问题。

  • 仍然需要安装 @babel/polyfill

    因为当我们使用 async/await 时需要引入 regenerator-runtime 这个库,或者我们手动去安装这个库也行。

那有什么方法能够解决这些问题呢?下面就要说到 @babel/plugin-transform-runtime这个插件了。

6、@babel/plugin-transform-runtime

首先我们需要安装两个依赖 @babel/plugin-transform-runtime@babel/runtime,前者是在开发的时候使用,但是最终运行时的代码依赖后者,所以我们需要在生产环境中安装 @babel/runtime

npm install --save-dev @babel/plugin-transform-runtime\
npm install --save @babel/runtime
复制代码

我们先来看看 @babel/runtime-corejs@3这个库中有什么:

  • core-js

    转换一些内置的类和一些静态方法,比如 Promise、Array.from、Array.includes,绝大部分的转换是在这里做的。

  • regenerator

    作为 corejs 的补丁,主要是针对 generator/yield 和 async/await 的支持。

  • helpers

image.png helpers 内置了一些方法的实现,比如使用async/await就需要用到 _asyncToGenerator2

下面就来使用这个插件,首先更改下 .babelrc 的配置,这里我们去掉了 presets 里的 useBuiltInscorejs,不然就会出现重复引入的问题。

{
  "presets": [[
    "@babel/env"
  ]],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}
复制代码

index.js 文件保持不变,我们重新运行之后看下结果:

"use strict";

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

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

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

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

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

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

var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map"));

var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));

var _context, _context2;

var fn = function fn() {
  console.log("babel");
};

var p = new _promise["default"](function (resolve, reject) {
  resolve("babel");
});
var list = (0, _map["default"])(_context = [1, 2, 3, 4]).call(_context, function (item) {
  return item * 2;
});
var list1 = (0, _includes["default"])(_context2 = [1, 2, 3, 4]).call(_context2, 9);

var Person = /*#__PURE__*/function () {
  function Person(name) {
    (0, _classCallCheck2["default"])(this, Person);
    this.name = name;
  }

  (0, _createClass2["default"])(Person, [{
    key: "play",
    value: function play() {
      console.log(this.name);
    }
  }]);
  return Person;
}();

var asyncFun = /*#__PURE__*/function () {
  var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
    return _regenerator["default"].wrap(function _callee$(_context3) {
      while (1) {
        switch (_context3.prev = _context3.next) {
          case 0:
            _context3.next = 2;
            return 1;

          case 2:
          case "end":
            return _context3.stop();
        }
      }
    }, _callee);
  }));

  return function asyncFun() {
    return _ref.apply(this, arguments);
  };
}();
复制代码

下面我们来分析他是怎么解决上面提到的问题的:

  1. 怎么避免重复引入的?

我们所有的 polyfill 实现都被保存在 @babel/runtime-corejs3 这个包里面,我们需要的时候通过 require 导入进来就可以直接使用了。

  1. 怎么避免全局环境污染的呢?

我们挑就拿 Array.includes 来讲,在之前我们是直接在全局对象的 prototype 上添加 includes 方法,而现在我们没有直接修改 prototype,而是使用 _includes 去替代 includes 方法,这样就有效的避免全局环境的污染。

3、@babel/runtime-corejs3 中已经集成了 regenerator 我们就不需要再手动安装。

7、@babel/preset-typescript

@babel/preset-typescript 主要就是帮我们将 ts 代码转换成 js,这里面有两个配置要注意一下:

  • isTSX

这个配置项默认值是 false,表示不开启 jsx 解析,此时意味着我们可以使用尖括号的语法进行断言,如果改为 true,即强制开启 jsx 解析, 此时我们使用尖括号就会出错。我们看下官网是怎么定义断言的。

image.png 这里明确的说明不能在 .tsx 文件里面使用尖括号的形式去断言。

  • allExtensions

这个配置项默认是 false,如果我们设为 true 表示每个文件都应该被解析为 TS、TSX 或没有 JSX 歧义的 TS,它和 isTSX 搭配使用。

下面我们通过一个例子来讲解下,我们将 index.js 改为 index.ts,同时也去 package.json 文件中把文件名也改过来。

// index.ts
const bar = 'bar';
const str = <string>bar;
复制代码

安装 @babel/preset-typescript 并修改下 .babelrc 的配置

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

我们此时运行下 npm run build 我们会看到 babel 顺利的进行转换,下面我们再去修改下 .babelrc 的配置,加上上面说的两个属性:

{
  "presets": [
    "@babel/env",
    [
      "@babel/preset-typescript",
      {
        "isTSX": true,
        "allExtensions": true
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}
复制代码

再运行下,我们控制台就会打印出报错信息

image.png 意思就是出现了未终止的 JSX 内容。

猜你喜欢

转载自juejin.im/post/7082976239421980679