使用AST去除项目重复请求代码

发布于:2024-04-25 ⋅ 阅读:(9) ⋅ 点赞:(0)

背景

在开发Vue项目时,我们经常会遇到一种情况:同样的API请求代码在多个组件或页面中被重复书写。这不仅增加了代码冗余,也降低了项目的可维护性。手动查找和替换这些重复的代码是一项耗时且容易出错的任务。

那么,有没有一种方法可以自动遍历代码,并帮助我们去除这些重复请求呢?答案是肯定的,我们可以使用抽象语法树(AST)来实现这一目标。

1. 实现思路

项目目录结构:

├─api.js
├─src
|  ├─index.vue
  1. 项目所有请求的api放在api.js文件中,使用ast遍历出重复url(判断下请求方式),输出文件
  2. 通过递归遍历src下的.vue文件,使用vue-compiler-template将script标签的中js代码进行ast分析,筛选出为api.xxx形式的代码,使用1中的结果,判断vue文件中是否是相同的请求,如果是则进行替换.
  3. 最后重新写入文件

2. 具体代码实现

2.1 安装依赖

首先,我们需要安装一些用于处理AST的依赖库。

  • @babel/parser: 解析代码并生成AST
  • @babel/traverse: 遍历AST
  • @babel/generator: 将修改后的AST重新生成代码
  • vue-template-compiler 解析vue文件内容

2.2 遍历api.js文件找出重复请求

例如:api.js文件内容

const api = {
  queryUrl:r => http.post("/api/hotel/queryImgUrl.json", r),
  query1Url:r => http.post("/api/hotel/queryImgUrl.json", r),
}
export default api;

遍历代码:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const fs = require("fs");

const findApi = (arg) => {
    if (arg.type !== 'StringLiteral') return findApi(arg.left)
    return arg.value.split('?')[0]
}

function getRepeatApi(filePath) {
    return new Promise((resolve, reject) => {

        const source = fs.readFileSync(filePath, 'utf-8');

        const ast = parser.parse(source, {
            sourceType: "module",
        });

        const map = new Map();

        // 遍历ast
        traverse(ast, {
            ObjectExpression({ node }) {
                // 过滤节点
                node.properties = node.properties.filter(item => {
                    const name = item.key.name // key
                    const { start, end } = item.loc; // 行列
                    const method = item.value.body.callee.property.name // get、post
                    const args = item.value.body.arguments // 参数

                    const api = findApi(args[0])

                    const mapKey = `${api}-${method}`
                    const mapValue = {
                        start,
                        end,
                        name,
                        api,
                    }

                    if (map.has(mapKey)) {
                        item.key.name = 'xxx'
                        // console.log('打印***str', start.line, `${mapKey} 重复`)
                        // 找到value值
                        const value = map.get(mapKey)
                        value.push(mapValue)
                        map.set(mapKey, value)
                        return false
                    } else {
                        map.set(mapKey, [mapValue])
                        return true
                    }
                })
            },
        });
        // 重新生成代码
        const { code } = generator(ast, {}, source);

        // 重写api文件
        fs.writeFileSync(filePath, code)
        resolve(new Map(Array.from(map).filter(([key, value]) => value.length > 1)))
    })
}

上述代码读取api.js文件内容,使用@babel/parser转换成AST,通过@babel/traverse进行节点遍历,只遍历对象节点,找出节点中存在重复的url存在map中,并且重新过滤该节点。

对象节点结构: image.png map重复结果: image.png

2.3 将结果写入文件

写入文件,可以直观的看下重复的名称。

function writeToFile(map, outputName = 'result') {
    const res = new Map()

    let txt = ''
    let sum = 0
    map.forEach((info, key) => {
        if (info.length > 1) {
            const resKey = info.map(item => item.name)
            const resValue = info[0].name
            res.set(resKey, resValue)
            sum++
            info.forEach(({ start, end, name, api }, index) => {
                if (index === 0) {
                    txt += `${api} 重复:
    第${index + 1}个位置: 开始于${start.line}行,第${start.column}列,结束于${end.line}行,第${end.column}列, ${name}\n`
                } else {
                    txt += `    第${index + 1}个位置: 开始于${start.line}行,第${start.column}列,结束于${end.line}行,第${end.column}列,${name} \n`
                }
            })

        }
    })
    txt = `重复的api数量: ${sum}\n` + txt

    fs.writeFileSync(`./${outputName}.txt`, txt);
    console.log('打印***res', res)
    return res
}

image.png

2.4 将vue文件中的api.xxx替换

vue文件中都是以import api from api.js形式导入。当使用api.xxxapi.yyy是重复时,我们暂时以在api.js第一次遍历的键为准,即后面重复的都使用api.xxx进行替换。

具体代码:

const fs = require('fs').promises;
const path = require('path');
const vueCompiler = require('vue-template-compiler');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

async function findVueFiles(dir, map) {
    // 读取目录中的所有文件和子目录
    const entries = await fs.readdir(dir, { withFileTypes: true });
    const vueReplaceFiles = [];
    // 遍历每个文件或子目录
    for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            // 如果是目录,递归查找  
            const nestedVueFiles = await findVueFiles(fullPath, map);
            // vueFiles.push(...nestedVueFiles);
        } else if (entry.name.endsWith('.vue')) {
            // 如果是.vue文件,添加到结果数组中  
            // vueFiles.push(fullPath);
            const found = await replaceApi(fullPath, map)
            found && vueReplaceFiles.push(fullPath);
        }
    }
    return vueReplaceFiles;
}

/** 处理vue文件中重复的api 替换成一个 */
async function replaceApi(filePath, map) {
    const fileContent = await fs.readFile(filePath, 'utf-8');
    const result = vueCompiler.parseComponent(fileContent/*, { pad: 'line' }*/);
    const scriptContent = result.script.content

    if (!scriptContent) return false

    let found = false
    const ast = parser.parse(scriptContent, {
        sourceType: "module",
        plugins: ['jsx']
    });
    traverse(ast, {
        // 遍历AST,查找重复的api调用
        MemberExpression(path) {
            // 检查是否为api.xxx的调用
            if (
                path.node.object.type === 'Identifier' &&
                path.node.object.name === 'api' &&
                path.node.property.type === 'Identifier'
            ) {
                const name = path.node.property.name;
                map.forEach((value, key) => {
                    if (key.includes(name) && name !== value) {
                        found = true
                        path.node.property.name = value;
                        console.log(`In ${filePath.slice(filePath.indexOf('/src'))},Found call api.${name} replaced to api.${value} success`);
                    }
                })

            }
        },

    })
    if (!found) return false

    // 将修改后的 JavaScript AST 转换为代码
    const modifiedScriptContent = generator(ast, {}).code;

    // 找到 <script> 标签的位置
    const scriptStartIndex = fileContent.indexOf('<script>') + '<script>'.length;
    const scriptEndIndex = fileContent.indexOf('</script>');

    // 替换 <script> 内容
    const modifiedFileContent =
        fileContent.substring(0, scriptStartIndex) +
        '\n' + // 确保添加一个换行符
        modifiedScriptContent +
        '\n' + // 确保添加一个换行符
        fileContent.substring(scriptEndIndex);


    // // 将修改后的内容写回到原始文件中
    await fs.writeFile(filePath, modifiedFileContent, 'utf-8');
    console.log(`File ${filePath} has been updated.`);
    return true
}

  • findVueFiles 找出所有的vue文件路径
  • replaceApi 替换vue文件中重复的api
    • 使用vue-template-compiler解析文件内容
    • 继续使用babel三剑客进行解析、遍历、生成
    • 替换script标签内容 生成文件

其中vue解析的结果如下: image.png

2.5 具体效果

image.png

image.png

3. 总结

最后一句话总结,不同文件使用插件解析成ast,替换满足条件的api,写入原文件。

如有错误,请指正O^O!