用nodejs fast-glob高效扫描搜索项目代码
前言
日常开发时,可能会遇到下面两种问题:
- 由于某个前端common控件更新,当使用某个属性时,可能会遇到bug,由于项目很大,需要快速找到有这样使用的地方,然后让对应开发检查下。
- 由于项目开发周期很久了,会积累一些页面没有用到的词条,需要删除。
笨一点方法是:
- 全局搜索控件名字,比如
<CommonButton
,然后挨个看是否使用了对应属性,由于控件属性写法很不一样,有的写一行,有的换行的,要写正则表达式也很费劲。 - 打开国际化资源文件(一般是json),然后挨个词条Key全局搜。
效率会很慢,需要大量人工时间去处理。如果写一个小tool去扫描文件,就会很方便。
fast-glob
首先需要一个全局扫描所有文件的工具,fast-glob,速度非常快的 glob
工具库。
glob
是什么?是一种语法概念,允许使用者通过 “通配符” 来匹配目录和文件,像在nodejs或者webpack plugin里经常遇到的 **/*.js
src/**/package.json
都属于这种语法。fast-glob
则是一款速度非常快的glob
工具库。
首先需要安装包:npm install fast-glob
,因为有很多第三方包已经包含了此依赖,所以如果有包含的,也可以不用单独安装,可以通过npm ls fast-glob
来检查是否包含。
用法也很简单,下面是扫描src路径下所有js、jsx、json类型文件,并输出文件路径、文件名以及文件内容。更多用法参考官网。
import glob from 'fast-glob';
async function scan() {
const files = await glob(['src/**/*.{js,jsx,json}']);
for (const file of files) {
const fileName = file.split('/').reverse()[0];
const content = fs.readFileSync(file, 'utf-8');
console.log(file, fileName, content);
}
}
Case1
需求:需要找出所有用到一个控件某一个或多个属性的地方。
举一个例子:比如React项目,控件名是<CommonButton
,有一个view
和multiple
属性,默认都是false
,需求是要扫出来view=false && multiple=true
的地方,需要考虑属性值,属性位置,换行等情况:
<CommonButton />
<CommonButton ... view={false} ... multiple />
<CommonButton
...
multiple
...
/>
<CommonButton
...
view={!editable}
multiple
...
/>
上面几种情况都是需要扫描出来的。
接下来,就可以写逻辑匹配<CommonButton
控件了,逻辑是扫描每个文件,拆分遍历每行,把每个控件组合成一行,然后判断属性的设置,最后把匹配项的文件名及控件代码保存到数组里。
const results = [];
...
const lines = content.split('\r\n'); // 拆分每行
let codes = [];
lines.forEach((line, i) => { // 遍历每行
const text = line.trim();
if (text.startsWith('<CommonButton')) { // 控件开始
codes.push(line);
}
else if (codes.length > 0) {
codes.push(line);
}
if (codes.length > 0 && text.endsWith('/>')) { // 控件结束
const all = codes.join(' '); // 将整体控件组合成一行
const isNotView = !all.includes(' view ') && !all.includes(' view={true} '); // match noview || `view={false}` || `view={xx}`
const isMultiple = all.includes('multiple') && !all.includes(' multiple={false} '); // match `multiple` `multiple={true}` || `multiple={xx}`
if (isNotView && isMultiple) {
results.push({ file: file, code: codes }); // 保存文件名和匹配代码
}
codes = [];
}
});
然后可以在结果里加个行数、以及给代码缩进,最后把结果生成到json文件里。全部代码如下:
// scan.mjs
import glob from 'fast-glob';
import fs from 'fs';
const results = [];
async function scan() {
const files = await glob(['src/**/*.{js,jsx,json}']);
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8');
const lines = content.split('\r\n');
let codes = [];
let linenum = 0;
let length = 0;
lines.forEach((line, i) => {
const text = line.trim();
if (text.startsWith('<CommonButton')) {
length = line.indexOf('<CommonButton')
codes.push(line.slice(length));
linenum = i + 1;
}
else if (codes.length > 0) {
codes.push(line.slice(length));
}
if (codes.length > 0 && text.endsWith('/>')) {
const all = codes.join(' ');
const isNotView = !all.includes(' view ') && !all.includes(' view={true} '); // match noview || `view={false}` || `view={xx}`
const isMultiple = all.includes('multiple') && !all.includes(' multiple={false} '); // match `multiple` `multiple={true}` || `multiple={xx}`
if (isNotView && isMultiple) {
results.push({ file: file.slice(3), line: linenum, code: codes });
}
codes = [];
}
});
}
fs.writeFileSync('./result.json', JSON.stringify(results, null, 2), { encoding: 'utf8' }, (err) => {
if (err) throw err;
});
}
await scan();
运行 node scan.mjs
结果:
// result.json
[
{
"file": "/App.jsx",
"line": 301,
"code": [
"<CommonButton p1=\"arg1\" multiple />"
]
},
{
"file": "/App.jsx",
"line": 303,
"code": [
"<CommonButton view={false} multiple={true} />"
]
},
{
"file": "/components/Container.jsx",
"line": 23,
"code": [
"<CommonButton",
" p1=\"arg1\"",
" view={!editable}",
" multiple",
" p2={() => { }}",
"/>"
]
}
]
Case2
需求:扫描代码里没有用到的词条。
举一个例子:比如项目里是通过读取json格式国际化资源文件,然后封装common get方法获取词条value。
// i18n/a.en.json
{
"I18N_A_Key1": "value1",
"I18N_A_Key2": "value2",
"I18N_A_Key3": "value3"
}
// i18n/index.js
import a from './a.en.json';
import b from './a.en.json';
const json = { ...a, ...b };
function get(key) {
return json[key];
}
export default { get };
// page.js
import i18n from './i18n';
const text = i18n.get('I18N_A_Key1');
思路是:扫描所有代码,然后粗暴匹配国际化key字符串,找出没有匹配的key。
import glob from 'fast-glob';
import fs from 'fs';
let allKeys = [];
let allContent = '';
async function scan() {
const files = await glob(['src/**/*.{js,jsx,json}']);
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8');
if (file.endsWith('en.json')) {
const json = JSON.parse(content);
allKeys.push('---' + file.slice(3));
allKeys.push(...Object.keys(json));
allKeys.push('');
} else {
allContent += content;
}
}
const duplicateKeys = [];
const sortKeys = [...allKeys].sort();
sortKeys.forEach((key, i) => {
if (key && key === sortKeys[i + 1]) {
duplicateKeys.push(key);
}
});
const unusedKeys = allKeys.filter(key => !key || !allContent.includes(key));
const output = [
'duplicate keys:',
...duplicateKeys,
'',
'unused keys:',
...unusedKeys
].join('\n');
fs.writeFileSync('./result.txt', output, { encoding: 'utf8' }, (err) => {
if (err) throw err;
});
}
await scan();
结果,收集到重复key和没用到的key:
// result.text
duplicate keys:
I18N_A_Key3
unused keys:
---/i18n/a.en.json
I18N_A_Key2
I18N_A_Key3
---/i18n/b.en.json
I18N_B_Key2
I18N_A_Key3
总结
使用fast-glob
可以快速的扫描代码,然后用字符串形式匹配想要的内容。
除了这个,还有别的方式可以解决,不确定有没有用,可以扩展下思维。
- 利用IDE全局正则表达式匹配。
- 需要写正则,还要匹配所有情况。
- AST 抽象语法树,通过解析语法,来找到匹配项。
- 更高级用法,不知道能不能实现上面的需求,我也只是听说过,没用过。
- 可以参考ESLint,自己写个自定义rule,然后扫描。
- 我也没写过。
- 如果各位大佬有啥更好方案,欢迎评论分享。
转载自:https://juejin.cn/post/7229169602420801593