• 手写小程序摇树工具(三)——遍历json文件


    见素包朴,少私寡欲,绝学无忧

    github: miniapp-shaking

    上一章我们介绍了遍历js文件的方法,接下来我们介绍其他文件的遍历。

    1. 遍历JSON文件

    对于json文件,我们直接读取json文件,然后转化为json对象来处理。json文件中我们主要处理的是组件的json和app.json,微信小程序中有一些特殊的字段会对外产生依赖:pagesusingComponentscomponentGenericscomponentPlaceholder,这里需要特别注意后面两个,即抽象节点组件和分包异步化之后的占位组件。另外我还添加了一个自定义的字段replaceComponents用于处理组名的问题,在上一章中我曾经介绍过config里面有一个字段叫groupName,这里就是用于定义不同业务组中组件的地方。

    /**
       * 收集json文件依赖
       * @param file
       * @returns {[]}
       */
      jsonDeps(file) {
        const deps = [];
        const dirName = path.dirname(file);
        // json中有关依赖的关键字段
        const { pages, usingComponents, replaceComponents,  componentGenerics, componentPlaceholder} = fse.readJsonSync(file);
        // 处理有pages的json,一般是主包
        if (pages && pages.length) {
          pages.forEach(page => {
            this.addPage(page);
          });
        }
        // 处理有usingComponents的json,一般是组件
        if (usingComponents && typeof usingComponents === 'object' && Object.keys(usingComponents).length) {
          // 获取改组件下的wxml的所有标签,用于下面删除无用的组件
          const tags = this.getWxmlTags(file.replace('.json', '.wxml'));
          Object.keys(usingComponents).forEach(key => {
            // 对于没有使用的组件,不需要依赖
            if (tags.size && !tags.has(key.toLocaleLowerCase())) return;
            let filePath;
            // 如有需要,替换组件
            const rcomponents = replaceComponents ? replaceComponents[this.config.groupName] : null;
            const component = getReplaceComponent(key, usingComponents[key], rcomponents);
    
            if (component.startsWith('../') || component.startsWith('./')) {
              // 处理相对路径
              filePath = path.resolve(dirName, component);
            } else if (component.startsWith('/')) {
              // 处理绝对路径
              filePath = path.join(this.config.sourceDir, component.slice(1));
            } else {
              // 处理npm包
              filePath = path.join(this.config.sourceDir, 'miniprogram_npm', component);
            }
            // 对于json里面依赖的组价,每一个路径对应组件的四个文件: .js,.json,.wxml,wxss
            this.config.fileExtends.forEach((ext) => {
              const temp = this.replaceExt(filePath, ext);
              if (this.isFile(temp)) {
                deps.push(temp);
              } else {
                const indexPath = this.getIndexPath(temp);
                if (this.isFile(indexPath)) {
                  deps.push(indexPath);
                }
              }
            });
          });
        }
        // 添加抽象组件依赖
        const genericDefaultComponents = this.getGenericDefaultComponents(componentGenerics, dirName);
        // 添加分包异步化占用组件
        const placeholderComponents = this.getComponentPlaceholder(componentPlaceholder, dirName);
        deps.push(...genericDefaultComponents);
        deps.push(...placeholderComponents);
        return deps;
      }
    
    • 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

    一般来说,pages只存在于app.json中,usingComponents存在于组件的json中,因此这两者在同一个文件中是互斥的,但我们这里用同一个函数来处理。对于pages的处理后面再统一说明,我们先说usingComponents的处理。

    1.1 删除不使用的组件

    对于usingComponents的处理,我先调用了一个getWxmlTags的方法来获取组件对应的wxml文件的标签,为什么要多于做这一步?因为在公司中我发现很多在json中定义的组件并没有真正的用于wxml中,或许是由于疏忽还是在迭代中忘记了,而且一个组件可能依赖很多其他的组件,依赖组件在依赖其他组件,这种深层次的递归所引用的文件是很庞大的 。因此在这里我会把在usingComponents中定义但没有实际在wxml中使用的组件给删除掉,这样就可以节省很大的空间,算是细节点之一。

    /**
       * 获取wxml所有的标签,包括组件泛型
       * @param filePath
       * @returns {Set}
       */
      getWxmlTags(filePath) {
        let needDelete = true;
        const tags = new Set();
        if (fse.existsSync(filePath)) {
          const content = fse.readFileSync(filePath, 'utf-8');
          const htmlParser = new htmlparser2.Parser({
            onopentag(name, attribs = {}) {
              if ((name === 'include' || name === 'import') && attribs.src) {
                // 不删除具有include和import的文件,因为不确定依赖的wxml文件是否会包含组件
                needDelete = false;
              }
              tags.add(name);
              // 特别处理泛型组件
              const genericNames = getGenericName(attribs);
              genericNames.forEach(item => tags.add(item.toLowerCase()));
            },
          });
          htmlParser.write(content);
          htmlParser.end();
        }
        if (!needDelete) {
          tags.clear();
        }
        return tags;
      }
    
    • 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

    获取wxml的标签需要使用到htmlparser2包,在这里我对于使用了includeimport导入外部文件的wxml不做递归的深入处理了,直接跳过偷下懒吧,以后有时间在做。在这里我们还要特别注意范型组件,json中定义的组件可能并不是作为一个标签使用,而是作为范型组件使用,这里也算是一个细节点之一。要想做好一个东西,需要考虑的东西实在太多了,cry。

    /**
     * 解析泛型组件名称
     * @param attribs
     * @returns {[]}
     */
    function getGenericName(attribs = {}) {
      let names = [];
      Object.keys(attribs).forEach(key => {
        if (/generic:/.test(key)) {
          names.push(attribs[key]);
        }
      });
      return names;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    1.2 取代业务组的组件

    上面我们说到了groupNamereplaceComponents字段,为什么我要新增这样一个字段呢?如果你的公司很庞大,有很多的业务组,为了减少重复开发和复用逻辑或者是客开逻辑,他们的代码往往是可能放在同一个页面或者组件的,例如你可能会做一个详情页,这个详情页有不同业务组的详情组件,通常你会通过wx:if来判断使用哪个详情组件,但这样也把其他业务组的组件打包进来了,实际上别的业务的组件对你来说毫无用处,这些代码是应该去掉的。举个的例子:
    detail.wxml

    <detail-a wx:if="{{ flag === 'A' }}">detail-a>
    <detail-b wx:elif="{{ flag === 'B' }}">detail-b>
    <detail-c wx:else>detail-c>
    
    • 1
    • 2
    • 3

    detail.json

    {
      "usingComponents": {
        'detail-a': A,
        'detail-b': B,
        'detail-c': C
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果你是业务组A,那么你就把业务组B和业务组C的代码都打包进来了。当然这是对于大公司的复杂逻辑而言,一般情况不必考虑这么复杂的场景。

    现在我们换一种写法,添加一个replaceComponents字段:
    detail.wxml

    <detail>detail>
    
    • 1

    detail.json

    {
      "usingComponents": {
        "detail": detail-c,
      },
      "replaceComponents": {
        "A": {
          "detail": detail-a
        },
        "B": {
          "detail": detail-c
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里我们只有一个详情页,打包工具在打包的时候对于groupNameA的会直接使用detail-a,对于B同理。即打包之后会变成:

    {
      "usingComponents": {
        "detail": detail-a,
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其他组B、组C的代码会被忽略,从而大大减少包的大小,对于大公司的复杂业务逻辑超包的问题尤其有用。但是有利也有弊,在开发的时候由于默认使用的是detail-c,所以A开发的时候需要暂时替换成detail-a,但是相对于超包发布不了或者提高加载性能而言,这些好像也能够接受,全看自己取舍吧。

    1.3 一个路径4个文件

    接下来就是路径的判断了,和js的处理差不多。

    对于json中定义的组件,我们知道微信的组件是由4个文件组成的js,json,wxml,wxss,所以接下来我们对于每个组件,需要生成四个依赖,即这段代码:

    // 对于json里面依赖的组价,每一个路径对应组件的四个文件: .js,.json,.wxml,wxss
            this.config.fileExtends.forEach((ext) => {
              const temp = this.replaceExt(filePath, ext);
              if (this.isFile(temp)) {
                deps.push(temp);
              } else {
                const indexPath = this.getIndexPath(temp);
                if (this.isFile(indexPath)) {
                  deps.push(indexPath);
                }
              }
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    /**
       *
       * @param filePath
       * @param ext
       * @returns {string}
       */
      replaceExt(filePath, ext = '') {
        const dirName = path.dirname(filePath);
        const extName = path.extname(filePath);
        const fileName = path.basename(filePath, extName);
        return path.join(dirName, fileName + ext);
      }
    /**
       * 获取index文件的路径
       * @param filePath
       * @returns {string}
       */
      getIndexPath(filePath) {
        const ext = path.extname(filePath);
        const index = filePath.lastIndexOf(ext);
        return filePath.substring(0, index) + path.sep + 'index' + ext;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.4 处理范型默认组件

    需要注意范型组件支持默认组件,我们也同样需要处理,这也是细节之一。

    /**
       * 处理泛型组件的默认组件
       * @param componentGenerics
       * @param dirName
       * @returns {[]}
       */
      getGenericDefaultComponents(componentGenerics, dirName) {
        const deps = [];
        if (componentGenerics && typeof componentGenerics === 'object') {
          Object.keys(componentGenerics).forEach(key => {
            if (componentGenerics[key].default) {
              let filePath = componentGenerics[key].default;
              if (filePath.startsWith('../') || filePath.startsWith('./')) {
                filePath = path.resolve(dirName, filePath);
              } else if (filePath.startsWith('/')) {
                filePath = path.join(this.config.sourceDir, filePath.slice(1));
              } else {
                filePath = path.join(this.config.sourceDir, 'miniprogram_npm', filePath);
              }
              this.config.fileExtends.forEach((ext) => {
                const temp = this.replaceExt(filePath, ext);
                if (this.isFile(temp)) {
                  deps.push(temp);
                } else {
                  const indexPath = this.getIndexPath(temp);
                  if (this.isFile(indexPath)) {
                    deps.push(indexPath);
                  }
                }
              });
            }
          });
        }
        return deps;
      }
    
    • 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

    1.5 处理分包异步化的占位组件

    分包异步化之后,又有了占位组件,也需要同样处理。

    /**
       * 处理分包异步化的站位组件
       * @param componentPlaceholder
       * @param dirName
       * @returns {[]}
       */
      getComponentPlaceholder(componentPlaceholder, dirName) {
        const deps = [];
        if (componentPlaceholder && typeof componentPlaceholder === 'object' && Object.keys(componentPlaceholder).length) {
          Object.keys(componentPlaceholder).forEach(key => {
            let filePath;
            const component = componentPlaceholder[key];
            // 直接写view的不遍历
            if (component === 'view' || component === 'text') return;
    
            if (component.startsWith('../') || component.startsWith('./')) {
              // 处理相对路径
              filePath = path.resolve(dirName, component);
            } else if (component.startsWith('/')) {
              // 绝对相对路径
              filePath = path.join(this.config.sourceDir, component.slice(1));
            } else {
              // 处理npm包
              filePath = path.join(this.config.sourceDir, 'miniprogram_npm', component);
            }
            this.config.fileExtends.forEach((ext) => {
              const temp = this.replaceExt(filePath, ext);
              if (this.isFile(temp)) {
                deps.push(temp);
              } else {
                const indexPath = this.getIndexPath(temp);
                if (this.isFile(indexPath)) {
                  deps.push(indexPath);
                }
              }
            });
          });
        }
        return deps;
      }
    
    • 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

    到此为止json的处理基本讲完了,有点长,细节也非常多,做这种工具需要平心静气,认真思考,不然一招不慎满盘皆输。还剩下一个pages字段的处理留到最后再说,这涉及到入口的遍历。

    欲知后文请关注下一章。

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

  • 相关阅读:
    安卓项目结构分析
    微软出品自动化神器Playwright,不用写一行代码(Playwright+Java)系列
    融云通信“三板斧”,“砍”到了银行的心坎上
    Nginx学习笔记09——URLRewrite伪静态
    excel每行数据按模板导出一个或多个文件,可插入图片、条形码或二维码
    关于CSDN编程竞赛第五期
    云服务器利用Docker搭建sqli-labs靶场环境
    Cairo介绍及源码构建安装(3)
    Java版本spring cloud + spring boot企业电子招投标系统源代码
    学习和认知的四个阶段,以及学习方法分享
  • 原文地址:https://blog.csdn.net/qq_28506819/article/details/127712605