注册
web

入职2个月,我写了一个VSCode插件解决团队遗留的any问题

背景

团队项目用的是React Ts,接口定义使用Yapi

但是项目中很多旧代码为了省事,都是写成 any,导致在使用的时候没有类型提示,甚至在迭代的时候还发现了不少因为传参导致的bug。

举个例子

表格分页接口定义的参数是 pageSize  offset ,但是代码里传的却是 size  offset ,导致每次都是全量拉数据,然而因为测试环境数据量少,完全没测出来。

在这种项目背景下,大致过了一个月,结合自己试用期的目标(我也不想搞啊......),想通过一个工具来快速解决这类问题。

目标

把代码中接口的 any 替换成 Yapi 上定义的类型,减少因为传参导致的bug数量。

交互流程

image.png

设计

鉴于当前项目中接口数量庞大(eslint扫出来包含any的接口有768个),手动逐一审查并替换类型显得既不现实又效率低下。

显然需要一种更加高效且可靠的方法来解决。

因为组内基本上都是使用 VSCode 开发,因此最终决定开发一个 VSCode 插件来实现类型的替换。

考虑到直接扫描整个项目进行替换风险较大,因此最终是 按文件维度,针对当前打开的文件执行替换

整个插件分为3个命令:

  • 单个接口替换
  • 整个文件所有接口替换
  • 新增接口

image.png

整体设计

插件按功能划分为6个模块:

image.png

环境检测

Easy Yapi需要和Yapi服务器交互,需要用户提供Yapi Project相关的信息,因此需要填写配置文件(由使用者提供)。

插件执行命令时会对配置文件内的信息进行检测。

缓存接口列表

从性能上考虑,一次批量替换后,会缓存当前Yapi项目所有接口的定义到cache文件中,下次替换不会重新请求。

接口捕获

不管是单个接口替换还是整个文件接口替换都需要先捕获接口,这里是通过将代码转成AST来实现。

image.png

image.png

类型生成

将接口定义转化成TS类型,通过循环+递归拼接字符串生成类型。

为什么不直接使用Yapi自带的ts类型?

  1. 命名问题,Yapi自带的ts类型命名过于简单粗暴,就是直接把接口路径拼接起来
  2. 有的字段因为粗心带了空格,最后还需要手动修改一遍类型 image.png
  3. 实际项目中有一层请求中间件,可能最终需要的类型只有data那一层,而Yapi定义的是整个类型
代码插入

  1. 将生成的类型插入文件中
    // 检查文件是否存在
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) {
......
}
}
  1. 替换原有函数字符串
   const nextFnStr = functionText
.replace(/(\w+:\s*)(any)/, (_, $1) => {
if (query.apiReq) {
return `${$1}${query.typeName}`;
}
// 没参数
else {
return "";
}
})
.replace(/Promise<([a-zA-Z0-9_]+|any)>/, (_, $1) => {
if (res?.apiRes) {
return `Promise<${res?.typeName}>`;
}
return `Promise`;
})
.replace(/,\s*\{\s*params\s*\}/, (_) => {
// 对于没有参数的case, 应该删除参数
if (!query.apiReq) {
return "";
}
return _;
});
  1. 调用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);
});

......

  1. 引入类型, 插入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`);
});
}
}

总结

开发这个插件自己学到了不少东西,团队也用上了,有同学给了插件使用的反馈。

最后,试用期过了。

不过,新公司ppt文化是真的很重!!!


作者:前端幼儿园
来源:juejin.cn/post/7423649211190591488

0 个评论

要回复文章请先登录注册