隨著瀏覽器版本的持續更新,瀏覽器對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。
圖片
箭頭函數 () => {} 轉為普通函數 function() {}。
const / let 轉譯為var
和多數轉譯器相同,Babel 運行的生命周期主要是 3 個階段: 解析、轉換、代碼生成。
這個過程涉及抽象語法樹:
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。
AST 是樹形對象,以結構化的形式表示編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。
圖片
源碼字符串需要經轉譯器生成 AST,轉譯器有很多種,不同轉譯器,生成的AST對象格式細節可能有差異,但共同點為: 都是樹形對象、該樹形對象描述了節點特征、各節點之間的關系(兄弟、父子等)。
以下是 Babel 生命周期的三個過程:
圖片
圖片
圖片
Babel 采用微內核架構,其內核保留核心功能,其余功能利用外部工具和插件機制實現,也體現了"開放-封閉"的設計原則。
圖片
除了微內核設計架構,Babel 的模塊設計也可以做如下分類:
圖片
轉譯模塊位于 Babel 微內核架構的"微內核"部分,該部分主要負責代碼轉譯,也就是上面提到的"解析-轉換-代碼生成"過程。
該模塊主要包括: babel-parser、babel-traverse、babel-generator。
babel-parser 本身并不會對 AST 做轉換操作,只是負責解析出 AST。AST 轉換部分交由各類 plugins 和 presets 處理。
babel-parser 內置了對 ESNext/TypeScript/JSX/Flow 最新版本語法的支持,但很多默認是不開啟的,目前沒有開放插件機制擴展新語法。
插件模塊包括 plugins、presets。
語法插件
值得注意的是,babel-parser 負責將 JavaScript 代碼解析出抽象語法樹(AST),它支持全面識別 ESNext/TypeScript/JSX/Flow 等語法,目前由 Babel 團隊開發維護,不支持插件化。
Babel 插件生態中的語法插件,其功能就是作為"開關",配置是否開啟 babel-parser 的某些語法轉譯功能。
語法插件在 Babel 源碼中,以 babel-plugin-syntax 開頭。
舉個例子:
babel-plugin-syntax-decorators負責開啟 babel-parser 對裝飾器的語法支持。
babel-plugin-syntax-dynamic-import負責開啟 babel-parser 對 import 語句的語法支持。
babel-plugin-syntax-jsx負責開啟 babel-parser 對 jsx 語法的支持。
// plugin 提供 visitor,在 visitor 中對 AST 節點操作const visitor = { Program: { enter() {}, exit() {}, }, CallExpression: { enter() {}, exit() {}, }, NumberLiteral: { enter() {}, exit() {}, }};traverse(ast, visitor);
轉換插件在Babel源碼中,以 babel-plugin-transform 開頭。
舉個例子:
babel-plugin-transform-strict-mode
該插件攔截 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); //# 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();
這樣的后果就是構建產物比較臃腫。
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var a = 1; var Base = function Base() { (0, _classCallCheck2["default"])(this, Base); }; new Promise();
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck")); var a = 1; var Base = function Base() { (0, _classCallCheck2["default"])(this, Base); }; new _promise"default";
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)。因為計算機的國際化,組織的標準牽涉到很多其他國家,因此組織決定改名表明其國際性。
Ecma International 的任務包括與有關組織合作開發通信技術和消費電子標準、鼓勵準確的標準落實、和標準文件與相關技術報告的出版。
Ecma International 負責多個國際標準的制定:
「TC39」全稱「Technical Committee 39」譯為「第 39 號技術委員會」,是 Ecma International 組織架構中的一部分。
TC39 負責迭代和發展 ECMAScript,它的成員由各個主流瀏覽器廠商的代表組成,通常每年召開約 6 次會議來討論未決提案的進展情況,會議的每一項決議必須得到大部分人的贊同,并且沒有人強烈反對才可以通過。
TC39 負責:
維護和更新 ECMAScript 語言標準
識別、開發、維護 ECMAScript 的擴展功能庫
開發測試套件
為 ISO/IEC JTC 1 提供標準
評估和考慮新添加的標準
國際標準化組織(英語: 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 標準的制作過程,包含了 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 版本為例:
該章節簡要描述了: JavaScript 和 ECMAScript 的發展歷史、不同 ECMAScript 規格的主要更新內容。
第 1 章用一句話描述了該規格的描述范圍。
第 2 章描述了基于規格的"實現"的一致性要求:
"實現"必須支持規格中描述的所有類型、值、對象、屬性、函數以及程序的語法和語義
"實現"必須按照 Unicode 標準和 ISO/IEC 10646 的最新版本處理文本輸入
"實現"如果提供了應用程序編程接口(API),那么該 API 需要適應不同的人文語言和國家差異,且必須實現最新版本的 ECMA-402 所定義的與本規范相兼容的接口
"實現"可以支持該規格中沒有提及的類型、值、對象、屬性、函數、正則表達式語法以及其他編程寫法
"實現"不能實現該規格中禁止的寫法
一般而言,除非寫編譯器,開發者無需閱讀 ECMAScript 的規格,規格的內容非常多,如無必要也無需通讀。只是在遇到一些奇怪的問題時,閱讀官方規格,是最穩妥的辦法。
通過閱讀規格解決一些問題
(以ECMAScript 11.0為例)
Babel 工具集中的 babel-highlight,可以實現在終端對代碼塊中的目標字符單元顯示不同的顏色。這里需要識別不同字符單元的類型,如關鍵字、保留字、標識符、數字、字符串等。
標識符、數字、字符串都很好理解和識別,但哪些字符應該被識別為關鍵字、保留字,而不是標識符呢?
此時可以閱讀 ECMAScript 規格了,ECMAScript 11.0 規格的 11.6.2 節介紹了關鍵詞和保留字列表。
關鍵詞(keywords)關鍵詞首先是標識符,同時有語義,包括 if、while、async、await...,個別關鍵詞是可以用作變量名的。
保留字(reserved word)保留字首先是標識符,但不能用作變量名。部分關鍵詞是保留字,但部分不是: if、while是保留字;await只有在 async方法和模塊中才是保留字;async不是保留字,它可以作為普通的變量名使用。
保留字列表
awaitbreakcasecatchclassconstcontinuedebuggerdefaultdeletedoelseenumexportextendsfalsefinallyforfunctionifimportininstanceofnewnullreturnsuperswitchthisthrowtruetrytypeofvarvoidwhilewithyield
讀完上述規格,也就知道哪些字符單元是需要識別為保留字與關鍵詞,并高亮的了。
繼續使用 babel-highlight 實現代碼塊中的全局對象高亮,那么,我們需要知道哪些是規格中描述的全局變量。
規格的 18 章介紹了全局對象,通過該章的描述,可以知道:
全局屬性全局屬性有: globalThis、Infinity、NaN、undefined。
全局方法
全局方法有: eval(x)、isFinite、isNaN、parseFloat、parseInt、decodeURIComponent、encodeURIComponent 等。
全局的構造函數有:
ArrayArrayBufferBigIntBigInt64ArrayBigUnit64ArrayBooleanDataViewDateErrorEvalErrorFloat32ArrayFloat64ArrayFunctionInt8ArrayInt16ArrayInt32ArrayMapNumberObjectPromiseProxyRangeErrorReferenceErrorRegExpSetSharedArrayBufferStringSymbolSyntaxErrorTypeErrorUint8ArrayUint8ClampedArrayUint16ArrayUint32ArrayURIErrorWeakMapWeakSet
其他的全局屬性Atomics、JSON、Math、Reflect。很顯然,當字符單元的名稱是上述名稱中的一員時,我們可以對其進行高亮處理了(若上下文中無重新定義的同名變量)。
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 自定義了錯誤實例的 name、message、hideStack 屬性,那么,問題是,原生的 Error 類有哪些屬性和方法,哪些是開發者可以自定義的呢?
規格的 19.5 章節,詳細介紹了 Error 的各類規范:
Error 作為函數被調用時(Error(...)),表現和 new Error(...) 一致,均會創建并返回 Error 的新實例
Error 可以被繼承,比如通過 extends 的方式,子類必須提供 constructor 方法,且該方法內必須提供 super() 調用
Error 構造函數必須有 prototype 屬性
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的設計思路、其與標準規范是怎么配合的”等也是很值得學習的地方,也是這篇文章的產生背景。
本文鏈接:http://www.www897cc.com/showinfo-26-112790-0.html深入理解 Babel - 微內核架構與 ECMAScript 標準化
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com