TheUnknownThing的技术分享站

Stay Hungry, Stay Foolish

我的第一个 VS Code 插件开发之旅 - 从零到一开发ACMOJ 助手

默认分类 0 评 53 阅读

作为一名经常在 ACMOJ (我们班级的 OnlineJudge) 上刷题、参与课程和比赛的学生,在编辑器(VS Code)和浏览器之间频繁切换实在繁琐。在浏览器查看题目描述,样例,输出后再比对结果,复制代码到 VS Code,编写调试,再复制回浏览器提交,最后又切回浏览器查看结果... 尽管有分屏,但是我平时用 macOS 的台前调度分屏体验不好。这个过程不仅打断思路,效率也不高。

能不能在 VSCode 里一站式完成这些操作呢?看到班级里有同学也在开发插件,似乎也不是这么难?况且前阵子学习了golang,感觉typescript也不是这么难学了😋怀着这个想法,我踏上了我的第一个 VSCode 插件开发之旅,目标是为 ACMOJ 打造一个便捷的助手。这篇文章记录了从构思到实现,再到踩坑和最终成型的过程。

一、启程:工具与蓝图

VS Code 插件主要使用 TypeScript (或 JavaScript) 编写,运行在 Node.js 环境中。开始之前,必要的工具是:

  1. Node.js & npm/yarn: 基础运行环境和包管理器。
  2. Yeoman & generator-code: VS Code 官方推荐的脚手架工具,快速生成项目结构。

    npm install -g yo generator-code
    yo code # 选择 TypeScript Extension
  3. VS Code: 开发和调试插件本身。

生成的项目结构清晰明了:

  • src/extension.ts: 插件的入口文件,包含 activate (激活时调用) 和 deactivate (停用时调用) 函数。
  • package.json: 核心清单文件,定义插件的元数据、贡献点 (Contributions)(如命令、视图、配置)和激活事件 (Activation Events)(决定何时加载插件)。
  • tsconfig.json: TypeScript 配置文件。

我的初步蓝图是实现以下核心功能:

  1. 认证: 连接到 ACMOJ API。
  2. 题目/作业浏览: 在 VS Code 侧边栏查看题目列表。
  3. 题目详情: 在 Webview 中展示题目描述、样例等。
  4. 代码提交: 从当前编辑器快速提交代码。
  5. 结果查看: 在侧边栏或 Webview 中查看提交状态和结果。

二、API 交互与认证

ACMOJ 提供了一套 OpenAPI 规范的 API,这是实现功能的基础。

1. API 客户端:
我选择了 axios 作为 HTTP 请求库,并封装了一个 ApiClient 类来统一处理请求发送、Base URL 配置和错误处理。关键在于设置请求拦截器,自动在 Authorization Header 中附加 Bearer <token>

2. 认证的“小插曲” - OAuth vs PAT:
API 文档同时提到了 OAuth2 (Authorization Code Flow) 和个人访问令牌 (Personal Access Token, PAT) 两种认证方式。

  • 初试 OAuth2: 我最初尝试实现 OAuth2 流程。这涉及到引导用户到浏览器授权,然后在本地启动一个临时 HTTP 服务器监听回调 URI 以获取 code,再用 codeclient_secret 去换取 access_token。这个流程对于需要多用户授权的应用是标准的,但对于个人使用的插件来说,实现起来相当复杂,尤其是在 VS Code 插件这种环境中安全地处理 client_secret 和本地回调比较棘手。(当然事实上阻止我第一步是我需要有一个 client secret 要向管理组索取。在当时我并不认识管理组的人员,虽然开发了这个插件后他们貌似认识我了XD)
  • 转向 PAT: 考虑到目标用户(主要是自己和同学)可以方便地在 ACMOJ 网站生成 PAT,我决定转向更简单的 PAT 认证。这大大简化了流程:

    • 创建一个 AuthService (或 TokenManager)。
    • 提供一个 acmoj.setToken 命令,使用 vscode.window.showInputBox({ password: true }) 提示用户输入 PAT。
    • 使用 VS Code 的 SecretStorage API (context.secrets.store / context.secrets.get) 安全地存储和读取 PAT。
    • 提供 acmoj.clearToken 命令清除存储的 PAT。
    • ApiClient 的请求拦截器中,直接从 AuthService 获取存储的 PAT 添加到请求头。
    • 在响应拦截器中,如果遇到 401 Unauthorized 错误,则调用 AuthService 的方法清除无效 token 并提示用户重新设置。

三、构建用户界面使用 TreeView 与 Webview

要在 VS Code 中展示信息和提供交互,主要使用了 TreeView 和 Webview。

1. TreeView (侧边栏):
使用 vscode.TreeDataProvider 接口为活动栏(Activity Bar)创建了两个视图:

  • Problemsets (比赛/作业):

    • 最初简单地列出所有题目,但很快发现信息量太大。
    • 改进: 改为显示用户加入的 Problemsets。
    • 再改进: 根据 Problemset 的开始/结束时间,将其分类为 "Ongoing", "Upcoming", "Passed" 三个顶级节点。这需要获取所有 Problemsets,然后在 getChildren 方法中根据当前时间和分类节点进行过滤和排序。使用了 CategoryTreeItemProblemsetTreeItem 两种自定义 TreeItem
    • 每个 Problemset 节点设置为可展开 (vscode.TreeItemCollapsibleState.Collapsed),点击后加载其包含的题目列表 (ProblemBriefTreeItem)。
  • Submissions (提交记录):

    • 显示用户的提交列表,包含 ID、题目、状态、语言、时间等。
    • 为不同的提交状态(AC, WA, TLE, RE...)设置了不同的图标 (ThemeIcon),使其更直观。

实现 TreeView 的关键在于 getChildren (获取子节点) 和 getTreeItem (定义节点外观和行为) 两个方法。通过 EventEmitteronDidChangeTreeData 事件可以通知 VS Code 刷新视图。

2. Webview (详情展示):
当用户点击 TreeView 中的题目或提交记录时,使用 vscode.window.createWebviewPanel 创建一个 Webview 来展示详细信息。为什么要用 webview? 因为要渲染 tex 公式,并且JSON请求回来的是Markdown结果。

  • 内容渲染: Webview 本质上是一个嵌入的浏览器环境,其内容是 HTML。我使用 markdown-it 库来将从 API 获取的 Markdown 格式的题目描述、输入输出格式等转换为 HTML。
  • 挑战:数学公式渲染: OJ 题目描述经常包含 LaTeX 公式。

    • 尝试一 (失败): 最初尝试在 Webview 的 HTML 中包含 KaTeX 的 JS 库和 auto-render 脚本,让客户端渲染。但这导致了公式被渲染两次的奇怪问题(一次是原始文本,一次是 KaTeX 渲染结果)。
    • 尝试二 (成功): 意识到问题在于渲染流程重复。最终方案是使用 markdown-it 的 KaTeX 插件 (@vscode/markdown-it-katex(这玩意使用npm装的时候是另外一个开发者的,然而已经年久失修并且存在跨域危险,然而好消息是vscode官方注意到了这个项目并且做了后续的修补,所以我用这个玩意))。在扩展端(Node.js 环境)使用 md.render() 时,这个插件直接将 Markdown 中的 LaTeX ($...$, $$...$$) 转换为最终的 KaTeX HTML 结构。这样,发送到 Webview 的 HTML 就已经是预渲染好的,Webview 端只需要包含 KaTeX 的 CSS (katex.min.css) 来正确显示样式即可,不再需要 KaTeX JS 和 auto-render 脚本。

3. 命令与状态栏:

  • 使用 vscode.commands.registerCommand 注册各种用户操作(设置 Token、刷新视图、提交代码、按 ID 查看题目等)。
  • 使用 vscode.window.createStatusBarItem 在状态栏左侧显示当前登录状态和用户名,点击时可以触发相应命令(如显示用户信息或设置 Token)。

四、最“坑”的打包与发布

开发和调试 (F5) 时一切顺利,但当我使用 vsce package 打包成 VSIX 文件,安装到另一台电脑上时,遇到了经典问题:Command 'acmoj.setToken' not foundCannot find module 'axios'

调试过程:

  1. 检查开发者工具: 在测试电脑上打开 VS Code 开发者工具 (Developer: Toggle Developer Tools) 的 Console。发现激活扩展时直接报错 Cannot find module 'axios'
  2. 检查 VSIX 内容: 使用 vsce ls 命令(或将 .vsix 重命名为 .zip 并解压)查看包内容。发现 node_modules 文件夹根本没有被打包进去!

根本原因:
我错误地将运行时必需的库(如 axios, markdown-it, katex, @vscode/markdown-it-katex)放在了 package.jsondevDependencies 下,而不是 dependencies 下。

  • dependencies: 扩展运行时必需的库,会被 vsce package 打包。
  • devDependencies: 开发时使用的库(编译器、类型定义、linter、打包工具等),不会被打包。

解决方案:
仔细检查 package.json,将所有运行时依赖项(axios 等)移到 dependencies 部分,而开发工具(typescript, @types/*, eslint, @vscode/vsce 等)保留在 devDependencies

{
  "dependencies": {
    "@vscode/markdown-it-katex": "...",
    "axios": "...",
    "katex": "...",
    "markdown-it": "..."
  },
  "devDependencies": {
    "@types/vscode": "...",
    "@types/node": "...",
    "@types/markdown-it": "...",
    "@vscode/vsce": "...", // The packaging tool itself is a dev dependency
    "typescript": "...",
    "eslint": "..."
  }
}

关键步骤: 修改 package.json 后,务必执行 “净空 & 重装”,一开始继续报错就是没有清空 node_modules 以及 package-lock.json:

这次生成的 VSIX 文件终于包含了正确的 node_modules,安装后命令可以正常找到,扩展也成功激活了。

五、TypeScript 小插曲

作为 TypeScript 项目,也遇到了一些典型的类型问题:

  • 找不到模块/类型: Cannot find module 'vscode' 或其他 @types 包,通常通过 npm install --save-dev @types/vscode @types/node ... 解决。
  • 隐式 any: 开启 strict 模式后,需要为回调函数参数(如 progress in withProgress, text in validateInput)显式添加类型。
  • API 签名不匹配: 调用 vscode.window.showQuickPick 时,如果提供选项对象,则需要传入 QuickPickItem[] 而不是 string[],需要进行映射。

六、告一段落了吗?

最终诞生的 ACMOJ 助手虽然还有很多可以改进的地方(例如:支持更多 API 功能、更完善的错误处理、UI 优化、测试覆盖),但它已经能满足我最初设定的核心需求,显著提升了在 VS Code 中处理 ACMOJ 相关任务的效率。

虽然 acmoj-helper 已经能够跑起来,甚至在日常使用中帮了我不少忙,但在开发过程中,我逐渐感受到了一些“成长的烦恼”。随着功能的迭代(哪怕只是微小的调整),我发现代码开始变得有些混乱:

  1. 职责不清: commands.ts 文件不仅负责注册命令,还包含了大量像 submitCurrentFile 这样的复杂业务逻辑实现。这使得这个文件异常臃肿,修改起来牵一发而动全身。
  2. 高度耦合: 修改一个模块(比如处理 API 缓存的 cache.ts)可能会意外地影响到视图(submissionProvider.ts)或者命令处理。我之前提到修改 submissionProvider 时几乎重写了它,就是一个典型的例子——视图层和数据获取、业务逻辑耦合太紧了。
  3. 注册混乱: 命令的注册散落在 extension.tscommands.ts 中,不够集中和清晰。
  4. 扩展困难: 如果我想添加新的功能,比如“比赛(Contest)”视图,或者更复杂的题目筛选逻辑,在现有结构下会非常痛苦,需要小心翼翼地在各个文件中穿梭,确保不会破坏现有功能。
  5. 测试障碍: 混合了 UI 逻辑、API 调用和业务处理的代码,非常难以进行单元测试。

这些问题让我意识到,当前的架构虽然能工作,但它并不“优雅”,也缺乏长期的生命力。为了让这个项目能够健康地发展下去,也为了提升我自己的代码设计能力,我决定进行一次彻底的重构。

重构目标:解耦、分层、职责单一

我现在正在进行修改的新架构大致分为以下几个层次:

  1. VS Code 集成层 (extension.ts, src/commands/index.ts)
  2. 服务层 (src/services/)

    • 职责: 封装核心的业务逻辑和与外部资源的交互(如 API、缓存)。每个服务对应一个明确的领域。
  3. 命令处理层 (src/commands/)

    • 职责: 命令处理器接收来自 VS Code 的调用,然后使用服务层来完成具体的任务。它们是 VS Code 命令和业务逻辑之间的桥梁。复杂的逻辑(如 submitCurrentFile)现在被清晰地封装在对应的命令处理器中。
  4. UI 层 (src/views/, src/webviews/)

    • 职责: 负责数据的展示和 UI 交互。
    • views/: 包含 TreeDataProvider(如 ProblemsetProvider, SubmissionProvider)。它们从服务层获取数据,并将其格式化为 VS Code TreeView 需要的结构。
    • webviews/: 包含 Webview Panel 的逻辑。重构后,我们为题目详情和提交详情创建了专门的类(ProblemDetailPanel, SubmissionDetailPanel),封装了各自的 HTML 生成、消息处理和生命周期管理。它们同样通过服务层获取数据,并且 Webview 内的操作(如“复制代码”)现在通常会通过 postMessage 发送消息给 VS Code,由对应的命令处理器来响应。
  5. 核心/数据层 (src/core/, src/types.ts)

    • 职责: 提供最基础的组件和定义。
    • 重构过程中一个非常典型的例子就是 core/apiClient.ts: 一个更纯粹的 HTTP 客户端,只负责发送请求、处理认证头、重试逻辑和基本的错误解释。它不再包含具体的业务端点逻辑。之前他妈的 getUserProfile, getSubmission, ...之类的全在这里面。

虽然重构的过程实在是让我吃了一坨狗屎,甚至会暂时引入新的 Bug,但它为 ACMOJ 助手的长期发展打下了坚实的基础。现在,我可以更有信心地去实现那些在 1.0 版本末尾构想的、更完善的功能了。

如果你也对 VSCode 插件开发感兴趣,或者想为自己常用的工具或平台构建集成,不要犹豫,动手去做吧!从 yo code 开始,遇到问题,解决问题,这个过程本身就是最好的学习。

项目仓库: [https://github.com/TheUnknownThing/vscode-acmoj]

感谢阅读!希望我的经历能对你有所帮助。

快来做第一个评论的人吧~

:D 获取中...