为无为,事无事,味无味。大小多少,抱怨以德。图难于其易,为大于其细。天下难事,必作于易;天下大事,必作于细
github: miniapp-shaking
通过以上各章我们已经知道了处理小程序中各种文件的方法,接下来我们就介绍从单一文件入手,递归的遍历收集这些依赖。还记得我们第二章的时候介绍的BaseDepends的构造器吗?这里再重复一下,以便更容易理解该章节。
class BaseDepend {
constructor(config, rootDir = '') {
// 文件树和相应的大小,用于生成依赖图
this.tree = {
size: 0,
children: {},
};
// 基本配置
this.config = config;
// 是否是主包的标志
this.isMain = true;
// 当前包的根目录
this.rootDir = rootDir;
// 缓存所有依赖的文件
this.files = new Set();
// 当前分包依赖的npm包名称
this.npms = new Set();
// 依赖映射
this.dependsMap = new Map();
// 不需要额外统计的文件
this.excludeFiles = {};
// 当前包的上下文,即包所处的目录
this.context = path.join(this.config.sourceDir, this.rootDir);
}
接下来我们从单一文件开始来深度遍历并收集这些依赖,我们使用files来保存这些依赖文件的路径,它是一个Set集合,可以保证不出现重复的文件。
/**
* 建立依赖树
* @param filePath
*/
addToTree(filePath) {
if (this.files.has(filePath) || this.excludeFiles[filePath]) return;
console.log(filePath);
// 有可能包含主包npm包也可能不包含主npm包
this.addNpmPackages(filePath);
const relPath = this.getRelative(filePath);
const size = this.getSize(filePath);
// 将文件路径转化成数组
// 'pages/index/index.js' =>
// ['pages', 'index', 'index.js']
const names = relPath.split(path.sep);
const lastIdx = names.length - 1;
this.tree.size += size;
let point = this.tree.children;
names.forEach((name, idx) => {
if (idx === lastIdx) {
point[name] = { size };
return;
}
if (!point[name]) {
point[name] = {
size, children: {},
};
} else {
point[name].size += size;
}
point = point[name].children;
});
this.files.add(filePath);
// ===== 获取文件依赖,并添加到树中 =====
const deps = this.getDeps(filePath);
// 保持依赖映射
this.dependsMap.set(filePath, deps);
console.log('deps:', deps);
deps.forEach(dep => {
this.addToTree(dep);
});
}
从一行可以看到,如果文件存在files集合中,说明已经遍历过该文件了,我们直接返回,因为可能有多个文件同时导入了一个文件,不需要重复遍历。
接下里我们看这个文件是否是一个npm包的文件,如果是,我们将该npm的包名存入npms集合中,npm包集合用来干什么我们后面再说。在config类中我们有看到npmRegexp是一个正则表达式,mac和window的路径分割符号是不同的,这里需要特别注意。
// npm包正则匹配表达式,兼容mac和window
const NPM_REGEXP = path.sep === '/' ? /miniprogram_npm\/(.*?)\// : /miniprogram_npm\\(.*?)\\/;
/**
* 收集该包依赖的npm包
* @param filePath
*/
addNpmPackages(filePath) {
const result = filePath.match(this.config.npmRegexp);
if (result) {
this.npms.add(result[1]);
}
}
接下来我们获取文件的相对路径和文件大小,这主要是用来构建依赖树的。
/**
* 获取相对当前包根目录的相对地址
* @param filePath
* @return {*}
*/
getRelative(filePath) {
return path.relative(this.context, filePath);
}
/**
* 计算文件的大小,转换为Kb
* @param filePath
* @returns {number}
*/
getSize(filePath) {
const stats = fse.statSync(filePath);
return stats.size / 1024;
}
可以在构造器中看到一个字段:
// 文件树和相应的大小,用于生成依赖图
this.tree = {
size: 0,
children: {},
};
这里就是一个依赖树,每个节点由size(当前节点的大小),children(当前的节点的孩子)组成,根节点的size就是整个包的大小。通过路径分割和累加,我们分别统计了每个文件夹和文件的大小。
然后我们会用该json对象构造一个类似微信开发者工具中的依赖分析图。通过可视化你能够更加清楚的知道自己摇树优化之后的文件依赖的关系,这个后面再说。
const relPath = this.getRelative(filePath);
const size = this.getSize(filePath);
// 将文件路径转化成数组
// 'pages/index/index.js' =>
// ['pages', 'index', 'index.js']
const names = relPath.split(path.sep);
const lastIdx = names.length - 1;
this.tree.size += size;
let point = this.tree.children;
names.forEach((name, idx) => {
if (idx === lastIdx) {
point[name] = { size };
return;
}
if (!point[name]) {
point[name] = {
size, children: {},
};
} else {
point[name].size += size;
}
point = point[name].children;
});
接下来我们将文件保存到文件集合中,并且通过我们前几章介绍的方法获取该文件的依赖,然后深度递归这些依赖。这样我们就从一个文件入口找到了与这个文件相关联的所有依赖了,并且我们保存了每个文件的依赖映射。
this.files.add(filePath);
// 获取文件依赖,并添加到树中
const deps = this.getDeps(filePath);
// 保持依赖映射
this.dependsMap.set(filePath, deps);
console.log('deps:', deps);
deps.forEach(dep => {
this.addToTree(dep);
});
现在我们知道了单个文件怎么遍历了,下一章我们介绍主包和子包的遍历方法。
欲知后文请关注下一章。
连载文章链接:
手写小程序摇树工具(一)——依赖分析介绍
手写小程序摇树工具(二)——遍历js文件
手写小程序摇树工具(三)——遍历json文件
手写小程序摇树工具(四)——遍历wxml、wxss、wxs文件
手写小程序摇树工具(五)——从单一文件开始深度依赖收集
手写小程序摇树工具(六)——主包和子包依赖收集
手写小程序摇树工具(七)——生成依赖图
手写小程序摇树工具(八)——移动独立npm包