Webpack-01AST_Babel_依赖

Webpack

前置工作

  • 下载代码仓库
  • 安装好依赖,参考README 运行我们的代码

Babel原理

  • parse:把代码code变成 AST
  • traverse:遍历 AST 进行修改
  • generate:把修改后的 AST 变成 code2

过程如下

1
2
       parse        traverse       generate
code --------> ast -------> ast2 -------> code2

为啥不用 “正则” 来实现

  • 因为代码很复杂 let a = 'let' 单纯一个变量声明,你就很难处理,更何况其他ES6及以上语法呢?

let to var

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

代码转换为ES5

1
2
3
4
5
6
7
8
9
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' })
const result = babel.transformFromAstSync(ast, code, {
presets: ['@babel/preset-env']
})
console.log(result.code)

将js文件转换为ES5

1
2
3
4
5
6
7
8
9
10
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)

除了转换ES5代码还能做什么

依赖分析: 004_dep.ts

  • project1目录的 index.js
1
2
3
import a from './a.js'
import b from './b.js'
console.log(a.value + b.value)

代码思路 004_dep.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 的依赖

升级:依赖里还有依赖 005_dep.ts

  • 三层依赖关系

  • index.js -> a -> dir/a2 -> dir/dir_in_dir/a3

  • index.js -> b -> dir/b2 -> dir/dir_in_dir/b3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 太长了,缩写为 collect
  • 调用 collect(‘index.js’)
  • 发现依赖 ‘./a.js’ 于是调用 collect(‘a.js’)
  • 发现依赖 ‘./dir/a2.js’ 于是调用 collect(‘dir/a2.js’)
  • 发现依赖 ‘./dir_in_dir/a3.js’ 于是调用 collect(‘dir/dir_in_dir/a3.js’)
  • 没有更多依赖了,a.js 这条线结束,发现下一个依赖 ‘./b.js’
  • 以此类推,其实就是递归

代码如果是循环依赖呢? 006_dep.ts

  • index -> a -> b
  • index -> b -> a
1
2
3
4
5
6
7
8
9
10
11
12
13
// a.js
import b from './b.js'
const a = {
value: b.value + 1,
}
export default a

// b.js
import a from './a.js'
const b = {
value: a.value + 1,
}
export default b
  • 求值
1
2
3
4
5
6
7
8
9
// a.js里
a.value = b.value +1
// b.js里
b.value = a.value +1

// 神经病,a 依赖b,但是 b必须先执行,同时 b又依赖 a
// 运行代码 报错
Maximum call stack size exceeded
// 爆栈了

难道不能 “循环依赖”吗?007_dep.ts

解决循环依赖问题,用一个map ,如果已经读取过,就return

1
2
3
4
if (Object.keys(depRelation).includes(key)) {
console.warn(`duplicated dependency: ${key}`) // 注意,重复依赖不一定是循环依赖
return
}
  • 分析依赖,是不需要执行代码的,所以这是可行的
  • 由于不需要执行代码,所以这个叫做“静态分析”
  • 假如执行代码,就会出现循环引用问题

结论

  • 模块里可以循环依赖
    • a->b,b->a
  • 但是不能有逻辑漏洞
    • a.value = b.value +1
    • b.value = a.value +1
  • 如何写出解决循环依赖的代码 project5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.js
import b from './b.js'
const a = {
value: 'a',
getB: () => b.value + ' from a.js'
}
export default a

// b.js
import a from './a.js'
const b = {
value: 'b',
getA: () => a.value + ' from b.js'
}
export default b

这样循环链就断了,运行就不会报错

总结

AST

  • parse 将 code 变成 ast
  • traverse: 遍历 AST 进行修改
  • generate: 把修改后的 AST 变成代码 code2

工具

  • babel 可以把高级代码翻译为 ES5
  • @babel/parser
  • @babel/traverse
  • @babel/generator
  • @babel/core 包含前三者
  • @babel/preset-env 内置很多规则

循环依赖

  • 有的循环依赖可以正常执行
  • 有的循环依赖不可以
  • 但都可以做静态分析