VSCode 插件开发实战

72 阅读7分钟

VSCode 插件开发实战:JSON 转 TypeScript 接口 & TypeScript 接口转 Mock 数据

前端开发者的效率神器,手把手教你开发属于自己的 VSCode 插件

一、前言

1.1 为什么要开发这个插件?

在前端日常开发中,我们经常遇到以下场景:

  • 后端返回 JSON 数据,需要手动编写对应的 TypeScript 接口 - 繁琐且容易出错
  • 编写单元测试时,需要手动编写 Mock 数据 - 耗时且数据不够真实
  • 前后端接口对接时,需要频繁转换数据格式 - 效率低下
  • 最重要的一点是:点亮这个VSCode 插件开发技术栈

为了解决这些痛点,我开发了 JSON TS Mock Helper 插件,提供两个核心功能:

  1. JSON 一键生成 TypeScript 接口 - 选中 JSON 文本,自动生成 TypeScript 类型定义
  2. TypeScript 接口一键生成 Mock 数据 - 选中 TypeScript 接口定义,自动生成真实的中文 Mock 数据

1.2 插件效果预览

// 选中以下 JSON:
{"name": "张三", "age": 25, "email": "test@example.com"}

// 一键生成 TypeScript 接口:
interface RootObject {
  name: string;
  age: number;
  email: string;
}
// 选中以下 TypeScript 接口:
interface User {
  name: string;
  age: number;
  email?: string;
}

// 一键生成 Mock 数据:
const mockUser = {
  name: "李四",
  age: 28,
  email: "sample_k9f2"
};

1.3 技术栈

  • TypeScript - 开发语言
  • VSCode API - 插件核心能力
  • quicktype-core - JSON 转 TypeScript 的核心库
  • Node.js - 运行时环境

二、环境准备

2.1 开发环境要求

在开始开发之前,你需要准备以下环境:

工具版本要求说明
Node.js>= 16.x建议使用 LTS 版本
npm>= 8.xNode.js 自带
VSCode>= 1.80.0用于开发和调试
TypeScript>= 5.0.0项目依赖

可以通过以下命令检查当前环境:

node --version  # Node.js 版本
npm --version   # npm 版本
code --version  # VSCode 版本

2.2 安装 VSCode 扩展开发工具

VSCode 提供了官方的扩展开发模板,可以快速创建新项目。确保 VSCode 已安装,然后创建一个新文件夹作为工作目录:

mkdir vscode-plugin-demo
cd vscode-plugin-demo

三、项目初始化

3.1 使用官方脚手架创建项目

VSCode 官方提供了 yo code 脚手架来快速创建插件项目。在终端中运行:

npm install -g yo generator-code
yo code

按照提示选择以下选项:

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? json-ts-mock-helper
? What's the identifier of your extension? json-ts-mock-helper
? What's the description of your extension? Convert JSON to TypeScript and generate mock data
? Initialize a git repo? Yes
? Which package manager to use? npm

3.2 项目结构

创建完成后的项目结构如下:

json-ts-mock-helper/
├── src/
│   └── extension.ts       # 插件入口文件
├── .vscode/
│   └── launch.json       # 调试配置
├── node_modules/         # 依赖目录
├── package.json          # 插件配置
├── tsconfig.json         # TypeScript 配置
└── README.md             # 说明文档

3.3 安装核心依赖

本插件使用 quicktype-core 来实现 JSON 到 TypeScript 的转换:

cd json-ts-mock-helper
npm install quicktype-core

quicktype-core 是一个强大的库,可以从 JSON、JSON Schema 等多种格式自动推断并生成 TypeScript、Go、Java、C# 等多种语言的类型定义。


四、核心功能实现

4.1 功能一:JSON 转 TypeScript 接口

4.1.1 实现思路
  1. 获取用户选中的 JSON 文本
  2. 使用 quicktype-core 库进行类型推断
  3. 将生成的 TypeScript 代码插入到编辑器中
4.1.2 代码实现

创建 src/jsonToTs.ts 文件:

import { jsonInputForTargetLanguage, quicktype, InputData } from 'quicktype-core';

/**
 * 将 JSON 字符串转换为 TypeScript 接口
 * @param jsonString 用户选中的 JSON 文本
 * @returns 生成的 TypeScript 代码
 */
export async function convertJsonToTs(jsonString: string): Promise<string> {
  // 1. 验证 JSON 格式
  try {
    JSON.parse(jsonString);
  } catch (error) {
    throw new Error('Invalid JSON format. Please check your JSON syntax.');
  }

  // 2. 使用 quicktype 生成 TypeScript
  const jsonInput = jsonInputForTargetLanguage('typescript');
  await jsonInput.addSource({ name: 'RootObject', samples: [jsonString] });

  const inputData = new InputData();
  inputData.addInput(jsonInput);

  const result = await quicktype({
    inputData,
    lang: 'typescript',
  });

  return result.lines.join('\n');
}
4.1.3 功能测试

假设我们有如下 JSON 数据:

{
  "name": "张三",
  "age": 25,
  "email": "zhangsan@example.com",
  "address": {
    "city": "北京",
    "country": "中国"
  },
  "friends": ["李四", "王五", "赵六"]
}

运行插件后,会生成如下 TypeScript 接口:

interface RootObject {
  name: string;
  age: number;
  email: string;
  address: Address;
  friends: string[];
}

interface Address {
  city: string;
  country: string;
}

4.2 功能二:TypeScript 接口转 Mock 数据

4.2.1 实现思路
  1. 解析用户选中的 TypeScript 接口定义
  2. 根据字段类型生成对应的 Mock 数据
  3. 支持嵌套对象、数组、枚举等复杂类型
  4. 生成中文的 Mock 数据(姓名、城市、地址等)
4.2.2 代码实现

创建 src/tsToMock.ts 文件。首先定义一些 Mock 数据生成函数:

// 中文 Mock 数据源
const chineseSurnames = ['张', '李', '王', '刘', '陈', '杨', '黄', '赵', '吴', '周'];
const chineseNames = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '军'];
const cities = ['北京', '上海', '广州', '深圳', '杭州', '南京', '成都', '重庆', '武汉', '西安'];
const streets = ['人民路', '中山路', '建设路', '解放路', '和平路'];

然后实现接口解析和 Mock 数据生成的核心逻辑:

interface TypeInfo {
  type: string;
  isOptional: boolean;
  arrayType?: string;
  enumValues?: string[];
}

/**
 * 解析 TypeScript 接口定义
 */
function parseInterface(interfaceText: string): Map<string, Map<string, TypeInfo>> {
  const interfaces = new Map<string, Map<string, TypeInfo>>();

  // 匹配 interface 定义
  const interfaceRegex = /interface\s+(\w+)\s*\{([^}]+)\}/g;
  let match;

  while ((match = interfaceRegex.exec(interfaceText)) !== null) {
    const interfaceName = match[1];
    const body = match[2];
    const fields = new Map<string, TypeInfo>();

    // 匹配字段定义
    const fieldRegex = /(\w+)(\??)\s*:\s*([^;]+);/g;
    let fieldMatch;

    while ((fieldMatch = fieldRegex.exec(body)) !== null) {
      const fieldName = fieldMatch[1];
      const isOptional = fieldMatch[2] === '?';
      const fieldType = fieldMatch[3].trim();
      fields.set(fieldName, parseType(fieldType));
    }

    interfaces.set(interfaceName, fields);
  }

  return interfaces;
}

/**
 * 根据字段类型生成 Mock 数据
 */
function generateValue(typeInfo: TypeInfo, interfaceMap: Map<string, Map<string, TypeInfo>>): unknown {
  const { type, isOptional } = typeInfo;

  // 30% 概率跳过可选字段
  if (isOptional && Math.random() > 0.7) {
    return undefined;
  }

  switch (type) {
    case 'string':
      return generateChineseName();
    case 'number':
    case 'Number':
      return Math.floor(Math.random() * 100) + 1;
    case 'boolean':
    case 'Boolean':
      return Math.random() > 0.5;
    case 'enum':
      return typeInfo.enumValues?.[0] || 'value';
    default:
      // 嵌套对象
      if (interfaceMap.has(type)) {
        return generateObject(type, interfaceMap);
      }
      return null;
  }
}

/**
 * 生成中文姓名
 */
function generateChineseName(): string {
  const surname = chineseSurnames[Math.floor(Math.random() * chineseSurnames.length)];
  const name = chineseNames[Math.floor(Math.random() * chineseNames.length)];
  return surname + name;
}

/**
 * 从 TypeScript 接口生成 Mock 数据
 */
export async function generateMockFromTs(tsCode: string): Promise<string> {
  const interfaceMap = parseInterface(tsCode);

  if (interfaceMap.size === 0) {
    throw new Error('No valid TypeScript interface found.');
  }

  const firstInterface = interfaceMap.keys().next().value;
  const mockData = generateObject(firstInterface, interfaceMap);

  return `// Generated Mock Data\nconst mock${firstInterface} = ${JSON.stringify(mockData, null, 2)};`;
}
4.2.3 功能测试

假设我们有如下 TypeScript 接口:

interface User {
  name: string;
  age: number;
  email?: string;
  address: Address;
  friends: string[];
}

interface Address {
  city: string;
  country: string;
}

运行插件后,会生成如下 Mock 数据:

const mockUser = {
  name: "李四",
  age: 32,
  address: {
    city: "上海",
    country: "中国"
  },
  friends: ["王五", "赵六", "孙七"]
};

五、命令注册与快捷键

5.1 修改 package.json

package.json 中添加命令定义和快捷键配置:

{
  "name": "json-ts-mock-helper",
  "displayName": "JSON TS Mock Helper",
  "contributes": {
    "commands": [
      {
        "command": "json-ts-mock-helper.convertJsonToTs",
        "title": "JSON to TS: Convert JSON to TypeScript Interface"
      },
      {
        "command": "json-ts-mock-helper.generateMockFromTs",
        "title": "JSON to TS: Generate Mock from TypeScript Interface"
      }
    ],
    "keybindings": [
      {
        "command": "json-ts-mock-helper.convertJsonToTs",
        "key": "cmd+shift+j",
        "when": "editorTextFocus"
      },
      {
        "command": "json-ts-mock-helper.generateMockFromTs",
        "key": "cmd+shift+m",
        "when": "editorTextFocus"
      }
    ]
  }
}

5.2 在 extension.ts 中注册命令

修改 src/extension.ts,注册两个命令:

import * as vscode from 'vscode';
import { convertJsonToTs } from './jsonToTs';
import { generateMockFromTs } from './tsToMock';

export function activate(context: vscode.ExtensionContext) {
  // 命令1: JSON 转 TypeScript
  const convertJsonToTsCommand = vscode.commands.registerCommand(
    'json-ts-mock-helper.convertJsonToTs',
    async () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showErrorMessage('No active editor found');
        return;
      }

      const selection = editor.selection;
      const selectedText = editor.document.getText(selection);

      if (!selectedText || selectedText.trim() === '') {
        vscode.window.showErrorMessage('Please select JSON text first');
        return;
      }

      try {
        const tsCode = await convertJsonToTs(selectedText);
        if (tsCode) {
          await editor.edit(editBuilder => {
            editBuilder.insert(selection.end, '\n\n' + tsCode);
          });
          vscode.window.showInformationMessage('TypeScript interface generated!');
        }
      } catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown error';
        vscode.window.showErrorMessage(`Error: ${message}`);
      }
    }
  );

  // 命令2: TypeScript 转 Mock
  const generateMockFromTsCommand = vscode.commands.registerCommand(
    'json-ts-mock-helper.generateMockFromTs',
    async () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showErrorMessage('No active editor found');
        return;
      }

      const selection = editor.selection;
      const selectedText = editor.document.getText(selection);

      if (!selectedText || selectedText.trim() === '') {
        vscode.window.showErrorMessage('Please select TypeScript interface first');
        return;
      }

      try {
        const mockCode = await generateMockFromTs(selectedText);
        if (mockCode) {
          await editor.edit(editBuilder => {
            editBuilder.insert(selection.end, '\n\n' + mockCode);
          });
          vscode.window.showInformationMessage('Mock data generated!');
        }
      } catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown error';
        vscode.window.showErrorMessage(`Error: ${message}`);
      }
    }
  );

  context.subscriptions.push(convertJsonToTsCommand);
  context.subscriptions.push(generateMockFromTsCommand);
}

export function deactivate() {}

六、调试与运行

6.1 本地调试

  1. 打开 VSCode,按 F5 或点击左侧调试图标
  2. 选择 "Debug VSCode Extension"
  3. 会弹出一个新的 VSCode 窗口(扩展开发主机)
  4. 在新窗口中:
    • 创建一个 .json 文件,粘贴一段 JSON
    • 选中 JSON 文本,按 Cmd+Shift+J 测试 JSON→TS 功能
    • 创建一个 .ts 文件,粘贴一段 TypeScript 接口定义
    • 选中接口文本,按 Cmd+Shift+M 测试 TS→Mock 功能

6.2 查看日志

在调试控制台可以看到插件的运行日志,有助于排查问题。

6.3 常见问题

问题解决方案
快捷键不生效检查 package.json 中 keybindings 配置
命令找不到检查 activationEvents 是否正确注册
JSON 解析错误确保选中的是有效的 JSON 格式
类型生成失败检查 quicktype-core 是否正确安装

七、打包与发布

7.1 安装打包工具

npm install -g @vscode/vsce

7.2 打包为 .vsix 文件

# 进入项目目录
cd json-ts-mock-helper

# 编译 TypeScript
npm run compile

# 打包
vsce package

打包完成后,会生成一个 .vsix 文件,可以直接拖拽到 VSCode 中安装。

7.3 发布到 VSCode 市场

  1. 登录 VSCode Market(需要 Microsoft 账号)
  2. 创建一个发布配置文件
  3. 使用 vsce publish 命令发布
vsce publish

7.4 本地安装使用

如果你不想发布到市场,也可以直接分发 .vsix 文件:

  1. 打开 VSCode
  2. 选择 "Extensions" → "Install from VSIX"
  3. 选择生成的 .vsix 文件

八、总结与展望

8.1 项目收获

通过这个插件的开发,我们学习到了:

  • ✅ VSCode 插件项目的创建与初始化
  • ✅ 使用 quicktype-core 进行类型推断
  • ✅ VSCode API 的使用(Editor、Selection、Commands)
  • ✅ 命令注册与快捷键配置
  • ✅ 插件的调试与打包发布

8.2 后续优化方向

这个插件还有很大的优化空间,欢迎大家一起参与贡献:

  • 支持更多数据类型(Date、UUID、URL 等)
  • 支持自定义 Mock 数据规则
  • 支持生成 JSON Schema
  • 支持更多目标语言(Java、Go、C# 等)
  • 添加 i18n 支持

8.3 参考资料


九、代码仓库

完整代码已开源到 GitHub,欢迎 Star、Fork 和贡献!

🔗 GitHub: github.com/qqq40837095…

如果你觉得这个插件对你有帮助,请给个 Star 鼓励一下!也欢迎提交 Issue 和 Pull Request 一起完善这个项目。


首发于 掘金 作者:前端从业猿 链接:juejin.cn/user/412364…