我们直接创建一个工程,然后执行:
npm install -D @babel/cli
我们用的是最新版本7.8.0
创建一个test1.js测试:
/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//测试插件1
var a=10;
创建一个babel配置文件.babelrc(先不写任何配置):
/* .babelrc */
{
}
然后我们执行:
npx babel test1.js -o test1.babel.js --config-file .babelrc
最后看一下结果test1.babel.js:
/* test.js */
const fn = () => {};
new Promise(() => {});
class Test {}
const c = [1, 2, 3].includes(1); //测试插件1
var a = 10;
哦?为啥一点变化都没有呢? 我们带着疑问研究一下源码~
为了更好的研究babel的源码,我们直接去github clone一份:
git clone https://github.com/babel/babel.git
然后当我们执行:
npx babel test1.js -o test1.babel.js --config-file
的时候,我们直接打开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();
}
}
然后执行files方法:
async function files(filenames: Array<string>): Promise<void> {
if (!cliOptions.skipInitialBuild) {
await walk(filenames);
}
然后执行walk方法:
async function walk(filenames: Array<string>): Promise<void> {
try {
return await util.compile(
);
} catch (err) {
);
}
可以看到最后执行了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);
});
});
}
可以看到,经过babel-cli后,获取我们传入的参数:
- 源文件test1.js
- 输入文件test1.babel.js
- babel配置文件.babelrc
npx babel test1.js -o test1.babel.js --config-file .babelrc
然后通过babel-core的babel.transformFile方法后获取编译后的代码,最后babel-cli根据传入的-o配置输出最后编译完成的代码。
所以我们重点研究一下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不,读取这个文件后然后获取里面的presets跟plugins属性,presets是一组preset跟plugin、plugins是一组plugin,我们试着改一下我们的.babelrc配置文件:
/* .babelrc */
{
"presets": [
["@babel/preset-env", {
"modules": false,
"useBuiltIns": "usage",
"targets": "ie >= 8"
}]
],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs":false
}],
["./plugins/PluginTest1.js"]
]
}
然后执行:
npx babel test1.js -o test1.babel.js --config-file .babelrc
结果:
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); //测试插件1
var aaa = 10;
关于配置文件、prestes跟plugins我们之后慢慢介绍,我们继续看一下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执行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对象、options是我们传入的参数、dirname是我们当前文件夹目录babel-test。
我们先提前写一个插件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!");
}
}
}
}
};
插件我们之后再具体解析哈,我们可以看到,我们提供的插件对应的参数也就是api、options、dirname:
module.exports = function (api, options, dirname) {
好啦,说完配置文件我们继续接着之前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后直接执行了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,
};
}
代码有点多,不要方!我们一步一步的来
首先我们看到执行了一个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的话,就会通过parser方法去获取一个ast(Abstract Syntax Tree)对象。
那么ast是什么呢?
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
好吧,到此babel的一个重量级选手parser登场了
parser是 Babel 的解析器。最初是 从Acorn项目fork出来的。Acorn非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构
通过babel的parse转换后我们的代码:
/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//测试插件1
var a=10;
就会被转换成:
{
"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转换器
我们先简单的看一下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;
}
}
代码还是很多,我们先提一下一个跟parse的插件有关的部分:
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);
}
}
所以当我们插件中有提供parserOverride方法的时候就直接走我们插件的parserOverride去覆盖babel/parser的解析了。
不懂也没关系哈!!parse具体用法我们放到后面解析。
简单看完parse后,我们继续run方法
packages/babel-core/src/transformation/index.js:
export function* run(
config: ResolvedConfig,
code: string,
ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
//获取ast对象
const file = yield* normalizeFile();
const opts = file.opts;
try {
//执行转换操作
yield* transformFile(file, config.passes);
} catch (e) {
}
可以看到继续执行了transformFile方法,然后把我们ast对象传给了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.`,
);
}
}
}
}
}
还是那句话 “不要方!!我们一步一步来看”,首先我们看到:
//遍历所有的插件,获取插件的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是什么呢? 可以看到我们之前有提到一个自定义一个插件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其实就是提供给之后的traverse使它能够去访问抽象语法树ast对象
所以接下来就是babel的又一个重量级选手traverse登场了
Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
再次回到run方法
packages/babel-core/src/transformation/index.js:
export function* run(
config: ResolvedConfig,
code: string,
ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
//获取ast对象(parser)
const file = yield* normalizeFile();
try {
//(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
yield* transformFile(file, config.passes);
} catch (e) {
}
({ outputCode, outputMap } = generateCode(config.passes, file));
可以看到执行了generateCode方法,这时babel的最后一个重量级选手babel-generator登场了
Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(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,
};
整个babel-cli到babel-core的过程随着我们的demo跟我们的源码就讲完了。
我们重新整理一下整个过程:
- babel-cli开始读取我们的参数(源文件test1.js、输出文件test1.babel.js、配置文件.babelrc)
- babel-core根据babel-cli的参数开始编译
- Babel Parser 把我们传入的源码解析成ast对象
- Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点(也就是结合我们传入的插件把es6转换成es5的一个过程)
- Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。
好啦,到此我们算是把babel的整个过程简单的跑了一下,为了加深对每个流程的理解,我们不经过babel-core跟babel-cli单独去用一下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转换成ast对象
const ats=parse(code,{
sourceType: "module"
});
//把ast对象通过traverse转换成es5代码
traverse(ats,{
enter(path) {
if (t.isIdentifier(path.node, { name: "a" })) {
path.node.name = "aa";
}
if (path.isArrowFunctionExpression()){ //es6转换成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转换ast最后输出es5代码
console.log(generator(ats));
我们运行一下代码:
$ node ./babel-test/demo/demo1.js
结果输出:
{
code: 'const result = aa * b;\n\nconst result1 = function () {};',
map: null,
rawMappings: null
}
可以看到,最终我们实现了把es6的箭头函数转换成es5的过程。
代码中我们可以看到:
//把ast对象通过traverse转换成es5代码
traverse(ats,{
enter(path) {
if (t.isIdentifier(path.node, { name: "a" })) {
path.node.name = "aa";
}
if (path.isArrowFunctionExpression()){ //es6转换成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,
});
}
}
});
我们打开一个官方的插件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,
});
},
},
};
});
哈哈!! 是不是很简单呢? 其实babel也就是把很多的一些插件组合起来最终实现代码的转换,好啦~ 接下来我们就围绕babel的一些常用的插件做解析了
未完待续~~
欢迎志同道合的小伙伴一起学习、一起进步
欢迎入群~~~~~~~
参考: