终于开始学习babel了
什么是Babel?
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
- 源码转换 (codemods)
还是不太懂,编译器是什么?
强烈建议通过the-super-tiny-compiler项目,找到答案。
编译器的工作过程
解析(词法分析 + 语法分析) => 转化 => 生成
以the-super-tiny-compiler这个项目为例子,需要将
(add 2 (subtract 4 2))
代码经过处理 生成add(2, subtract(4, 2));
代码。
- 词法分析(Lexical Analysis)
解析一般来说会分成两个阶段:词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。
词法分析接收原始代码,然后把它分割成一些被称为Tokens的东西,这个过程是在词法分析器(Tokenizer或者Lexer)中完成的
Token 是一个数组,由一些代码语句的碎片组成。它们可以是数字、标签、标点符号、运算符、或者其他任何东西。(add 2 (subtract 4 2))
经过词法分析器,得到的 Token 如下:
[
{ "type": "paren","value": "(" },
{ "type": "name","value": "add"},
{ "type": "number", "value": "2"},
{ "type": "paren","value": "("},
{ "type": "name","value": "subtract"},
{ "type": "number","value": "4"},
{ "type": "number","value": "2"},
{ "type": "paren","value": ")"},
{ "type": "paren","value": ")"}
]
- 语法分析(Syntactic Analysis)
语法分析接收之前生成的 Token,把它们转换成一种抽象的表示,这种抽象的表示描述了代码语句中的每一个片段以及它们之间的关系。这被称为中间表示(intermediate representation)或抽象语法树(Abstract Syntax Tree, 缩写为AST)
辅助开发的网站:
抽象语法树是一个嵌套程度很深的对象,用一种更容易处理的方式代表了代码本身,也能给我们更多信息。词法分析后的 Token 经过语法分析器处理后得到AST如下:
{
"type": "Program",
"body": [
{
"type": "CallExpression",
"name": "add",
"params": [
{
"type": "NumberLiteral",
"value": "2"
},
{
"type": "CallExpression",
"name": "subtract",
"params": [
{
"type": "NumberLiteral",
"value": "4"
},
{
"type": "NumberLiteral",
"value": "2"
}
],
"_context": [
{
"type": "NumberLiteral",
"value": "4"
},
{
"type": "NumberLiteral",
"value": "2"
}
]
}
],
"_context": [
{
"type": "NumberLiteral",
"value": "2"
},
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "subtract"
},
"arguments": [
{
"type": "NumberLiteral",
"value": "4"
},
{
"type": "NumberLiteral",
"value": "2"
}
]
}
]
}
],
"_context": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "add"
},
"arguments": [
{
"type": "NumberLiteral",
"value": "2"
},
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "subtract"
},
"arguments": [
{
"type": "NumberLiteral",
"value": "4"
},
{
"type": "NumberLiteral",
"value": "2"
}
]
}
]
}
}
]
}
- 转化(Transformation)
当转换 AST 的时候我们可以添加、移动、替代这些结点,也可以根据现有的 AST 生成一个全新的 AST
由于我们例子中是需要将输入的代码转化为一种新的语言,所以我们将会着重于产生一个针对新语言的全新的 AST, 具体可以参考the-super-tiny-compiler。
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "add"
},
"arguments": [
{
"type": "NumberLiteral",
"value": "2"
},
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "subtract"
},
"arguments": [
{
"type": "NumberLiteral",
"value": "4"
},
{
"type": "NumberLiteral",
"value": "2"
}
]
}
]
}
}
]
}
- 代码生成
编译器的最后一个阶段是代码生成,这个阶段做的事情有时候会和转换(transformation)重叠,但是代码生成最主要的部分还是根据 AST 来输出代码。
add(2, subtract(4, 2));
babel中的包
上面我们讲了一个编译器是如何工作的。babel的工作原理也大体相同
@babel/babel-parser 是 Babel 中负责解析成AST(词法分析 + 语法分析)
var result = babel.transformSync("code();", options);
result.code;
result.map;
result.ast;
@babel/babel-traverse 是 Babel 中负责转化的包
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
}
});
@babel/generator 是 Babel 中负责将ast生成新code的包
import { parse } from "@babel/parser";
import generate from "@babel/generator";
const code = "class Example {}";
const ast = parse(code);
const output = generate(
ast,
{
/* options */
},
code
);
配置 Babel
其实,仅仅通过上述的包,目前为止通过运行 Babel 本身,并没能“翻译”代码,而仅仅是把代码从一处拷贝到了另一处。
这是因为我们还没告诉 Babel 要做什么。
由于 Babel 是一个可以用各种花样去使用的通用编译器,因此默认情况下它反而什么都不做。你必须明确地告诉 Babel 应该要做什么。如果想要 Babel 做一些实际的工作,就需要为其添加插件。
官方提供了多种babel配置的方法
- 在项目的根目录创建.babelrc
{
"presets": [...],
"plugins": [...]
}
- 在项目的根目录创建 babel.config.json
{
"presets": [...],
"plugins": [...]
}
- 你还可以用 JavaScript 编写 babel.config.json 和 .babelrc.json文件:
const presets = [ ... ];
const plugins = [ ... ];
module.exports = { presets, plugins };
你还可以调用 Node.js 的任何 API,例如基于进程环境进行动态配置:
const presets = [ ... ];
const plugins = [ ... ];
if (process.env["ENV"] === "prod") {
plugins.push(...);
}
module.exports = { presets, plugins };
babel插件
官方提供了很多插件,大体分为以下几类
- 对ES新特性的转化
- 模块化转化
- 实验中的功能
- 缩小代码体积的插件
- React相关插件
- 其他
以 @babel/plugin-transform-arrow-functions 插件为例,来感受一下babel。经过 plugin-transform-arrow-functions 插件能将我们的ES语法的箭头函数转化成普通的函数形式。
使用cli命令行的方式
- 初始化项目,新建src目录
npm init -y
- 安装@babel/core、@babel/cli
由于,我们现在需要以编程的方式来体验babel, 并且希望能在命令行中输入babel命令,就能编译我们的代码,所以需要安装@babel/core, @babel/cli 这两个包
cnpm install -D @babel/core @babel/cli
@babel/core 这个包实际上已经提供了 解析 > 转化 > 生成的功能,从该包的package.json中可以看出,包含了@babel/parser、@babel/traverse、@babel/generator这三个包
"dependencies": {
"@babel/code-frame": "workspace:^7.12.13",
"@babel/generator": "workspace:^7.13.0", // 生成
"@babel/helper-compilation-targets": "workspace:^7.13.0",
"@babel/helper-module-transforms": "workspace:^7.13.0",
"@babel/helpers": "workspace:^7.13.0",
"@babel/parser": "workspace:^7.13.0", // 解析
"@babel/template": "workspace:^7.12.13",
"@babel/traverse": "workspace:^7.13.0", // 转化
"@babel/types": "workspace:^7.13.0",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"escape-string-regexp": "condition:BABEL_8_BREAKING ? ^4.0.0 : ",
"gensync": "^1.0.0-beta.2",
"json5": "^2.1.2",
"lodash": "^4.17.19",
"semver": "condition:BABEL_8_BREAKING ? ^7.3.4 : 7.0.0",
"source-map": "^0.5.0"
},
- 安装@babel/plugin-transform-arrow-functions插件
cnpm install --save-dev @babel/plugin-transform-arrow-functions
- 在pakage.json文件中添加 babel 编译的命令(也可以再接在命令行输入)
"scripts": {
"build:babel": "babel src/arrow-functions.js -d dist --plugins @babel/plugin-transform-arrow-functions",
"test": "echo \"Error: no test specified\" && exit 1"
},
- 在src目录下新建 arrow-functions.js 文件,并且写入代码
const func = () => {
console.log('Hello Babel');
}
- 在终端运行 npm run build:babel 命令,查看dsit目录下编译后的结果
const func = function () {
console.log('Hello Babel');
};
可以看到,我们箭头函数语法就被babel转译了
使用babel.config.json的方式
除了在命令行输入插件的配置外,我们也可以通过写配置文件的方式来配置 Babel,步骤如下
- 在项目根目录创建 babel.config.json 文件
这次,我们在babel.config.json中新增一个插件:plugin-transform-function-name
{
"plugins": [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-function-name"
]
}
- 安装插件
cnpm install --save-dev @babel/plugin-transform-function-name
- 在package.json中新增命令build:babel-config
"scripts": {
"build:babel": "babel src/arrow-functions.js -d dist --plugins @babel/plugin-transform-arrow-functions",
"build:babel-config": "babel src/arrow-functions.js -d dist",
"test": "echo \"Error: no test specified\" && exit 1"
},
- 运行 npm run build:babel-config, 并查看结果
const func = function func() {
console.log('Hello Babel');
};
我们看到,不仅箭头函数被转化了,新增了transform-function-name插件之后,函数名也被转化了。
babel预设(Presets)
通过插件的例子也许你发现了,Babel 的设计思路是,我们需要明确告诉他需要干A、B、C,他才会去干。如果你觉得组合插件这个活儿比较麻烦,preset 可以作为 Babel 插件的组合,甚至可以作为可以共享的 options 配置。
官方 Preset
- @babel/preset-env
- @babel/preset-flow
- @babel/preset-react
- @babel/preset-typescript
除此之外,你还可以自己动手写一个 preset
plugin 和 preset 两者的执行顺序有差别,preset是倒序执行的,plugin是顺序执行的,并且plugin的优先级会高于preset。
babel-polyfill
Babel 插件几乎可以编译所有新的 JavaScript 语法,但对于 APIs 来说却并非如此。
比方说,下列含有箭头函数的需要编译的代码:
function addAll() {
return Array.from(arguments).reduce((a, b) => a + b);
}
经过编译后,会变成这样:
function addAll() {
return Array.from(arguments).reduce(function(a, b) {
return a + b;
});
}
虽然,箭头函数的语法是被 Babel 转化了,但是 Array.from 这个API并不会被转化,而现实是并非所有的浏览器都支持 Array.from 这个API
Uncaught TypeError: Array.from is not a function
为了解决这个问题,我们使用一种叫polyfill(代码补充,也可译作垫片)的技术。Babel 中的 polyfill 本质上是 core-js 和 facebook的 regenerator的组合,它的目的是模拟一整套ES6的运行环境,所以它会以全局变量的方式去polyfill promise、Map这些类型,也会以Array.prototype.includes()这种方式去污染原型对象。
值得注意的是,从 Babel 7.4.0 开始@babel-polyfill 这个包已经被废弃了,在后来的版本里也被删除了,官方建议直接引入 core-js/stable 和 regenerator-runtime/runtime, 并且配合@babel/preset-env使用
编译后,我们发现,自动引入了 core-js/modules/es.array.from.js
core-js会帮我们兼容低版本的浏览器
...
require("core-js/modules/es.array.from.js");
...
function addAll() {
return Array.from(arguments).reduce(function (a, b) {
return a + b;
});
}
注意区分 plugins 和 polyfill 的区别,plugins 能够通过强化编译器中的
转化
步骤,将高级的JavaScript语法转化成能向下兼容的语法。而 polyfill 则是提供了像Arra.from 等APIs的语法补充。
babel-runtime 和 plugin-transform-runtime
babel-runtime是一个包含了Babel模块化运行的helpers函数(其实本质上也是core-js)和regenerator-runtime组成的(是不是感觉和@babel-polyfill很像?)。
babel-runtime 通常和 plugin-transform-runtime一起使用。
假设有这样一段代码:
class Circle {}
被转化成了
function _classCallCheck(instance, Constructor) {
//...
}
var Circle = function Circle() {
_classCallCheck(this, Circle);
};
当我们有多个文件都使用了这样的语法时,也就意味着我们在每个文件中都会重复申明_classCallCheck
函数。
使用了@babel/plugin-transform-runtime插件之后,将会从@babel/runtime中引入_classCallCheck
,避免重发引入(类似于我们平时抽出公共方法)
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() {
_classCallCheck(this, Circle);
};
polyfill vs babel-runtime?
细心的你也许发现了 ployfill 和 babel-runtime 都使用了corejs和 regenerator-runtime,有什么区别呢?
如果你直接导入core-js或@babel/polyfill以及它提供的内置组件,如Promise、Set和Map,这些会污染全局作用域。虽然这对于应用程序或命令行工具来说没有问题,但如果你的代码是一个库,你打算发布给别人使用,或者你不能完全控制代码运行的环境,那么这就成了一个问题。
所以官方推荐babel-polyfill在独立的业务开发中使用,即使全局和原型被污染也没有太大的影响;而babel-runtime适合用于第三方库的开发,不会污染全局。