AST、Babel、依赖

Babel

babel 的原理

  1. parse: 把代码 code 变成 AST
  2. traverse: 遍历 AST 进行修改
  3. generate: 把 AST 变成代码 code2
    即 code --(1)-> ast --(2)-> ast2 --(3)-> code2

demo: 将 let 变成 var

  • package.json
"dependencies": {
    "@babel/core": "7.12.3",
    "@babel/generator": "7.12.5",
    "@babel/parser": "7.12.5",
    "@babel/preset-env": "7.12.1",
    "@babel/traverse": "7.12.5",
    "@types/babel__traverse": "7.0.15",
    "ts-node": "9.0.0",
    "typescript": "4.0.5"
  },
  "devDependencies": {
    "@types/babel__core": "7.1.12",
    "@types/babel__generator": "7.6.2",
    "@types/babel__parser": "7.1.1",
    "@types/babel__preset-env": "7.9.1",
    "@types/node": "14.14.6"
  }

1). 将 code 变成 ast

  • let_to_var.ts
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)

在终端运行

node -r ts-node/register --inspect-brk let_to_var.ts

打开终端的链接

点击跳转打开控制台

找到我们的文件 在 console.log 那一行 打一个断点

program 是我们当前的进程,里面有两个 Node 也就是有两句话

第一个节点的类型是 VaribaleDeclarator 也就是变量声明,kind就是变量声明用的关键字,它的变量名和值都在declartions 里,id 里的 name 就是 key, init 里的value就是初始值

2). 使用 traverse 遍历 AST 进行修改

traverse(ast, {
    enter: item => {
      // 如果 type 是变量声明
       if (item.node.type === 'VariableDeclaration') {
           // 如果变量声明的类型是 let,就把 let 改成 var
           if (item.node.kind === 'let') {
               item.node.kind = 'var'
           }
       } 
    }
})

enter 钩子是每进入一个节点时就会触发,item 就是每个节点

3). 使用 generate 把 最新的 AST 变成新的代码

const result = generate(ast, {}, code)
console.log(result.code)

运行 node -r ts-node/register let_to_var.ts

为什么必须要用 AST

1). 你很难用正则表达式来替换,正则很容易把 let a = 'let' 变成 var a = 'var'
2). 你需要识别每个单词的意思,才能做到只修改用于声明变量的 let,而 AST 能明确地告诉你每个 let 的意思

自动的将代码转成 es5

上面我们手动的修改了 let 变为 var,那如果是 const 或者还有其他的 es6 代码,我们又得每次手动的在我们的代码里添加代码,所以我们可以使用 @babel/core 和 @babel/preset-env

import { parse } from '@babel/parser';
import * as babel from '@babel/core';

const code = `let a = 'let'; let b = 2; const c = 3`;
const ast = parse(code, { sourceType: 'module' });
// babel.transformFromAstSync 把 ast 变成新的 code
const result = babel.transformFromAstSync(ast, code, {
    presets: ['@babel/preset-env']
})
console.log(result.code)

把单独文件里的代码转换成 es5

我们上面代码自动转换但是我们的代码是写在字符串里的,那么如果把我们单独的代码文件转成 es5 那

  • file_to_es5.ts
import { parse } from '@babel/parser';
import * as babel from '@babel/core';
import * as fs from 'fs';

const code = fs.readFileSync('./test.js').toString();
const ast = parse(code, { sourceType: 'module' });
// babel.transformFromAstSync 把 ast 变成新的 code
const result = babel.transformFromAstSync(ast, code, {
    presets: ['@babel/preset-env']
})
fs.writeFileSync('./test.es5.js', result.code);
  • test.js
let a = 'let'; let b = 2; const c = 3

运行 node -r ts-node/register file_to_es5.ts
会生成一个 test.es5.js 文件

分析依赖

新建一个project_1 和 deps_1.ts
project_1 下有三个 js 文件

// 请确保你的 Node 版本大于等于 14
// 请先运行 yarn 或 npm i 来安装依赖
// 然后使用 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';

// 设置根目录
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' }) 
  // 分析文件依赖,将内容放至 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, '/')
}

deps_1.ts 主要代码思路

  • 步骤
    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 的依赖
启发:我们可以用哈希表来存储文件依赖

用递归获取嵌套依赖

  • 三层依赖关系
    index -> a -> dir/a2 -> dir/dir_in_dir/a3
    index -> b -> dir/b2 -> dir/dir_in_dir/b3

  • 思路
    1). collectCodeAndDeps 太长了,缩写为 collect 调用collect('index.js')
    2). 发现依赖 './a.js' 于是调用 collect('a.js')
    3). 发现依赖 './dir/a2.js' 于是调用 collect('dir/a2.js')
    4). 发现依赖 './dir_in_dir/a3.js' 于是调用 collect('dir/dir_in_dir/a3.js')
    5). 没有更多依赖了,a.js 这条线结束,发现下一个依赖 './b.js'
    以此类推,其实就是递归

  • deps_2.ts

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)
      }
    }
  })

完整代码
https://github.com/wanglifa/webpack-demo/commit/3ed790d85232fb7f2bbbaad25c8d724b73bbc878

循环依赖

node -r ts-node/register deps_3.ts
报错:调用栈 溢出了
为什么:分析过程 a -> b -> a -> b -> a -> b -> ... 把调用栈撑满了

解决方法:
避免重复进入同一个文件

  • deps_4.ts
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
+  if (Object.keys(depRelation).includes(key)) {
+    console.warn(`duplicated dependency: ${key}`)
+    return
+  }
...
  • 思路

一旦发现这个 key 已经在 keys 里了,就 return
这样分析过程就不是 a -> b -> a -> b -> ... 而是 a -> b -> return
注意我们只需要分析依赖,不需要执行代码,所以这样是可行的
由于我们的分析不需要执行代码,所以叫做静态分析
但如果我们执行代码,就会发现还是出现了循环

执行 index.js
node project_4/index.js

发现报错:不能在 'a' 初始化之前访问 a
原因:执行过程 a -> b -> a 此处报错,因为 node 发现计算 a 的时候又要计算 a

结论
  • 模块间可以循环依赖
    a 依赖 b,b 依赖 a
    a 依赖 b,b 依赖 c,c 依赖 a
  • 但不能有逻辑漏洞
    a.value = b.value + 1
    b.value = a.value + 1

总结

你可能感兴趣的:(AST、Babel、依赖)