手写小程序摇树优化工具(六)——主包和子包依赖收集

本文详细介绍了如何实现小程序的主包和子包依赖收集,通过遍历文件和处理json,实现初级摇树优化,以减少不必要的资源打包,提高小程序性能。主要涉及文件遍历、依赖树构建、静态文件拷贝以及组件路径替换等关键步骤。
摘要由CSDN通过智能技术生成

合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下;为者败之,执者失之。是以圣人无为故无败。无执故无失。

github: miniapp-shaking

上一章我们介绍了单个文件怎么深度遍历收集依赖,这一章我们介绍怎么从主包和子包收集整个小程序的依赖。

1. 主包依赖收集

主包的依赖入口文件是很多的,基本上根目录下的文件都可以作为入口,但目录不一样,只有特定的目录才可以作为入口,例如自定义tabbar目录custom-tab-bar,其他的目录如:node_moduleminiprogram_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.jsonsubpackages字段就可以了。

我们新建一个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包
手写小程序摇化工具(九)——删除业务组代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值