这个系列的主题是关于webpack的,希望能通过对webpack的深入学习,更进一步提升自己的前端能力。
先从babel讲起
- babel 的原理,大致的原理如下:
- parse: 把代码 code 变成 AST
- traverse: 遍历 AST 进行修改
- generate: 把 AST 变成代码 code2
- 即 code --(1)-> ast --(2)-> ast2 --(3)-> code2
- 什么是ast,ast就是将代码转变成一个对象,接下来我们会通过手动把 let 变成 var来进行实践观察
示例:手动把 let 变成 var
- 代码见 let_to_var.ts,将 code 中的 let 全部变成 var
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)
- 安装依赖的话通过package.json安装会比较方便
{ "name": "my-webpack-demo-1", "version": "0.0.1", "engines": { "node": ">=14" }, "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" } }
调试命令
- 运行 TS 代码
node -r ts-node/register let_to_var.ts
- 因为终端不好观察所以我们需要用浏览器的控制台
点击链接,然后点击控制台左上角的图标,sources下面就会出现我们的ts文件变成js之后的代码。node -r ts-node/register --inspect-brk let_to_var.ts
打断点观察ast
- 见图一,打上断点,否则程序运行完毕,控制台就点不开ast了 [url=https://sm.ms/image/rxhdofJYqXzNK1t][img]https://i-blog.csdnimg.cn/blog_migrate/f25cf191f55ffc047ad7b4aaa10bd7ee.png[/img][/url]
- 见图二,其中主要看program,program就是我们程序代码,body里面有两个节点,表示程序有两句话。最外的type:"VariableDeclaration"表示变量声明,kind的let表示了用到的关键字是let,里面的type表示类型。id下的name的a表示变量,init下的value的let表示值,即我们的第一句代码let a = 'let',通过ast表示出来了
把let变成var
- 代码
// let_to_var.js
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
const code = `let a = 'let'; let b = 2`
// code变成ast
const ast = parse(code, { sourceType: 'module' })
// 遍历整个ast,回调函数enter,ast变成ast2
traverse(ast, {
enter: item => {
if(item.node.type === 'VariableDeclaration'){
if(item.node.kind ==='let') {
item.node.kind = 'var'
}
}
}
})
// ast2 转成code2
const result = generate(ast, {}, code)
console.log('22222', result.code)
- 效果见图3
为什么要用AST
- 你很难用正则表达式来替换,正则很容易把 let a = 'let' 变成 var a = 'var'
- 你需要识别每个单词的意思,才能做到只修改用于声明变量的 let
- 而 AST 能明确地告诉你每个 let 的意思
将es6转成es5
- 新建to_es5.ts
- 使用 @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' })
// transformFromAstSync 这个方法可以把ast变成code
// 传入原始的ast, 原始的code,这样就能生成map
const result = babel.transformFromAstSync(ast, code, {
presets: ['@babel/preset-env']
})
console.log(result.code)
- 重点知识
- babel.transformFromAstSync 可以把 ast 变成 code2
- 如果图方便,可以用 babel.transformSync 直接把 code 变成 code2
- @babel/preset-env 内置了很多转换规则
- 运行结果
- node -r ts-node/register to_es5.ts
- 结果与 let_to_es5.ts 差不多,还会把所有高级语法转为 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' })
// transformFromAstSync 这个方法可以把ast变成code
// 传入原始的ast, 原始的code,这样就能生成map
const result = babel.transformFromAstSync(ast, code, {
presets: ['@babel/preset-env']
})
fs.writeFileSync('./test.es5.js', result.code)
- 新建test.js,并在其中写入代码,运行file_to_es5,结束发现多了test.es5.js,里面是转化完毕的后es5代码
除了转换 JS 语法,还能做啥?
- 分析 JS 文件的依赖关系
- 利用deps_1.ts,和project_1文件夹下的内容进行实践,代码如下
// deps_1.ts
// 请确保你的 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文件夹
// index.js
import a from './a.js'
import b from './b.js'
console.log(a.value + b.value)
// a.js
const a = {
value: 1,
}
export default a
// b.js
const b = {
value: 2,
}
export default b
启发: 用哈希表来储存文件依赖
- 哈希表是一个数据结构术语,js中一个对象就可以看作一个哈希表
递归分析多层依赖关系
三层依赖关系
- index -> a -> dir/a2 -> dir/dir_in_dir/a3
- index -> b -> dir/b2 -> dir/dir_in_dir/b3
- 文件放在 project_2 目录里
思路
- 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'
- 以此类推,其实就是递归
循环依赖
- 见图3
- node -r ts-node/register deps_3.ts
- 报错:调用栈 溢出了
- 为什么:分析过程 a -> b -> a -> b -> a -> b -> ... 把调用栈撑满了
静态分析循环依赖
- 解决循环依赖
- 一旦发现这个 key 已经在 keys 里了,就 return
- 这样分析过程就不是 a -> b -> a -> b -> ... 而是 a -> b -> return
- 注意我们只需要分析依赖,不需要执行代码,所以这样是可行的
- 由于我们的分析不需要执行代码,所以叫做静态分析
- 但如果我们执行代码,就会发现还是出现了循环
执行 project_4/index.js
- 发现报错:不能在 'a' 初始化之前访问 a
- 原因:执行过程 a -> b -> a 此处报错,因为 node 发现计算 a 的时候又要计算 a
合法的循环依赖(没有逻辑漏洞)
- 见图7