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

背景

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

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

1. 实现思路

项目目录结构:

css 复制代码
├─api.js
├─src
|  ├─index.vue
  1. 项目所有请求的api放在api.js文件中,使用ast遍历出重复url(判断下请求方式),输出文件
  2. 通过递归遍历src下的.vue文件,使用vue-comiler-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文件内容

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

遍历代码:

js 复制代码
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中,并且重新过滤该节点。

对象节点结构: map重复结果:

2.3 将结果写入文件

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

js 复制代码
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
}

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

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

具体代码:

js 复制代码
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解析的结果如下:

2.5 具体效果

3. 总结

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

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

相关推荐
lovingsoft27 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly37 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝41 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦42 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡1 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队2 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端