得物技术 2024年09月04日
深入理解 Babel - 微内核架构与 ECMAScript 标准化|得物技术
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

Babel是一款JavaScript转译器,它允许开发者使用最新的ECMAScript语法进行开发,无需担心浏览器或其他JS运行环境对新特性的支持问题。Babel可以将高版本ECMAScript语法转译为低版本语法,确保代码在各种环境中都能顺利运行。同时,开发者也可以根据自身需要,创建属于自己的JavaScript语法。

😊 **Babel简介** Babel作为一款JavaScript转译器,其核心功能是将高版本ECMAScript语法转译为低版本语法,例如将箭头函数 `() => {}` 转为普通函数 `function(){} `,以及将 `const/let` 转译为 `var`。除此之外,Babel还通过与 `core-js` 等工具配合,实现API部分对目标环境的兼容性,例如 `[1,2,3].include` 、`Promise` 等API,Babel会根据目标环境情况,在转译结果中嵌入自定义实现,确保API在低版本浏览器等环境中也能正常运行。 Babel的运行生命周期主要包括三个阶段:解析、转换、代码生成。解析阶段,代码字符串被解析为抽象语法树(AST),这个过程由 `babel-parser` 完成。转换阶段,Babel根据配置的插件和预设,对AST进行遍历和修改,这个过程由 `babel-traverse` 和插件/预设共同完成。代码生成阶段,Babel将修改后的AST对象转为目标代码字符串,这个过程由 `babel-generator` 完成。

😄 **Babel微内核架构** Babel采用了微内核架构,其核心功能保留在内核中,其他功能则通过外部工具和插件机制实现。Babel的模块设计可以分为转译模块、插件模块、工具模块和运行时相关模块。 转译模块是Babel的核心部分,主要负责代码转译,包括 `babel-parser` 、`babel-traverse` 和 `babel-generator` 。插件模块包含插件和预设,插件的功能更细粒度,而预设则提供了一组插件的集合,方便开发者快速配置。工具模块提供了Babel相关模块所需的工具,例如 `babel-core` 、`babel-cli` 、`babel-standalone` 、`babel-register` 、`babel-loader` 等。运行时相关模块则关注转译产物的运行时环境,例如提供API polyfill、代码优化等,该模块涉及 `babel-preset-env` 、`babel-plugin-transform-runtime` 、`babel-runtime` 等子包。

😉 **标准化** Babel生态涉及的一些标准化组织,包括ECMAScript和Web标准。ECMAScript是JavaScript的标准化语言规范,由EcmaInternational组织制定。Web标准则涵盖了HTML、DOM、URL等领域,确保不同运行环境下有统一的表现。Babel转译也需要遵循这些标准,才能确保代码在各种环境中都能正常运行。 Babel的转译过程是一个复杂的过程,它涉及到多种工具和模块,并且需要遵循相关的标准规范。Babel的设计思路和背后依赖的ECMAScript标准化思想,仍然值得开发者借鉴。

😎 **总结** 随着浏览器版本的持续更新,浏览器对JavaScript的支持越来越强大,Babel的重要性显得较低了。但Babel的设计思路、背后依赖的ECMAScript标准化思想仍然值得借鉴。

🤩 **BabelRuntime** Babel在转译过程中,除了语法转译之外,还需要处理运行时相关的API问题。为了解决这个问题,Babel提供了运行时模块,例如 `babel-preset-env` 、`babel-plugin-transform-runtime` 、`babel-runtime` 等子包。这些模块可以提供API polyfill,以及对转译产物的代码优化,从而确保代码在各种环境中都能正常运行。

🥳 **Babel插件生态** Babel拥有丰富的插件生态,开发者可以根据自己的需求选择合适的插件,例如语法插件、转换插件等。语法插件负责开启 `babel-parser` 对某些语法的支持,例如 `babel-plugin-syntax-decorators` 、`babel-plugin-syntax-dynamic-import` 等。转换插件则负责转换AST节点,例如 `babel-plugin-transform-strict-mode` 、`babel-plugin-transform-object-assign` 等。Babel插件生态的丰富性,是Babel成功的重要原因之一。

🤓 **Babel的未来** 随着JavaScript语言的不断发展,Babel将会继续扮演重要的角色,为开发者提供更强大的工具和功能,帮助开发者更高效地开发JavaScript应用。Babel的未来发展方向,包括对新语法的支持、对运行时环境的优化、对工具链的整合等。

🧐 **Babel的应用场景** Babel可以应用于各种场景,例如: - 开发需要使用最新ECMAScript语法的项目 - 将JavaScript代码转译为其他语言,例如TypeScript - 对JavaScript代码进行优化,例如压缩、混淆 - 在浏览器中运行JavaScript代码,例如使用 `babel-standalone`

😮 **Babel的优势** Babel的优势包括: - 支持最新的ECMAScript语法 - 能够将代码转译为多种版本 - 拥有丰富的插件生态 - 性能优异 - 使用简单方便 - 拥有庞大的社区支持

原创 hoperyy 2024-09-04 18:47 上海

利用 Babel,开发者可以使用 ECMAScript 的各种新特性进行开发,同时花极少的精力关注浏览器或其他JS运行环境对新特性的支持。甚至,开发者可以根据自身需要,创造属于自己的 JavaScript 语法。

目录

一、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简介

Babel是什么

Babel是JavaScript转译器,通过Babel,开发者可以自由使用下一代ECMAScript 语法。高版本ECMAScript语法将被转译为低版本语法,以便顺利运行在各类环境,如低版本浏览器、低版本 Node.js 等。


Babel 是转译器,不是编译器。下面是转译和编译的区别:


编译,一般指将一种语言转换为另一种语法和抽象程度等都不同的语言,常见的比如 gcc 编译器。


转译,一般指将一种语言转换为不同版本或者抽象程度相同的语言,比如 Babel 可以把 ECMAScript 6 语法转译为 ECMAScript 5语法。


利用 Babel,开发者可以使用 ECMAScript 的各种新特性进行开发,同时花极少的精力关注浏览器或其他JS运行环境对新特性的支持。甚至,开发者可以根据自身需要,创造属于自己的 JavaScript 语法。


Babel在转译的时候,会对源码进行以下处理: 语法转译(Syntax)和添加API Polyfill。




转译过程

和多数转译器相同,Babel 运行的生命周期主要是 3 个阶段: 解析、转换、代码生成。


这个过程涉及抽象语法树:


抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。


AST 是树形对象,以结构化的形式表示编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

源码字符串需要经转译器生成 AST,转译器有很多种,不同转译器,生成的AST对象格式细节可能有差异,但共同点为: 都是树形对象、该树形对象描述了节点特征、各节点之间的关系(兄弟、父子等)。


以下是 Babel 生命周期的三个过程:


Babel微内核架构

微内核架构

Babel 采用微内核架构,其内核保留核心功能,其余功能利用外部工具和插件机制实现,也体现了"开放-封闭"的设计原则。


除了微内核设计架构,Babel 的模块设计也可以做如下分类:


转译模块

转译模块位于 Babel 微内核架构的"微内核"部分,该部分主要负责代码转译,也就是上面提到的"解析-转换-代码生成"过程。


该模块主要包括: babel-parser、babel-traverse、babel-generator。





插件模块

插件模块包括 plugins、presets。



// plugin 提供 visitor,在 visitor 中对 AST 节点操作const visitor = {    Program: {        enter() {},        exit() {},    },
CallExpression: { enter() {}, exit() {}, },
NumberLiteral: { enter() {}, exit() {}, }};traverse(ast, visitor);

转换插件在Babel源码中,以 babel-plugin-transform 开头。


举个例子:


{    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 相关模块所需的各类工具,以下一一简要介绍:




<!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); //# sourceMappingURL=data:application/json;charset=utf-8;base64...</script>  </head>  <body></body></html>


提供在命令行执行高级语法的环境。


例如:

// index.js 里可以使用高级语法     babel-node -e index.js

index.js 文件以及被其引入的其他文件均可以使用高级语法了。和 babel-cli 不同的是,babel-cli 只负责转换,不在 node 运行时执行;babel-node 会在 node 运行时执行转换,不适合生产环境使用。



在源文件中,引入babel-register,如 index.js:


index.js

require('babel-register');     require('./run');

run.js

import fs from 'fs';     console.log(fs);

执行 node index 时,run.js 就不需要被转码了。


babel-register 通过拦截 node require 方法,为 node 运行时引入了 Babel 的转译能力。



babel-loader 是利用 babel-core 的 API 封装的 webpack loader,用于 webpack 构建过程。



babel-types 是一个非常强大的工具集合,它集成了节点校验、增删改查等功能,是 Babel 核心模块开发、插件开发等场景下不可或缺的工具。


例如:

const t = require('@babel/types');const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2));


模板引擎,负责将代码字符串转为 AST 节点对象。

    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 |


向控制台输出有颜色的代码片段。该工具可以识别 JavaScript 中的操作符号、标识符、保留字等类型的词法单元,并在终端环境下显示不同的颜色。


运行时相关模块

Babel 配合其插件可以对静态代码进行转译,但有一些遗漏点:



为此,运行时模块(runtime)关注的是转译产物的运行时环境,对运行时提供 API polyfill、代码优化等,该模块涉及几个子包:



接下来以案例解释 runtime 模块的作用。


源码文件 index.js 的内容:

const a = 1; // const 为语法部分class Base {} // class 为语法部分new Promise() // Promise 为 API 部分

这段源码包含了语法和 API 部分:



如果希望这段源码转为 ES5 版本,使构建产物可以运行在不支持 ES6 和 Promise 的环境里,该怎么做呢?


用 babel 命令行执行转译,其中源文件为 index.js,转译产物文件为 index-compiled.js。

npx babel index.js --out-file index-compiled.js

需要配置.babelrc 帮助 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标准等。


ECMAScript

JavaScript诞生

1995 年,JavaScript 的第一个版本发布。用时间线的方式描述 JavaScript 的诞生过程会更清晰: 


ECMAScript发布

1996 年,微软模仿 JavaScript 实现了 JScript 并内置在 IE3.0,随后,Netscape 公司着手推动 JavaScript 标准化。


这里涉及几个组织:



Ecma International 是一家国际性会员制度的信息和电信标准组织。1994年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名表明其国际性。


Ecma International 的任务包括与有关组织合作开发通信技术和消费电子标准、鼓励准确的标准落实、和标准文件与相关技术报告的出版。


Ecma International 负责多个国际标准的制定:



「TC39」全称「Technical Committee 39」译为「第 39 号技术委员会」,是 Ecma International 组织架构中的一部分。


TC39 负责迭代和发展 ECMAScript,它的成员由各个主流浏览器厂商的代表组成,通常每年召开约 6 次会议来讨论未决提案的进展情况,会议的每一项决议必须得到大部分人的赞同,并且没有人强烈反对才可以通过。


TC39 负责:




国际标准化组织(英语: International Organization for Standardization,简称: ISO)成立于 1947 年 2 月 23 日,制定全世界工商业国际标准的国际标准建立机构。


ISO 的国际标准以数字表示,例如: "ISO 11180:1993" 的 "11180" 是标准号码,而 "1993" 是出版年份。


ISO/IEC JTC 1 是国际标准化组织和国际电工委员会联合技术委员会。其目的是开发、维护和促进信息技术以及信息和通信技术领域的标准。JTC 1 负责了许多关键的 IT 标准,从 MPEG 视频格式到 C++ 编程语言。


ECMAScript 各版本

ECMAScript 经历了多个版本,每个版本有自己的特点,简单列举如下: 


ECMAScript 迭代过程

一个 ECMAScript 标准的制作过程,包含了 Stage 0 到 Stage 4 共 5 个阶段,每个阶段提交至下一阶段都需要 TC39 审批通过。

特性进入 Stage-4 后,才有可能被加入标准中,还需要 ECMA General Assembly 表决通过才能进入下一次的 ECMAScript 标准中。


如何阅读 ECMAScript

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.0ECMAScript 7.0 有 26 章,之后的版本有 27-29 章,虽然章节数量不同,规格章节的分布是保持一定规律的,以 ECMAScript 11.0 版本为例:



该章节简要描述了: JavaScript 和 ECMAScript 的发展历史、不同 ECMAScript 规格的主要更新内容。



一般而言,除非写编译器,开发者无需阅读 ECMAScript 的规格,规格的内容非常多,如无必要也无需通读。只是在遇到一些奇怪的问题时,阅读官方规格,是最稳妥的办法。


通过阅读规格解决一些问题

(以ECMAScript 11.0为例)


Babel 工具集中的 babel-highlight,可以实现在终端对代码块中的目标字符单元显示不同的颜色。这里需要识别不同字符单元的类型,如关键字、保留字、标识符、数字、字符串等。


标识符、数字、字符串都很好理解和识别,但哪些字符应该被识别为关键字、保留字,而不是标识符呢?


此时可以阅读 ECMAScript 规格了,ECMAScript 11.0 规格的 11.6.2 节介绍了关键词和保留字列表。



继续使用 babel-highlight 实现代码块中的全局对象高亮,那么,我们需要知道哪些是规格中描述的全局变量。


规格的 18 章介绍了全局对象,通过该章的描述,可以知道: 


ArrayArrayBufferBigIntBigInt64ArrayBigUnit64ArrayBooleanDataViewDateErrorEvalErrorFloat32ArrayFloat64ArrayFunctionInt8ArrayInt16ArrayInt32ArrayMapNumberObjectPromiseProxyRangeErrorReferenceErrorRegExpSetSharedArrayBufferStringSymbolSyntaxErrorTypeErrorUint8ArrayUint8ClampedArrayUint16ArrayUint32ArrayURIErrorWeakMapWeakSet



babel-loader 自身维护了私有的 LoaderError 对象,该对象继承自原生 Error 类,并且订制了部分实例属性。代码如下: 

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); }}

可以看到,babel-loader 自定义了错误实例的 namemessagehideStack 属性,那么,问题是,原生的 Error 类有哪些属性和方法,哪些是开发者可以自定义的呢?


规格的 19.5 章节,详细介绍了 Error 的各类规范:


    Error.prototype.constructor: 指向构造函数

    Error.prototype.message: 描述错误信息,默认是空字符串

    Error.prototype.name: 描述错误名称,默认值是 Error

LoaderError 的源码可以看到,LoaderError 做了以下几件事情:



web标准

是在解决 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的设计思路、其与标准规范是怎么配合的”等也是很值得学习的地方,也是这篇文章的产生背景。


希望本文对你有所帮助!


往期回顾


1. 得物App白屏优化系列|归因篇

2. 浅析JVM invokedynamic指令和Java Lambda语法|得物技术

3. 利用多Lora节省大模型部署成本|得物技术

4. 解密JVM崩溃(Crash):如何通过日志分析揭开神秘面纱|得物技术

5. 基于MySQL内核的SQL限流设计与实现|得物技术


文 / hoperyy

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:

线下活动推荐


快快点击下方图片报名吧!


跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Babel JavaScript ECMAScript 转译器 标准化 微内核架构 插件 运行时
相关文章