背景
团队项目用的是React Ts,接口定义使用Yapi
。
但是项目中很多旧代码为了省事,都是写成 any
,导致在使用的时候没有类型提示,甚至在迭代的时候还发现了不少因为传参导致的bug。
举个例子
表格分页接口定义的参数是 pageSize
和 offset
,但是代码里传的却是 size
和 offset
,导致每次都是全量拉数据,然而因为测试环境数据量少,完全没测出来。
在这种项目背景下,大致过了一个月,结合自己试用期的目标(我也不想搞啊......),想通过一个工具来快速解决这类问题。
目标
把代码中接口的 any
替换成 Yapi
上定义的类型,减少因为传参导致的bug数量。
交互流程
设计
鉴于当前项目中接口数量庞大(eslint扫出来包含any的接口有768个),手动逐一审查并替换类型显得既不现实又效率低下。
显然需要一种更加高效且可靠的方法来解决。
因为组内基本上都是使用 VSCode
开发,因此最终决定开发一个 VSCode
插件来实现类型的替换。
考虑到直接扫描整个项目进行替换风险较大,因此最终是 按文件维度,针对当前打开的文件执行替换。
整个插件分为3个命令:
- 单个接口替换
- 整个文件所有接口替换
- 新增接口
整体设计
插件按功能划分为6个模块:
环境检测
Easy Yapi需要和Yapi服务器交互,需要用户提供Yapi Project相关的信息,因此需要填写配置文件(由使用者提供)。
插件执行命令时会对配置文件内的信息进行检测。
缓存接口列表
从性能上考虑,一次批量替换后,会缓存当前Yapi项目所有接口的定义到cache文件中,下次替换不会重新请求。
接口捕获
不管是单个接口替换还是整个文件接口替换都需要先捕获接口,这里是通过将代码转成AST来实现。
类型生成
将接口定义转化成TS类型,通过循环+递归拼接字符串生成类型。
为什么不直接使用Yapi自带的ts类型?
- 命名问题,Yapi自带的ts类型命名过于简单粗暴,就是直接把接口路径拼接起来
- 有的字段因为粗心带了空格,最后还需要手动修改一遍类型
- 实际项目中有一层请求中间件,可能最终需要的类型只有data那一层,而Yapi定义的是整个类型
代码插入
- 将生成的类型插入文件中
// 检查文件是否存在
if (fs.existsSync(targetFilePath)) {
const currentContent = fs.readFileSync(targetFilePath);
if (!currentContent.includes(typeName)) { // 判断类型是否已存在
try {
fs.appendFileSync(targetFilePath, content); // 追加内容
editor.document.save(); // 调用vscode api保存文件
return true;
} catch (err: any) {
......
return false;
}
} else {
......
return false;
}
} else { // 文件不存在,创建并写入类型
try {
fs.writeFileSync(targetFilePath, content);
editor.document.save();
return true;
} catch (err: any) {
......
}
}
- 替换原有函数字符串
const nextFnStr = functionText
.replace(/(\w+:\s*)(any)/, (_, $1) => {
if (query.apiReq) {
return `${$1}${query.typeName}`;
}
// 没参数
else {
return "";
}
})
.replace(/Promise<([a-zA-Z0-9_]+<any>|any)>/, (_, $1) => {
if (res?.apiRes) {
return `Promise<${res?.typeName}>`;
}
return `Promise<void>`;
})
.replace(/,\s*\{\s*params\s*\}/, (_) => {
// 对于没有参数的case, 应该删除参数
if (!query.apiReq) {
return "";
}
return _;
});
- 调用vscode api替换函数字符串
const startPosition = new vscode.Position(functionStartLine - 1, 0); // 减1因为VS Code的行数是从0开始的
const endPosition = new vscode.Position(
functionEndLine - 1,
document.lineAt(functionEndLine - 1).text.length
);
const textRange = new vscode.Range(startPosition, endPosition);
const editApplied = await editor.edit((editBuilder) => {
editBuilder.replace(textRange, nextFnStr);
});
......
- 引入类型, 插入import语句
const document = editor.document;
const fullText = document.getText(); // 调用vscode api 拿到当前文件字符串
// 匹配单引号或双引号,并确保结束引号与开始引号相匹配
const importRegex =
/(import\s+(type\s+)?\{\s*[^}]*)(}\s+from\s+(['"])\.\/types(\.ts)?['"]);?/g;
let matchIndex = fullText.search(importRegex); // 使用search得到全局匹配的起始索引
if (matchIndex !== -1) {
// 已经有类型语句
let matchText = fullText.match(importRegex)?.[0]; // 获取完整的匹配文本
// 去重,如果 import { a, b } from './types'中已经有typeNames中的类型,则不需要重复引入
// existingTypes = ['a', 'b']
const existingTypes = (
/\{\s*([^}]+)\s*\}/g.exec(matchText)?.[1] as string
)
.split(",")
.map((v) => v.trim());
const uniqueTypeNames = typeNames.filter(
(v) => !existingTypes.includes(v)
);
// 将生成的类型插入原有的import type语句中
// 例如: import { a } from './types'
// 生成了类型 b c 则变成 import { a, b, c } from './types'
let updatedImport = matchText?.replace(
importRegex,
(_, group1, group2, group3) => {
// group1 对应 $1,即 import 语句到第一个 "}" 之前的所有内容
// group3 对应 $3,即 "}" 到语句末尾的部分
return `${
(group1.trim() as string).endsWith(",") ? group1 : `${group1}, `
}${uniqueTypeNames.join(", ")} ${group3}`;
}
);
// 计算确切的起始和结束位置
let startPos = document.positionAt(matchIndex);
let endPos = document.positionAt(matchIndex + matchText.length);
let range = new vscode.Range(startPos, endPos);
// 替换
await editor.edit((editBuilder) => {
editBuilder.replace(range, updatedImport as string);
});
} else {
// 直接插入import type
await editor.edit((editBuilder) => {
editBuilder.insert(
new vscode.Position(0, 0),
`import type { ${typeNames.join(",")} } from './types';\n`
);
});
}
// importStr导入语句需要进行判断再导入
// 例如:import request from '@service/request';
if (importStr && requestName) {
const importStatementRegex = new RegExp(
`import\\s+(?:\\{\\s*${requestName}\\s*\\}|${requestName})\\s+from\\s+['"]([^'"]+)['"];?`
);
const match = importStatementRegex.exec(editor.document.getText());
// 当前文件没有这个语句,插入
if (!match) {
await editor.edit((editBuilder) => {
editBuilder.insert(new vscode.Position(0, 0), `${importStr};\n`);
});
}
}
效果
替换后,会在当前文件同级目录下生成对应类型,然后import到当前文件。
总结
开发这个插件自己学到了不少东西,团队也用上了,有同学给了插件使用的反馈。
最后,试用期过了。
不过,新公司ppt文化是真的很重!!!