Babel
原理
- parse:把代码code变成AST
- traverse:遍历AST进行修改
- generate:把AST变成代码code2
- 即 code (1) > AST (2) > AST2 (3) > code2
示例 let to var
- 手动把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)
traverse(ast, {
enter: item => {
if (item.node.type === ‘VariableDeclaration’) {
if (item.node.kind === “let”) {
item.node.kind = ‘var’
}
}
}
})
`$ node -r ts-node/register let_to_var.ts `<br /><br />或者在chrome中调试<br />`$ node -r ts-node/register --inspect-brk let_to_var.ts `打开Chrome,F12 <br /><br /><br /><a name="PhvvZ"></a>## AST- 为什么用AST- 很难用正则表达式来替换,正则很容易把let a = 'let' 变成 var a = 'var'- 需要识别每个单词的意思,才能做到只修改声明变量的let- 而AST能明确告诉每个let的意思<a name="q3bDz"></a>## 转ES5使用@babel/core 和 @babel/preset-env```typescriptimport { 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" })const result = babel.transformFromAstSync(ast, code, {presets: ['@babel/preset-env']})console.log(result.code)
文件转es5
/test.js
let a = '123'let b = 456const c = 789
/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" })const result = babel.transformFromAstSync(ast, code, {presets: ['@babel/preset-env']})fs.writeFileSync('./test.es5.js', result.code)
$ node -r ts-node/register file_to_es5.ts/test.es5.js
依赖分析
- /deps_1.ts ```typescript // 请确保你的 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, ‘/‘) }
- /project_1/<br /><br /><a name="zeQBb"></a>## 思路1. 调用`collectCodeAndDeps('index.js')`1. 先把`depRelation['index.js']`初始化为`{deps:[],code:'index.js的源码'}`1. 然后把index.js源码code变成ast1. 遍历ast,看看import了哪些依赖,假如依赖了a.js和b.js1. 把a.js和b.js写到`depRelation['index.js'].deps`中1. 最终得到depRelation就收集了index.js的依赖<a name="oEEII"></a>## 递归分析嵌套依赖- 三层依赖- index→a→dir/a2→dir/dir_in_dir/a3- index→b→dir/b2→dir/dir_in_dir/b3- 关键代码- `collectCodeAndDeps(depAbsolutePath)`- /deps_2.ts```typescript// 请确保你的 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_2')// 类型声明type DepRelation = { [key: string]: { deps: string[], code: string } }// 初始化一个空的 depRelation,用于收集依赖const depRelation: DepRelation = {}// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.jscollectCodeAndDeps(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' })// 分析文件依赖,将内容放至 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)}}})}// 获取文件相对于根目录的相对路径function getProjectPath(path: string) {return relative(projectRoot, path).replace(/\\/g, '/')}
思路
循环依赖
- index→a→b
- index→b→a
- 调用栈溢出
- index→a→b→a→b→a→b→a→b→a→b→a……

关键代码
if(Object.keys(depRelation).includes(key)){console.warn(`duplicated dependency: ${key}`) // 注意,重复依赖不一定是循环依赖return}
/deps_4.ts ```typescript // 请确保你的 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_4’) // 类型声明 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
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)
}
}
})
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\/g, ‘/‘)
}
- 思路- 一旦发现key已经在keys中,就return- 这样分析过程就不会a→b→a→b→a……,而是a→b→return- 只需要分析依赖,不需要执行代码- 由于分析不需要执行代码,这就叫做静态分析- 如果执行代码就会发现依然出现循环- 结论- 模块间可以循环依赖- 但是不能有逻辑漏洞- 合理的循环引用```typescriptimport a from './a.js'import b from './b.js'console.log(a.getB())console.log(b.getA())
import b from './b.js'const a = {value: 'a',getB: () => b.value + ' from a.js'}export default a
import a from './a.js'const b = {value: 'b',getA: () => a.value + ' from b.js'}export default b
最好还是不要用循环依赖
