babel原理
- parse:把代码code变成AST
- traverse:遍历AST进行修改
- generate:把AST变成另一份代码code2
一个简易的babel过程
手动把let变为var ```typescript import { parse } from “@babel/parser”; import traverse from “@babel/traverse”; import generate from “@babel/generator”;
const code = “let a = ‘let’;let b = 2;”; const ast = parse(code, { sourceType: “module” });
console.log(ast);
使用命令行运行文件,在Chrome中调试
```typescript
node -r ts-node/register --inspect-brk let_to_var.ts
点开控制台的node图标,得到调试工具
打断点得到ast对象
包含两段代码
其中的变量声明
接下来进行代码的转换
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
// TODO: 第一步:将code变为ast
const code = "let a = 'let';let b = 2;";
const ast = parse(code, { sourceType: "module" });
// 分析ast对象
// console.log(ast);
// TODO: 第二步,遍历ast,改变其内容
traverse(ast, {
// 每次进入结点时调用
enter: item => {
// 判断当前结点类型是否为变量声明
if (item.node.type === "VariableDeclaration") {
if (item.node.kind === "let") {
// 如果类型是let,则将其变为var
item.node.kind = "var";
}
}
},
});
// TODO: 第三步,将ast重新变为代码
const res = generate(ast, {}, code);
console.log(res.code);
AST
ast能帮我们识别每一个单词的意思
转化为ES5
自动转为ES5
import { parse } from "@babel/parser";
import * as babel from "@babel/core";
import * as fs from "fs";
const code = fs.readFileSync("./test.js").toString();
console.log(code)
const ast = parse(code, { sourceType: "module" });
// console.log(ast);
const res = babel.transformFromAstSync(ast, code, {
presets: ["@babel/preset-env"],
});
console.log(res.code)
index.js的依赖关系
一个文件的依赖
如果babelindex入口文件,这个入口文件引入了多个其他文件,如何来解析其中的文件依赖关系呢
代码思路
- 调用collectCodeAndDeps(“index.js”),引入入口文件
- 先把 depRelation[‘index.js’] 初始化为 { deps: [], code: ‘index.js的源码’ }
- 然后把 index.js 源码 code 变成 ast
- 遍历 ast,看看 import 了哪些依赖,假设依赖了 a.js 和 b.js
- 把 a.js 和 b.js 写到 depRelation[‘index.js’].deps 里
- 最终得到的 depRelation 就收集了 index.js 的依赖
// 使用 node -r ts-node/register 文件路径 来运行,
// 如果需要调试,可以加一个选项 --inspect-brk,再打开 Chrome 开发者工具,点击 Node 图标即可调试
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import { readFileSync } from "fs";
import { resolve, relative, dirname } from "path";
/*
代码思路
1. 调用collectCodeAndDeps("index.js")
2. 先把 depRelation['index.js'] 初始化为 { deps: [], code: 'index.js的源码' }
3. 然后把 index.js 源码 code 变成 ast
4. 遍历 ast,看看 import 了哪些依赖,假设依赖了 a.js 和 b.js
5. 把 a.js 和 b.js 写到 depRelation['index.js'].deps 里
6. 最终得到的 depRelation 就收集了 index.js 的依赖
*/
// 设置根目录
const projectRoot = resolve(__dirname, "project_1");
// 类型声明,依赖关系
type DepRelation = { [key: string]: { deps: string[]; code: string } };
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = {};
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
// 收集源代码和依赖
collectCodeAndDeps(resolve(projectRoot, "index.js"));
console.log(depRelation);
console.log("done");
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
// 获取文件内容,将内容放至 depRelation
const code = readFileSync(filepath).toString();
// 初始化 depRelation[key]
depRelation[key] = { deps: [], code: code };
// 将代码转为 AST
const ast = parse(code, { sourceType: "module" });
console.log(ast);
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: path => {
if (path.node.type === "ImportDeclaration") {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(dirname(filepath), path.node.source.value);
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath);
// 把依赖写进 depRelation
depRelation[key].deps.push(depProjectPath);
}
},
});
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\\/g, "/");
}
这里可以看到两个import 声明语句,通过import来解析依赖关系
可以用一个哈希表来存储文件依赖关系
深层嵌套文件依赖
如果一个import引入文件中 还引入了其他文件,这就形成了一个深度依赖的关系。如何解析这种嵌套的依赖?
例如
- index.js - a.js - a2.js
- index.js - b.js = b2.js
- 解决方法,递归判断依赖
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
// 获取文件内容,将内容放至 depRelation
const code = readFileSync(filepath).toString();
// 初始化 depRelation[key]
depRelation[key] = { deps: [], code: code };
// 将代码转为 AST
const ast = parse(code, { sourceType: "module" });
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: path => {
if (path.node.type === "ImportDeclaration") {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(dirname(filepath), path.node.source.value);
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath);
// 把依赖写进 depRelation
depRelation[key].deps.push(depProjectPath);
// 分析依赖文件的依赖
collectCodeAndDeps(depAbsolutePath);
}
},
});
}
输出结果
{
'index.js': {
deps: [ 'a.js', 'b.js' ],
code: 'import a from "./a.js";\r\n' +
'import b from "./b.js";\r\n' +
'console.log(a.value + b.value);\r\n'
},
'a.js': {
deps: [ 'dir/a2.js' ],
code: 'import a2 from "./dir/a2.js";\r\n' +
'const a = {\r\n' +
' value: 1,\r\n' +
' value2: a2,\r\n' +
'};\r\n' +
'export default a;\r\n'
},
'dir/a2.js': {
deps: [ 'dir/dir_in_dir/a3.js' ],
code: 'import a3 from "./dir_in_dir/a3.js";\r\n' +
'const a2 = {\r\n' +
' value: 12,\r\n' +
' value3: a3,\r\n' +
'};\r\n' +
'export default a2;\r\n'
},
'dir/dir_in_dir/a3.js': {
deps: [],
code: 'const a3 = {\r\n value: 123,\r\n}\r\nexport default a3\r\n'
},
'b.js': {
deps: [ 'dir/b2.js' ],
code: 'import b2 from "./dir/b2.js";\r\n' +
'const b = {\r\n' +
' value: 2,\r\n' +
' value2: b2,\r\n' +
'};\r\n' +
'export default b;\r\n'
},
'dir/b2.js': {
deps: [ 'dir/dir_in_dir/b3.js' ],
code: 'import b3 from "./dir_in_dir/b3.js";\r\n' +
'const b2 = {\r\n' +
' value: 22,\r\n' +
' value3: b3,\r\n' +
'};\r\n' +
'export default b2;\r\n'
},
'dir/dir_in_dir/b3.js': {
deps: [],
code: 'const b3 = {\r\n value: 123,\r\n}\r\nexport default b3\r\n'
}
}
done
循环文件依赖-静态分析
如果a 指向 b, b又指向 a,这样如果进行递归分析会造成死循环,最终爆栈
解决思路:
- 避免重复进入同一个文件
- 一旦发现这个key已经在keys里面了,就return
- 这样就只需要分析依赖而不会执行代码,这也可以称作静态分析
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
// 分析depRelation中所有的key是否包含当前key,包含了则直接return
if (Object.keys(depRelation).includes(key)) {
console.warn(`duplicated dependency: ${key}`); // 注意,重复依赖不一定是循环依赖
return;
}
// 获取文件内容,将内容放至 depRelation
const code = readFileSync(filepath).toString();
// 初始化 depRelation[key]
depRelation[key] = { deps: [], code: code };
// 将代码转为 AST
const ast = parse(code, { sourceType: "module" });
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: path => {
if (path.node.type === "ImportDeclaration") {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(dirname(filepath), path.node.source.value);
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath);
// 把依赖写进 depRelation
depRelation[key].deps.push(depProjectPath);
// 分析依赖文件的依赖
collectCodeAndDeps(depAbsolutePath);
}
},
});
}
duplicated dependency: a.js
duplicated dependency: b.js
{
'index.js': {
deps: [ 'a.js', 'b.js' ],
code: 'import a from "./a.js";\r\n' +
'import b from "./b.js";\r\n' +
'console.log(a.value + b.value);\r\n'
},
'a.js': {
deps: [ 'b.js' ],
code: 'import b from "./b.js";\r\n' +
'const a = {\r\n' +
' value: b.value + 1,\r\n' +
'};\r\n' +
'export default a;\r\n'
},
'b.js': {
deps: [ 'a.js' ],
code: 'import a from "./a.js";\r\n' +
'const b = {\r\n' +
' value: a.value + 1,\r\n' +
'};\r\n' +
'export default b;\r\n'
}
}
done
但是如果执行代码,代码逻辑中的循环引入还是会爆栈,因为代码本身没有被改变