本文正在参加「金石计划」
大家好,我是小杜杜,在之前的文章讲述了一个概念叫 cli,我们利用 cli 画了一个属于自己标识的 lego,并且完成了一些基础功能,那么今天在此基础上再衍生出一种使用方式,就是:组件统计。
有对 cli 不理解的小伙伴可以先看看之前的两篇文章:
起因
在我们公司中有很多的系统,同时存在多种组件库,比如公共组件(只在当前系统内的组件)、业务组件(所有系统都可使用的组件库)。
当组件的数目越来越多就会造成很多的麻烦的问题。比如:组件迭代进行不下的时候,就会考虑重构,当重构好后就要进行组件更换,我们如何快速去统计组件在系统中的使用次数,以及涉及的页面呢?这时,我们可以依赖 cli 的组件统计功能,帮我们去解决这个问题。
当然,优点不止这一个,我们也可以看看组件的使用率,判断哪些组件是高频组件等作用。
恰好,我对统计组件的功能比较感兴趣,所以在之前的 cli 基础上,加上这个小功能,接下来,我们一起来看看这个功能该如何做。
功能分析
为了演示方便,这里我们在第一条加上统计组件使用次数的功能,如:
我们要想完成这个功能,先进行下简单的分析:
- 首先要有一个针对检索的文件目录,这个目录下的文件我们都要进行检索,统计组件的次数;
- 其次,我们检索的文件并不是所有的文件,组件使用的文件只会是
jsx、或tsx文件,其他文件; - 我们需要判断这些组件的来源是什么,只需要判断引用就可统计出组件的使用次数,当统计后,同时记录组件使用的文件位置即可。
只要完成以上三点就可以完成这个小功能。
遍历文件目录
我们想要遍历对应的文件夹,可以通过 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)
效果
当我们做完这些功能后看看此时的效果:
收集为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 的文件中。如:
domesy-components.xlsx 中:
咦,发现我们的 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);
}
})
同时,我们多加些条件,顺便验证下,之前的多种可能的情况:
完善功能
统计单个组件
我们如果想要看单个组件的使用情况,我们只需要用户去选择是否是全部组件,然后输入想要查看的组件,当然想看的组件也有可能是多个,所以我们简单的处理成数据就 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){
...
}
展示下效果:
其他问题
统计是否准确
问题一:
相信有一些小伙伴可能发现,我们判断的逻辑是 import 引入的方式,但在现实中,虽然文件引入了,但没有使用,这种组件统计进来是否不准。
关于这点,我认为是代码问题,如果没有用到,干嘛要去引用呢?所以建议规范使用。
问题二:
我单独封装了一个组件 A, 组件A 中用到了 shineout 中的 Tabs 和 From,但在cli 中,只会统计组件 A 的次数,并不会统计 Tabs 和 From 的次数,这种是不是不太合理?
如果要这样统计,代码的复杂度会高很多,并不太好做,其次如果引用的是业务组件这种格式,我们并没有办法去获取对应的库,只能统计公共组件的个数。
所以个人感觉并没有太大的必要做这些事。
使用库的地址
在这里,我写的 way 相对简单了,同一个组件也有可能有多种写法,这里的 way 只是记录了最后一个,感兴趣的小伙伴可以自行优化下。
当然有更好的方式,欢迎在评论区讨论,多多讨论~
小结
本章中,我们学会了使用 cli 去统计组件的使用次数,其实还是蛮好玩的,如果有更加好玩的 cli 欢迎评论区讨论。