vscode扩展开发实战篇从0到1详细教学,附源码

534 阅读9分钟

vscode扩展开发从0到1实战篇,详细教学,附源码

前言

在我接手公司项目后,最开始的多语言翻译工作是在公司开发的多语言网站上进行文字翻译,然后复制词条到项目替换对应的文字,最后还要下载多语言文件到项目里,整个过程耗时耗力。作为爱偷懒的程序员肯定是不想干这种脏活,就开始琢磨怎么解放双手,提升效率,于是就开发了这个多语言翻译和下载的扩展。

实战是学习技术最快的方式,本文将从0到1记录下多语言翻译扩展从创建、开发到发布的完整的开发过程,供大家学习参考。

项目概述

这是一个VSCode扩展,主要用于处理多语言翻译相关的功能。该扩展提供了一系列工具,方便进行多语言翻译和管理,包括:

  • 添加翻译标记(##标签)
  • 一键翻译多语言内容
  • 复制多语言文件到项目
  • 匹配并显示翻译内容在左侧视图

📖 扩展开发学习要点

本扩展涉及以下 VSCode 扩展开发知识点: 看完这个项目,你可以学到以下知识点

  1. 基础概念

    • 扩展激活事件
    • 命令注册与实现
    • 快捷键绑定
  2. 核心 API 使用

    • 文本选择与编辑
    • 文件系统操作
    • 配置管理
  3. 进阶特性

    • 左侧自定义视图的使用
    • API 调用集成
    • 错误处理

VSCode扩展开发完整流程

1. 创建VSCode扩展项目

1.1 环境准备

在开始创建VSCode扩展之前,需要确保已安装以下工具:

  • Node.js(推荐使用LTS版本)
  • npmpnpm包管理工具
  • Visual Studio Code编辑器
  • YeomanVS Code Extension Generator

安装Yeoman和VS Code Extension Generator:

npm install -g yo generator-code
1.2 创建项目

使用VS Code Extension Generator创建一个新的扩展项目:

# 创建一个新的文件夹,然后运行
 yo code

在运行yo code命令后,会出现一系列问题,根据需要进行选择:

  1. 选择扩展类型:New Extension (TypeScript)
  2. 输入扩展名称:workextension
  3. 输入标识符:workextension
  4. 输入描述:多语言翻译工具
  5. 是否初始化Git仓库:根据需要选择
  6. 包管理器选择:npmpnpm

test.png

命令执行完成后,会生成一个基本的VSCode扩展项目结构。

1.3 项目结构

生成的项目结构如下:

.
├── .vscode/             # VSCode配置文件
├── src/                 # 源代码目录
│   └── extension.ts     # 扩展入口文件
├── package.json         # 项目配置文件
├── tsconfig.json        # TypeScript配置
└── README.md            # 项目说明文档

2. 扩展配置

2.1 package.json配置

package.json是VSCode扩展的核心配置文件,定义了扩展的元数据、激活事件、命令、菜单等。以下是我们多语言翻译扩展的关键配置:

{
  "name": "workextension",
  "displayName": "workextension",
  "description": "",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.85.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [],
  "main": "./dist/extension.js",
  "contributes": {
    "keybindings": [
      {
        "command": "workExtension.addHashTags",
        "key": "ctrl+3",
        "mac": "ctrl+3",
        "when": "editorTextFocus"
      }
    ],
    "commands": [
      {
        "command": "workExtension.translationI18n",
        "title": "翻译多语言"
      },
      {
        "command": "workExtension.addHashTags",
        "title": "添加##标签"
      },
      {
        "command": "workExtension.copyI18n",
        "title": "复制多语言到项目"
      },
      {
        "command": "workExtension.matchAndShow",
        "title": "匹配并显示内容"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "when": "editorFocus",
          "command": "workExtension.translationI18n"
        },
        {
          "when": "editorFocus",
          "command": "workExtension.addHashTags"
        }
      ],
      "view/title": [
        {
          "command": "workExtension.matchAndShow",
          "when": "view == workExtensionActivity",
          "group": "navigation@1"
        },
        {
          "command": "workExtension.copyI18n",
          "when": "view == workExtensionActivity",
          "group": "navigation@2"
        }
      ]
    },
    "viewsContainers": {
      "activitybar": [
        {
          "id": "workExtension-explorer",
          "title": "工作扩展",
          "icon": "icons/auto.svg"
        }
      ]
    },
    "views": {
      "workExtension-explorer": [
        {
          "id": "workExtensionActivity",
          "name": "workExtension"
        }
      ]
    }
  }
}

主要配置说明:

  • keybindings:定义快捷键,如Ctrl+3用于添加##标签
  • commands:定义扩展提供的命令
  • menus:定义命令在哪些菜单中显示
  • viewsContainers:定义活动栏中的自定义视图容器
  • views:定义视图容器中的视图

3. 核心功能实现

3.1 扩展入口文件

扩展的入口文件extension.ts负责激活扩展并注册命令。以下是我们的入口文件实现:

import * as vscode from 'vscode';
import { Command } from './typings';
import { getAllCommands } from './commands';
import { DataProvider } from './class/dataProvider';
import { ViewItem } from './class/view-item';

export function activate(context: vscode.ExtensionContext) {
  // 获取所有命令
  const commands: Array<Command> = getAllCommands();
  // 遍历所有命令,注册命令
  for (const { name, handler } of commands) {
    const disposable = vscode.commands.registerCommand(name, (viewItem:ViewItem) => {
      handler(provider,viewItem);
    });
    context.subscriptions.push(disposable)
  }
  const provider = new DataProvider(context);
  vscode.window.registerTreeDataProvider('workExtensionActivity', provider);
}

export function deactivate() { }

这里我们使用了模块化的方式组织代码,通过getAllCommands()获取所有命令,然后遍历注册。同时,我们还注册了一个树视图数据提供者,用于左侧视图的显示。

3.2 命令管理

我们在commands/index.ts中集中管理所有命令:

import { Command } from '../typings';
import { addHashTagsCommand } from './addHashTags';
import { translationI18nCommand } from './translationI18n';
import { copyI18nCommand } from './copyI18n';
import { matchAndShowCommand } from './matchAndShow';

//获取所有命令
export function getAllCommands(): Array<Command> {
  return [
    translationI18nCommand(),  //翻译多语言功能
    addHashTagsCommand(),//添加翻译标记功能
    copyI18nCommand(),//复制多语言文件功能
    matchAndShowCommand(),//在左侧视图显示匹配的内容
  ];
}
3.3 添加翻译标记功能

addHashTags.ts实现了添加翻译标记的功能,将选中的文本添加##前后缀:

import * as vscode from 'vscode';
import { Command, } from '../typings';

/**
 * 添加多语言翻译标记命令
 * 该命令用于在选中的文本前后添加 ## 标记,用于后续的多语言翻译处理
 * 使用方式:选中文本后按下快捷键或通过命令面板触发
 */
export function addHashTagsCommand(): Command {
    return {
        // 注册的命令名称,在 package.json 中需要与 contributes.commands 对应
        name: 'workExtension.addHashTags',
        
        /**
         * 命令处理函数
         * 将选中的文本添加 ## 前后缀标记
         * 例如:'用户名' -> '##用户名##'
         */
        handler: async () => {
            // 获取当前活动的编辑器实例
            const editor = vscode.window.activeTextEditor;
            if (!editor) {
                return; // 如果没有打开的编辑器,直接返回
            }

            // 使用 editor.edit 进行文本编辑
            editor.edit((editBuilder: vscode.TextEditorEdit) => {
                // 获取当前选中的文本区域
                const selection = editor.selection;
                // 获取选中区域的文本内容
                const text = editor.document.getText(selection);
                // 在文本前后添加 ## 标记
                const modifiedText = `##${text}##`;
                // 使用新文本替换选中区域的内容
                editBuilder.replace(selection, modifiedText);
            });
        },
    };
}
3.4 翻译多语言功能

translationI18n.ts实现了翻译多语言的功能,通过调用公司API接口将标记的文本翻译成多语言,还增加了一些接口调用报错写入文件的处理。以下代码是针对我司API的处理,仅供参考。

import * as vscode from 'vscode';
import workspace from "../class/workspace";
import { Command } from '../typings';
import fs from 'fs/promises';
import path from 'path';
import dayjs from 'dayjs';
import http from '../class/request';

export function translationI18nCommand(): Command {
  return {
    name: 'workExtension.translationI18n',
    handler: async () => {
      const baseConfig = workspace.getRequestConfig();
      const translationI18nProjectType = workspace.get('translationI18nProjectType');

      // 获取多语言的Alias
      async function geti18n(params: any) {
        const res = await http.post('/MultiLanguageResource/AddResource', params, baseConfig);
        if (res.Data) {
          return res.Data.ResourceAlias;
        } else {
          // 处理错误
          vscode.window.showErrorMessage('Error: 翻译报错');
          const formattedTime = dayjs().format('YYYY-MM-DD HH:mm');
          const errorMessage = `错误消息:${formattedTime}${JSON.stringify(res)}`;
          const workspaceFolder = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0] : null;
          if (!workspaceFolder) {
            vscode.window.showErrorMessage('Error: 无法写入日志文件,因为未找到工作区文件夹。');
            return '';
          }
          const logFilePath = path.join(workspaceFolder.uri.fsPath, 'log.txt');
          await fs.appendFile(logFilePath, errorMessage);
          return '';
        }
      }

      // 使用 withProgress 来显示进度
      await vscode.window.withProgress({
        location: vscode.ProgressLocation.Notification,
        title: "正在翻译文档...",
        cancellable: true
      }, async (progress, token) => {
        // 获取当前编辑器
        const editor = vscode.window.activeTextEditor;
        if (!editor) {
          vscode.window.showErrorMessage('没有打开的编辑器');
          return;
        }

        // 获取文档文本
        const documentText = editor.document.getText();
        // 匹配所有 ##...## 格式的文本
        const matches = documentText.match(/##([\S\s]+?)##/g);

        if (!matches || matches.length === 0) {
          vscode.window.showInformationMessage('没有找到需要翻译的内容');
          return;
        }

        // 处理每个匹配项
        for (let i = 0; i < matches.length; i++) {
          const match = matches[i];
          const text = match.substring(2, match.length - 2); // 去掉前后的##
          
          // 调用翻译API
          const alias = await geti18n({
            ResourceName: text,
            ProjectType: translationI18nProjectType
          });

          if (alias) {
            // 替换文本
            const newDocumentText = editor.document.getText().replace(match, alias);
            const fullRange = new vscode.Range(
              editor.document.positionAt(0),
              editor.document.positionAt(editor.document.getText().length)
            );
            
            // 应用编辑
            await editor.edit(editBuilder => {
              editBuilder.replace(fullRange, newDocumentText);
            });
          }

          // 更新进度
          progress.report({ increment: (100 / matches.length), message: `已翻译 ${i + 1}/${matches.length}` });
        }

        vscode.window.showInformationMessage('翻译完成!');
      });
    },
  };
}
3.5 复制多语言文件功能

copyI18n.ts实现了复制多语言文件到项目的功能:

import * as vscode from 'vscode';
import { Command, } from '../typings';
import workspace from '../class/workspace';
import path from 'path';
import fs from 'fs';
import axios from 'axios';

// 复制多语言到项目
export function copyI18nCommand(): Command {
    return {
        name: 'workExtension.copyI18n',
        handler: async () => {
            // 获取当前活动的工作区文件夹
            const workspaceFolder = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0] : null;
            if (!workspaceFolder) {
                vscode.window.showErrorMessage('没有打开的工作区文件夹。');
                return;
            }
            
            const translationI18nProjectType = workspace.get('translationI18nProjectType')
            let langArr = [{ name: 'ZhCn', value: 1, filePath: 'ZhCn.js' },]
            // 目前只有企业版需要英文文件
            if (translationI18nProjectType === 5500) {
                langArr.push({ name: 'EnUs', value: 2, filePath: 'EnUs.js' })
            }
            
            // 获取工作区根路径
            const rootPath = workspaceFolder.uri.fsPath;
            
            // 下载文件函数
            async function downFile(language: number) {
                const translationI18nService = workspace.get('translationI18nService')
                const translationI18nToken = workspace.get('translationI18nToken')
                const res = await axios({
                    method: 'get',
                    url: `/MultiLanguageResource/DownWebJson?language=${language}`,
                    baseURL: translationI18nService,
                    responseType: 'stream',
                    headers: {
                        'Cookie': translationI18nToken, // 添加 token 到请求头
                    }
                })
                return res
            }
            
            try {
                // 遍历语言数组,下载并保存文件
                for (let lang of langArr) {
                    const langTargetPath = path.join(rootPath, 'src/lang/source', lang.filePath);
                    const tempFilePath = path.join(rootPath, lang.filePath);
                    
                    // 确保目标目录存在
                    const targetDir = path.dirname(langTargetPath);
                    if (!fs.existsSync(targetDir)) {
                        fs.mkdirSync(targetDir, { recursive: true });
                    }
                    
                    // 下载文件
                    const response = await downFile(lang.value);
                    
                    // 创建写入流并保存文件
                    const writer = fs.createWriteStream(tempFilePath);
                    response.data.pipe(writer);
                    
                    // 等待文件写入完成
                    await new Promise((resolve, reject) => {
                        writer.on('finish', resolve);
                        writer.on('error', reject);
                    });
                    
                    // 移动文件到目标位置
                    fs.renameSync(tempFilePath, langTargetPath);
                    
                    vscode.window.showInformationMessage(`${lang.name}多语言文件下载成功!`);
                }
            } catch (error) {
                vscode.window.showErrorMessage(`下载多语言文件失败: ${error.message}`);
            }
        },
    };
}
3.6 匹配并显示内容功能

matchAndShow.ts实现了匹配并显示文件中的多语言标记的功能:

import * as vscode from 'vscode';
import { Command, } from '../typings';

/**
 * 实现左侧视图匹配并显示文件中的多语言标记
 * 该命令会扫描当前打开的文件,查找所有 ##...## 格式的多语言标记
 * 并将结果显示在左侧视图中
 */
export function matchAndShowCommand(): Command {
    return {
        name: 'workExtension.matchAndShow',
        handler: async (
            provider, // TreeDataProvider实例,用于更新左侧视图
            viewItem  // 视图项数据
        ) => {
            // 获取当前活动的编辑器
            const editor = vscode.window.activeTextEditor;
            if (!editor) {
                return; // 如果没有打开的编辑器,则直接返回
            }

            // 获取当前文档的全部文本内容
            const documentText = editor.document.getText();
            
            // 使用正则表达式匹配所有 ##...## 格式的文本
            // [\S\s] 匹配任意字符(包括换行)
            // +? 非贪婪模式匹配,确保正确匹配嵌套的标记
            const matches = documentText.match(/##([\S\s]+?)##/g); 

            // 更新左侧视图的数据
            if (matches) {
                provider.setMatches(matches); // 有匹配项时更新数据
            } else {
                provider.setMatches([]); // 没有匹配项时清空数据
            }
        },
    };
}

4. 自定义视图实现

4.1 数据提供者

dataProvider.ts实现了树视图数据提供者,用于左侧视图的显示: sidebar.png

import * as vscode from "vscode";
import { ViewItem } from "./view-item";

export class DataProvider implements vscode.TreeDataProvider<ViewItem> {
    // 发布订阅模式,用于通知视图更新
    private _onDidChangeTreeData: vscode.EventEmitter<ViewItem | undefined | null> = new vscode.EventEmitter<ViewItem | undefined | null>();
    readonly onDidChangeTreeData: vscode.Event<ViewItem | undefined | null> = this._onDidChangeTreeData.event;

    // 存储匹配到的多语言标记
    public matches: ViewItem[] = [];

    constructor(private context: vscode.ExtensionContext) { }

    // 设置匹配项并触发视图更新
    public setMatches(matches: string[]) {
        this.matches = matches.map(match => new ViewItem(match));
        this._onDidChangeTreeData.fire(null);
    }

    // 获取树项
    getTreeItem(element: ViewItem): vscode.TreeItem {
        return element;
    }

    // 获取子项
    getChildren(element?: ViewItem): Thenable<ViewItem[]> {
        if (!element) {
            return Promise.resolve(this.matches);
        }
        return Promise.resolve([]);
    }
}
4.2 视图项

view-item.ts实现了视图项,用于在左侧视图中显示匹配到的多语言标记:

import * as vscode from "vscode";

export class ViewItem extends vscode.TreeItem {
    constructor(
        public readonly label: string
    ) {
        super(label);
        this.tooltip = this.label;
        this.description = '匹配到的内容';
    }
}

5. 工作区配置管理

workspace.ts实现了工作区配置管理,用于获取和保存配置项:

import * as vscode from "vscode";
import { WorkspaceConfiguration } from "../typings";
import { CONFIG_TAG } from "../constants";
import { AxiosRequestConfig } from "axios";

// 用于获取配置项的类
export class Workspace {
    public constructor() {}

    // 获取配置项
    public get<T extends keyof WorkspaceConfiguration>(key: T): WorkspaceConfiguration[T] {
        const config = vscode.workspace.getConfiguration(CONFIG_TAG);
        return config.get(key) as WorkspaceConfiguration[T];
    }

    // 获取翻译服务的请求配置
    public getRequestConfig(): AxiosRequestConfig {
        const config = vscode.workspace.getConfiguration(CONFIG_TAG);
        const requestConfig = {
            baseURL: config.get<string>('translationI18nService') || '',
            headers: {
                'Cookie': config.get<string>('translationI18nToken') || '',
                'Content-Type': 'application/json'
            }
        }
        return requestConfig
    }

    // 获取全局配置项
    public getGlobal(key: string): any {
        const config = vscode.workspace.getConfiguration();
        return config.get(key);
    }

    // 保存配置项
    public save<T extends keyof WorkspaceConfiguration>(key: T, value: WorkspaceConfiguration[T]): Promise<void> {
        const config = vscode.workspace.getConfiguration(CONFIG_TAG);
        config.update(key, value, false);
        return Promise.resolve();
    }
}

export default new Workspace();

6. 调试与测试

6.1 调试扩展

VSCode提供了强大的调试功能,可以方便地调试扩展。在.vscode/launch.json文件中已经配置好了调试设置:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "args": ["--extensionDevelopmentPath=${workspaceFolder}"],
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "preLaunchTask": "${defaultBuildTask}"
    }
  ]
}

调试步骤:

  1. 按下F5或点击调试面板中的运行按钮
  2. VSCode会启动一个新的扩展开发主机窗口
  3. 在这个窗口中,可以测试扩展功能
6.2 测试扩展功能

测试多语言翻译扩展的各项功能:

  1. 添加翻译标记

    • 在编辑器中选中文本
    • 按下Ctrl+3快捷键或右键菜单选择"添加##标签"
    • 验证选中的文本是否被添加了##前后缀
  2. 翻译多语言

    • 确保文件中有##...##格式的文本
    • 右键菜单选择"翻译多语言"
    • 验证标记的文本是否被替换为多语言别名
  3. 匹配并显示内容

    • 打开包含##...##格式文本的文件
    • 点击左侧视图中的"匹配并显示内容"按钮
    • 验证左侧视图中是否显示了匹配到的内容
  4. 复制多语言到项目

    • 点击左侧视图中的"复制多语言到项目"按钮
    • 验证多语言文件是否被下载并复制到项目中

7. 打包与发布

7.1 安装vsce工具

使用npm安装vsce工具,用于打包和发布VSCode扩展:

npm install -g @vscode/vsce
7.2 打包扩展

在项目根目录下执行以下命令打包扩展:

vsce package

这将生成一个.vsix文件,可以手动安装或分享给他人。

8. 一些唠叨

本扩展项目源于实际工作中的痛点解决,在开发这个扩展开发完后,不仅为我提升了工作效率,也帮助我学习了VSCode扩展的核心机制,包括命令注册、视图定制、文件操作以及API集成等关键技术点。这些知识不仅适用于多语言翻译场景,也为我今后开发其他类型的扩展奠定了坚实基础。

所以我们程序员在工作中,如果想快速提升,在完成日常工作的同时,就得主动思考如何优化流程、提升效率,在为了解决某一问题而学习相关技术的时候会发现更容易掌握,因为实战比理论更有趣。

源码获取

完整源码已上传到GitHub,欢迎各位前端er下载学习和提出改进建议:

github项目地址

如果这个项目对你有所启发,希望点个Star支持!我将持续更新更多前后端技术进阶内容,一起在技术道路上共同成长。