likes
comments
collection
share

用nodejs fast-glob高效扫描搜索项目代码

作者站长头像
站长
· 阅读数 18

前言

日常开发时,可能会遇到下面两种问题:

  1. 由于某个前端common控件更新,当使用某个属性时,可能会遇到bug,由于项目很大,需要快速找到有这样使用的地方,然后让对应开发检查下。
  2. 由于项目开发周期很久了,会积累一些页面没有用到的词条,需要删除。

笨一点方法是:

  1. 全局搜索控件名字,比如<CommonButton,然后挨个看是否使用了对应属性,由于控件属性写法很不一样,有的写一行,有的换行的,要写正则表达式也很费劲。
  2. 打开国际化资源文件(一般是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,有一个viewmultiple属性,默认都是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,然后扫描。
    • 我也没写过。
  • 如果各位大佬有啥更好方案,欢迎评论分享。