Babel 是一款將將來的 JavaScript 語法編譯成過去語法的 Node.js 工具。本文從 2019 年 11 月的 master 分支源碼入手,介紹 Babel 在解決這類問題時是如何劃分模塊。html
其中 babel-loader 隸屬於 webpack,不在 Babel 主倉庫。node
常見的解析器有 acorn、@babel/parser (babylon)、flow、traceur、typescript、uglify-js 等,各自的 AST 語法樹大體相同。webpack
let c = 0; while (a < 10) { const b = a % 2; if (b == 0) { c++; } } console.log(c);
上面的這段代碼經過 @babel/parser 解析後獲得的 AST 語法樹以下:c++
大部分模塊代碼量在百行左右,其中 StatementParser、ExpressionParser 和 Tokenizer 有較多複雜邏輯。git
提供遍歷 AST 語法樹的能力,如:github
traverse(ast, { FunctionDeclaration: function(path) { path.node.id.name = "x"; } }); traverse(ast, { enter(path) { if (path.isIdentifier({ name: "n" })) { path.node.name = "x"; } } });
path
對象上有下面的屬性和方法:web
屬性typescript
方法數組
將 AST 轉爲代碼文本。示例用法:瀏覽器
import { parse } from '@babel/parser'; import generate from '@babel/generator'; const ast = parse('class Example {}'); generate(ast); // => { code: 'class Example {}' }
能夠生成 source map。
import { parse } from '@babel/parser'; import generate from '@babel/generator'; const code = 'class Example {}'; const ast = parse(code); const output = generate(ast, { sourceMaps: true, sourceFileName: code }); // => { code: 'class Example {}', rawMappings: ... } // or const output = generate(ast, { sourceMaps: true, sourceFileName: 'source.js' }, code); // => { code: 'class Example {}', rawMappings: ... }
還能夠合併多個文件,同時生成 source map。
import { parse } from '@babel/parser'; import generate from '@babel/generator'; const a = 'var a = 1;'; const b = 'var b = 2;'; const astA = parse(a, { sourceFilename: 'a.js' }); const astB = parse(b, { sourceFilename: 'b.js' }); const ast = { type: 'Program', body: [...astA.program.body, ...astB.program.body] }; const { code, map } = generate(ast, { sourceMaps: true }, { 'a.js': a, 'b.js': b });
主要提供 transform 和 parse 相關的 API。
transform 的流程主要是 parse -> traverse -> generate。
parse 主要提供對 @babel/parser 的封裝。
經過插件開關打卡語法解析能力。@babel/parser 中判斷了 plugin 開關,實現了這些語法解析能力。如 @babel/plugin-syntax-jsx:
parserOpts.plugins.push("jsx");
實現語法的轉換。如 @babel/plugin-transform-exponentiation-operator:
export default { name: "transform-exponentiation-operator", visitor: build({ operator: "**", build(left, right) { return t.callExpression( t.memberExpression(t.identifier("Math"), t.identifier("pow")), [left, right], ); }, }), }
支持草案級別的語法轉換。如 @babel/plugin-proposal-numeric-separator:
export default { name: "proposal-numeric-separator", inherits: syntaxNumericSeparator, visitor: { CallExpression: replaceNumberArg, NewExpression: replaceNumberArg, NumericLiteral({ node }) { const { extra } = node; if (extra && /_/.test(extra.raw)) { extra.raw = extra.raw.replace(/_/g, ""); } }, }, }
提供各種組合好的 plugins、syntax 和 helpers。
經常使用的是 @babel/preset-env,結合 browserslist 設置代碼的兼容性。
從 Babel 7.4.0 起廢棄,推薦使用 core-js 和 regenerator-runtime。其中 core-js 提供了 ECMAScript 的全部兼容代碼,regenerator-runtime 提供了 async、generator 等函數的執行環境。
定義了 Babel 運行環境的輔助函數。如在 class 模塊前插入 classCallCheck 的 helper。
plugin 內的函數調用方式:
export default { visitor: { ClassExpression(path) { this.addHelper("classCallCheck"); // ... } };
生成的代碼中將包含 classCallCheck:
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Circle = function Circle() { _classCallCheck(this, Circle); };
提供 Babel 的運行環境,包括 regenerator-runtime。運行環境會提供一些輔助代碼,如:
使用 @babel/helpers 的狀況:
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Circle = function Circle() { _classCallCheck(this, Circle); };
使用 @babel/plugin-transform-runtime 能夠把這些代碼複用起來:
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck"); var Circle = function Circle() { _classCallCheck(this, Circle); };
@babel/runtime 源碼中沒有內容,依賴構建腳本將 @babel/helpers 中的代碼複製過去。
除了這個運行環境以外,Babel 還提供了 @babel/runtime-corejs2 和 @babel/runtime-corejs3,分別是基於 core-js v2 和 v3 提供的運行環境。能夠在 @babel/plugin-transform-runtime 的 corejs 參數中設置使用的運行環境。
提供基礎的類型值,建立類型的函數,便於 @babel/plugin、@babel/parser 等使用。
const binaryExpression = t.binaryExpression('+', t.numericLiteral(1), t.numericLiteral(2))
打印出錯位置。示例代碼:
import { codeFrameColumns } from '@babel/code-frame'; const rawLines = `class Foo { constructor() }`; const location = { start: { line: 2, column: 16 } }; codeFrameColumns(rawLines, location);
輸出的結果是:
1 | class Foo { > 2 | constructor() | ^ 3 | }
面向控制檯輸出有顏色的代碼片斷。
import highlight from "@babel/highlight"; const code = `class Foo { constructor() }`; highlight(code); // => "\u001b[36mclass\u001b[39m \u001b[33mFoo\u001b[39m {\n constructor()\n}"
展現在控制檯上:
模板引擎。
import 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"), }); generate(ast).code // => var myModule = require('my-module');
Babel 的輔助函數,包含經常使用操做、測試函數等,內容比較龐雜。
在命令行編譯。
babel script.js # 輸出編譯的結果
在瀏覽器編譯。如:Babel 官網等會用到。
<div id="input"></div> <div id="output"></div> <button id="transform">轉換</button> <!-- 加載 @babel/standalone --> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script> document.getElementById('transform').addEventListener('click', function() { const input = document.getElementById('input').value; const output = Babel.transform(input, { presets: ['es2015'] }).code; document.getElementById('output').value = output; }); </script>
@babel/standalone 也會自動編譯和執行 <script type="text/babel"></script>
和 <script type="text/jsx"></script>
中的代碼。
<div id="output"></div> <!-- 加載 @babel/standalone --> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <!-- ES2015 代碼會被編譯執行 --> <script type="text/babel"> const getMessage = () => "Hello World"; document.getElementById('output').innerHTML = getMessage(); </script>
提供在命令行執行高級語法的環境。@babel/cli 只轉換,不執行,@babel/node 會執行。不適合生產環境使用。
babel-node -e script.js # script.js 裏面可使用高級語法
提供在 Node.js 運行環境內編譯和執行高級語法。不適合生產環境使用。
require("@babel/register")(); require("./script.js"); // script.js 裏面可使用高級語法
Array.from
// input Array.from([1, 2, 3]) // output var _array_from_ = require('@babel/runtime-corejs3/core-js-stable/array/from'); _array_from_([1, 2, 3]);
// input <div className="text">{content}</div> // output React.createElement('div', { className: 'text' }, content);
class
// input class Example extends Component { constructor(props) { super(props) } } // output var _inherits_ = require('@babel/runtime-corejs3/helpers/interits'); var _class_call_check_ = require('@babel/runtime-corejs3/helpers/classCallCheck'); var _possible_constructor_return_ = require('@babel/runtime-corejs3/helpers/possibleConstructorReturn'); var _get_prototype_of_ = require('@babel/runtime-corejs3/helpers/getPrototypeOf'); var _create_class_ = require('@babel/runtime-corejs3/helpers/createClass'); var Example = function (_Component) { _inherits_(Example, _Component); function Example(props) { _class_call_check_(this, Example); return _possible_constructor_return_(this, _get_prototype_of_(Example).call(this, props)); } _create_class_(Example, []); return Example; }(Component);