什么是AST
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);
我们通过命令打出这个ast的log看看:
node -r ts-node/register --inspect-brk xx.ts
在控制台我们看到ast长啥样:
ast就是const code = `let a = 'let'; let b = 2`的树状结构,对象中详细地描述了变量声明为let,声明的值是多少。通过这个对象我们就能做一些替换,例如将ES6转化ES5(kind = ’let‘ 转化为 kind = 'var')。
为啥不用正则做es6 => es5 的替换呢?
很难,很容易出错。我们需要用一种方式来识别每个单词的意思,才能做到只修改用于声明变量的 let,并且AST能够明确地告诉你每个let的意思。
所以,JS的编译必需要AST。
Babel相关使用
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' })
traverse(ast, {
enter: item => {
if(item.node.type === 'VariableDeclaration'){
if(item.node.kind === 'let'){
item.node.kind = 'var'
}
}
}
})
const result = generate(ast, {}, code)
console.log(result.code)
bebel的原理:
- parse:把代码code变成AST
- traverse:遍历AST进行修改
- generate:把AST变成代码code2
等于:code => 1 => AST => 2 => AST2 => 3 => code2
如果求方便可直接使用 babel.transformSync,可以将code直接变成code2,即将所有高级语法转为ES5。@babel/preset-env内置了很多转换规则。
通过AST来分析JS文件的依赖关系
分析JS文件的依赖关系
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, '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, '/')
}
最终得出依赖关系,通过hash来存储依赖关系:
通过递归分析嵌套依赖
如果依赖当中还有依赖的话,那么我们就要使用递归:
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)
}
}
})
}
实际上就加了一行代码:
// 收集嵌套的依赖
collectCodeAndDeps(depAbsolutePath)
通过检测key来避免重复依赖
如果有嵌套的依赖中有重复的依赖,我们需要做个判断,与depRelation对象中对比key,防止重复依赖发生:
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
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)
}
}
})
}
if(Object.keys(depRelation).includes(key)){
console.warn(`duplicated dependency: ${key}`) // 注意,重复依赖不一定是循环依赖
return
}
循环依赖
如果a文件依赖于b文件,b文件又依赖于a文件,那么它们就有可能是循环依赖。例如互相依赖求值
a文件:
import b from './b.js'
const a = {
value: b.value + 1,
}
export default a
b文件:
import a from './a.js'
const b = {
value: a.value + 1,
}
export default b
这样互相之间的两个文件就会引起循环依赖,从而报错。程序根本不知道value值是多少。
但两个文件互相依赖也不一定会形成循环依赖,例如:
a文件:
import b from './b.js'
const a = {
value: 'a',
getB: () => b.value + ' from a.js'
}
export default a
b文件:
import a from './a.js'
const b = {
value: 'b',
getA: () => a.value + ' from b.js'
}
export default b
文件间互相引用的value值没有继续依赖其它文件,不会引起循环依赖。
总结:
AST:
- parse:把代码 code1变成AST
- traverse:遍历AST进行修改
- generate:把AST变成代码 code2
babel:
- babel把高级代码翻译成ES5
- @babel/parse
- @babel/traverse
- @babel/generate
- @babel/core 包含前三者
- @babel/preset-env 内置了很多规则
循环依赖:
- 有的循环依赖可以正常执行
- 有的循环依赖不可以
- 但是都可以通过AST做静态分析