babel原理

  1. parse:把代码code变成AST
  2. traverse:遍历AST进行修改
  3. 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);

  1. 使用命令行运行文件,在Chrome中调试
  2. ```typescript
  3. node -r ts-node/register --inspect-brk let_to_var.ts

点开控制台的node图标,得到调试工具
image.png
打断点得到ast对象
包含两段代码
image.png
其中的变量声明
image.png

接下来进行代码的转换

  1. import { parse } from "@babel/parser";
  2. import traverse from "@babel/traverse";
  3. import generate from "@babel/generator";
  4. // TODO: 第一步:将code变为ast
  5. const code = "let a = 'let';let b = 2;";
  6. const ast = parse(code, { sourceType: "module" });
  7. // 分析ast对象
  8. // console.log(ast);
  9. // TODO: 第二步,遍历ast,改变其内容
  10. traverse(ast, {
  11. // 每次进入结点时调用
  12. enter: item => {
  13. // 判断当前结点类型是否为变量声明
  14. if (item.node.type === "VariableDeclaration") {
  15. if (item.node.kind === "let") {
  16. // 如果类型是let,则将其变为var
  17. item.node.kind = "var";
  18. }
  19. }
  20. },
  21. });
  22. // TODO: 第三步,将ast重新变为代码
  23. const res = generate(ast, {}, code);
  24. console.log(res.code);

最终得到babel结果
image.png

AST

ast能帮我们识别每一个单词的意思

Babel与AST - 图5

转化为ES5

自动转为ES5

  1. import { parse } from "@babel/parser";
  2. import * as babel from "@babel/core";
  3. import * as fs from "fs";
  4. const code = fs.readFileSync("./test.js").toString();
  5. console.log(code)
  6. const ast = parse(code, { sourceType: "module" });
  7. // console.log(ast);
  8. const res = babel.transformFromAstSync(ast, code, {
  9. presets: ["@babel/preset-env"],
  10. });
  11. console.log(res.code)

index.js的依赖关系

一个文件的依赖

如果babelindex入口文件,这个入口文件引入了多个其他文件,如何来解析其中的文件依赖关系呢
代码思路

  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 的依赖
  1. // 使用 node -r ts-node/register 文件路径 来运行,
  2. // 如果需要调试,可以加一个选项 --inspect-brk,再打开 Chrome 开发者工具,点击 Node 图标即可调试
  3. import { parse } from "@babel/parser";
  4. import traverse from "@babel/traverse";
  5. import { readFileSync } from "fs";
  6. import { resolve, relative, dirname } from "path";
  7. /*
  8. 代码思路
  9. 1. 调用collectCodeAndDeps("index.js")
  10. 2. 先把 depRelation['index.js'] 初始化为 { deps: [], code: 'index.js的源码' }
  11. 3. 然后把 index.js 源码 code 变成 ast
  12. 4. 遍历 ast,看看 import 了哪些依赖,假设依赖了 a.js 和 b.js
  13. 5. 把 a.js 和 b.js 写到 depRelation['index.js'].deps 里
  14. 6. 最终得到的 depRelation 就收集了 index.js 的依赖
  15. */
  16. // 设置根目录
  17. const projectRoot = resolve(__dirname, "project_1");
  18. // 类型声明,依赖关系
  19. type DepRelation = { [key: string]: { deps: string[]; code: string } };
  20. // 初始化一个空的 depRelation,用于收集依赖
  21. const depRelation: DepRelation = {};
  22. // 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
  23. // 收集源代码和依赖
  24. collectCodeAndDeps(resolve(projectRoot, "index.js"));
  25. console.log(depRelation);
  26. console.log("done");
  27. function collectCodeAndDeps(filepath: string) {
  28. const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
  29. // 获取文件内容,将内容放至 depRelation
  30. const code = readFileSync(filepath).toString();
  31. // 初始化 depRelation[key]
  32. depRelation[key] = { deps: [], code: code };
  33. // 将代码转为 AST
  34. const ast = parse(code, { sourceType: "module" });
  35. console.log(ast);
  36. // 分析文件依赖,将内容放至 depRelation
  37. traverse(ast, {
  38. enter: path => {
  39. if (path.node.type === "ImportDeclaration") {
  40. // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
  41. const depAbsolutePath = resolve(dirname(filepath), path.node.source.value);
  42. // 然后转为项目路径
  43. const depProjectPath = getProjectPath(depAbsolutePath);
  44. // 把依赖写进 depRelation
  45. depRelation[key].deps.push(depProjectPath);
  46. }
  47. },
  48. });
  49. }
  50. // 获取文件相对于根目录的相对路径
  51. function getProjectPath(path: string) {
  52. return relative(projectRoot, path).replace(/\\/g, "/");
  53. }

这里可以看到两个import 声明语句,通过import来解析依赖关系
image.png
可以用一个哈希表来存储文件依赖关系

深层嵌套文件依赖

如果一个import引入文件中 还引入了其他文件,这就形成了一个深度依赖的关系。如何解析这种嵌套的依赖?
例如

  • index.js - a.js - a2.js
  • index.js - b.js = b2.js
  • 解决方法,递归判断依赖
    1. function collectCodeAndDeps(filepath: string) {
    2. const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
    3. // 获取文件内容,将内容放至 depRelation
    4. const code = readFileSync(filepath).toString();
    5. // 初始化 depRelation[key]
    6. depRelation[key] = { deps: [], code: code };
    7. // 将代码转为 AST
    8. const ast = parse(code, { sourceType: "module" });
    9. // 分析文件依赖,将内容放至 depRelation
    10. traverse(ast, {
    11. enter: path => {
    12. if (path.node.type === "ImportDeclaration") {
    13. // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
    14. const depAbsolutePath = resolve(dirname(filepath), path.node.source.value);
    15. // 然后转为项目路径
    16. const depProjectPath = getProjectPath(depAbsolutePath);
    17. // 把依赖写进 depRelation
    18. depRelation[key].deps.push(depProjectPath);
    19. // 分析依赖文件的依赖
    20. collectCodeAndDeps(depAbsolutePath);
    21. }
    22. },
    23. });
    24. }

输出结果

  1. {
  2. 'index.js': {
  3. deps: [ 'a.js', 'b.js' ],
  4. code: 'import a from "./a.js";\r\n' +
  5. 'import b from "./b.js";\r\n' +
  6. 'console.log(a.value + b.value);\r\n'
  7. },
  8. 'a.js': {
  9. deps: [ 'dir/a2.js' ],
  10. code: 'import a2 from "./dir/a2.js";\r\n' +
  11. 'const a = {\r\n' +
  12. ' value: 1,\r\n' +
  13. ' value2: a2,\r\n' +
  14. '};\r\n' +
  15. 'export default a;\r\n'
  16. },
  17. 'dir/a2.js': {
  18. deps: [ 'dir/dir_in_dir/a3.js' ],
  19. code: 'import a3 from "./dir_in_dir/a3.js";\r\n' +
  20. 'const a2 = {\r\n' +
  21. ' value: 12,\r\n' +
  22. ' value3: a3,\r\n' +
  23. '};\r\n' +
  24. 'export default a2;\r\n'
  25. },
  26. 'dir/dir_in_dir/a3.js': {
  27. deps: [],
  28. code: 'const a3 = {\r\n value: 123,\r\n}\r\nexport default a3\r\n'
  29. },
  30. 'b.js': {
  31. deps: [ 'dir/b2.js' ],
  32. code: 'import b2 from "./dir/b2.js";\r\n' +
  33. 'const b = {\r\n' +
  34. ' value: 2,\r\n' +
  35. ' value2: b2,\r\n' +
  36. '};\r\n' +
  37. 'export default b;\r\n'
  38. },
  39. 'dir/b2.js': {
  40. deps: [ 'dir/dir_in_dir/b3.js' ],
  41. code: 'import b3 from "./dir_in_dir/b3.js";\r\n' +
  42. 'const b2 = {\r\n' +
  43. ' value: 22,\r\n' +
  44. ' value3: b3,\r\n' +
  45. '};\r\n' +
  46. 'export default b2;\r\n'
  47. },
  48. 'dir/dir_in_dir/b3.js': {
  49. deps: [],
  50. code: 'const b3 = {\r\n value: 123,\r\n}\r\nexport default b3\r\n'
  51. }
  52. }
  53. done

循环文件依赖-静态分析

如果a 指向 b, b又指向 a,这样如果进行递归分析会造成死循环,最终爆栈

image.png
解决思路:

  1. 避免重复进入同一个文件
  2. 一旦发现这个key已经在keys里面了,就return
  3. 这样就只需要分析依赖而不会执行代码,这也可以称作静态分析


  1. function collectCodeAndDeps(filepath: string) {
  2. const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
  3. // 分析depRelation中所有的key是否包含当前key,包含了则直接return
  4. if (Object.keys(depRelation).includes(key)) {
  5. console.warn(`duplicated dependency: ${key}`); // 注意,重复依赖不一定是循环依赖
  6. return;
  7. }
  8. // 获取文件内容,将内容放至 depRelation
  9. const code = readFileSync(filepath).toString();
  10. // 初始化 depRelation[key]
  11. depRelation[key] = { deps: [], code: code };
  12. // 将代码转为 AST
  13. const ast = parse(code, { sourceType: "module" });
  14. // 分析文件依赖,将内容放至 depRelation
  15. traverse(ast, {
  16. enter: path => {
  17. if (path.node.type === "ImportDeclaration") {
  18. // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
  19. const depAbsolutePath = resolve(dirname(filepath), path.node.source.value);
  20. // 然后转为项目路径
  21. const depProjectPath = getProjectPath(depAbsolutePath);
  22. // 把依赖写进 depRelation
  23. depRelation[key].deps.push(depProjectPath);
  24. // 分析依赖文件的依赖
  25. collectCodeAndDeps(depAbsolutePath);
  26. }
  27. },
  28. });
  29. }
  1. duplicated dependency: a.js
  2. duplicated dependency: b.js
  3. {
  4. 'index.js': {
  5. deps: [ 'a.js', 'b.js' ],
  6. code: 'import a from "./a.js";\r\n' +
  7. 'import b from "./b.js";\r\n' +
  8. 'console.log(a.value + b.value);\r\n'
  9. },
  10. 'a.js': {
  11. deps: [ 'b.js' ],
  12. code: 'import b from "./b.js";\r\n' +
  13. 'const a = {\r\n' +
  14. ' value: b.value + 1,\r\n' +
  15. '};\r\n' +
  16. 'export default a;\r\n'
  17. },
  18. 'b.js': {
  19. deps: [ 'a.js' ],
  20. code: 'import a from "./a.js";\r\n' +
  21. 'const b = {\r\n' +
  22. ' value: a.value + 1,\r\n' +
  23. '};\r\n' +
  24. 'export default b;\r\n'
  25. }
  26. }
  27. done

但是如果执行代码,代码逻辑中的循环引入还是会爆栈,因为代码本身没有被改变