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

背景

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

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

举个例子

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

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

目标

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

交互流程

设计

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

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

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

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

整个插件分为3个命令:

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

整体设计

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

环境检测

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

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

缓存接口列表

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

接口捕获

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

类型生成

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

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

  1. 命名问题,Yapi自带的ts类型命名过于简单粗暴,就是直接把接口路径拼接起来
  2. 有的字段因为粗心带了空格,最后还需要手动修改一遍类型
  3. 实际项目中有一层请求中间件,可能最终需要的类型只有data那一层,而Yapi定义的是整个类型
代码插入
  1. 将生成的类型插入文件中
typescript 复制代码
    // 检查文件是否存在
  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. 替换原有函数字符串
typescript 复制代码
   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 _;
      });
  1. 调用vscode api替换函数字符串
typescript 复制代码
    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语句
typescript 复制代码
    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文化是真的很重!!!

相关推荐
Cachel wood27 分钟前
Vue.js前端框架教程8:Vue消息提示ElMessage和ElMessageBox
linux·前端·javascript·vue.js·前端框架·ecmascript
桃园码工2 小时前
4_使用 HTML5 Canvas API (3) --[HTML5 API 学习之旅]
前端·html5·canvas
桃园码工2 小时前
9_HTML5 SVG (5) --[HTML5 API 学习之旅]
前端·html5·svg
人才程序员2 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
m0_548514772 小时前
前端三大主流框架:React、Vue、Angular
前端·vue.js·react.js
m0_748232393 小时前
单页面应用 (SPA):现代 Web 开发的全新视角
前端
孤留光乩3 小时前
从零搭建纯前端飞机大战游戏(附源码)
前端·javascript·游戏·html·css3
伊泽瑞尔.3 小时前
el-tabs标签过多
前端·javascript·vue.js
2401_854391083 小时前
智能挂号系统设计典范:SSM 结合 Vue 在医院的应用实现
前端·javascript·vue.js
觉醒的程序猿3 小时前
vue2设置拖拽选中时间区域
开发语言·前端·javascript