上一章我们介绍了遍历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;
  }

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

1.1 删除不使用的组件

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

/**
   * 获取wxml所有的标签,包括组件泛型
   * @param filePath
   * @returns {Set<unknown>}
   */
  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;
  }

获取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 取代业务组的组件

上面我们说到了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>

detail.json

{
  "usingComponents": {
    'detail-a': A,
    'detail-b': B,
    'detail-c': C
  }
}

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

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

<detail></detail>

detail.json

{
  "usingComponents": {
    "detail": detail-c,
  },
  "replaceComponents": {
    "A": {
      "detail": detail-a
    },
    "B": {
      "detail": detail-c
    }
  }
}

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

{
  "usingComponents": {
    "detail": detail-a,
  }
}

其他组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);
            }
          }
        });
/**
   *
   * @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.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.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;
  }

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