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


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

    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,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    这里我们写了一个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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    现在只要运行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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    现在这个子包类还比较简单,这只是一个初级的摇树优化,之后为了减少主包的体积,我们将介绍一种更加高级的摇树优化,那时这包就会变得非常复杂了,后面再说。

    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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    首先我们清空输出目录,然后收集主包依赖:

    initMainDepend() {
      console.log('正在生成主包依赖...');
      this.mainDepend = new MainDepend(this.config, '');
      this.mainDepend.run();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后我们收集子包依赖:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    现在所有的依赖文件都已经保存在了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);
            });
          });
        });
      }
      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    我们首先遍历找到所有的静态文件,然后合并主包和子包的文件,并且做了一次去重,然后拷贝这些文件到输出目录。

    前面几章我们介绍到我们在处理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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    最后将主包的依赖树和子包的依赖树组合成一颗树,用于后面生成依赖图。

    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 });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    到这里整个小程序的初级摇树优化就基本完成,更高级的摇树优化请关注后面章节。

    连载文章链接:
    手写小程序摇树工具(一)——依赖分析介绍
    手写小程序摇树工具(二)——遍历js文件
    手写小程序摇树工具(三)——遍历json文件
    手写小程序摇树工具(四)——遍历wxml、wxss、wxs文件
    手写小程序摇树工具(五)——从单一文件开始深度依赖收集
    手写小程序摇树工具(六)——主包和子包依赖收集
    手写小程序摇树工具(七)——生成依赖图
    手写小程序摇树工具(八)——移动独立npm包
    手写小程序摇化工具(九)——删除业务组代码

  • 相关阅读:
    基于ssm+html的小区物业管理系统
    weak的底层原理
    OpenWrt 软路由介绍
    数据查询优化技术方案
    Python网络物品采购系统毕业设计源码031035
    Python爬虫(入门版)
    LyScriptTools Control 调试类API手册
    MySQL数据库干货_29——SQL注入
    获取url动态参数
    Swift 5.9 有哪些新特性(二)
  • 原文地址:https://blog.csdn.net/qq_28506819/article/details/127716487