包阅导读总结
1. 关键词:Babel、ECMAScript、微内核架构、转译、标准化
2. 总结:
本文介绍了 Babel 的相关知识,包括其简介、微内核架构、转译过程、模块分类,还讲述了 ECMAScript 标准化,如发展历程、标准制定组织等,并强调了 Babel 设计思路和标准化思想的借鉴意义。
3. 主要内容:
– Babel 简介
– Babel 是什么及转译过程
– 转译与编译的区别
– Babel 转译的处理
– Babel 的使用方式
– Babel 运行的生命周期
– Babel 微内核架构
– 微内核架构特点
– 模块分类
– 转译模块
– 插件模块
– 工具模块
– 运行时相关模块
– 标准化
– ECMAScript 发展历程
– 相关标准化组织
– ECMAScript 标准制作过程及版本
– 如何阅读 ECMAScript
– 总结
– Babel 重要性变化及借鉴意义
– 分析思路对当前 Babel 设计的适用性
思维导图:
文章地址:https://mp.weixin.qq.com/s/cyOyB9-Q5Gz2xhQd0Kkwpg
文章来源:mp.weixin.qq.com
作者:hoperyy
发布时间:2024/9/4 10:46
语言:中文
总字数:7910字
预计阅读时间:32分钟
评分:89分
标签:Babel,ECMAScript,微内核架构,JavaScript 转译,插件机制
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
一、Babel简介
1.Babel是什么
2.转译过程
二、Babel微内核架构
1.微内核架构
2.转译模块
3. 插件模块
4. 工具模块
5.运行时相关模块
三、标准化
1.ECMAScript
2.如何阅读 ECMAScript
3.web标准
四、总结
随着浏览器版本的持续更新,浏览器对JavaScript的支持越来越强大,Babel的重要性显得较低了。但Babel的设计思路、背后依赖的ECMAScript标准化思想仍然值得借鉴。
本文涉及的Babel版本主要是V7.16及以下,截至发文时,Babel最新发布的版本是V7.25.6,未出现大版本更新,近2年也进入了稳定迭代期,本文的分析思路基本适用目前的Babel设计。
Babel是JavaScript转译器,通过Babel,开发者可以自由使用下一代ECMAScript 语法。高版本ECMAScript语法将被转译为低版本语法,以便顺利运行在各类环境,如低版本浏览器、低版本 Node.js 等。
Babel 是转译器,不是编译器。下面是转译和编译的区别:
编译,一般指将一种语言转换为另一种语法和抽象程度等都不同的语言,常见的比如 gcc 编译器。
转译,一般指将一种语言转换为不同版本或者抽象程度相同的语言,比如 Babel 可以把 ECMAScript 6 语法转译为 ECMAScript 5语法。
利用 Babel,开发者可以使用 ECMAScript 的各种新特性进行开发,同时花极少的精力关注浏览器或其他JS运行环境对新特性的支持。甚至,开发者可以根据自身需要,创造属于自己的 JavaScript 语法。
Babel在转译的时候,会对源码进行以下处理: 语法转译(Syntax)和添加API Polyfill。
-
API Polyfill
有些运行时相关的 API,语法转译无法解决它们对低版本浏览器等环境的兼容性问题,因此 Babel 通过与 core-js 等工具的配合,实现 API 部分对目标环境(通常是低版本浏览器等)的兼容。
例如
[1, 2, 3].include
、Promise
等 API,Babel 在处理时,如果目标环节可能不支持原生的include / Promise
的话,Babel 会在转译结果中嵌入include / Promise
的自定义实现。有多种方式可以使用 Babel,如: 命令行(babel-cli、babel-node)、浏览器(babel-standalone)、API 调用(babel-core)、webpack loader(babel-loader)等。
和多数转译器相同,Babel 运行的生命周期主要是 3 个阶段: 解析、转换、代码生成。
这个过程涉及抽象语法树:
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。
AST 是树形对象,以结构化的形式表示编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
源码字符串需要经转译器生成 AST,转译器有很多种,不同转译器,生成的AST对象格式细节可能有差异,但共同点为: 都是树形对象、该树形对象描述了节点特征、各节点之间的关系(兄弟、父子等)。
以下是 Babel 生命周期的三个过程:
-
解析(Parsing): Code1 ==> 抽象语法树1
解析过程包括 2 个环节: 词法解析、语法解析,最终生成抽象语法树。
词法解析阶段,代码字符串被解析为 token 令牌数组,数组项是一个对象,包括: 代码字符碎片的值、位置、类型等信息。
token 数组是平铺式的数组,没有嵌套的结构信息,它是为语法解析阶段做准备的。
语法解析阶段,token 令牌数组被解析为结构化的抽放语法树对象(AST)。
babel-parser 完成该阶段的主要功能。
-
代码生成(Generation): AST2 ==> Code2
Babel 将修改后的 AST 对象转目标代码字符串。
babel-generator 完成该阶段的主要功能。
Babel 采用微内核架构,其内核保留核心功能,其余功能利用外部工具和插件机制实现,也体现了”开放-封闭”的设计原则。
除了微内核设计架构,Babel 的模块设计也可以做如下分类:
转译模块位于 Babel 微内核架构的”微内核”部分,该部分主要负责代码转译,也就是上面提到的”解析-转换-代码生成”过程。
该模块主要包括: babel-parser、babel-traverse、babel-generator。
-
babel-parser
负责将代码字符串转为 AST 对象。
有 2 点值得一提:
插件模块包括 plugins、presets。
-
plugins
丰富的插件,帮助 Babel 成为一个非常成功的转译工具。
对 AST 的遍历、转换是 Babel 转译的核心功能,但 Babel 本身并不参与该过程,将这些功能作为插件引入到运行时。
具体来说,babel-core 作为核心工具,不提供对 AST 的修改逻辑,通过调用各类插件,实现对 AST 的修改。
Babel的插件分为语法插件和转换插件。
-
babel-plugin-syntax-decorators
负责开启 babel-parser 对装饰器的语法支持。
-
babel-plugin-syntax-dynamic-import
负责开启 babel-parser 对
import
语句的语法支持。 -
babel-plugin-syntax-jsx
负责开启 babel-parser 对 jsx 语法的支持。
-
转换插件
转换插件就是社区里常说的 Babel 插件,负责转换 AST 节点。
在介绍babel-traverse时提到,它负责遍历AST对象,每个AST节点会被访问到,等待转换,转换的部分,由”转换插件”负责。
转换插件会提供一个叫做”Visitor”的对象,该对象的 Key 为节点名称,Value 部分提供进入该节点时、离开该节点时的回调函数,在回调函数里,可以对该节点进行一系列操作。
“Visitor” 又称为 “访问者”。
值得注意的是,babel-parser 负责将 JavaScript 代码解析出抽象语法树(AST),它支持全面识别 ESNext/TypeScript/JSX/Flow 等语法,目前由 Babel 团队开发维护,不支持插件化。
Babel 插件生态中的语法插件,其功能就是作为”开关”,配置是否开启 babel-parser 的某些语法转译功能。
语法插件在 Babel 源码中,以 babel-plugin-syntax
开头。
举个例子:
const visitor = {
Program: {
enter() {},
exit() {},
},
CallExpression: {
enter() {},
exit() {},
},
NumberLiteral: {
enter() {},
exit() {},
}
};
traverse(ast, visitor);
babel-plugin-transform
开头。该插件拦截 Program
节点,也就是整个程序的根节点,添加 "use strict"
指令。
visitor 节点值为函数时,是 enter 回调的快捷方式。
{
name: "transform-strict-mode",
visitor: {
Program(path) {
const { node } = path;
for (const directive of node.directives) {
if (directive.value.value === "use strict") return;
}
path.unshiftContainer(
"directives",
t.directive(t.directiveLiteral("use strict")),
);
},
},
};
}
该插件负责拦截函数调用表达式节点 CallExpression
,将 Object.assign
转为 extends
写法。
{
name: "transform-object-assign",
visitor: {
CallExpression(path, file) {
if (path.get("callee").matchesPattern("Object.assign")) {
path.node.callee = file.addHelper("extends");
}
},
},
}
-
需要支持哪些特性,就分别引入支持该特性的插件
-
直接引入一个插件集合,涵盖所需的各类插件功能
很显然,第一种做法是相对麻烦的。针对第二种做法,Babel提供了插件集 preset。
preset 在 Babel 源码中,以 babel-preset 开头。
例如,Babel 已经提供了几种常用的 preset 供开发者使用:
-
babel-preset-env
-
babel-preset-react
-
babel-preset-flow
-
babel-preset-typescript
-
plugins 在 presets之前运行
-
plugins 按照数组正序执行
-
presets 按照数组倒序执行
工具模块提供 Babel 相关模块所需的各类工具,以下一一简要介绍:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test babel-standalone</title>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const arr = [1, 2, 3];
console.log(...arr);
</script>
</head>
<body></body>
</html>
在浏览器运行该 html,可以看到,页面结构变成了:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>test babel-standalone</title>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const arr = [1, 2, 3];
console.log(...arr);
</script>
<script>
"use strict";
var _console;
var arr = [1, 2, 3];
(_console = console).log.apply(_console, arr);
</script>
</head>
<body></body>
</html>
babel-register
,如 index.js:require('babel-register');
require('./run');
import fs from 'fs';
console.log(fs);
node index
时,run.js 就不需要被转码了。const t = require('@babel/types');
const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2));
import { smart as template } from '@babel/template';
import generate from '@babel/generator';
import * as t from '@babel/types';
const buildRequire = template( var %%importName%% = require(%%source%%); );
const ast = buildRequire({
importName: t.identifier('myModule'),
source: t.stringLiteral("my-module"),
});
const code = generate(ast).code
console.log(code)
运行结果:
var myModule = require("my-module");
const { codeFrameColumns } = require('@babel/code-frame');
const testCode = `
class Run {
constructor() {}
}
`;
const location = {
start: {
line: 2,
column: 2,
}
};
const result = codeFrameColumns(testCode, location);
console.log(result);
1 | class Run {
> 2 | constructor() {}
| ^
3 | }
4 |
Babel 配合其插件可以对静态代码进行转译,但有一些遗漏点:
为此,运行时模块(runtime)关注的是转译产物的运行时环境,对运行时提供 API polyfill、代码优化等,该模块涉及几个子包:
接下来以案例解释 runtime 模块的作用。
源码文件 index.js 的内容:
const a = 1;
class Base {}
new Promise()
这段源码包含了语法和 API 部分:
-
const
、class
为语法部分 -
Promise
为 API 部分
如果希望这段源码转为 ES5 版本,使构建产物可以运行在不支持 ES6 和 Promise 的环境里,该怎么做呢?
用 babel 命令行执行转译,其中源文件为 index.js,转译产物文件为 index-compiled.js。
npx babel index.js --out-file index-compiled.js
需要配置.bab
elrc
帮助 Babel 完成语法和 API 部分的转译:
.babelrc
:
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
简要解释下该配置的原理:
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var a = 1;
var Base = function Base() {
_classCallCheck(this, Base);
};
new Promise();
这样的后果就是构建产物比较臃肿。
Babel 转译过程的运行时优化是一个繁琐的过程,为此将单独用一章讲解运行时优化,感兴趣的同学可以直接阅读 “Babel Runtime” 章节详细了解。
Babel 生态涉及的一些标准化组织。无论是 JavaScript、HTML、DOM、URL 等领域,均需要统一的标准,才能在不同的运行环境下有统一的表现。Babel 转译也需要遵循这些标准,包括 ECMAScript、web标准等。
1995 年,JavaScript 的第一个版本发布。用时间线的方式描述 JavaScript 的诞生过程会更清晰:
1996 年,微软模仿 JavaScript 实现了 JScript 并内置在 IE3.0,随后,Netscape 公司着手推动 JavaScript 标准化。
这里涉及几个组织:
Ecma International 是一家国际性会员制度的信息和电信标准组织。1994年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名表明其国际性。
-
CD-ROM 格式(之后被国际标准化组织批准为ISO 9660)
-
C# 语言规范
-
C++/CLI 语言规范
-
通用语言架构(CLI)
-
Eiffel 语言
-
电子产品环境化设计要素
-
Universal 3D 标准
-
OOXML
-
Dart 语言规范
-
ECMAScript 语言规范(以 JavaScript 为基础)ECMA-262
其中就包括 JavaScript 标准语言规范 ECMAScript。cma International 拥有 ECMAScript 的商标。
-
ECMA TC39
ECMAScript 经历了多个版本,每个版本有自己的特点,简单列举如下:
一个 ECMAScript 标准的制作过程,包含了 Stage 0 到 Stage 4 共 5 个阶段,每个阶段提交至下一阶段都需要 TC39 审批通过。
特性进入 Stage-4 后,才有可能被加入标准中,还需要 ECMA General Assembly 表决通过才能进入下一次的 ECMAScript 标准中。
ECMAScript 的规格,可以在 ECMA 国际标准组织的官方网站免费下载和在线阅读。
查看ECMAScript 不同版本的地址:https://ecma-international.org/publications-and-standards/standards/ecma-262/。
截至 2023年底,已发布的版本如下:
(https://262.ecma-international.org/5.1/index.html)
(https://262.ecma-international.org/6.0/index.html)
(https://262.ecma-international.org/7.0/index.html)
(https://262.ecma-international.org/8.0/index.html)
(https://262.ecma-international.org/9.0/index.html)
(https://262.ecma-international.org/10.0/index.html)
(https://262.ecma-international.org/11.0/index.html)
(https://262.ecma-international.org/12.0/index.html)
(https://262.ecma-international.org/13.0/index.html)
(https://262.ecma-international.org/14.0/index.html)
每个版本有独立的网址,格式为: https://262.ecma-international.org/{version}/,比如 ECMAScript 14.0 版本的网址为 https://262.ecma-international.org/14.0/。
从章节数量上,ECMAScript 6.0
、ECMAScript 7.0
有 26 章,之后的版本有 27-29 章,虽然章节数量不同,规格章节的分布是保持一定规律的,以 ECMAScript 11.0
版本为例:
一般而言,除非写编译器,开发者无需阅读 ECMAScript 的规格,规格的内容非常多,如无必要也无需通读。只是在遇到一些奇怪的问题时,阅读官方规格,是最稳妥的办法。
通过阅读规格解决一些问题
(以ECMAScript 11.0为例)
class LoaderError extends Error {
constructor(err) {
super();
const { name, message, codeFrame, hideStack } = format(err);
this.name = "BabelLoaderError";
this.message = ${name ? ${name}: ` : ""}${message}\n\n${codeFrame}\n`;
this.hideStack = hideStack;
Error.captureStackTrace(this, this.constructor);
}
}
name
、message
、hideStack
属性,那么,问题是,原生的 Error
类有哪些属性和方法,哪些是开发者可以自定义的呢?Error
的各类规范:-
Error
作为函数被调用时(Error(...)
),表现和new Error(...)
一致,均会创建并返回Error
的新实例 -
Error
可以被继承,比如通过extends
的方式,子类必须提供constructor
方法,且该方法内必须提供super()
调用 -
Error
构造函数必须有prototype
属性
-
Error.prototype.constructor
: 指向构造函数 -
Error.prototype.message
: 描述错误信息,默认是空字符串 -
Error.prototype.name
: 描述错误名称,默认值是 Error
从 LoaderError
的源码可以看到,LoaderError
做了以下几件事情:
是在解决 API Polyfil 的时候,Babel 配合使用的 core-js 除了提供 ECMAScript 标准下的 JavaScript API 实现,也提供了 DOM/URL 等实现。而 DOM/URL 所属的 web 标准,由 W3C/WHATWG 制定。
经过多年发展,WHATWG 和 W3C 目前是合作关系,其中,WHATWG 维护 HTML 和 DOM 标准,W3C 使用 WHATWG 存储库中的 HTML 和 DOM 标准描述,W3C 在 HTML 部分的工作集中在 XHTML/XML 上。
本文介绍了 Babel 的概述/微内核架构/ECMAScript标准化方面的设计思想和部分实现原理。
上述内容其实在很早之前就已经成型了,笔者也查看了Babel最近的迭代内容,发现并没有太大的变化。至于代码转译领域,目前是Babel还是其他工具哪个更有优势,不在本文的讨论范围内。除了比较社区哪些工具更好而言,“Babel的设计思路、其与标准规范是怎么配合的”等也是很值得学习的地方,也是这篇文章的产生背景。
希望本文对你有所帮助!
文 /hoperyy
关注得物技术,每周一、三更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: