webpack篇之依赖分析
前置知识 babel & ast
babel的原理
-
parse: 把代码code 变为 ast
-
traverse: 遍历ast并修改代码
-
generate: 将ast变为新的代码 newcode
经过这三步,就可以对代码进行转换
转换的例子
-
下面是一个把代码中的let转化为var的例子
import { writeFileSync } from 'fs'; import { parse } from '@babel/parser'; import _traverse from '@babel/traverse'; const traverse = _traverse.default; import _generator from '@babel/generator'; const generator = _generator.default; const code = `let a = 'let'; let b = 2;const c = false`; 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 = generator(ast, {}, code); writeFileSync('./file_let_to_var.js', result.code);效果如下
-
将es6的代码转为es5
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)效果如下:
开始 什么是打包器?
最简单的打包 则是把多个文件整合成一个文件
下面就来研究如何通过ast写一个简易的打包器
打包器的构建
假设有如下的场景
- 有一个index.js[入口文件]
- index中有
import a from './a.js' - index中又有
import b from './b.js
如何把这三个文件的代码打包?
想要打包 首先得知道index.js依赖了谁
收集依赖的思路
- 先从入口文件开始,即调用
collectDepsAndCode(index.js) - 初始化
depRelation[入口] = {deps: [],code: `# 入口文件的代码`} - 把入口文件的代码code变为ast
- 遍历ast找到依赖
- 把依赖写入
depRelation[入口].deps数组中 - 最终得到的就是index.js的依赖
代码
import { parse } from '@babel/parser';
import _traverse from '@babel/traverse';
const traverse = _traverse.default;
import { readFileSync } from 'fs';
import { resolve, relative, dirname } from 'path';
// 设置根目录
const projectRoot = './project_1'
// 类型声明
// type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一个空的 depRelation,用于收集依赖
const depRelation = {};
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'));
console.log(depRelation);
console.log('done');
function collectCodeAndDeps(filepath) {
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) {
return relative(projectRoot, path).replace(/\\/g, '/');
}
效果如下
上述代码可以完成依赖收集 但是又有其他的问题 问题如下
问题1?
假设依赖中还有依赖 上述代码就没办法分析到全部的依赖了
- 有一个index.js[入口文件]
- index中有
import a from './a.js' - index中又有
import b from './b.js - a.js中依赖了其他文件
- b.js中也依赖了其他文件
如何解决这个问题?
问题1解决思路
- 依然使用
collectDepsAndCode(index.js) - 发现a.js 调用
collectDepsAndCode(a.js) - 又发现了a.js依赖了其他依赖 调用
collectDepsAndCode(a的依赖.js),直到没有依赖 - 重复a.js的逻辑 去处理b.js
- 其实这就是递归
问题1解决代码
import { parse } from '@babel/parser';
import _traverse from '@babel/traverse';
const traverse = _traverse.default;
import { readFileSync } from 'fs';
import { resolve, relative, dirname } from 'path';
// 设置根目录
const projectRoot = 'project_2';
// 类型声明
// type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一个空的 depRelation,用于收集依赖
const depRelation = {};
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'));
console.log(depRelation);
console.log('done');
function collectCodeAndDeps(filepath) {
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); // 重点 递归的处理依赖
}
},
});
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path) {
return relative(projectRoot, path).replace(/\\/g, '/');
}
代码效果图
上述代码实现了对深层依赖情况的处理 but 还有问题
问题2
假设存在循环依赖 上述代码就会报错 因为递归没有出口 如何解决这个问题?
例如:
- 有index.js
- index.js 依赖了a.js和b.js
- a.js 依赖了b.js
- b.js 又依赖了a.js
问题2解决思路
- 如果发现这个依赖已经被分析过,则退出递归 这样递归就有出口了 也就不会死循环了
问题2解决代码
import { parse } from '@babel/parser';
import _traverse from '@babel/traverse';
const traverse = _traverse.default;
import { readFileSync } from 'fs';
import { resolve, relative, dirname } from 'path';
// 设置根目录
const projectRoot = 'project_3';
// 类型声明
// type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一个空的 depRelation,用于收集依赖
const depRelation = {};
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'));
console.log(depRelation);
console.log('done');
function collectCodeAndDeps(filepath) {
const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
// 获取文件内容,将内容放至 depRelation
if (Object.keys(depRelation).indexOf(key) >= 0) return; // 如果这个文件被分析过 则return 相当于递归的出口
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,需要先把它转为一个绝对路径
console.log(dirname(filepath), path.node.source.value);
const depAbsolutePath = resolve(
dirname(filepath),
path.node.source.value
);
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath);
// 把依赖写进 depRelation
depRelation[key].deps.push(depProjectPath);
collectCodeAndDeps(depAbsolutePath);
}
},
});
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path) {
return relative(projectRoot, path).replace(/\\/g, '/');
}
问题2代码效果
结论
本文探索了babel、ast 以及如何使用ast去分析文件的依赖,为接下来的打包器打下了基础