Babel
babel 的原理
- parse: 把代码 code 变成 AST
- traverse: 遍历 AST 进行修改
- 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