如何用cli统计项目中组件的使用次数?

3,001 阅读8分钟

本文正在参加「金石计划」

大家好,我是小杜杜,在之前的文章讲述了一个概念叫 cli,我们利用 cli 画了一个属于自己标识的 lego,并且完成了一些基础功能,那么今天在此基础上再衍生出一种使用方式,就是:组件统计

有对 cli 不理解的小伙伴可以先看看之前的两篇文章:

起因

在我们公司中有很多的系统,同时存在多种组件库,比如公共组件(只在当前系统内的组件)、业务组件(所有系统都可使用的组件库)。

当组件的数目越来越多就会造成很多的麻烦的问题。比如:组件迭代进行不下的时候,就会考虑重构,当重构好后就要进行组件更换,我们如何快速去统计组件在系统中的使用次数,以及涉及的页面呢?这时,我们可以依赖 cli 的组件统计功能,帮我们去解决这个问题。

当然,优点不止这一个,我们也可以看看组件的使用率,判断哪些组件是高频组件等作用。

恰好,我对统计组件的功能比较感兴趣,所以在之前的 cli 基础上,加上这个小功能,接下来,我们一起来看看这个功能该如何做。

功能分析

为了演示方便,这里我们在第一条加上统计组件使用次数的功能,如:

image.png

我们要想完成这个功能,先进行下简单的分析:

  1. 首先要有一个针对检索的文件目录,这个目录下的文件我们都要进行检索,统计组件的次数;
  2. 其次,我们检索的文件并不是所有的文件,组件使用的文件只会是 jsx、或tsx文件,其他文件;
  3. 我们需要判断这些组件的来源是什么,只需要判断引用就可统计出组件的使用次数,当统计后,同时记录组件使用的文件位置即可。

只要完成以上三点就可以完成这个小功能。

遍历文件目录

我们想要遍历对应的文件夹,可以通过 glob 去遍历、 path 去寻找路径、fs 读取文件即可。

同时判断其文件是否属于 jsx、tsx 符合条件进行遍历即可,如:

const glob = require("glob");
const fs = require("fs");
const path = require("path");

const moduleDir = path.join(process.cwd(), "project");

module.exports = async () => {
    glob("**/*.{jsx,tsx}", { cwd: moduleDir }, (err, files) => {
       // 处理逻辑
    }
}

组件来源

在上面的三个点中,最难处理的就是统计来源,因为涉及到正则,试了好久才成功,真是哭了~

因为我们在引用时,会有多种引入形式,也就是模块化,这里我就以我们公司的 sheinout 作为引用的来源,作为讲解。(并非正确的使用方式)

引用的组件

首先分为两种形式:

// 第一种
import { Form, List } from 'shineout' 

// 第二种
import Tabs from 'shineout'

相信这是大家常用的引用方式,对于第一种来说,我们应该统计的是 From 、List 两个组件,对于第二种是 Tabs 一种组件。

分别做两种处理,说白了,第一种应该是个数组,而第二种就是单纯的 Tabs。

// 第一种的索引
const Regex1 = /import\s*{\s*([\w\s,]+)\s*}\s*from\s*['"]shineout['"]/g;

// 第二种的索引
const Regex2 = /import\s*([\w]+)\s*from\s*['"]shineout['"]/g;

有了这两种方式,我们就可以正常的做匹配了。

引用地址

当然,除了模块化的问题,还有引用地址的问题,我们不但要知道组件的使用页面,还要知道它的库是哪个,来更好的定位。

但这里也会有多种情况,比如:

import XX from 'sheinout'
import XX from "sheinout"
import XX from "sheinout/XX"
import XX from "../sheinout"
import XX from "../../sheinout/XX/XX"

例如单引号、双引号,前后的路径,都要去做匹配,所以我们要将上面的正则在改一改,如。

// 第一种的索引
const Regex1 = /import\s*{\s*([\w\s,]+)\s*}\s*from\s*['"](?:.*\/)?shineout(?:\/[^'"]+)?['"]/g;

// 第二种的索引
const Regex2 = /import\s*([\w]+)\s*from\s*['"](?:.*\/)?shineout(?:\/[^'"]+)?['"]/g;

同时我们在检索到 import { Form, List } from 'shineout' 这样的数据后需要把 From、List 组件提取出,并把 from 后的引用地址提取出来,可以通过正则去匹配,如:

 const componentNames = match[1].split(",").map((name) => name.trim());
 
 // wayMatch[1] 就是from的内容
 const wayMatch = match[0].match(/from\s+['"]([^'"]+)['"]/); 

统计

当我们把正则弄好后,就大功告成了,接下来我们只需要创建一个结果的对象,进行收集即可,收集的内容主要有四个:

  • componentName:组件的名字;
  • count:使用次数;
  • way:使用库的地址
  • files:使用组件具体的文件地址

如:

    for (const file of files) {
      const extname = path.extname(file);
      if ([ ".jsx", ".tsx"].includes(extname)) {
        const content = fs.readFileSync(path.join(moduleDir, file), "utf-8");
        let match;
        // 第一种:import { Form, List } from 'shineout';
        while ((match = Regex1.exec(content))) {
          const componentNames = match[1].split(",").map((name) => name.trim());
          const wayMatch = match[0].match(/from\s+['"]([^'"]+)['"]/);
          for (const componentName of componentNames) {
            resFile[componentName] = resFile[componentName] || {
              count: 0,
              files: new Set(),
            };
            resFile[componentName].count++;
            resFile[componentName].way = wayMatch[1] || ''
            resFile[componentName].files.add(path.join(moduleDir, file));
          }
        }
      }
    }

第二种处理方式的方式同理,这里不过多赘述。

排序

因为我们要一眼看出哪个组件的使用频率高,因此我们做个简单的排序,如:

const resFileSort = Object.entries(resFile).sort((a, b) => b[1].count - a[1].count)

效果

当我们做完这些功能后看看此时的效果:

img.gif

收集为excel表

我们完善主要的功能后,其实还面临一个问题,就是收集功能。这里只是做了个简单的测试,但项目中的组件肯定不止这些,会非常多,这样搞得控制台会非常多,并且不好做收集。所以我们把这些功能收集成一个 excel 文件,这样能更有效的帮助我们。

这里推荐使用:xlsx-populate

不做过多的赘述,我们直接来看看代码:

    //将统计结果输出到 Excel 表格
    const workbook = XlsxPopulate.fromBlankAsync().then((workbook) => {
      workbook.sheet("Sheet1").cell("A1").value("组件");
      workbook.sheet("Sheet1").cell("B1").value("使用次数");
      workbook.sheet("Sheet1").cell("C1").value("使用库");
      workbook.sheet("Sheet1").cell("D1").value("使用路径");

      let rowNum = 2;
      resFileSort.forEach(([componentName, { count, way, files }]) => {
        const row = workbook.sheet("Sheet1").row(rowNum++);
        row.cell("A").value(componentName);
        row.cell("B").value(count);
        row.cell("c").value(way);
        row.cell("D").value(Array.from(files).join("\n"));
      });

      return workbook.outputAsync();
    });

    workbook.then((data) => {
      fs.writeFileSync(path.join(process.cwd(), "domesy-components.xlsx"), data, "binary");
      console.log("组件统计结果已经输出到 domesy-components.xlsx 文件中。");
    });

我们只需要遍历 resFileSort 即可,输出到对应的表格中,最终会输出一个 domesy-components 的文件中。如:

image.png

domesy-components.xlsx 中: image.png

咦,发现我们的 Tab 引用了两次,但地址都挤在了一行,这时我们对 D 简单的进行下处理就 ok 了,如:

    Array.from(files).map((item, index) => {
      if(index === 0){
        row.cell("D").value(item);
      }else{
        const row = workbook.sheet("Sheet1").row(rowNum++);
        row.cell("D").value(item);
      }
    })

同时,我们多加些条件,顺便验证下,之前的多种可能的情况: img1.gif

image.png

完善功能

统计单个组件

我们如果想要看单个组件的使用情况,我们只需要用户去选择是否是全部组件,然后输入想要查看的组件,当然想看的组件也有可能是多个,所以我们简单的处理成数据就 ok 了。如:

  const answer = await inquirer.prompt([{
      type: "rawlist",
      message: "接下来的操作😎😎😎",
      name: "operation",
      choices: Object.keys({
        "全部组件": "all",
        "特定组件": "only",
      }),
    }]);

    let component_list = []
    if (answer.operation === "特定组件") {
      const name = await inquirer.prompt([{
        type: 'input',
        message: '请输入要检索的组件名称😎😎😎',
        name: 'name',
      }])
      component_list = name.name.split(',')
    }

    ...
    resFileSort.forEach(([componentName, { count, way, files }]) => {
      if(component_list.length !== 0){
        if(component_list.find(v => v === componentName)){
          log(`组件 ${componentName} 被引用了 ${count} 次,使用的库是${way},具体使用的文件位置:`);
          log(Array.from(files).join("\n"));
          log("-------------------------");
        }
      }
      ....
    });

是否下载excel

因为下载 excel 通常只会统计一次,所以我们也给用户一个选项,让用户选择是否去下载,默认为 false,方便用户更好的操作:

    const download = await inquirer.prompt([{
      type: 'confirm',
      message: `是否要生成Excel表(${component_list.length === 0 ? '' : '会统计所有组件,'} 默认为false)?`,
      name: 'flag',
      default: false
    }])

    if(download.flag){
        ...
    }

展示下效果:

img2.gif

其他问题

统计是否准确

问题一:

相信有一些小伙伴可能发现,我们判断的逻辑是 import 引入的方式,但在现实中,虽然文件引入了,但没有使用,这种组件统计进来是否不准。

关于这点,我认为是代码问题,如果没有用到,干嘛要去引用呢?所以建议规范使用。

问题二:

我单独封装了一个组件 A, 组件A 中用到了 shineout 中的 Tabs 和 From,但在cli 中,只会统计组件 A 的次数,并不会统计 Tabs 和 From 的次数,这种是不是不太合理?

如果要这样统计,代码的复杂度会高很多,并不太好做,其次如果引用的是业务组件这种格式,我们并没有办法去获取对应的库,只能统计公共组件的个数。

所以个人感觉并没有太大的必要做这些事。

使用库的地址

在这里,我写的 way 相对简单了,同一个组件也有可能有多种写法,这里的 way 只是记录了最后一个,感兴趣的小伙伴可以自行优化下。

当然有更好的方式,欢迎在评论区讨论,多多讨论~

小结

本章中,我们学会了使用 cli 去统计组件的使用次数,其实还是蛮好玩的,如果有更加好玩的 cli 欢迎评论区讨论。