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中调试```typescriptnode -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变为astconst 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,则将其变为varitem.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 变成 ast4. 遍历 ast,看看 import 了哪些依赖,假设依赖了 a.js 和 b.js5. 把 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// 获取文件内容,将内容放至 depRelationconst code = readFileSync(filepath).toString();// 初始化 depRelation[key]depRelation[key] = { deps: [], code: code };// 将代码转为 ASTconst ast = parse(code, { sourceType: "module" });console.log(ast);// 分析文件依赖,将内容放至 depRelationtraverse(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);// 把依赖写进 depRelationdepRelation[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// 获取文件内容,将内容放至 depRelationconst code = readFileSync(filepath).toString();// 初始化 depRelation[key]depRelation[key] = { deps: [], code: code };// 将代码转为 ASTconst ast = parse(code, { sourceType: "module" });// 分析文件依赖,将内容放至 depRelationtraverse(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);// 把依赖写进 depRelationdepRelation[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,包含了则直接returnif (Object.keys(depRelation).includes(key)) {console.warn(`duplicated dependency: ${key}`); // 注意,重复依赖不一定是循环依赖return;}// 获取文件内容,将内容放至 depRelationconst code = readFileSync(filepath).toString();// 初始化 depRelation[key]depRelation[key] = { deps: [], code: code };// 将代码转为 ASTconst ast = parse(code, { sourceType: "module" });// 分析文件依赖,将内容放至 depRelationtraverse(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);// 把依赖写进 depRelationdepRelation[key].deps.push(depProjectPath);// 分析依赖文件的依赖collectCodeAndDeps(depAbsolutePath);}},});}
duplicated dependency: a.jsduplicated 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
但是如果执行代码,代码逻辑中的循环引入还是会爆栈,因为代码本身没有被改变

