背景
在开发Vue项目时,我们经常会遇到一种情况:同样的API请求代码在多个组件或页面中被重复书写。这不仅增加了代码冗余,也降低了项目的可维护性。手动查找和替换这些重复的代码是一项耗时且容易出错的任务。
那么,有没有一种方法可以自动遍历代码,并帮助我们去除这些重复请求呢?答案是肯定的,我们可以使用抽象语法树(AST)来实现这一目标。
1. 实现思路
项目目录结构:
            
            
              css
              
              
            
          
          ├─api.js
├─src
|  ├─index.vue
        - 项目所有请求的api放在api.js文件中,使用ast遍历出重复url(判断下请求方式),输出文件
 - 通过递归遍历src下的
.vue文件,使用vue-comiler-template将script标签的中js代码进行ast分析,筛选出为api.xxx形式的代码,使用1中的结果,判断vue文件中是否是相同的请求,如果是则进行替换. - 最后重新写入文件
 
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!