背景
在开发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!