合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下;为者败之,执者失之。是以圣人无为故无败。无执故无失。
github: miniapp-shaking
上一章我们介绍了单个文件怎么深度遍历收集依赖,这一章我们介绍怎么从主包和子包收集整个小程序的依赖。
1. 主包依赖收集
主包的依赖入口文件是很多的,基本上根目录下的文件都可以作为入口,但目录不一样,只有特定的目录才可以作为入口,例如自定义tabbar目录custom-tab-bar
,其他的目录如:node_module
,miniprogram_npm
,以及子包目录不应该作为入口。
我们写一个MainDepend
类来遍历主包的文件,该类继承了BaseDepend
const path = require('path');
const fse = require('fs-extra');
const { BaseDepend } = require('./BaseDepend');
class MainDepend extends BaseDepend {
constructor(config, rootDir = '') {
super(config, rootDir);
}
run() {
let files = fse.readdirSync(this.context);
files = files.filter(file => {
return !this.config.excludeFiles.includes(file) && this.config.fileExtends.includes(path.extname(file));
});
let tabBarFiles = [];
if (this.config.needCustomTabBar) {
tabBarFiles = fse.readdirSync(path.join(this.context, 'custom-tab-bar'));
if (tabBarFiles.length) {
tabBarFiles = tabBarFiles.map(item => {
return `custom-tab-bar/${item}`;
});
}
}
console.log(files);
files.push(...tabBarFiles);
files.forEach(file => {
const filePath = this.getAbsolute(file);
if (fse.pathExistsSync(filePath)) {
this.addToTree(filePath);
}
});
return this;
}
}
module.exports = {
MainDepend,
};
这里我们写了一个run方法来遍历整个主包,我们首先读取根目录下的所有文件,然后过滤需要排除的文件(默认的排除文件是'package-lock.json', 'package.json'
)。通过文件后缀名,我们过滤掉了所有的目录。并且单独处理了自定tabbar的目录。
然后对于每一个文件我们获取它的绝对路径,然后调用上一章我们提到的addToTree
方法深度递归遍历这些文件树。这样我们就得到了整个主包的文件依赖。
当遇到app.json
的时候它就会遍历它的pages
字段,前几章处理json
文件的时候并没有说明如何处理pages
字段。这里说明一下:
/**
* 添加一个页面
* @param page
*/
addPage(page) {
const absPath = this.getAbsolute(page);
// 每一个页面对应四个文件
this.config.fileExtends.forEach(ext => {
const filePath = this.replaceExt(absPath, ext);
if (this.isFile(filePath)) {
// 处理定位到文件的情况
this.addToTree(filePath);
} else {
// 可能省略index的情况
const indexPath = this.getIndexPath(filePath);
if (this.isFile(indexPath)) {
this.addToTree(filePath);
}
}
});
}
/**
* 获取当前文件的绝对路径
* @param file
* @returns {string}
*/
getAbsolute(filePath) {
return path.join(this.context, filePath);
}
现在只要运行run
方法,整个主包的文件依赖都已经保存在files
集合里了。未使用的文件统统被过滤掉了,包括了npm
包中的文件。
试想一下,如果你引入了一个npm
包(如iview
),你就使用了里面的几个组件,完全就没必要把整个iview
打包进来。
2. 子包依赖收集
子包的依赖收集就更加简单了,只要遍历app.json
的subpackages
字段就可以了。
我们新建一个SubDepend
继承BaseDepend
:
class SubDepend extends BaseDepend {
constructor(config, rootDir, mainDepend) {
super(config, rootDir);
this.isMain = false;
// 主包已经依赖过的文件
this.excludeFiles = this.initExcludesFile(mainDepend.files);
}
}
现在这个子包类还比较简单,这只是一个初级的摇树优化,之后为了减少主包的体积,我们将介绍一种更加高级的摇树优化,那时这包就会变得非常复杂了,后面再说。
3. 处理整个小程序的依赖收集
我们已经有了主包和子包依赖收集的类,现在缺少一个类把这两者组合起来。
为此我们新建一个依赖容器类DependContainer
class DependContainer {
constructor(options) {
this.config = new ConfigService(options);
}
async init() {
this.clear();
this.initMainDepend();
this.initSubDepend();
const allFiles = await this.copyAllFiles();
this.replaceComponentsPath(allFiles);
if (this.config.analyseDir) {
this.createTree();
}
console.log('success!');
}
clear() {
fse.removeSync(this.config.targetDir);
}
}
首先我们清空输出目录,然后收集主包依赖:
initMainDepend() {
console.log('正在生成主包依赖...');
this.mainDepend = new MainDepend(this.config, '');
this.mainDepend.run();
}
然后我们收集子包依赖:
initSubDepend() {
console.log('正在生成子包依赖...');
const { subPackages, subpackages } = fse.readJsonSync(path.join(this.config.sourceDir, 'app.json'));
const subPkgs = subPackages || subpackages;
console.log('subPkgs', subPkgs);
const subDepends = [];
if (subPkgs && subPkgs.length) {
subPkgs.forEach(item => {
const subPackageDepend = new SubDepend(this.config, item.root, this.mainDepend);
item.pages.forEach(page => {
subPackageDepend.addPage(page);
});
subDepends.push(subPackageDepend);
});
}
this.subDepends = subDepends;
}
现在所有的依赖文件都已经保存在了files
集合中了,然后我们拷贝这些文件到输出目录:
async copyAllFiles() {
let allFiles = this.getAllStaticFiles();
console.log('正在拷贝文件....');
const allDepends = [this.mainDepend].concat(this.subDepends);
allDepends.forEach(item => {
allFiles.push(...Array.from(item.files));
});
allFiles = Array.from(new Set(allFiles));
await this._copyFile(allFiles);
return allFiles;
}
getAllStaticFiles() {
console.log('正在寻找静态文件...');
const staticFiles = [];
this._walkDir(this.config.sourceDir, staticFiles);
return staticFiles;
}
_walkDir(dirname, result) {
const files = fse.readdirSync(dirname);
files.forEach(item => {
const filePath = path.join(dirname, item);
const data = fse.statSync(filePath);
if (data.isFile()) {
if (this.config.staticFileExtends.includes(path.extname(filePath))) {
result.push(filePath);
}
} else if (dirname.indexOf('node_modules') === -1 && !this.config.excludeFiles.includes(dirname)) {
const can = this.config.excludeFiles.some(file => {
return dirname.indexOf(file) !== -1;
});
if (!can) {
this._walkDir(filePath, result);
}
}
});
}
_copyFile(files) {
return new Promise((resolve) => {
let count = 0;
files.forEach(file => {
const source = file;
const target = file.replace(this.config.sourceDir, this.config.targetDir);
fse.copy(source, target).then(() => {
count++;
if (count === files.length) {
resolve();
}
}).catch(err => {
console.error(err);
});
});
});
}
我们首先遍历找到所有的静态文件,然后合并主包和子包的文件,并且做了一次去重,然后拷贝这些文件到输出目录。
前面几章我们介绍到我们在处理usingComponents
字段的时候,去掉了未使用的组件以及使用groupName
去掉了非自己业务组的组件。虽然组件被排除在了遍历树之外,其json还是没有改变的,这会导致编译报错,因此我们还要改变这些json
。此时我们已经拷贝完文件了,我们完全可以修改这些json而完全不会影响源码。
replaceComponentsPath(allFiles) {
console.log('正在取代组件路径...');
const jsonFiles = allFiles.filter(file => file.endsWith('.json'));
jsonFiles.forEach(file => {
const targetPath = file.replace(this.config.sourceDir, this.config.targetDir);
const content = fse.readJsonSync(targetPath);
const { usingComponents, replaceComponents } = content;
// 删除未使用的组件
let change = false;
if (usingComponents && typeof usingComponents === 'object' && Object.keys(usingComponents).length) {
change = this.deleteUnusedComponents(targetPath, usingComponents);
}
// 替换组件
const groupName = this.config.groupName;
if (
replaceComponents
&& typeof replaceComponents[groupName] === 'object'
&& Object.keys(replaceComponents[groupName]).length
&& usingComponents
&& Object.keys(usingComponents).length
) {
Object.keys(usingComponents).forEach(key => {
usingComponents[key] = getReplaceComponent(key, usingComponents[key], replaceComponents[groupName]);
});
delete content.replaceComponents;
}
// 全部写一遍吧,顺便压缩
fse.writeJsonSync(targetPath, content);
});
}
/**
* 删除掉未使用组件
* @param jsonFile
* @param usingComponents
*/
deleteUnusedComponents(jsonFile, usingComponents) {
let change = false;
const file = jsonFile.replace('.json', '.wxml');
if (fse.existsSync(file)) {
let needDelete = true;
const tags = new Set();
const content = fse.readFileSync(file, 'utf-8');
const htmlParser = new htmlparser2.Parser({
onopentag(name, attribs = {}) {
if ((name === 'include' || name === 'import') && attribs.src) {
// 不删除具有include和import的文件
needDelete = false;
}
tags.add(name);
const genericNames = getGenericName(attribs);
genericNames.forEach(item => tags.add(item.toLocaleLowerCase()));
},
});
htmlParser.write(content);
htmlParser.end();
if (needDelete) {
Object.keys(usingComponents).forEach(key => {
if (!tags.has(key.toLocaleLowerCase())) {
change = true;
delete usingComponents[key];
}
});
}
}
return change;
}
最后将主包的依赖树和子包的依赖树组合成一颗树,用于后面生成依赖图。
createTree() {
console.log('正在生成依赖图...');
const tree = { [this.config.mainPackageName]: this.mainDepend.tree };
this.subDepends.forEach(item => {
tree[item.rootDir] = item.tree;
});
fse.copySync(path.join(__dirname, '../analyse'), this.config.analyseDir);
fse.writeJSONSync(path.join(this.config.analyseDir, 'tree.json'), tree, { spaces: 2 });
}
到这里整个小程序的初级摇树优化就基本完成,更高级的摇树优化请关注后面章节。
连载文章链接:
手写小程序摇树工具(一)——依赖分析介绍
手写小程序摇树工具(二)——遍历js文件
手写小程序摇树工具(三)——遍历json文件
手写小程序摇树工具(四)——遍历wxml、wxss、wxs文件
手写小程序摇树工具(五)——从单一文件开始深度依赖收集
手写小程序摇树工具(六)——主包和子包依赖收集
手写小程序摇树工具(七)——生成依赖图
手写小程序摇树工具(八)——移动独立npm包
手写小程序摇化工具(九)——删除业务组代码