我們直接創(chuàng)建一個(gè)工程陌僵,然后執(zhí)行:
npm install -D @babel/cli
我們用的是最新版本7.8.0
創(chuàng)建一個(gè)test1.js測(cè)試:
/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//測(cè)試插件1
var a=10;
創(chuàng)建一個(gè)babel配置文件.babelrc(先不寫任何配置):
/* .babelrc */
{
}
然后我們執(zhí)行:
npx babel test1.js -o test1.babel.js --config-file .babelrc
最后看一下結(jié)果test1.babel.js:
/* test.js */
const fn = () => {};
new Promise(() => {});
class Test {}
const c = [1, 2, 3].includes(1); //測(cè)試插件1
var a = 10;
哦笨蚁?為啥一點(diǎn)變化都沒有呢? 我們帶著疑問研究一下源碼~
為了更好的研究babel的源碼,我們直接去github clone一份:
git clone https://github.com/babel/babel.git
然后當(dāng)我們執(zhí)行:
npx babel test1.js -o test1.babel.js --config-file
的時(shí)候闰非,我們直接打開packages/babel-cli/bin/babel.js:
#!/usr/bin/env node
require("../lib/babel");
packages/babel-cli/src/babel/index.js:
#!/usr/bin/env node
import parseArgv from "./options";
import dirCommand from "./dir";
import fileCommand from "./file";
const opts = parseArgv(process.argv);
if (opts) {
const fn = opts.cliOptions.outDir ? dirCommand : fileCommand;
fn(opts).catch(err => {
console.error(err);
process.exitCode = 1;
});
} else {
process.exitCode = 2;
}
packages/babel-cli/src/babel/file.js:
export default async function({
cliOptions,
babelOptions,
}: CmdOptions): Promise<void> {
if (cliOptions.filenames.length) {
await files(cliOptions.filenames);
} else {
await stdin();
}
}
然后執(zhí)行files方法:
async function files(filenames: Array<string>): Promise<void> {
if (!cliOptions.skipInitialBuild) {
await walk(filenames);
}
然后執(zhí)行walk方法:
async function walk(filenames: Array<string>): Promise<void> {
try {
return await util.compile(
);
} catch (err) {
);
}
可以看到最后執(zhí)行了util的compile方法(packages/babel-cli/src/babel/util.js):
export function compile(
filename: string,
opts: Object | Function,
): Promise<Object> {
opts = {
...opts,
caller: CALLER,
};
return new Promise((resolve, reject) => {
babel.transformFile(filename, opts, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
可以看到剩盒,經(jīng)過babel-cli后,獲取我們傳入的參數(shù):
- 源文件test1.js
- 輸入文件test1.babel.js
- babel配置文件.babelrc
npx babel test1.js -o test1.babel.js --config-file .babelrc
然后通過babel-core的babel.transformFile方法后獲取編譯后的代碼台夺,最后babel-cli根據(jù)傳入的-o配置輸出最后編譯完成的代碼径玖。
所以我們重點(diǎn)研究一下babel.transformFile方法
packages/babel-core/src/index.js:
export {
transformFile
} from "./transform-file";
const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
//加載配置文件
const config: ResolvedConfig | null = yield* loadConfig(options);
if (config === null) return null;
//加載源文件
const code = yield* fs.readFile(filename, "utf8");
//開始編譯
return yield* run(config, code);
},
);
我們先說一下loadConfig方法,還記得我們傳入的.babelrc不颤介,讀取這個(gè)文件后然后獲取里面的presets跟plugins屬性梳星,presets是一組preset跟plugin、plugins是一組plugin滚朵,我們?cè)囍囊幌挛覀兊?babelrc配置文件:
/* .babelrc */
{
"presets": [
["@babel/preset-env", {
"modules": false,
"useBuiltIns": "usage",
"targets": "ie >= 8"
}]
],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs":false
}],
["./plugins/PluginTest1.js"]
]
}
然后執(zhí)行:
npx babel test1.js -o test1.babel.js --config-file .babelrc
結(jié)果:
import "core-js/modules/es7.array.includes";
import "core-js/modules/es6.string.includes";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import "core-js/modules/es6.promise";
/* test.js */
var fn = function fn() {};
new Promise(function () {});
var Test = function Test() {
_classCallCheck(this, Test);
};
var c = [1, 2, 3].includes(1); //測(cè)試插件1
var aaa = 10;
關(guān)于配置文件冤灾、prestes跟plugins我們之后慢慢介紹,我們繼續(xù)看一下babel-core是怎么加載我們的配置文件的辕近。
babel-core/src/config/full.js:
export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
inputOpts: mixed,
): Handler<ResolvedConfig | null> {
const result = yield* loadPrivatePartialConfig(inputOpts);
if (!result) {
return null;
}
const { options, context } = result;
const optionDefaults = {};
const passes = [[]];
try {
const { plugins, presets } = options;
}
可以看到我們獲取了配置文件的plugins跟presets屬性然后遍歷所有的preset跟plugin執(zhí)行preset跟plugin提供的方法:
import * as context from "../index";
const loadDescriptor = makeWeakCache(function*(
{ value, options, dirname, alias }: UnloadedDescriptor,
cache: CacheConfigurator<SimpleContext>,
): Handler<LoadedDescriptor> {
try {
const api = {
...context,
...makeAPI(cache),
};
item = value(api, options, dirname);
} catch (e) {
throw e;
}
}
api就是我們傳入的babel-core對(duì)象韵吨、options是我們傳入的參數(shù)、dirname是我們當(dāng)前文件夾目錄babel-test亏推。
我們先提前寫一個(gè)插件PluginTest1.js(把變量var a=10變成var aaa=10):
module.exports = function (api, options, dirname) {
let t = api.types;
console.log(options)
console.log(dirname)
return {
visitor: {
VariableDeclarator: {
enter(path,state) {
console.log(path)
if(path.node.id.name == 'a'){
path.node.id.name="aaa";
}
},
exit() {
console.log("Exited!");
}
}
}
}
};
插件我們之后再具體解析哈学赛,我們可以看到年堆,我們提供的插件對(duì)應(yīng)的參數(shù)也就是api、options盏浇、dirname:
module.exports = function (api, options, dirname) {
好啦变丧,說完配置文件我們繼續(xù)接著之前packages/babel-core/src/transform-file.js往下走:
const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
//加載配置文件
const config: ResolvedConfig | null = yield* loadConfig(options);
if (config === null) return null;
//加載源文件
const code = yield* fs.readFile(filename, "utf8");
//開始編譯
return yield* run(config, code);
},
);
可以看到,獲取完config后直接執(zhí)行了run方法
packages/babel-core/src/transformation/index.js:
config: ResolvedConfig,
code: string,
ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
const file = yield* normalizeFile(
config.passes,
normalizeOptions(config),
code,
ast,
);
const opts = file.opts;
try {
yield* transformFile(file, config.passes);
} catch (e) {
e.message = `${opts.filename ?? "unknown"}: ${e.message}`;
if (!e.code) {
e.code = "BABEL_TRANSFORM_ERROR";
}
throw e;
}
let outputCode, outputMap;
try {
if (opts.code !== false) {
({ outputCode, outputMap } = generateCode(config.passes, file));
}
} catch (e) {
e.message = `${opts.filename ?? "unknown"}: ${e.message}`;
if (!e.code) {
e.code = "BABEL_GENERATE_ERROR";
}
throw e;
}
return {
metadata: file.metadata,
options: opts,
ast: opts.ast === true ? file.ast : null,
code: outputCode === undefined ? null : outputCode,
map: outputMap === undefined ? null : outputMap,
sourceType: file.ast.program.sourceType,
};
}
代碼有點(diǎn)多绢掰,不要方痒蓬!我們一步一步的來
首先我們看到執(zhí)行了一個(gè)normalizeFile方法:
export function* run(
config: ResolvedConfig,
code: string,
ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
const file = yield* normalizeFile(
config.passes,
normalizeOptions(config),
code,
ast,
);
packages/babel-core/src/transformation/normalize-file.js:
export default function* normalizeFile(
pluginPasses: PluginPasses,
options: Object,
code: string,
ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<File> {
code = `${code || ""}`;
if (ast) {
if (ast.type === "Program") {
ast = t.file(ast, [], []);
} else if (ast.type !== "File") {
throw new Error("AST root must be a Program or File node");
}
ast = cloneDeep(ast);
} else {
ast = yield* parser(pluginPasses, options, code);
}
可以看到,如果我們傳入不是ast的話滴劲,就會(huì)通過parser方法去獲取一個(gè)ast(Abstract Syntax Tree)對(duì)象攻晒。
那么ast是什么呢?
在計(jì)算機(jī)科學(xué)中班挖,抽象語法樹(Abstract Syntax Tree鲁捏,AST),或簡(jiǎn)稱語法樹(Syntax tree)萧芙,是源代碼語法結(jié)構(gòu)的一種抽象表示给梅。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)双揪。
好吧动羽,到此babel的一個(gè)重量級(jí)選手parser登場(chǎng)了
parser是 Babel 的解析器。最初是 從Acorn項(xiàng)目fork出來的渔期。Acorn非吃讼牛快,易于使用疯趟,并且針對(duì)非標(biāo)準(zhǔn)特性(以及那些未來的標(biāo)準(zhǔn)特性) 設(shè)計(jì)了一個(gè)基于插件的架構(gòu)
通過babel的parse轉(zhuǎn)換后我們的代碼:
/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//測(cè)試插件1
var a=10;
就會(huì)被轉(zhuǎn)換成:
{
"type": "Program",
"start": 0,
"end": 120,
"body": [
{
"type": "VariableDeclaration",
"start": 14,
"end": 33,
"declarations": [
{
"type": "VariableDeclarator",
"start": 20,
"end": 33,
"id": {
"type": "Identifier",
"start": 20,
"end": 22,
"name": "fn"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 25,
"end": 33,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 31,
"end": 33,
"body": []
}
}
}
],
"kind": "const"
},
{
"type": "ExpressionStatement",
"start": 34,
"end": 55,
"expression": {
"type": "NewExpression",
"start": 34,
"end": 55,
"callee": {
"type": "Identifier",
"start": 38,
"end": 45,
"name": "Promise"
},
"arguments": [
{
"type": "ArrowFunctionExpression",
"start": 46,
"end": 54,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 52,
"end": 54,
"body": []
}
}
]
}
},
{
"type": "ClassDeclaration",
"start": 56,
"end": 69,
"id": {
"type": "Identifier",
"start": 62,
"end": 66,
"name": "Test"
},
"superClass": null,
"body": {
"type": "ClassBody",
"start": 67,
"end": 69,
"body": []
}
},
{
"type": "VariableDeclaration",
"start": 70,
"end": 101,
"declarations": [
{
"type": "VariableDeclarator",
"start": 76,
"end": 101,
"id": {
"type": "Identifier",
"start": 76,
"end": 77,
"name": "c"
},
"init": {
"type": "CallExpression",
"start": 80,
"end": 101,
"callee": {
"type": "MemberExpression",
"start": 80,
"end": 98,
"object": {
"type": "ArrayExpression",
"start": 80,
"end": 89,
"elements": [
{
"type": "Literal",
"start": 81,
"end": 82,
"value": 1,
"raw": "1"
},
{
"type": "Literal",
"start": 84,
"end": 85,
"value": 2,
"raw": "2"
},
{
"type": "Literal",
"start": 87,
"end": 88,
"value": 3,
"raw": "3"
}
]
},
"property": {
"type": "Identifier",
"start": 90,
"end": 98,
"name": "includes"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"start": 99,
"end": 100,
"value": 1,
"raw": "1"
}
]
}
}
],
"kind": "const"
},
{
"type": "VariableDeclaration",
"start": 110,
"end": 119,
"declarations": [
{
"type": "VariableDeclarator",
"start": 114,
"end": 118,
"id": {
"type": "Identifier",
"start": 114,
"end": 115,
"name": "a"
},
"init": {
"type": "Literal",
"start": 116,
"end": 118,
"value": 10,
"raw": "10"
}
}
],
"kind": "var"
}
],
"sourceType": "module"
}
小伙伴可以直接使用:在線版的ast轉(zhuǎn)換器
我們先簡(jiǎn)單的看一下parse方法:
export default function* parser(
pluginPasses: PluginPasses,
{ parserOpts, highlightCode = true, filename = "unknown" }: Object,
code: string,
): Handler<ParseResult> {
try {
const results = [];
for (const plugins of pluginPasses) {
for (const plugin of plugins) {
const { parserOverride } = plugin;
if (parserOverride) {
const ast = parserOverride(code, parserOpts, parse);
if (ast !== undefined) results.push(ast);
}
}
}
if (results.length === 0) {
return parse(code, parserOpts);
} else if (results.length === 1) {
yield* []; // If we want to allow async parsers
if (typeof results[0].then === "function") {
throw new Error(
`You appear to be using an async parser plugin, ` +
`which your current version of Babel does not support. ` +
`If you're using a published plugin, you may need to upgrade ` +
`your @babel/core version.`,
);
}
return results[0];
}
throw new Error("More than one plugin attempted to override parsing.");
} catch (err) {
if (err.code === "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED") {
err.message +=
"\nConsider renaming the file to '.mjs', or setting sourceType:module " +
"or sourceType:unambiguous in your Babel config for this file.";
// err.code will be changed to BABEL_PARSE_ERROR later.
}
const { loc, missingPlugin } = err;
if (loc) {
const codeFrame = codeFrameColumns(
code,
{
start: {
line: loc.line,
column: loc.column + 1,
},
},
{
highlightCode,
},
);
if (missingPlugin) {
err.message =
`${filename}: ` +
generateMissingPluginMessage(missingPlugin[0], loc, codeFrame);
} else {
err.message = `${filename}: ${err.message}\n\n` + codeFrame;
}
err.code = "BABEL_PARSE_ERROR";
}
throw err;
}
}
代碼還是很多拘哨,我們先提一下一個(gè)跟parse的插件有關(guān)的部分:
for (const plugins of pluginPasses) {
for (const plugin of plugins) {
const { parserOverride } = plugin;
if (parserOverride) {
const ast = parserOverride(code, parserOpts, parse);
if (ast !== undefined) results.push(ast);
}
}
所以當(dāng)我們插件中有提供parserOverride方法的時(shí)候就直接走我們插件的parserOverride去覆蓋babel/parser的解析了。
不懂也沒關(guān)系哈Q赴臁宅静!parse具體用法我們放到后面解析章蚣。
簡(jiǎn)單看完parse后站欺,我們繼續(xù)run方法
packages/babel-core/src/transformation/index.js:
export function* run(
config: ResolvedConfig,
code: string,
ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
//獲取ast對(duì)象
const file = yield* normalizeFile();
const opts = file.opts;
try {
//執(zhí)行轉(zhuǎn)換操作
yield* transformFile(file, config.passes);
} catch (e) {
}
可以看到繼續(xù)執(zhí)行了transformFile方法,然后把我們ast對(duì)象傳給了transformFile方法:
function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
for (const pluginPairs of pluginPasses) {
const passPairs = [];
const passes = [];
const visitors = [];
for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
const pass = new PluginPass(file, plugin.key, plugin.options);
passPairs.push([plugin, pass]);
passes.push(pass);
visitors.push(plugin.visitor);
}
for (const [plugin, pass] of passPairs) {
const fn = plugin.pre;
if (fn) {
const result = fn.call(pass, file);
yield* [];
if (isThenable(result)) {
throw new Error(
`You appear to be using an plugin with an async .pre, ` +
`which your current version of Babel does not support. ` +
`If you're using a published plugin, you may need to upgrade ` +
`your @babel/core version.`,
);
}
}
}
// merge all plugin visitors into a single visitor
const visitor = traverse.visitors.merge(
visitors,
passes,
file.opts.wrapPluginVisitorMethod,
);
traverse(file.ast, visitor, file.scope);
for (const [plugin, pass] of passPairs) {
const fn = plugin.post;
if (fn) {
const result = fn.call(pass, file);
yield* [];
if (isThenable(result)) {
throw new Error(
`You appear to be using an plugin with an async .post, ` +
`which your current version of Babel does not support. ` +
`If you're using a published plugin, you may need to upgrade ` +
`your @babel/core version.`,
);
}
}
}
}
}
還是那句話 “不要方O舜埂矾策!我們一步一步來看”,首先我們看到:
//遍歷所有的插件峭沦,獲取插件的visitor屬性贾虽,然后傳給visitors
for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
const pass = new PluginPass(file, plugin.key, plugin.options);
passPairs.push([plugin, pass]);
passes.push(pass);
visitors.push(plugin.visitor);
}
那么visitor是什么呢? 可以看到我們之前有提到一個(gè)自定義一個(gè)插件PluginTest1.js:
module.exports = function (api, options, dirname) {
let t = api.types;
console.log(options)
console.log(dirname)
return {
visitor: {
VariableDeclarator: {
enter(path,state) {
console.log(path)
if(path.node.id.name == 'a'){
path.node.id.name="aaa";
}
},
exit() {
console.log("Exited!");
}
}
}
}
};
先提前說一下吼鱼,visitor其實(shí)就是提供給之后的traverse使它能夠去訪問抽象語法樹ast對(duì)象
所以接下來就是babel的又一個(gè)重量級(jí)選手traverse登場(chǎng)了
Babel Traverse(遍歷)模塊維護(hù)了整棵樹的狀態(tài)蓬豁,并且負(fù)責(zé)替換绰咽、移除和添加節(jié)點(diǎn)。
再次回到run方法
packages/babel-core/src/transformation/index.js:
export function* run(
config: ResolvedConfig,
code: string,
ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
//獲取ast對(duì)象(parser)
const file = yield* normalizeFile();
try {
//(遍歷)模塊維護(hù)了整棵樹的狀態(tài)地粪,并且負(fù)責(zé)替換取募、移除和添加節(jié)點(diǎn)。
yield* transformFile(file, config.passes);
} catch (e) {
}
({ outputCode, outputMap } = generateCode(config.passes, file));
可以看到執(zhí)行了generateCode方法蟆技,這時(shí)babel的最后一個(gè)重量級(jí)選手babel-generator登場(chǎng)了
Babel Generator模塊是 Babel 的代碼生成器玩敏,它讀取AST并將其轉(zhuǎn)換為代碼和源碼映射(sourcemaps)。
最后run方法返回generator生成的代碼:
return {
metadata: file.metadata,
options: opts,
ast: opts.ast === true ? file.ast : null,
code: outputCode === undefined ? null : outputCode,
map: outputMap === undefined ? null : outputMap,
sourceType: file.ast.program.sourceType,
};
整個(gè)babel-cli到babel-core的過程隨著我們的demo跟我們的源碼就講完了质礼。
我們重新整理一下整個(gè)過程:
- babel-cli開始讀取我們的參數(shù)(源文件test1.js旺聚、輸出文件test1.babel.js、配置文件.babelrc)
- babel-core根據(jù)babel-cli的參數(shù)開始編譯
- Babel Parser 把我們傳入的源碼解析成ast對(duì)象
- Babel Traverse(遍歷)模塊維護(hù)了整棵樹的狀態(tài)眶蕉,并且負(fù)責(zé)替換砰粹、移除和添加節(jié)點(diǎn)(也就是結(jié)合我們傳入的插件把es6轉(zhuǎn)換成es5的一個(gè)過程)
- Babel Generator模塊是 Babel 的代碼生成器,它讀取AST并將其轉(zhuǎn)換為代碼和源碼映射(sourcemaps)造挽。
好啦伸眶,到此我們算是把babel的整個(gè)過程簡(jiǎn)單的跑了一下,為了加深對(duì)每個(gè)流程的理解刽宪,我們不經(jīng)過babel-core跟babel-cli單獨(dú)去用一下parser厘贼、traverse、generator圣拄。
//我們的es6源碼
const code = `
const result=a*b;
const result1=()=>{};
`;
const {parse}=require("@babel/parser");
const traverse =require("@babel/traverse").default;
const t = require("babel-types");
const generator = require("@babel/generator").default;
//把es6源碼通過parser轉(zhuǎn)換成ast對(duì)象
const ats=parse(code,{
sourceType: "module"
});
//把a(bǔ)st對(duì)象通過traverse轉(zhuǎn)換成es5代碼
traverse(ats,{
enter(path) {
if (t.isIdentifier(path.node, { name: "a" })) {
path.node.name = "aa";
}
if (path.isArrowFunctionExpression()){ //es6轉(zhuǎn)換成es5
path.arrowFunctionToExpression({
// While other utils may be fine inserting other arrows to make more transforms possible,
// the arrow transform itself absolutely cannot insert new arrow functions.
allowInsertArrow: false,
specCompliant: false,
});
}
}
});
//通過generator轉(zhuǎn)換ast最后輸出es5代碼
console.log(generator(ats));
我們運(yùn)行一下代碼:
$ node ./babel-test/demo/demo1.js
結(jié)果輸出:
{
code: 'const result = aa * b;\n\nconst result1 = function () {};',
map: null,
rawMappings: null
}
可以看到嘴秸,最終我們實(shí)現(xiàn)了把es6的箭頭函數(shù)轉(zhuǎn)換成es5的過程。
代碼中我們可以看到:
//把a(bǔ)st對(duì)象通過traverse轉(zhuǎn)換成es5代碼
traverse(ats,{
enter(path) {
if (t.isIdentifier(path.node, { name: "a" })) {
path.node.name = "aa";
}
if (path.isArrowFunctionExpression()){ //es6轉(zhuǎn)換成es5
path.arrowFunctionToExpression({
// While other utils may be fine inserting other arrows to make more transforms possible,
// the arrow transform itself absolutely cannot insert new arrow functions.
allowInsertArrow: false,
specCompliant: false,
});
}
}
});
我們打開一個(gè)官方的插件babel-plugin-transform-arrow-functions:
import { declare } from "@babel/helper-plugin-utils";
import type NodePath from "@babel/traverse";
export default declare((api, options) => {
api.assertVersion(7);
const { spec } = options;
return {
name: "transform-arrow-functions",
visitor: {
ArrowFunctionExpression(
path: NodePath<BabelNodeArrowFunctionExpression>,
) {
// In some conversion cases, it may have already been converted to a function while this callback
// was queued up.
if (!path.isArrowFunctionExpression()) return;
path.arrowFunctionToExpression({
// While other utils may be fine inserting other arrows to make more transforms possible,
// the arrow transform itself absolutely cannot insert new arrow functions.
allowInsertArrow: false,
specCompliant: !!spec,
});
},
},
};
});
哈哈1幼弧岳掐! 是不是很簡(jiǎn)單呢? 其實(shí)babel也就是把很多的一些插件組合起來最終實(shí)現(xiàn)代碼的轉(zhuǎn)換饭耳,好啦~ 接下來我們就圍繞babel的一些常用的插件做解析了
未完待續(xù)~~
歡迎志同道合的小伙伴一起學(xué)習(xí)串述、一起進(jìn)步
歡迎入群~~~~~~~
參考: