1. O que é Babel?
Vamos dar uma olhada na explicação dada pelo site oficial:
Simplificando, é converter a nova sintaxe do es5+ em JavaScript em uma sintaxe compatível com versões anteriores, para que o código possa ser executado em navegadores de versão inferior ou outros ambientes. Mas não acho correto dizer isso. Pessoalmente, acho que babel é na verdade uma plataforma que fornece recursos de conversão de código. Na hora certa, chamamos nossos plugins registrados para converter o código alvo.
2. Introdução ao princípio de funcionamento de Babel
1. Compiladores e Transpiladores
编译
Refere-se à conversão de linguagens de alto nível para linguagens de baixo nível. Geralmente usamos linguagens de desenvolvimento de alto nível para desenvolvimento, como JavaScript, C++, Java, etc. O objetivo das linguagens de alto nível é fornecer gramáticas e APIs de alto nível para os desenvolvedores desenvolverem e entenderem. Melhorar a eficiência do desenvolvimento, mas nosso computador não conhece essas linguagens de desenvolvimento de alto nível, então vem o papel do compilador, ele converte a linguagem de desenvolvimento de alto nível que nós escrever em linguagens de baixo nível que as máquinas possam entender e executar, como linguagem assembly, linguagem de máquina, etc.
转译
Refere-se à conversão de linguagem de alto nível para linguagem de alto nível, como TypeScript para ferrugem. Geralmente, o tradutor é composto parse
por três etapas: , transform
, e :generate
-
parse inclui três estágios de análise léxica, análise sintática e análise semântica e, finalmente, gera uma árvore sintática abstrata (AST) para familiarizar o código-fonte.
-
A fase de transformação chamará vários plugins para adicionar, excluir e modificar o AST.
-
A fase de geração converterá o AST no código de objeto final e gerará o mapa de origem.
2. Fluxo de trabalho Babel
Depois de falar sobre a diferença entre um compilador e um transpilador, fica claro de relance a qual categoria nosso babel pertence.Babel é um transpilador, e o código js que ele traduziu é finalmente entregue ao mecanismo js para análise. A parte mais importante para o babel é a fase de transformação, os plugins que registramos no babel são executados aqui, se não registrarmos nenhum plugin naturalmente o babel não fará nenhuma transformação em nosso código, o que me verifica Como mencionado no início, o babel é realmente uma plataforma.
3. Configuração básica do Babel
1. Construção do projeto
在学习 babel 前我们先手动搭建一个项目。
mkdir babel-test && cd babel-test
npm init
mkdir src && cd src
touch index.js
复制代码
新建好项目之后我们先装几个 babel 必备的依赖:
- @babel/core
这个包是 babel 的核心库,我们的 parse
和 transform
两个核心方法都是在这个库中,具体怎么使用我们不需要知道,我们现安装它。
npm i --save-dev @babel/core
//使用
// var babel = require("@babel/core");
// import { transform, parse } from "@babel/core";
复制代码
- @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
的中文意思是垫片,垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用,也就是说对于像 Promise
、class
、Array.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
helpers 内置了一些方法的实现,比如使用async/await
就需要用到 _asyncToGenerator2
。
下面就来使用这个插件,首先更改下 .babelrc 的配置,这里我们去掉了 presets 里的 useBuiltIns
和 corejs
,不然就会出现重复引入的问题。
{
"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);
};
}();
复制代码
下面我们来分析他是怎么解决上面提到的问题的:
- 怎么避免重复引入的?
我们所有的 polyfill 实现都被保存在 @babel/runtime-corejs3
这个包里面,我们需要的时候通过 require 导入进来就可以直接使用了。
- 怎么避免全局环境污染的呢?
我们挑就拿 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 解析, 此时我们使用尖括号就会出错。我们看下官网是怎么定义断言的。
这里明确的说明不能在 .tsx 文件里面使用尖括号的形式去断言。
- allExtensions
Este item de configuração é falso por padrão, se for definido como verdadeiro significa que todo arquivo deve ser analisado como TS, TSX ou TS sem ambiguidade JSX, isTSX
é usado com .
Vamos usar um exemplo para explicar, vamos mudar index.js para index.ts, e também mudar o nome do arquivo no arquivo package.json.
// index.ts
const bar = 'bar';
const str = <string>bar;
复制代码
Instale @babel/preset-typescript
e modifique a configuração do .babelrc
{
"presets": [
"@babel/env",
"@babel/preset-typescript"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
复制代码
Ao executá-lo neste momento, npm run build
veremos que o babel foi convertido com sucesso. Em seguida, modificaremos a configuração do .babelrc, mais as duas propriedades mencionadas acima:
{
"presets": [
"@babel/env",
[
"@babel/preset-typescript",
{
"isTSX": true,
"allExtensions": true
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
复制代码
Depois de executar novamente, nosso console imprimirá uma mensagem de erro
Isso significa que há conteúdo JSX não finalizado.