前言
关于这本书
VSDB 是一个 VSCode 全功能数据库客户端扩展,为开发者提供在 VSCode 内直接管理 MySQL 和 PostgreSQL 数据库的能力。它支持自动扫描项目配置文件发现数据库连接、统一管理连接、浏览数据库结构、编辑数据、执行 SQL 查询以及导入导出数据。
本书是 VSDB 的技术实现文档,深入剖析系统架构、核心模块设计、关键技术实现,帮助开发者理解如何构建一个生产级的 VSCode 扩展。
为什么需要 VSDB
传统数据库开发流程中,开发者面临以下痛点:
工具切换频繁
graph LR
A[VSCode 编辑代码] --> B[切换到 DataGrip]
B --> C[查看数据库结构]
C --> D[切换回 VSCode]
D --> E[修改代码]
E --> F[再次切换...]
- 开发者需要频繁切换 IDE 和数据库工具
- 上下文丢失:切换导致思路中断
- 效率损耗:每次切换需要重新定位
连接信息分散
graph TD
subgraph "项目配置"
E1[".env 文件"]
E2["docker-compose.yml"]
E3["框架配置文件"]
end
subgraph "数据库工具"
D1["手动配置连接"]
D2["密码存储问题"]
D3["团队协作困难"]
end
E1 --> D1
E2 --> D1
E3 --> D1
D1 --> D2
D2 --> D3
- 数据库连接信息散落在多处
- 每个开发者需要手动配置
- 团队协作时连接配置难以共享
稳定性问题
数据库操作直接在主进程执行时:
graph TD
A[执行复杂查询] --> B[阻塞主线程]
B --> C[VSCode 卡顿]
C --> D[用户体验差]
A --> E[数据库驱动异常]
E --> F[扩展崩溃]
F --> G[需要重启]
VSDB 通过双进程架构解决这些问题。
VSDB 的解决方案
双进程隔离架构
graph TD
subgraph "VSCode Extension Host"
A[UI层 - TreeView/Webview]
B[管理层 - Connection/IPC]
end
subgraph "Worker Process"
C[数据库驱动]
D[查询队列]
E[Schema Inspector]
end
A --> B
B -->|"IPC (child_process)"| C
C --> D
D --> E
数据库操作隔离在独立进程:
- Worker 崩溃不影响 VSCode
- 自动重启恢复机制
- UI 响应不受阻塞
自动扫描连接
graph LR
subgraph "扫描引擎"
S1[".env 解析"]
S2["docker-compose 解析"]
S3["框架配置解析"]
end
S1 --> R["发现连接"]
S2 --> R
S3 --> R
R --> M["自动添加到 VSDB"]
支持的扫描来源:
.env文件:DATABASE_URL,DB_HOST等docker-compose.yml:MySQL/PostgreSQL 服务- 框架配置:TypeORM, Laravel, Django, Prisma
安全存储机制
graph TD
A[连接配置] --> B["connections.json<br/>(项目级/全局)"]
A --> C["SecretStorage<br/>(密码加密)"]
B --> D["团队可共享"]
C --> E["安全不可读"]
- 连接元数据存储在 JSON 文件,团队可共享
- 密码通过 VSCode SecretStorage API 加密存储
这本书解决什么问题
当你面对以下需求时,这本书会给你答案:
- 理解系统架构? — 双进程架构设计原理
- 掌握 IPC 通信? — 进程间消息协议实现
- 如何实现驱动? — MySQL/PostgreSQL 驱动封装
- 如何做扫描引擎? — 多源配置解析实现
- Webview 如何设计? — React + Monaco 编辑器集成
- 如何扩展功能? — 新数据库类型、新功能的开发方法
你将学到什么
概览篇:建立全局认知
- VSDB 的定位与价值
- 核心概念:连接、IPC、Worker、Schema Inspector
架构篇:理解系统设计
- 双进程架构设计原理
- IPC 通信机制完整实现
- 数据库驱动抽象层设计
- 扫描引擎架构
功能篇:掌握具体实现
- UI 组件实现:TreeView、Webview
- 数据操作:查询、导入导出
- 辅助功能:历史记录、书签、搜索
实战篇:落地应用
- 开发调试流程
- 扩展开发指南
- 最佳实践建议
核心技术要点
本书贯穿以下核心技术要点:
$$ \text{稳定性} = \text{进程隔离} + \text{自动恢复} + \text{超时控制} $$
graph LR
A[数据库操作] --> B[Worker进程]
B --> C[超时控制]
C --> D[异常捕获]
D --> E[自动恢复]
$$ \text{安全性} = \text{SecretStorage} + \text{无明文密码} $$
$$ \text{效率} = \text{自动扫描} + \text{流式传输} + \text{虚拟滚动} $$
适用读者
- VSCode 扩展开发者:学习构建复杂扩展的最佳实践
- 全栈工程师:理解数据库工具的设计思路
- TypeScript 开发者:学习进程通信、异步编程模式
- 技术架构师:理解双进程架构的设计决策
项目背景
VSDB 是一个开源项目,目标是为 VSCode 用户提供媲美 DataGrip 的数据库管理体验,同时保持:
- 轻量:不离开 VSCode 即可完成数据库操作
- 安全:密码加密存储,不泄露敏感信息
- 稳定:Worker 崩溃自动恢复,不影响 VSCode
- 智能:自动发现项目中的数据库连接
"好的工具应该融入工作流,而不是打断工作流。"
第一章:项目概述
本章介绍 VSDB 项目的整体定位、功能特性、技术选型和项目结构。
1.1 项目定位
VSDB 是一个为 Visual Studio Code 设计的全功能数据库客户端扩展。它的核心目标是:
graph TD
A["VSDB 核心目标"] --> B["一体化数据库管理"]
A --> C["开发效率提升"]
A --> D["稳定性保障"]
A --> E["安全性保证"]
B --> B1["连接管理"]
B --> B2["结构浏览"]
B --> B3["数据编辑"]
B --> B4["SQL执行"]
C --> C1["自动扫描连接"]
C --> C2["无需切换工具"]
D --> D1["双进程架构"]
D --> D2["自动恢复"]
E --> E1["SecretStorage"]
E --> E2["无明文存储"]
与竞品的定位差异
| 特性 | DataGrip | VSDB | DBeaver |
|---|---|---|---|
| 独立应用 | ✓ | ✗ | ✓ |
| VSCode 集成 | ✗ | ✓ | ✗ |
| 自动扫描 | ✗ | ✓ | ✗ |
| 轻量 | ✗ | ✓ | ✗ |
| 功能完整度 | ★★★★★ | ★★★☆☆ | ★★★★★ |
| 学习成本 | 中 | 低 | 中 |
VSDB 选择轻量集成路线,牺牲部分高级功能换取无缝的 VSCode 体验。
1.2 功能特性
连接管理
// 核心功能:连接 CRUD
interface ConnectionFeatures {
add: '手动添加连接';
scan: '自动扫描项目配置';
edit: '编辑连接属性';
delete: '删除连接';
group: '连接分组';
color: '颜色标记';
scope: '项目级/全局';
}
支持的扫描来源:
| 来源 | 文件 | 解析内容 |
|---|---|---|
.env | .env, .env.local, .env.development | DATABASE_URL, DB_*, MYSQL_*, PG_* |
docker-compose | docker-compose.yml | MySQL/PostgreSQL 服务定义 |
| TypeORM | ormconfig.json, data-source.ts | 连接配置对象 |
| Laravel | config/database.php | MySQL/PG 连接数组 |
| Django | settings.py | DATABASES 字典 |
| Prisma | schema.prisma | 数据库 URL |
数据库浏览
graph TD
A["连接节点"] --> B["数据库列表"]
B --> C["表列表"]
C --> D["列信息"]
B --> E["视图列表"]
B --> F["存储过程"]
D --> G["类型"]
D --> H["是否NULL"]
D --> I["默认值"]
D --> J["主键"]
D --> K["自增"]
C --> L["索引"]
C --> M["约束"]
C --> N["触发器"]
SQL 编辑器
- Monaco 编辑器集成,语法高亮
- SQL 关键字自动补全
Ctrl+Enter快速执行- 多结果标签页
- 执行状态指示
数据操作
graph LR
subgraph "查询执行"
Q1["普通查询"]
Q2["流式查询<br/>(大数据集)"]
end
subgraph "结果处理"
R1["分页显示"]
R2["内联编辑"]
R3["虚拟滚动"]
end
subgraph "导入导出"
E1["CSV"]
E2["JSON"]
E3["SQL"]
end
Q1 --> R1
Q2 --> R3
R1 --> E1
R1 --> E2
R1 --> E3
辅助功能
- 查询历史:记录执行的 SQL,支持固定防清理
- 书签系统:收藏连接、表、查询,支持分组
- 全局搜索:搜索表名、列名、数据内容
1.3 技术选型
语言与框架
语言: TypeScript
扩展框架: VSCode Extension API
前端框架: React + Vite
编辑器: Monaco Editor
数据库驱动: mysql2 / pg
构建工具: webpack (扩展) + Vite (Webview)
测试框架: Vitest
选型决策记录
| 决策点 | 选择 | 理由 |
|---|---|---|
| 双进程 vs 单进程 | 双进程 | 数据库操作隔离,崩溃不影响 VSCode |
| Worker 通信 | child_process IPC | 原生支持,无需额外依赖 |
| 前端框架 | React | Webview 标准方案,生态丰富 |
| 编辑器 | Monaco | VSCode 同款,语法支持完善 |
| 数据库驱动 | mysql2 / pg | 纯 JS 实现,支持 Promise |
| 流式传输 | 自定义 chunk | 避免大数据集阻塞 IPC |
为什么选择 TypeScript
graph TD
A["TypeScript"] --> B["类型安全"]
A --> C["VSCode 原生"]
A --> D["生态丰富"]
B --> B1["IPC协议类型化"]
B --> B2["配置验证"]
C --> C1["调试支持"]
C --> C2["开发体验"]
D --> D1["数据库驱动"]
D --> D2["UI 框架"]
1.4 项目结构
vsdb/
├── src/ # 扩展源代码
│ ├── extension.ts # 扩展入口,命令注册
│ ├── core/ # 核心管理模块
│ │ ├── connectionManager.ts # 连接 CRUD
│ │ ├── ipcManager.ts # Worker 通信
│ │ ├── storage.ts # 配置存储
│ │ ├── scanner.ts # 扫描协调器
│ │ ├── historyManager.ts # 查询历史
│ │ ├── bookmarkManager.ts # 书签管理
│ │ └── searchEngine.ts # 全局搜索
│ ├── scanner/ # 配置解析器
│ │ ├── envParser.ts # .env 解析
│ │ ├── dockerComposeParser.ts # docker-compose 解析
│ │ └eworkParser.ts # 框架配置解析
│ ├── shared/ # 共享类型和常量
│ │ ├── types.ts # 类型定义
│ │ ├── constants.ts # 常量定义
│ ├── ui/ # UI 层
│ │ ├── tree/ # TreeView
│ │ │ ├── treeProvider.ts # 树数据提供者
│ │ │ ├── treeItems.ts # 树节点类型
│ │ │ └commands.ts # 树命令
│ │ ├── webview/ # Webview Panel
│ │ │ ├── sqlEditor.ts # SQL 编辑器
│ │ │ ├── dataGrid.ts # 数据网格
│ │ │ ├── connectionForm.ts # 连接表单
│ │ │ ├── tableStructure.ts # 表结构面板
│ │ │ ├── importExport.ts # 导入导出
│ ├── worker/ # Worker 进程
│ │ ├── worker.ts # Worker 入口
│ │ ├── driver/ # 数据库驱动
│ │ │ ├── mysql.ts # MySQL 驱动
│ │ │ ├── postgresql.ts # PostgreSQL 驱动
│ │ │ ├── schema/ # Schema Inspector
│ │ │ ├── mysqlSchema.ts # MySQL Schema
│ │ │ ├── pgSchema.ts # PostgreSQL Schema
│ │ ├── queryQueue.ts # 查询队列
├── webview-ui/ # Webview React 应用
│ ├── src/
│ │ ├── App.tsx # 入口组件
│ │ ├── main.tsx # React 入口
│ │ ├── components/
│ │ │ ├── SqlEditor.tsx # SQL 编辑器组件
│ │ │ ├── DataGrid.tsx # 数据网格组件
│ │ │ ├── GridToolbar.tsx # 工具栏
│ │ │ ├── ResultPanel.tsx # 结果面板
│ │ │ ├── ImportExport.tsx # 导入导出组件
│ ├── package.json
│ ├── vite.config.ts
├── __tests__/ # 测试目录
│ ├── core/
│ │ ├── ipcManager.test.ts
│ │ ├── connectionManager.test.ts
│ ├── scanner/
│ │ ├── envParser.test.ts
│ ├── fixtures/
│ │ ├── config/
│ │ │ ├── database.yml
│ │ │ ├── ormconfig.json
│ │ ├── docker-compose.yaml
├── dist/ # 构建输出
├── resources/ # 资源文件 (图标等)
├── docs/ # 设计文档
├── package.json # 扩展配置
├── tsconfig.json # TypeScript 配置
├── webpack.config.js # webpack 配置
├── vite.config.ts # Vite 配置
└── CLAUDE.md # Claude Code 指导文件
1.5 关键文件说明
入口文件
| 文件 | 作用 |
|---|---|
src/extension.ts | 扩展入口,注册命令、视图、初始化管理器 |
src/worker/worker.ts | Worker 入口,处理 IPC 消息,路由到驱动 |
核心管理器
| 文件 | 作用 |
|---|---|
src/core/connectionManager.ts | 连接 CRUD,项目级/全局存储协调 |
src/core/ipcManager.ts | Worker 生命周期,请求/响应匹配,崩溃恢复 |
src/core/storage.ts | JSON 文件存储 + SecretStorage |
src/core/scanner.ts | 协调多个 Parser 执行扫描 |
类型定义
| 文件 | 作用 |
|---|---|
src/shared/types.ts | 所有接口定义:DbConnection, WorkerRequest, QueryResult 等 |
src/shared/constants.ts | 常量:默认端口、命令 ID、视图 ID |
1.6 构建与开发
构建命令
# 生产构建
npm run compile
# 开发监听
npm run watch
# 构建 Webview
npm run build:webview
# 运行测试
npm run test
# 测试监听
npm run test:watch
# Lint
npm run lint
# 打包扩展
vsce package
开发流程
graph TD
A["克隆项目"] --> B["npm install"]
B --> C["npm run compile"]
C --> D["cd webview-ui && npm install"]
D --> E["npm run build"]
E --> F["F5 启动调试"]
F --> G["修改代码"]
G --> H{"Webview 变更?"}
H -->|是| I["rebuild webview"]
H -->|否| J["自动重编译"]
I --> K["刷新 Webview"]
J --> K
K --> G
调试配置
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"]
}
]
}
1.7 小结
本章介绍了 VSDB 的:
- 定位:VSCode 内的轻量数据库客户端
- 功能:连接管理、结构浏览、SQL 编辑、数据操作
- 技术选型:TypeScript + 双进程架构 + React Webview
- 项目结构:核心模块、扫描引擎、Worker、UI 层
下一章将深入介绍核心概念和数据模型。
第二章:核心概念
本章深入介绍 VSDB 的核心数据模型、类型定义和关键概念。
2.1 连接模型
DbConnection 接口
export interface DbConnection {
id: string; // UUID,唯一标识
name: string; // 用户自定义名称,如 "dev-mysql"
type: 'mysql' | 'postgresql'; // 数据库类型
host: string; // 主机地址
port: number; // 端口号
username: string; // 用户名
password: string; // 密码(存储时加密)
database?: string; // 默认数据库
options?: Record<string, any>; // 扩展选项(SSL、charset 等)
group?: string; // 分组名称
color?: string; // 颜色标记
scope: 'project' | 'global'; // 存储范围
}
连接范围
graph TD
subgraph "项目级 scope: project"
P1[".vsdb/connections.json"]
P2["仅当前项目可见"]
P3["团队可共享(git)"]
end
subgraph "全局级 scope: global"
G1["~/.vsdb/connections.json"]
G2["所有项目可见"]
G3["个人私有"]
end
P1 --> P2
P2 --> P3
G1 --> G2
G2 --> G3
| 范围 | 存储位置 | 可见性 | 用途 |
|---|---|---|---|
project | .vsdb/connections.json | 当前项目 | 团队共享开发数据库 |
global | ~/.vsdb/connections.json | 所有项目 | 个人私有数据库 |
连接状态
export interface ConnectionState {
connectionId: string;
status: 'disconnected' | 'connecting' | 'connected' | 'error';
error?: string;
activeDatabase?: string;
lastQuery?: string;
lastQueryTime?: number;
}
状态流转:
stateDiagram-v2
[*] --> disconnected
disconnected --> connecting: 用户点击连接
connecting --> connected: 连接成功
connecting --> error: 连接失败
connected --> disconnected: 用户断开
error --> connecting: 重试
connected --> error: 连接丢失
2.2 扫描模型
ScannedConnection 接口
export interface ScannedConnection {
name: string; // 生成的连接名称
type: 'mysql' | 'postgresql';
host: string;
port: number;
username: string;
password: string;
database?: string;
source: 'env' | 'docker-compose' | 'framework';
sourceFile: string; // 来源文件路径
confidence: 'high' | 'medium' | 'low';
}
置信度分级
| 置信度 | 来源 | 特点 |
|---|---|---|
high | DATABASE_URL, docker-compose | 信息完整,可直接使用 |
medium | 框架配置解析 | 信息可能不完整 |
low | 部分 DB_* 变量 | 缺少关键信息(如密码) |
ScannerResult 接口
export interface ScannerResult {
connections: ScannedConnection[]; // 发现的连接
errors: Array<{ file: string; error: string }>; // 解析错误
scannedFiles: string[]; // 扫描过的文件列表
}
2.3 IPC 协议模型
WorkerRequest 接口
export interface WorkerRequest {
id: string; // 请求 UUID
type: 'connect' | 'disconnect' | 'query' |
'streamQuery' | 'schema' | 'cancel' | 'ping' | 'shutdown';
connectionId: string;
payload: {
config?: DbConnection;
sql?: string;
params?: unknown[];
schemaType?: 'databases' | 'tables' | 'columns' |
'views' | 'procedures' | 'indexes' |
'constraints' | 'triggers' | 'ddl';
database?: string;
table?: string;
};
}
WorkerResponse 接口
export interface WorkerResponse {
id: string; // 匹配请求 UUID
type: 'result' | 'stream' | 'streamEnd' | 'error' | 'pong';
data?: QueryResult | SchemaResult;
error?: {
code: string;
message: string;
errorClass?: 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';
retryable?: boolean;
};
}
StreamChunk 接口(流式传输)
export interface StreamChunk {
requestId: string; // 匹配请求 UUID
chunkIndex: number; // 分块索引
rows: Record<string, unknown>[]; // 数据行
totalRows?: number; // 总行数(仅最后一块)
}
流式传输示意:
sequenceDiagram
participant Host as Extension Host
participant Worker as Worker Process
Host->>Worker: streamQuery (id=001)
Worker-->>Host: StreamChunk (index=0, rows=[...1000])
Worker-->>Host: StreamChunk (index=1, rows=[...1000])
Worker-->>Host: StreamChunk (index=2, rows=[...500], totalRows=2500)
Worker-->>Host: streamEnd (id=001)
2.4 查询结果模型
QueryResult 接口
export interface QueryResult {
columns: string[]; // 列名数组
rows: Record<string, unknown>[]; // 数据行数组
rowCount: number; // 行数
affectedRows?: number; // 影响行数(INSERT/UPDATE/DELETE)
executionTime: number; // 执行时间(毫秒)
}
SchemaResult 接口
export interface SchemaResult {
type: 'databases' | 'tables' | 'columns' | 'views' |
'procedures' | 'indexes' | 'constraints' | 'triggers' | 'ddl';
data: unknown[];
}
2.5 Schema 信息模型
表信息
export interface TableInfo {
name: string;
schema?: string; // PostgreSQL schema
columns: ColumnInfo[];
indexes: IndexInfo[];
rowCount?: number;
}
列信息
export interface ColumnInfo {
name: string;
type: string; // 如 "varchar(255)", "int"
nullable: boolean;
defaultValue?: string;
isPrimaryKey: boolean;
isAutoIncrement: boolean;
}
索引信息
export interface IndexInfo {
name: string;
columns: string[]; // 索引包含的列
isUnique: boolean;
isPrimary: boolean;
}
Schema 结构示例
graph TD
A["数据库"] --> B["表: users"]
B --> C["列"]
C --> C1["id: int, PK, auto_increment"]
C --> C2["name: varchar(255), NOT NULL"]
C --> C3["email: varchar(255), NULL"]
B --> D["索引"]
D --> D1["PRIMARY (id)"]
D --> D2["idx_email (email), UNIQUE"]
B --> E["约束"]
E --> E1["PRIMARY KEY (id)"]
E --> E2["UNIQUE (email)"]
2.6 查询历史模型
export interface QueryHistoryItem {
id: string; // UUID
sql: string; // SQL 语句
connectionId: string; // 执行连接
connectionName: string; // 连接名称
executedAt: Date; // 执行时间
rowCount?: number; // 结果行数
executionTime?: number; // 执行时间(毫秒)
pinned?: boolean; // 固定标记(不被自动清理)
}
历史记录管理策略:
graph TD
A["执行 SQL"] --> B["记录到历史"]
B --> C["检查上限"]
C -->|"超过 100 条"| D["清理未固定的旧记录"]
C -->|"未超过"| E["保持"]
D --> F["保留固定记录"]
2.7 书签模型
Bookmark 接口
export interface Bookmark {
id: string;
type: 'connection' | 'database' | 'table' | 'query';
name: string; // 显示名称
target: {
connectionId?: string;
database?: string;
table?: string;
sql?: string;
};
createdAt: Date;
groupId?: string; // 所属分组 ID
}
BookmarkGroup 接口
export interface BookmarkGroup {
id: string;
name: string;
color?: string; // 颜色标识
}
书签类型结构:
graph TD
A["Bookmark"] --> B["connection"]
A --> C["database"]
A --> D["table"]
A --> E["query"]
B --> B1["target: connectionId"]
C --> C1["target: connectionId + database"]
D --> D1["target: connectionId + database + table"]
E --> E1["target: connectionId + sql"]
2.8 搜索模型
SearchResult 接口
export interface SearchResult {
type: 'table' | 'column' | 'data';
connectionId: string;
connectionName: string;
database: string;
name: string; // 表名/列名
detail?: string; // 列类型、表名等详细信息
}
SearchOptions 接口
export interface SearchOptions {
searchTables?: boolean; // 搜索表名(默认 true)
searchColumns?: boolean; // 搜索列名(默认 true)
searchData?: boolean; // 搜索数据内容(默认 false)
connectionIds?: string[]; // 限制特定连接
tables?: string[]; // 数据搜索范围
caseSensitive?: boolean; // 大小写敏感
}
Schema 缓存
export interface SchemaCacheEntry {
tables: TableInfo[];
columns: ColumnInfo[];
cachedAt: Date;
}
缓存策略:
graph TD
A["搜索请求"] --> B{"缓存存在?"}
B -->|是| C["使用缓存"]
B -->|否| D["查询 Worker"]
D --> E["存入缓存"]
E --> C
C --> F["返回结果"]
2.9 小结
本章介绍了 VSDB 的核心概念:
| 概念 | 用途 |
|---|---|
DbConnection | 数据库连接配置 |
ConnectionState | 连接状态跟踪 |
ScannedConnection | 扫描发现的连接 |
WorkerRequest/Response | IPC 通信协议 |
StreamChunk | 大数据流式传输 |
QueryResult | 查询结果封装 |
TableInfo/ColumnInfo | Schema 元信息 |
QueryHistoryItem | 查询历史记录 |
Bookmark | 书签收藏 |
SearchResult | 搜索结果 |
下一章将深入系统架构设计。
第三章:系统架构
本章详细介绍 VSDB 的双进程架构设计、分层结构和各模块职责。
3.1 整体架构
VSDB 采用双进程架构,将数据库操作隔离在独立的 Worker 进程中。
graph TD
subgraph "VSCode Extension Host (主进程)"
A["extension.ts<br/>入口与命令注册"]
B["核心管理层"]
C["UI 层"]
subgraph "核心管理层"
B1["ConnectionManager"]
B2["IpcManager"]
B3["Storage"]
B4["ScannerEngine"]
B5["HistoryManager"]
B6["BookmarkManager"]
B7["SearchEngine"]
end
subgraph "UI 层"
C1["TreeView<br/>(连接树)"]
C2["Webview Panels<br/>(React 应用)"]
end
end
subgraph "Worker Process (子进程)"
D["worker.ts<br/>Worker 入口"]
E["QueryQueue<br/>查询队列"]
subgraph "数据库驱动层"
F1["MySqlDriver"]
F2["PostgreSqlDriver"]
end
subgraph "Schema Inspector"
G1["MySqlSchema"]
G2["PgSchema"]
end
end
A --> B
B --> C
B2 -->|"IPC (child_process)"| D
D --> E
E --> F1
E --> F2
F1 --> G1
F2 --> G2
3.2 双进程架构原理
为什么选择双进程
graph TD
subgraph "单进程架构的问题"
A1["数据库操作"] --> A2["阻塞 Extension Host"]
A2 --> A3["VSCode 卡顿"]
A4["驱动异常"] --> A5["扩展崩溃"]
A5 --> A6["需要重启 VSCode"]
end
subgraph "双进程架构的优势"
B1["数据库操作"] --> B2["Worker 进程"]
B2 --> B3["Extension Host 不受影响"]
B4["Worker 崩溃"] --> B5["自动重启恢复"]
B5 --> B6["用户体验连续"]
end
| 问题 | 单进程 | 双进程 |
|---|---|---|
| 复杂查询阻塞 | 阻塞 VSCode | Worker 执行,UI 正常 |
| 驱动异常 | 扩展崩溃 | Worker 崩溃自动恢复 |
| 内存泄漏 | 影响 VSCode | Worker 可重启清理 |
| 长时间操作 | UI 无响应 | 异步 IPC 通信 |
进程通信方式
VSDB 使用 Node.js child_process.fork() 创建 Worker:
// IpcManager.ts
this.worker = fork(this.workerPath, [], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
});
IPC 通信特点:
- 原生支持:无需额外依赖
- JSON 序列化:消息自动序列化/反序列化
- 双向通信:主进程和 Worker 都可发送消息
3.3 分层架构
graph TB
subgraph "UI 层"
UI1["TreeView"]
UI2["Webview Panel"]
end
subgraph "管理层"
M1["ConnectionManager"]
M2["IpcManager"]
M3["ScannerEngine"]
M4["HistoryManager"]
M5["BookmarkManager"]
M6["SearchEngine"]
end
subgraph "Worker 层"
W1["QueryQueue"]
W2["Driver"]
W3["SchemaInspector"]
end
subgraph "存储层"
S1["JSON Storage"]
S2["SecretStorage"]
S3["Schema Cache"]
end
UI1 --> M1
UI1 --> M2
UI2 --> M2
M1 --> S1
M1 --> S2
M3 --> S1
M2 -->|"IPC"| W1
W1 --> W2
W2 --> W3
M6 --> S3
各层职责
| 层级 | 模块 | 职责 |
|---|---|---|
| UI 层 | TreeView | 连接树展示、节点交互 |
| Webview | SQL 编辑器、数据网格、表单 | |
| 管理层 | ConnectionManager | 连接 CRUD、范围管理 |
| IpcManager | Worker 生命周期、消息路由 | |
| ScannerEngine | 协调配置扫描 | |
| HistoryManager | 查询历史存储 | |
| BookmarkManager | 书签管理 | |
| SearchEngine | 全局搜索协调 | |
| Worker 层 | QueryQueue | 并发控制、超时管理 |
| Driver | 数据库连接、查询执行 | |
| SchemaInspector | 元信息查询 | |
| 存储层 | JSON Storage | 连接配置持久化 |
| SecretStorage | 密码加密存储 | |
| Schema Cache | 搜索缓存 |
3.4 Extension Host(主进程)
入口模块
extension.ts 是扩展入口,负责:
// 主要职责
export function activate(context: vscode.ExtensionContext) {
// 1. 初始化管理器
const storage = new Storage(context.globalState, context.secrets);
const connectionManager = new ConnectionManager(storage, projectRoot);
const ipcManager = new IpcManager({ ... });
// 2. 初始化扫描引擎
const scannerEngine = new ScannerEngine();
// 3. 初始化辅助管理器
const historyManager = new HistoryManager(context.globalState);
const bookmarkManager = new BookmarkManager(context.globalState);
const searchEngine = new SearchEngine(ipcManager, connectionManager);
// 4. 初始化 TreeView
const treeProvider = new TreeProvider(connectionManager, ipcManager, ...);
const treeView = vscode.window.createTreeView(VIEWS.CONNECTIONS, {
treeDataProvider: treeProvider,
});
// 5. 注册命令
registerCommands(context);
// 6. 自动扫描(如果有工作区)
autoScanProject(workspaceRoot);
}
命令注册
VSDB 注册两类命令:
graph TD
subgraph "全局命令"
C1["vsdb.addConnection"]
C2["vsdb.scanProject"]
C3["vsdb.newQuery"]
C4["vsdb.search"]
end
subgraph "树节点命令"
T1["vsdb.tree.connect"]
T2["vsdb.tree.disconnect"]
T3["vsdb.tree.newQuery"]
T4["vsdb.viewTableStructure"]
T5["vsdb.exportData"]
end
C1 --> F1["打开连接表单"]
C2 --> F2["扫描项目配置"]
C3 --> F3["打开 SQL 编辑器"]
C4 --> F4["搜索表/列"]
3.5 Worker Process(子进程)
Worker 入口
worker.ts 是 Worker 进程入口:
// Worker 生命周期
process.on('message', (request: WorkerRequest) => {
handleMessage(request); // 处理请求
});
process.send?.({ type: 'ready' }); // 通知主进程就绪
process.on('disconnect', () => {
shutdown(); // 父进程断开时清理
});
process.on('SIGTERM', () => {
shutdown(); // 信号终止时清理
});
请求处理流程
sequenceDiagram
participant Host as Extension Host
participant Worker as Worker Process
participant Queue as QueryQueue
participant Driver as DatabaseDriver
Host->>Worker: WorkerRequest (type=query)
Worker->>Queue: enqueue(request)
Queue->>Driver: execute(sql)
Driver-->>Queue: QueryResult
Queue-->>Worker: WorkerResponse
Worker-->>Host: WorkerResponse
查询队列
QueryQueue 实现并发控制和超时管理:
class QueryQueue {
private queue = new Map<string, PendingQuery>();
private maxConcurrency = 5;
private defaultTimeout = 30000;
enqueue(request: WorkerRequest, handler: () => Promise<WorkerResponse>) {
// 1. 检查并发限制
// 2. 设置超时定时器
// 3. 执行查询
// 4. 返回结果或超时错误
}
cancel(requestId: string) {
// 取消正在执行的查询
}
}
3.6 数据流分析
连接建立流程
sequenceDiagram
participant User as 用户
participant Tree as TreeView
participant CM as ConnectionManager
participant IPC as IpcManager
participant Worker as Worker Process
participant Driver as MySqlDriver
User->>Tree: 点击连接节点
Tree->>IPC: sendRequest(connect)
IPC->>Worker: WorkerRequest(type=connect)
Worker->>CM: 检查连接配置
Worker->>Driver: driver.connect(config)
Driver-->>Worker: 连接成功
Worker-->>IPC: WorkerResponse(type=result)
IPC-->>Tree: 更新连接状态
Tree-->>User: 显示连接图标
查询执行流程
sequenceDiagram
participant User as 用户
participant Webview as SQL Editor
participant IPC as IpcManager
participant Worker as Worker Process
participant Queue as QueryQueue
participant Driver as Driver
User->>Webview: 输入 SQL,点击执行
Webview->>IPC: postMessage(executeQuery)
IPC->>Worker: WorkerRequest(type=query)
Worker->>Queue: enqueue
Queue->>Driver: driver.query(sql)
Driver-->>Queue: QueryResult
Queue-->>Worker: WorkerResponse
Worker-->>IPC: WorkerResponse
IPC-->>Webview: WorkerResponse
Webview-->>User: 显示结果表格
流式查询流程(大数据)
sequenceDiagram
participant User as 用户
participant Webview as SQL Editor
participant IPC as IpcManager
participant Worker as Worker Process
participant Driver as Driver
User->>Webview: 执行大查询
Webview->>IPC: sendRequest(streamQuery)
IPC->>Worker: WorkerRequest(type=streamQuery)
Worker->>Driver: driver.streamQuery(sql)
loop 每 1000 行
Driver-->>Worker: chunk
Worker-->>IPC: StreamChunk
IPC-->>Webview: StreamChunk
Webview-->>User: 渲染数据块
end
Driver-->>Worker: end
Worker-->>IPC: streamEnd
IPC-->>Webview: streamEnd
Webview-->>User: 完成
3.7 错误处理架构
Worker 崩溃恢复
stateDiagram-v2
[*] --> Running
Running --> Crashed: 进程退出
Crashed --> Restarting: 重启尝试 < max
Restarting --> Running: 重启成功
Restarting --> Failed: 重试次数超限
Failed --> [*]
Running --> Shutdown: 正常关闭
Shutdown --> [*]
恢复策略:
// IpcManager.ts
worker.on('exit', (code, signal) => {
if (!isShuttingDown && restartOnCrash && restartAttempts < maxRestartAttempts) {
restartAttempts++;
const delay = Math.min(500 * Math.pow(2, restartAttempts - 1), 5000);
setTimeout(() => {
this.start(); // 重启 Worker
this.reconnectActiveConnections(); // 恢复连接
}, delay);
}
});
错误分类与处理
// Worker 错误分类
type QueryErrorClass = 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';
function classifyQueryError(error: unknown): ClassifiedError {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('ECONNREFUSED')) {
return { errorClass: 'connection', retryable: true };
}
if (message.includes('syntax error')) {
return { errorClass: 'syntax', retryable: false };
}
if (message.includes('timeout')) {
return { errorClass: 'timeout', retryable: true };
}
// ...
}
| 错误类 | 典型原因 | retryable | 用户提示 |
|---|---|---|---|
connection | 网络问题、服务未启动 | ✓ | "连接失败,请检查网络" |
syntax | SQL 语法错误 | ✗ | "SQL 语法错误" |
timeout | 查询超时 | ✓ | "查询超时,可重试" |
permission | 权限不足 | ✗ | "权限不足" |
unknown | 其他错误 | ✗ | "未知错误" |
3.8 性能考量
IPC 消息大小限制
IPC 消息通过 JSON 序列化传输,大消息会影响性能:
// 流式传输策略
const chunkSize = 1000; // 每块最多 1000 行
for await (const chunk of driver.streamQuery(sql)) {
send({
requestId,
chunkIndex,
rows: chunk.rows, // 限制在 chunkSize
});
}
内存管理
// Worker 内存监控
const memoryLimitMb = 512;
setInterval(() => {
const usage = process.memoryUsage();
const heapUsedMb = usage.heapUsed / (1024 * 1024);
if (heapUsedMb > memoryLimitMb) {
send({ type: 'error', error: { code: 'WORKER_MEMORY_EXCEEDED' } });
}
}, 10000);
心跳机制
// 防止僵尸进程
const heartbeatInterval = 15000; // 15秒发送 ping
const maxMissedHeartbeats = 2; // 2次未响应视为崩溃
setInterval(() => {
worker.send({ type: 'ping' });
missedHeartbeats++;
if (missedHeartbeats >= maxMissedHeartbeats) {
worker.kill('SIGKILL'); // 强制终止
}
}, heartbeatInterval);
3.9 小结
本章介绍了 VSDB 的系统架构:
- 双进程架构:Extension Host + Worker Process
- 分层设计:UI 层 → 管理层 → Worker 层 → 存储层
- IPC 通信:JSON 序列化,请求/响应匹配
- 错误处理:崩溃恢复,错误分类
- 性能优化:流式传输,内存监控,心跳机制
下一章将深入 IPC 通信机制的实现细节。
第四章:IPC通信机制
本章深入介绍 VSDB 的 IPC 通信实现,包括消息协议、请求匹配、流式传输和崩溃恢复。
4.1 IPC 概述
IPC(Inter-Process Communication)是双进程架构的核心,负责 Extension Host 和 Worker Process 之间的消息传递。
graph LR
subgraph "Extension Host"
A["IpcManager"]
B["sendRequest()"]
C["handleMessage()"]
end
subgraph "Worker Process"
D["worker.ts"]
E["handleMessage()"]
F["send()"]
end
B -->|"JSON over IPC"| D
D --> E
E --> F
F -->|"JSON over IPC"| A
A --> C
Node.js child_process IPC
VSDB 使用 child_process.fork() 创建 Worker:
// fork 创建子进程,自动建立 IPC 通道
this.worker = fork(this.workerPath, [], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
});
IPC 特性:
- 自动序列化:
process.send()自动 JSON 序列化 - 事件驱动:
process.on('message')接收消息 - 双向通信:主进程和子进程都可发送/接收
4.2 消息协议设计
请求类型
interface WorkerRequest {
id: string; // UUID,用于匹配响应
type: RequestType; // 请求类型
connectionId: string; // 目标连接
payload: RequestPayload;
}
type RequestType =
| 'connect' // 建立连接
| 'disconnect' // 断开连接
| 'query' // 执行查询
| 'streamQuery' // 流式查询
| 'schema' // 查询 Schema
| 'cancel' // 取消查询
| 'ping' // 心跳检测
| 'shutdown'; // 关闭 Worker
interface RequestPayload {
config?: DbConnection; // 连接配置
sql?: string; // SQL 语句
params?: unknown[]; // 查询参数
schemaType?: SchemaType; // Schema 类型
database?: string; // 数据库名
table?: string; // 表名
}
响应类型
interface WorkerResponse {
id: string; // 匹配请求 ID
type: ResponseType; // 响应类型
data?: QueryResult | SchemaResult;
error?: WorkerError;
}
type ResponseType =
| 'result' // 查询结果
| 'streamEnd' // 流式传输结束
| 'error' // 错误响应
| 'pong'; // 心跳响应
interface WorkerError {
code: string;
message: string;
errorClass?: ErrorClass;
retryable?: boolean;
}
流式分块
interface StreamChunk {
requestId: string; // 匹配请求 ID
chunkIndex: number; // 分块索引
rows: Record<string, unknown>[]; // 数据行
totalRows?: number; // 总行数(仅最后一块)
}
4.3 IpcManager 实现
核心结构
class IpcManager {
private workerPath: string;
private worker: ChildProcess | null = null;
private pendingRequests = new Map<string, PendingRequest>();
// 配置参数
private restartOnCrash: boolean;
private maxRestartAttempts: number;
private requestTimeout: number;
private heartbeatInterval: number;
// 心跳监控
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
private missedHeartbeats = 0;
// 连接跟踪(崩溃恢复用)
private activeConnections = new Map<string, DbConnection>();
}
PendingRequest 结构
interface PendingRequest {
resolve: (response: WorkerResponse) => void;
reject: (error: Error) => void;
streamChunks: StreamChunk[]; // 流式分块累积
isStream: boolean; // 是否流式请求
timer: ReturnType<typeof setTimeout>; // 超时定时器
}
启动 Worker
start(): void {
if (this.worker && !this.worker.killed) {
return; // 已运行
}
this.isShuttingDown = false;
// 创建 Worker 进程
this.worker = fork(this.workerPath, [], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
});
// 消息处理
this.worker.on('message', (message: WorkerResponse | StreamChunk) => {
this.handleMessage(message);
});
// 进程退出处理
this.worker.on('exit', (code, signal) => {
this.handleWorkerExit(code, signal);
});
// 启动心跳
this.startHeartbeat();
}
发送请求
sendRequest(request: Omit<WorkerRequest, 'id'>): Promise<WorkerResponse> {
return new Promise((resolve, reject) => {
if (!this.worker || this.worker.killed) {
reject(new Error('Worker process is not running'));
return;
}
// 生成完整请求
const fullRequest: WorkerRequest = {
...request,
id: randomUUID(),
};
// 创建 PendingRequest
const pending: PendingRequest = {
resolve,
reject,
streamChunks: [],
isStream: request.type === 'streamQuery',
timer: setTimeout(() => {
this.pendingRequests.delete(fullRequest.id);
reject(new Error(`Request timed out`));
}, this.requestTimeout),
};
this.pendingRequests.set(fullRequest.id, pending);
// 跟踪活动连接
if (request.type === 'connect' && request.payload.config) {
this.activeConnections.set(request.connectionId, request.payload.config);
}
// 发送请求
this.worker.send(fullRequest);
});
}
处理响应
private handleMessage(message: WorkerResponse | StreamChunk): void {
// 重置心跳计数
this.missedHeartbeats = 0;
// 心跳响应
if (message.type === 'pong') {
return;
}
// 流式分块
if ('requestId' in message) {
this.handleStreamChunk(message as StreamChunk);
return;
}
// 普通响应
const response = message as WorkerResponse;
const pending = this.pendingRequests.get(response.id);
if (!pending) {
return; // 无匹配的请求(可能已超时)
}
clearTimeout(pending.timer);
// 流式结束
if (pending.isStream && response.type === 'streamEnd') {
this.assembleStreamResponse(pending, response);
} else {
this.pendingRequests.delete(response.id);
pending.resolve(response);
}
}
流式响应组装
private assembleStreamResponse(
pending: PendingRequest,
response: WorkerResponse
): void {
// 合并所有分块
const assembledRows: Record<string, unknown>[] = [];
let columns: string[] = [];
let totalRows = 0;
for (const chunk of pending.streamChunks) {
assembledRows.push(...chunk.rows);
if (chunk.totalRows !== undefined) {
totalRows = chunk.totalRows;
}
}
// 提取列名
if (assembledRows.length > 0) {
columns = Object.keys(assembledRows[0]);
}
// 构造 QueryResult
const queryResult: QueryResult = {
columns,
rows: assembledRows,
rowCount: assembledRows.length,
executionTime: 0,
...(totalRows > 0 ? { affectedRows: totalRows } : {}),
};
this.pendingRequests.delete(response.id);
pending.resolve({
id: response.id,
type: 'result',
data: queryResult,
});
}
4.4 流式传输详解
为什么需要流式传输
graph TD
A["查询返回 100 万行"] --> B{"传输方式"}
B -->|普通查询| C["单次 IPC 消息"]
C --> D["消息过大"]
D --> E["IPC 阻塞"]
E --> F["UI 卡顿"]
B -->|流式查询| G["分块传输"]
G --> H["每块 1000 行"]
H --> I["渐进渲染"]
I --> J["UI 流畅"]
Worker 端流式实现
async function handleStreamQuery(request: WorkerRequest): Promise<void> {
const entry = connections.get(request.connectionId);
const { driver } = entry;
const sql = request.payload.sql;
activeStreams.add(request.id); // 注册活动流
queryQueue.enqueue(request, async () => {
try {
const stream = driver.streamQuery(sql);
const chunkSize = 1000;
let chunkIndex = 0;
for await (const chunk of stream) {
// 检查是否被取消
if (!activeStreams.has(request.id)) {
return { id: request.id, type: 'streamEnd' };
}
// 发送分块
send({
requestId: request.id,
chunkIndex,
rows: chunk.rows,
totalRows: chunk.totalRows,
});
chunkIndex++;
}
// 发送结束标记
return { id: request.id, type: 'streamEnd' };
} finally {
activeStreams.delete(request.id);
}
});
}
Driver 端流式实现
// mysql.ts
async *streamQuery(
sql: string,
params?: any[],
chunkSize = 1000
): AsyncGenerator<StreamChunkData> {
const streamConn = await mysql.createConnection(config);
try {
const [result] = await streamConn.execute(sql, params);
if (!Array.isArray(result)) {
yield { chunkIndex: 0, rows: [], totalRows: 0 };
return;
}
const allRows = result;
const totalRows = allRows.length;
// 分块 yield
for (let offset = 0; offset < allRows.length; offset += chunkSize) {
const chunk = allRows.slice(offset, offset + chunkSize);
const isLast = offset + chunkSize >= allRows.length;
yield {
chunkIndex: Math.floor(offset / chunkSize),
rows: chunk,
...(isLast ? { totalRows } : {}),
};
}
} finally {
await streamConn.end();
}
}
分块超时控制
// IpcManager 设置分块超时
private setupStreamChunkTimeout(requestId: string): void {
const timer = setTimeout(() => {
const pending = this.pendingRequests.get(requestId);
if (pending) {
this.pendingRequests.delete(requestId);
clearTimeout(pending.timer);
pending.reject(new Error('STREAM_TIMEOUT'));
}
}, this.streamChunkTimeout);
this.streamChunkTimers.set(requestId, timer);
}
// 收到分块时重置
private resetStreamChunkTimer(requestId: string): void {
// 清除旧定时器
const existing = this.streamChunkTimers.get(requestId);
if (existing) clearTimeout(existing);
// 设置新定时器
this.setupStreamChunkTimeout(requestId);
}
4.5 心跳机制
心跳设计
sequenceDiagram
participant Host as IpcManager
participant Worker as Worker Process
loop 每 15 秒
Host->>Worker: ping
Worker-->>Host: pong
Host->>Host: missedHeartbeats = 0
end
Note over Host: Worker 未响应
Host->>Host: missedHeartbeats++
Host->>Host: missedHeartbeats++
Host->>Worker: SIGKILL (强制终止)
Host->>Host: handleWorkerExit()
实现
private startHeartbeat(): void {
this.stopHeartbeat();
this.missedHeartbeats = 0;
this.heartbeatTimer = setInterval(() => {
if (!this.worker || this.worker.killed) {
this.stopHeartbeat();
return;
}
// 发送 ping
try {
this.worker.send({ type: 'ping', id: 'heartbeat' });
} catch {
return;
}
// 增加计数
this.missedHeartbeats++;
if (this.missedHeartbeats >= this.maxMissedHeartbeats) {
console.error('Worker heartbeat timeout');
this.handleWorkerHang();
}
}, this.heartbeatInterval);
}
private handleWorkerHang(): void {
if (this.worker && !this.worker.killed) {
this.worker.kill('SIGKILL'); // 强制终止
}
}
Worker 端心跳响应
// worker.ts
case 'ping':
send({ id: 'heartbeat', type: 'pong' });
break;
4.6 崩溃恢复
崩溃检测
worker.on('exit', (code, signal) => {
console.error(`Worker exited: code=${code}, signal=${signal}`);
this.stopHeartbeat();
// 收集丢失的连接
const lostConnectionIds = Array.from(this.activeConnections.keys());
// 拒绝所有待处理请求
this.rejectAllPending(`Worker exited unexpectedly`);
this.worker = null;
// 判断是否需要重启
if (!this.isShuttingDown && this.restartOnCrash) {
this.handleCrashRecovery(lostConnectionIds);
}
});
重启策略
private handleCrashRecovery(lostConnectionIds: string[]): void {
if (this.restartAttempts < this.maxRestartAttempts) {
this.crashed = true;
this.restartAttempts++;
// 指数退避
const delay = Math.min(
500 * Math.pow(2, this.restartAttempts - 1),
5000
);
console.log(`Auto-restarting worker (attempt ${this.restartAttempts})`);
// 通知回调
this.callbacks.onWorkerCrash?.(this.restartAttempts);
this.callbacks.onConnectionsLost?.(lostConnectionIds);
setTimeout(() => {
this.start();
this.reconnectActiveConnections();
this.callbacks.onWorkerRestarted?.();
}, delay);
}
}
连接恢复
private reconnectActiveConnections(): void {
for (const [connectionId, config] of this.activeConnections) {
console.log(`Reconnecting: ${connectionId}`);
this.sendRequest({
type: 'connect',
connectionId,
payload: { config },
}).catch((err) => {
console.error(`Failed to reconnect ${connectionId}: ${err.message}`);
});
}
}
重启时序
sequenceDiagram
participant User as 用户
participant IPC as IpcManager
participant Worker1 as Worker (旧)
participant Worker2 as Worker (新)
Worker1-->>IPC: exit (crash)
IPC->>User: onWorkerCrash callback
IPC->>IPC: wait (指数退避)
IPC->>Worker2: fork (新进程)
Worker2-->>IPC: ready
IPC->>Worker2: reconnect all activeConnections
Worker2-->>IPC: connect results
IPC->>User: onWorkerRestarted callback
4.7 优雅关闭
关闭流程
async shutdown(): Promise<void> {
this.isShuttingDown = true;
this.stopHeartbeat();
// 等待待处理请求完成
if (this.pendingRequests.size > 0 && this.worker) {
// 发送关闭信号
try {
this.worker.send({ type: 'shutdown' });
} catch {}
// 等待请求排空
const drainStart = Date.now();
while (this.pendingRequests.size > 0 &&
(Date.now() - drainStart) < this.gracefulShutdownTimeout) {
await new Promise(resolve => setTimeout(resolve, 200));
}
}
// 拒绝剩余请求
this.rejectAllPending('IPCManager shutting down');
// 终止进程
if (this.worker && !this.worker.killed) {
this.worker.kill('SIGTERM');
// 等待退出或强制终止
await new Promise(resolve => {
const timeout = setTimeout(() => {
if (!this.worker.killed) {
this.worker.kill('SIGKILL');
}
resolve();
}, this.gracefulShutdownTimeout);
this.worker.on('exit', () => {
clearTimeout(timeout);
resolve();
});
});
}
this.worker = null;
this.activeConnections.clear();
}
Worker 端关闭
// worker.ts
async function shutdown(): Promise<void> {
stopMemoryMonitoring();
queryQueue.dispose();
// 断开所有连接
for (const [id, entry] of connections) {
try {
await entry.driver.disconnect();
} catch {}
}
connections.clear();
activeStreams.clear();
}
process.on('disconnect', () => {
shutdown().then(() => process.exit(0));
});
process.on('SIGTERM', () => {
shutdown().then(() => process.exit(0));
});
4.8 Worker 就绪机制
就绪信号
// worker.ts - 启动时发送就绪信号
process.send?.({ type: 'ready' });
等待就绪
// IpcManager.ts
private workerReady: boolean = false;
private workerReadyResolve?: () => void;
private workerReadyPromise?: Promise<void>;
start(): void {
this.workerReady = false;
this.workerReadyPromise = new Promise<void>((resolve) => {
this.workerReadyResolve = resolve;
});
this.worker = fork(this.workerPath, ...);
this.worker.on('message', (message) => {
if ((message as any).type === 'ready') {
this.workerReady = true;
this.workerReadyResolve?.();
return;
}
this.handleMessage(message);
});
}
async waitReady(timeoutMs = 5000): Promise<boolean> {
if (this.workerReady) return true;
const timeout = new Promise<false>((resolve) => {
setTimeout(() => resolve(false), timeoutMs);
});
return Promise.race([
this.workerReadyPromise!.then(() => true),
timeout,
]);
}
4.9 配置参数
IpcManagerOptions
interface IpcManagerOptions {
workerScriptPath: string; // Worker 脚本路径
restartOnCrash?: boolean; // 崩溃时重启(默认 true)
maxRestartAttempts?: number; // 最大重启次数(默认 3)
requestTimeout?: number; // 请求超时(默认 60000ms)
heartbeatInterval?: number; // 心跳间隔(默认 15000ms)
heartbeatTimeout?: number; // 心跳超时(默认 30000ms)
streamChunkTimeout?: number; // 分块超时(默认 30000ms)
gracefulShutdownTimeout?: number; // 关闭超时(默认 10000ms)
callbacks?: IpcManagerCallbacks; // 事件回调
}
默认值
const DEFAULT_REQUEST_TIMEOUT = 60000; // 60 秒
const DEFAULT_MAX_RESTART_ATTEMPTS = 3;
const DEFAULT_HEARTBEAT_INTERVAL = 15000; // 15 秒
const DEFAULT_HEARTBEAT_TIMEOUT = 30000; // 30 秒
const DEFAULT_STREAM_CHUNK_TIMEOUT = 30000; // 30 秒
const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT = 10000; // 10 秒
4.10 小结
本章介绍了 VSDB 的 IPC 通信机制:
| 特性 | 实现 |
|---|---|
| 消息协议 | WorkerRequest / WorkerResponse / StreamChunk |
| 请求匹配 | UUID + pendingRequests Map |
| 流式传输 | 分块发送 + 组装响应 |
| 心跳机制 | ping/pong + missedHeartbeats 计数 |
| 崩溃恢复 | 重启 + 重连 activeConnections |
| 优雅关闭 | 等待排空 + SIGTERM/SIGKILL |
| Worker 就绪 | ready 信号 + waitReady() |
下一章将深入数据库驱动的实现细节。
第五章:数据库驱动实现
本章介绍 VSDB 的数据库驱动抽象层和 MySQL/PostgreSQL 驱动的具体实现。
5.1 驱动抽象层设计
驱动接口
VSDB 没有显式定义 Driver 接口,但所有驱动遵循相同的方法签名:
// 隐式驱动接口
interface DatabaseDriver {
// 连接管理
connect(config: DbConnection): Promise<void>;
disconnect(): Promise<void>;
isConnected(): boolean;
// 查询执行
query(sql: string, params?: unknown[]): Promise<QueryResult>;
streamQuery(sql: string, params?: unknown[], chunkSize?: number): AsyncGenerator<StreamChunkData>;
// 访问底层连接(用于 Schema 查询)
getConnection(): Connection | Pool | null;
getPool(): Pool | null; // PostgreSQL
}
驱动选择策略
// worker.ts
function createDriver(config: DbConnection): DatabaseDriver {
return config.type === 'mysql'
? new MySqlDriver()
: new PostgreSqlDriver();
}
graph TD
A["DbConnection.type"] --> B{"数据库类型"}
B -->|"mysql"| C["MySqlDriver"]
B -->|"postgresql"| D["PostgreSqlDriver"]
C --> E["mysql2/promise"]
D --> F["pg"]
E --> G["MySQL 连接"]
F --> H["PostgreSQL Pool"]
5.2 MySQL 驱动实现
MySqlDriver 类
import mysql from 'mysql2/promise';
import type { DbConnection, QueryResult } from '../../shared/types';
export class MySqlDriver {
private connection: mysql.Connection | null = null;
// 连接配置
async connect(config: DbConnection): Promise<void> {
if (this.connection) {
await this.disconnect(); // 先断开旧连接
}
this.connection = await mysql.createConnection({
host: config.host,
port: config.port,
user: config.username,
password: config.password,
database: config.database,
connectTimeout: 10000, // 10 秒连接超时
...config.options, // 扩展选项(SSL、charset 等)
});
}
// 断开连接
async disconnect(): Promise<void> {
if (this.connection) {
try {
await this.connection.end();
} catch {
// 连接可能已关闭,忽略错误
}
this.connection = null;
}
}
// 检查连接状态
isConnected(): boolean {
return this.connection !== null;
}
// 获取底层连接
getConnection(): mysql.Connection | null {
return this.connection;
}
}
查询执行
async query(sql: string, params?: any[]): Promise<QueryResult> {
if (!this.connection) {
throw new Error('MySQL: not connected');
}
const startTime = Date.now();
const [result] = await this.connection.execute(sql, params);
const executionTime = Date.now() - startTime;
// 区分 SELECT 和 INSERT/UPDATE/DELETE
if (result && typeof result === 'object' && 'affectedRows' in result) {
// INSERT/UPDATE/DELETE 返回 ResultSetHeader
return {
columns: [],
rows: [],
rowCount: (result as any).affectedRows,
affectedRows: (result as any).affectedRows,
executionTime,
};
}
// SELECT 返回 RowDataPacket[]
const rows = Array.isArray(result) ? result as Record<string, unknown>[] : [];
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
return {
columns,
rows,
rowCount: rows.length,
executionTime,
};
}
流式查询
async *streamQuery(
sql: string,
params?: any[],
chunkSize = 1000
): AsyncGenerator<{ chunkIndex: number; rows: Record<string, unknown>[]; totalRows?: number }> {
if (!this.connection) {
throw new Error('MySQL: not connected');
}
// 创建独立连接避免阻塞主连接
const streamConn = await mysql.createConnection(
(this.connection as any).connection?.config || {}
);
try {
const [result] = await streamConn.execute(sql, params);
if (!Array.isArray(result)) {
yield { chunkIndex: 0, rows: [], totalRows: 0 };
return;
}
const allRows = result;
const totalRows = allRows.length;
let chunkIndex = 0;
// 分块 yield
for (let offset = 0; offset < allRows.length; offset += chunkSize) {
const chunk = allRows.slice(offset, offset + chunkSize);
const isLast = offset + chunkSize >= allRows.length;
yield {
chunkIndex,
rows: chunk,
...(isLast ? { totalRows } : {}),
};
chunkIndex++;
}
} finally {
await streamConn.end(); // 确保关闭流连接
}
}
mysql2 特性
VSDB 选择 mysql2 的原因:
| 特性 | mysql2 | mysql (旧版) |
|---|---|---|
| Promise 支持 | ✓(原生) | ✗(需包装) |
| Prepare Statement | ✓ | ✓ |
| 流式查询 | ✓ | ✓ |
| 性能 | 更快 | 较慢 |
| 维护状态 | 活跃 | 已停止 |
5.3 PostgreSQL 驱动实现
PostgreSqlDriver 类
import { Pool } from 'pg';
import type { DbConnection, QueryResult } from '../../shared/types';
export class PostgreSqlDriver {
private pool: Pool | null = null;
async connect(config: DbConnection): Promise<void> {
if (this.pool) {
await this.disconnect();
}
// PostgreSQL 使用连接池
this.pool = new Pool({
host: config.host,
port: config.port,
user: config.username,
password: config.password,
database: config.database || 'postgres',
max: 5, // 最大连接数
connectionTimeoutMillis: 10000,
...config.options,
});
// 测试连接
try {
await this.pool.query('SELECT 1');
} catch (err) {
this.pool.end();
this.pool = null;
throw err;
}
}
async disconnect(): Promise<void> {
if (this.pool) {
await this.pool.end();
this.pool = null;
}
}
isConnected(): boolean {
return this.pool !== null;
}
getPool(): Pool | null {
return this.pool;
}
}
查询执行
async query(sql: string, params?: any[]): Promise<QueryResult> {
if (!this.pool) {
throw new Error('PostgreSQL: not connected');
}
const startTime = Date.now();
const result = await this.pool.query(sql, params);
const executionTime = Date.now() - startTime;
// PostgreSQL 结果格式不同于 MySQL
if (result.command !== 'SELECT') {
// INSERT/UPDATE/DELETE
return {
columns: [],
rows: [],
rowCount: result.rowCount || 0,
affectedRows: result.rowCount || 0,
executionTime,
};
}
// SELECT
const rows = result.rows as Record<string, unknown>[];
const columns = result.fields?.map(f => f.name) ||
(rows.length > 0 ? Object.keys(rows[0]) : []);
return {
columns,
rows,
rowCount: rows.length,
executionTime,
};
}
流式查询
async *streamQuery(
sql: string,
params?: any[],
chunkSize = 1000
): AsyncGenerator<StreamChunkData> {
if (!this.pool) {
throw new Error('PostgreSQL: not connected');
}
// 使用单一连接执行流式查询
const client = await this.pool.connect();
try {
// 使用 cursor 实现真正的流式
const cursor = client.query(new Cursor(sql, params));
let chunkIndex = 0;
while (true) {
const rows = await cursor.read(chunkSize);
if (rows.length === 0) break;
yield {
chunkIndex,
rows: rows as Record<string, unknown>[],
...(rows.length < chunkSize ? { totalRows: chunkIndex * chunkSize + rows.length } : {}),
};
chunkIndex++;
}
} finally {
client.release(); // 归还连接到池
}
}
pg 库特性
VSDB 选择 pg 的原因:
| 特性 | pg | 其他库 |
|---|---|---|
| 纯 JavaScript | ✓ | 部分 C++ |
| 连接池 | ✓(内置) | 需额外配置 |
| Promise 支持 | ✓ | ✓ |
| Cursor 流式 | ✓ | 有限 |
| TypeScript 类型 | ✓(@types/pg) | 变化 |
5.4 连接池策略
MySQL vs PostgreSQL
graph TD
subgraph "MySQL 连接策略"
M1["单一连接"]
M2["每次查询复用"]
M3["流式查询使用独立连接"]
end
subgraph "PostgreSQL 连接策略"
P1["连接池(max=5)"]
P2["查询自动从池获取"]
P3["流式查询使用独立客户端"]
end
| 数据库 | 连接模型 | 原因 |
|---|---|---|
| MySQL | 单一连接 | mysql2 默认不池化,简单够用 |
| PostgreSQL | 连接池 | pg 内置池,PostgreSQL 连接开销大 |
连接池配置
// PostgreSQL 默认配置
const poolConfig = {
max: 5, // 最大连接数
connectionTimeoutMillis: 10000, // 连接超时
idleTimeoutMillis: 10000, // 空闲超时
};
5.5 Schema Inspector
MySqlSchema 实现
export class MySqlSchema {
async getDatabases(conn: mysql.Connection): Promise<SchemaResult> {
const [rows] = await conn.execute('SHOW DATABASES');
return {
type: 'databases',
data: rows.map(r => ({ name: r.Database })),
};
}
async getTables(conn: mysql.Connection, database: string): Promise<SchemaResult> {
const [rows] = await conn.execute(
`SHOW TABLES FROM ${mysql.escapeId(database)}`
);
const key = `Tables_in_${database}`;
return {
type: 'tables',
data: rows.map(r => ({ name: r[key] })),
};
}
async getColumns(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
const [rows] = await conn.execute(
`SHOW COLUMNS FROM ${mysql.escapeId(database)}.${mysql.escapeId(table)}`
);
return {
type: 'columns',
data: rows.map(r => ({
name: r.Field,
type: r.Type,
nullable: r.Null === 'YES',
defaultValue: r.Default,
isPrimaryKey: r.Key === 'PRI',
isAutoIncrement: r.Extra?.includes('auto_increment'),
})),
};
}
async getIndexes(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
const [rows] = await conn.execute(
`SHOW INDEX FROM ${mysql.escapeId(database)}.${mysql.escapeId(table)}`
);
// 合并同一索引的多行
const indexMap = new Map();
for (const r of rows) {
if (!indexMap.has(r.Key_name)) {
indexMap.set(r.Key_name, {
name: r.Key_name,
columns: [],
isUnique: !r.Non_unique,
isPrimary: r.Key_name === 'PRIMARY',
});
}
indexMap.get(r.Key_name).columns.push(r.Column_name);
}
return {
type: 'indexes',
data: Array.from(indexMap.values()),
};
}
async getConstraints(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
const [rows] = await conn.execute(
`SELECT * FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
[database, table]
);
return {
type: 'constraints',
data: rows.map(r => ({
name: r.CONSTRAINT_NAME,
type: r.CONSTRAINT_TYPE,
})),
};
}
async getDDL(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
const [rows] = await conn.execute(
`SHOW CREATE TABLE ${mysql.escapeId(database)}.${mysql.escapeId(table)}`
);
return {
type: 'ddl',
data: [{ ddl: rows[0]['Create Table'] }],
};
}
}
PgSchema 实现
export class PgSchema {
async getDatabases(pool: Pool): Promise<SchemaResult> {
const result = await pool.query(
`SELECT datname FROM pg_database WHERE datistemplate = false`
);
return {
type: 'databases',
data: result.rows.map(r => ({ name: r.datname })),
};
}
async getTables(pool: Pool, schema: string): Promise<SchemaResult> {
const result = await pool.query(
`SELECT table_name FROM information_schema.tables
WHERE table_schema = $1 AND table_type = 'BASE TABLE'`,
[schema]
);
return {
type: 'tables',
data: result.rows.map(r => ({ name: r.table_name, schema })),
};
}
async getColumns(pool: Pool, schema: string, table: string): Promise<SchemaResult> {
const result = await pool.query(
`SELECT column_name, data_type, is_nullable, column_default,
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_pk
FROM information_schema.columns c
LEFT JOIN (
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = $1 AND tc.table_name = $2
) pk ON c.column_name = pk.column_name
WHERE c.table_schema = $1 AND c.table_name = $2`,
[schema, table]
);
return {
type: 'columns',
data: result.rows.map(r => ({
name: r.column_name,
type: r.data_type,
nullable: r.is_nullable === 'YES',
defaultValue: r.column_default,
isPrimaryKey: r.is_pk,
isAutoIncrement: false, // PostgreSQL 无原生 auto_increment
})),
};
}
async getDDL(pool: Pool, schema: string, table: string): Promise<SchemaResult> {
// PostgreSQL DDL 需要拼接,无直接命令
// 实际实现需要更复杂的逻辑
const result = await pool.query(
`SELECT
'CREATE TABLE ' || quote_ident($2) || ' (' ||
string_agg(
quote_ident(column_name) || ' ' || data_type ||
CASE WHEN is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END,
', '
) || ')' as ddl
FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2`,
[schema, table]
);
return {
type: 'ddl',
data: [{ ddl: result.rows[0]?.ddl || '' }],
};
}
}
5.6 查询队列
QueryQueue 实现
class QueryQueue {
private queue = new Map<string, PendingQuery>();
private runningCount = 0;
private maxConcurrency = 5;
private defaultTimeout = 30000;
enqueue(
request: WorkerRequest,
handler: () => Promise<WorkerResponse>
): Promise<WorkerResponse> {
return new Promise((resolve, reject) => {
const pending: PendingQuery = {
request,
handler,
resolve,
reject,
timer: setTimeout(() => {
this.queue.delete(request.id);
reject(new Error('Query timeout'));
}, this.defaultTimeout),
};
this.queue.set(request.id, pending);
this.processQueue();
});
}
private processQueue(): void {
while (this.runningCount < this.maxConcurrency && this.queue.size > 0) {
const [id, pending] = Array.from(this.queue.entries())[0];
this.queue.delete(id);
this.runningCount++;
pending.handler()
.then(response => {
clearTimeout(pending.timer);
pending.resolve(response);
this.runningCount--;
this.processQueue();
})
.catch(error => {
clearTimeout(pending.timer);
pending.reject(error);
this.runningCount--;
this.processQueue();
});
}
}
cancel(requestId: string): void {
const pending = this.queue.get(requestId);
if (pending) {
clearTimeout(pending.timer);
pending.reject(new Error('Query cancelled'));
this.queue.delete(requestId);
}
}
dispose(): void {
for (const [id, pending] of this.queue) {
clearTimeout(pending.timer);
pending.reject(new Error('QueryQueue disposed'));
}
this.queue.clear();
}
}
并发控制示意
graph TD
Q["请求队列"] --> C{"并发检查"}
C -->|"runningCount < max"| E["执行查询"]
C -->|"runningCount >= max"| W["等待"]
E --> R["runningCount++"]
R --> D["完成"]
D --> Dec["runningCount--"]
Dec --> C
5.7 错误处理
错误分类
type QueryErrorClass = 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';
function classifyQueryError(error: unknown): ClassifiedError {
const message = error instanceof Error ? error.message : String(error);
// 连接错误(可重试)
if (
message.includes('ECONNREFUSED') ||
message.includes('ETIMEDOUT') ||
message.includes('PROTOCOL_CONNECTION_LOST')
) {
return { errorClass: 'connection', retryable: true };
}
// 语法错误(不可重试)
if (
message.includes('syntax error') ||
message.includes('ER_PARSE_ERROR') ||
message.includes('42601') // PostgreSQL syntax error code
) {
return { errorClass: 'syntax', retryable: false };
}
// 超时错误(可重试)
if (message.includes('timeout') || message.includes('timed out')) {
return { errorClass: 'timeout', retryable: true };
}
// 权限错误(不可重试)
if (
message.includes('Access denied') ||
message.includes('permission denied') ||
message.includes('42501') // PostgreSQL permission denied
) {
return { errorClass: 'permission', retryable: false };
}
return { errorClass: 'unknown', retryable: false };
}
错误响应格式
function sendError(requestId: string, code: string, message: string, error?: unknown): void {
const classification = error ? classifyQueryError(error) : undefined;
send({
id: requestId,
type: 'error',
error: {
code,
message,
...(classification ? {
errorClass: classification.errorClass,
retryable: classification.retryable,
} : {}),
},
});
}
5.8 小结
本章介绍了 VSDB 的数据库驱动实现:
| 驱动 | 库 | 连接模型 | 特点 | |------|----|---------:----| | MySqlDriver | mysql2/promise | 单一连接 | Promise 原生,快速 | | PostgreSqlDriver | pg | 连接池 | 内置池,Cursor 流式 |
关键设计决策:
- MySQL 单连接:简单够用,流式查询用独立连接
- PostgreSQL 连接池:连接开销大,池化提升性能
- Schema Inspector:SHOW 命令 vs information_schema
- QueryQueue:并发控制 + 超时管理
- 错误分类:区分可重试/不可重试错误
下一章将介绍扫描引擎的设计与实现。
第六章:扫描引擎设计
本章介绍 VSDB 的自动扫描引擎,它能从项目配置文件中自动发现数据库连接。
6.1 扫描引擎概述
功能定位
VSDB 扫描引擎解决的核心问题:
graph TD
subgraph "传统方式"
A1["连接信息分散"] --> A2["手动配置"]
A2 --> A3["重复劳动"]
A3 --> A4["团队协作困难"]
end
subgraph "VSDB 扫描引擎"
B1["自动发现连接"] --> B2["一键添加"]
B2 --> B3["团队共享配置"]
B3 --> B4["效率提升"]
end
支持的扫描来源
| 来源 | 文件模式 | 解析难度 | 信息完整度 |
|---|---|---|---|
.env | .env, .env.local, .env.* | 低 | 高(DATABASE_URL) |
docker-compose | docker-compose.yml | 中 | 高(服务定义) |
| TypeORM | ormconfig.json, data-source.ts | 中 | 高 |
| Laravel | config/database.php | 高 | 中 |
| Django | settings.py | 高 | 中 |
| Prisma | schema.prisma | 低 | 高 |
6.2 扫描引擎架构
ScannerEngine 协调器
export class ScannerEngine {
private envParser = new EnvParser();
private dockerComposeParser = new DockerComposeParser();
private frameworkParser = new FrameworkParser();
async scan(
root: string,
existingConnections: DbConnection[]
): Promise<ScannerResult> {
const results: ScannerResult = {
connections: [],
errors: [],
scannedFiles: [],
};
// 执行各 Parser
const envResult = await this.envParser.scan(root);
const dockerResult = await this.dockerComposeParser.scan(root);
const frameworkResult = await this.frameworkParser.scan(root);
// 合并结果
results.connections.push(...envResult.connections);
results.connections.push(...dockerResult.connections);
results.connections.push(...frameworkResult.connections);
results.errors.push(...envResult.errors);
results.errors.push(...dockerResult.errors);
results.errors.push(...frameworkResult.errors);
results.scannedFiles.push(...envResult.scannedFiles);
results.scannedFiles.push(...dockerResult.scannedFiles);
results.scannedFiles.push(...frameworkResult.scannedFiles);
// 去重(与已存在连接比较)
results.connections = this.deduplicate(results.connections, existingConnections);
return results;
}
private deduplicate(
scanned: ScannedConnection[],
existing: DbConnection[]
): ScannedConnection[] {
return scanned.filter(conn => {
// 检查 host+port 是否已存在
return !existing.some(e =>
e.host === conn.host && e.port === conn.port
);
});
}
}
扫描流程
graph TD
A["开始扫描"] --> B["EnvParser.scan()"]
B --> C["DockerComposeParser.scan()"]
C --> D["FrameworkParser.scan()"]
D --> E["合并结果"]
E --> F["去重处理"]
F --> G["返回 ScannerResult"]
B --> B1["查找 .env 文件"]
B1 --> B2["解析 DATABASE_URL"]
B2 --> B3["解析 DB_* 变量"]
C --> C1["查找 docker-compose.yml"]
C1 --> C2["识别 mysql/postgres 服务"]
C2 --> C3["提取端口和环境变量"]
D --> D1["查找框架配置文件"]
D1 --> D2["解析连接数组"]
6.3 EnvParser 实现
.env 文件查找
const ENV_FILE_PATTERNS = [
'.env',
'.env.local',
'.env.development',
'.env.production',
'.env.test',
'.env.staging',
];
const MAX_DEPTH = 3; // 最大搜索深度
private findEnvFiles(dir: string, depth: number): string[] {
if (depth > MAX_DEPTH) return [];
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
const files: string[] = [];
for (const entry of entries) {
if (entry.isDirectory()) {
// 跳过 node_modules, .git 等
if (entry.name === 'node_modules' ||
entry.name === '.git' ||
entry.name.startsWith('.')) {
continue;
}
files.push(...this.findEnvFiles(path.join(dir, entry.name), depth + 1));
} else if (ENV_FILE_PATTERNS.includes(entry.name)) {
files.push(path.join(dir, entry.name));
}
}
return files;
}
.env 内容解析
private parseEnvContent(content: string): Record<string, string> {
const vars: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
// 跳过空行和注释
if (!trimmed || trimmed.startsWith('#')) continue;
// 解析 KEY=VALUE
const eqIdx = trimmed.indexOf('=');
if (eqIdx < 0) continue;
const key = trimmed.substring(0, eqIdx).trim();
let value = trimmed.substring(eqIdx + 1).trim();
// 去除引号
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length - 1);
}
vars[key] = value;
}
return vars;
}
连接信息提取
async parseEnvFile(filePath: string): Promise<ScannedConnection[]> {
const content = fs.readFileSync(filePath, 'utf-8');
const vars = this.parseEnvContent(content);
const connections: ScannedConnection[] = [];
// 1) DATABASE_URL 格式
const urlKeys = ['DATABASE_URL', 'DB_URL', 'DATABASE_URI'];
for (const key of urlKeys) {
if (vars[key]) {
const conn = this.parseConnectionString(vars[key], filePath);
if (conn) connections.push(conn);
}
}
// 2) DB_* 变量组
const dbHost = vars['DB_HOST'];
if (dbHost) {
const port = vars['DB_PORT']
? parseInt(vars['DB_PORT'], 10)
: DEFAULT_PORTS.postgresql;
connections.push(this.buildFromVars(
dbHost, port, vars['DB_USER'] || '', vars['DB_PASSWORD'] || '',
vars['DB_NAME'], 'postgresql', filePath, 'DB_*'
));
}
// 3) MYSQL_* 变量组
const mysqlHost = vars['MYSQL_HOST'];
if (mysqlHost) {
const port = vars['MYSQL_PORT']
? parseInt(vars['MYSQL_PORT'], 10)
: DEFAULT_PORTS.mysql;
connections.push(this.buildFromVars(
mysqlHost, port, vars['MYSQL_USER'] || '', vars['MYSQL_PASSWORD'] || '',
vars['MYSQL_DATABASE'], 'mysql', filePath, 'MYSQL_*'
));
}
// 4) PG_* 变量组
const pgHost = vars['PG_HOST'];
if (pgHost) {
const port = vars['PG_PORT']
? parseInt(vars['PG_PORT'], 10)
: DEFAULT_PORTS.postgresql;
connections.push(this.buildFromVars(
pgHost, port, vars['PG_USER'] || '', vars['PG_PASSWORD'] || '',
vars['PG_DATABASE'], 'postgresql', filePath, 'PG_*'
));
}
return connections;
}
DATABASE_URL 解析
parseConnectionString(url: string, sourceFile: string): ScannedConnection | null {
try {
// 判断数据库类型
let type: 'mysql' | 'postgresql' | null = null;
let normalizedUrl = url;
if (url.startsWith('postgresql://') || url.startsWith('postgres://')) {
type = 'postgresql';
} else if (url.startsWith('mysql://') || url.startsWith('mysql2://')) {
type = 'mysql';
if (url.startsWith('mysql2://')) {
normalizedUrl = 'mysql://' + url.substring('mysql2://'.length);
}
} else {
return null; // 不支持的协议
}
const parsed = new URL(normalizedUrl);
return {
name: `${type}:${parsed.hostname}:${parsed.port}`,
type,
host: parsed.hostname || 'localhost',
port: parsed.port
? parseInt(parsed.port, 10)
: DEFAULT_PORTS[type],
username: decodeURIComponent(parsed.username || ''),
password: decodeURIComponent(parsed.password || ''),
database: parsed.pathname.startsWith('/')
? parsed.pathname.substring(1)
: parsed.pathname,
source: 'env',
sourceFile,
confidence: 'high',
};
} catch {
return null; // URL 解析失败
}
}
DATABASE_URL 格式示例
# PostgreSQL
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
# MySQL
DATABASE_URL=mysql://root:secret@127.0.0.1:3306/testdb
# MySQL (mysql2 driver)
DATABASE_URL=mysql2://user:pass@host:3306/db
6.4 DockerComposeParser 实现
docker-compose.yml 解析
export class DockerComposeParser {
async scan(root: string): Promise<ScannerResult> {
const result: ScannerResult = {
connections: [],
errors: [],
scannedFiles: [],
};
const composeFiles = this.findComposeFiles(root);
for (const file of composeFiles) {
result.scannedFiles.push(file);
try {
const content = fs.readFileSync(file, 'utf-8');
const config = yaml.parse(content);
const connections = this.parseComposeConfig(config, file);
result.connections.push(...connections);
} catch (err: any) {
result.errors.push({ file, error: err.message });
}
}
return result;
}
private parseComposeConfig(
config: any,
sourceFile: string
): ScannedConnection[] {
const connections: ScannedConnection[] =();
if (!config.services) return connections;
for (const [name, service] of Object.entries(config.services)) {
const conn = this.parseService(name, service as any, sourceFile);
if (conn) connections.push(conn);
}
return connections;
}
}
服务定义解析
private parseService(
name: string,
service: any,
sourceFile: string
): ScannedConnection | null {
const image = service.image || '';
// 识别 MySQL/PostgreSQL 服务
let type: 'mysql' | 'postgresql' | null = null;
if (image.includes('mysql') || image.includes('mariadb')) {
type = 'mysql';
} else if (image.includes('postgres') || image.includes('postgresql')) {
type = 'postgresql';
} else {
return null;
}
// 提取端口
const ports = service.ports || [];
let hostPort = DEFAULT_PORTS[type];
for (const portDef of ports) {
// 格式: "3306:3306" 或 "3306"
const parts = portDef.toString().split(':');
if (parts.length === 2) {
hostPort = parseInt(parts[0], 10);
}
}
// 提取环境变量
const envVars = this.parseEnvVars(service.environment || {});
const username = envVars['MYSQL_USER'] ||
envVars['POSTGRES_USER'] ||
'root';
const password = envVars['MYSQL_PASSWORD'] ||
envVars['POSTGRES_PASSWORD'] ||
'';
const database = envVars['MYSQL_DATABASE'] ||
envVars['POSTGRES_DB'];
return {
name: `${type}:${name}:${hostPort}`,
type,
host: 'localhost',
port: hostPort,
username,
password,
database,
source: 'docker-compose',
sourceFile,
confidence: 'high',
};
}
docker-compose.yml 示例
version: '3'
services:
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass
postgres:
image: postgres:15
ports:
- "5432:5432"
environment:
POSTGRES_USER: pguser
POSTGRES_PASSWORD: pgpass
POSTGRES_DB: myapp
6.5 FrameworkParser 实现
支持的框架配置
graph TD
A["FrameworkParser"] --> B["TypeORM"]
A --> C["Laravel"]
A --> D["Django"]
A --> E["Prisma"]
B --> B1["ormconfig.json"]
B --> B2["data-source.ts"]
C --> C1["config/database.php"]
D --> D1["settings.py"]
E --> E1["schema.prisma"]
TypeORM 配置解析
parseTypeOrmConfig(filePath: string): ScannedConnection[] {
const content = fs.readFileSync(filePath, 'utf-8');
const config = JSON.parse(content);
const connections: ScannedConnection[] = [];
// TypeORM 支持多连接配置
const connectionConfigs = Array.isArray(config) ? config : [config];
for (const conn of connectionConfigs) {
if (conn.type === 'mysql' || conn.type === 'postgres') {
connections.push({
name: `${conn.type}:${conn.host || 'localhost'}:${conn.port}`,
type: conn.type === 'postgres' ? 'postgresql' : 'mysql',
host: conn.host || 'localhost',
port: conn.port || DEFAULT_PORTS[conn.type === 'postgres' ? 'postgresql' : 'mysql'],
username: conn.username || '',
password: conn.password || '',
database: conn.database,
source: 'framework',
sourceFile: filePath,
confidence: 'high',
});
}
}
return connections;
}
ormconfig.json 示例
[
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "secret",
"database": "myapp"
},
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "pguser",
"password": "pgpass",
"database": "myapp"
}
]
Prisma schema 解析
parsePrismaSchema(filePath: string): ScannedConnection[] {
const content = fs.readFileSync(filePath, 'utf-8');
const connections: ScannedConnection[] = [];
// 提取 datasource url
const urlMatch = content.match(/url\s*=\s*"([^"]+)"/);
if (urlMatch) {
const url = urlMatch[1];
const conn = this.parseConnectionString(url, filePath);
if (conn) connections.push(conn);
}
// 也支持 env() 格式
const envMatch = content.match(/url\s*=\s*env\("([^"]+)"\)/);
if (envMatch) {
// 标记为需要额外解析环境变量
connections.push({
name: `prisma:env:${envMatch[1]}`,
type: 'postgresql', // Prisma 默认 PostgreSQL
host: '',
port: 0,
username: '',
password: '',
source: 'framework',
sourceFile: filePath,
confidence: 'low',
});
}
return connections;
}
schema.prisma 示例
datasource db {
provider = "postgresql"
url = "postgresql://user:pass@localhost:5432/mydb"
}
generator client {
provider = "prisma-client-js"
}
6.6 去重与合并策略
去重逻辑
private deduplicate(
scanned: ScannedConnection[],
existing: DbConnection[]
): ScannedConnection[] {
return scanned.filter(conn => {
// 按 host+port 去重
const existsByHostPort = existing.some(e =>
e.host === conn.host && e.port === conn.port
);
// 按数据库文件内的重复也去重
// 同一文件可能定义多个指向同一数据库的连接
return !existsByHostPort;
});
}
多源合并示意
graph TD
subgraph "扫描结果"
A[".env: mysql:localhost:3306"]
B["docker-compose: mysql:localhost:3306"]
C["ormconfig: mysql:localhost:3306"]
D["docker-compose: postgres:localhost:5432"]
end
A --> E["合并去重"]
B --> E
C --> E
D --> E
E --> F["最终结果"]
F --> G["mysql:localhost:3306"]
F --> H["postgres:localhost:5432"]
6.7 置信度评估
置信度分级
| 置信度 | 条件 | 示例 |
|---|---|---|
high | 信息完整,可直接使用 | DATABASE_URL, docker-compose 服务 |
medium | 信息基本完整,可能缺密码 | 框架配置 |
low | 信息不完整,需用户补充 | 部分 DB_* 变量,env() 引用 |
置信度使用
// 在 UI 中展示置信度
const items = result.connections.map(conn => ({
label: conn.name,
description: `${conn.type}://${conn.host}:${conn.port}`,
detail: `Source: ${conn.source} (${conn.confidence} confidence)`,
connection: conn,
}));
// 根据置信度决定是否需要用户输入
if (conn.confidence === 'low') {
// 提示用户补充缺失信息
const password = await vscode.window.showInputBox({
prompt: `Enter password for ${conn.name}`,
password: true,
});
}
6.8 自动扫描时机
激活时扫描
// extension.ts
export function activate(context: vscode.ExtensionContext) {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
if (workspaceRoot) {
autoScanProject(workspaceRoot);
}
}
async function autoScanProject(workspaceRoot: string) {
const existingConnections = await connectionManager!.listConnections();
const result = await scannerEngine!.scan(workspaceRoot, existingConnections);
if (result.connections.length > 0) {
// 发现新连接,提示用户添加
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `Found ${result.connections.length} connection(s). Select to add:`,
canPickMany: true,
});
if (selected) {
for (const item of selected) {
await connectionManager!.addConnection({
...item.connection,
scope: 'project',
});
}
treeProvider?.refresh();
}
}
}
手动扫描命令
const scanProjectCmd = vscode.commands.registerCommand('vsdb.scanProject', async () => {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
if (!workspaceRoot) {
vscode.window.showWarningMessage('No workspace folder open.');
return;
}
vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'VSDB: Scanning project for database connections...',
}, async () => {
const existingConnections = await connectionManager!.listConnections();
const result = await scannerEngine!.scan(workspaceRoot, existingConnections);
// 处理结果...
});
});
6.9 错误处理
解析错误收集
interface ScannerResult {
connections: ScannedConnection[];
errors: Array<{ file: string; error: string }>; // 收集但不中断
scannedFiles: string[];
}
错误示例处理
try {
const config = yaml.parse(content);
const connections = this.parseComposeConfig(config, file);
result.connections.push(...connections);
} catch (err: any) {
// 收集错误,继续扫描其他文件
result.errors.push({ file, error: err.message });
}
错误报告
// 扫描完成后报告错误
if (result.errors.length > 0) {
console.warn('VSDB scan errors:', result.errors);
// 可选:向用户展示错误摘要
}
6.10 小结
本章介绍了 VSDB 的扫描引擎设计:
| Parser | 目标 | 解析方式 | 置信度 |
|---|---|---|---|
| EnvParser | .env 文件 | DATABASE_URL + DB_* 变量 | 高 |
| DockerComposeParser | docker-compose.yml | 服务 image + ports + env | 高 |
| FrameworkParser | 框架配置文件 | JSON/YAML/自定义解析 | 中~高 |
关键设计:
- 协调器模式:ScannerEngine 协调多个 Parser
- 去重合并:host+port 去重避免重复添加
- 置信度评估:区分可直接使用 vs 需补充信息
- 错误收集:解析失败不中断,收集报告
- 自动触发:激活时自动扫描,也可手动触发
下一章将介绍 UI 组件的实现。
第七章:UI组件实现
本章介绍 VSDB 的 UI 层实现,包括 TreeView 连接树和 Webview React 应用。
7.1 UI 层架构
graph TD
subgraph "UI 层"
A["TreeView"]
B["Webview Panels"]
end
subgraph "TreeView"
A1["treeProvider.ts"]
A2["treeItems.ts"]
A3["commands.ts"]
end
subgraph "Webview"
B1["sqlEditor.ts"]
B2["dataGrid.ts"]
B3["connectionForm.ts"]
B4["tableStructure.ts"]
end
A --> A1
A1 --> A2
A1 --> A3
B --> B1
B --> B2
B --> B3
B --> B4
7.2 TreeView 实现
TreeProvider 类
export class TreeProvider implements vscode.TreeDataProvider<TreeItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
onDidChangeTreeData = this._onDidChangeTreeData.event;
constructor(
private connectionManager: ConnectionManager,
private ipcManager: IpcManager,
private historyManager?: HistoryManager,
private bookmarkManager?: BookmarkManager,
) {}
refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}
getTreeItem(element: TreeItem): vscode.TreeItem {
return element;
}
async getChildren(element?: TreeItem): Promise<TreeItem[]> {
if (!element) {
// 根节点:连接列表
return this.getConnectionNodes();
}
// 根据节点类型获取子节点
if (element instanceof ConnectionNode) {
return this.getDatabaseNodes(element);
}
if (element instanceof DatabaseNode) {
return this.getTableNodes(element);
}
if (element instanceof TableNode) {
return this.getColumnNodes(element);
}
return [];
}
}
树节点类型
// 连接节点
class ConnectionNode extends vscode.TreeItem {
constructor(
public connection: DbConnection,
public state: ConnectionState,
) {
super(connection.name, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = `connection-${state.status}`;
this.iconPath = this.getIconPath(state.status);
this.tooltip = `${connection.type}://${connection.host}:${connection.port}`;
}
private getIconPath(status: string): vscode.ThemeIcon {
switch (status) {
case 'connected': return new vscode.ThemeIcon('database', new vscode.ThemeColor('charts.green'));
case 'connecting': return new vscode.ThemeIcon('loading~spin');
case 'error': return new vscode.ThemeIcon('error', new vscode.ThemeColor('charts.red'));
default: return new vscode.ThemeIcon('database');
}
}
}
// 数据库节点
class DatabaseNode extends vscode.TreeItem {
constructor(
public connectionId: string,
public database: string,
) {
super(database, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = 'database';
this.iconPath = new vscode.ThemeIcon('folder');
}
}
// 表节点
class TableNode extends vscode.TreeItem {
constructor(
public connectionId: string,
public database: string,
public table: string,
) {
super(table, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = 'table';
this.iconPath = new vscode.ThemeIcon('table');
}
}
// 列节点
class ColumnNode extends vscode.TreeItem {
constructor(
public column: ColumnInfo,
) {
super(column.name, vscode.TreeItemCollapsibleState.None);
this.contextValue = 'column';
this.iconPath = new vscode.ThemeIcon('symbol-field');
this.description = `${column.type}${column.isPrimaryKey ? ' (PK)' : ''}`;
}
}
树节点展开逻辑
graph TD
A["点击连接节点"] --> B{"连接状态"}
B -->|"disconnected"| C["发送 connect 请求"]
C --> D["更新为 connecting"]
D --> E["收到响应"]
E -->|"success"| F["更新为 connected"]
E -->|"error"| G["更新为 error"]
F --> H["查询 databases"]
H --> I["返回 DatabaseNode[]"]
B -->|"connected"| H
自动连接实现
async getChildren(element?: TreeItem): Promise<TreeItem[]> {
if (element instanceof ConnectionNode) {
const conn = element.connection;
// 自动连接
if (element.state.status === 'disconnected') {
await this.ipcManager.sendRequest({
type: 'connect',
connectionId: conn.id,
payload: { config: conn },
});
// 刷新状态
this.refresh();
return []; // 等待刷新后展开
}
// 已连接,查询数据库列表
const response = await this.ipcManager.sendRequest({
type: 'schema',
connectionId: conn.id,
payload: { schemaType: 'databases' },
});
return response.data?.data.map(db =>
new DatabaseNode(conn.id, db.name)
) || [];
}
}
7.3 Webview 架构
Webview Panel 管理
export class SqlEditorPanel {
public static currentPanel: SqlEditorPanel | undefined;
private panel: vscode.WebviewPanel;
private extensionUri: vscode.Uri;
public static createOrShow(
extensionUri: vscode.Uri,
ipcManager: IpcManager,
connectionManager: ConnectionManager,
): SqlEditorPanel {
if (SqlEditorPanel.currentPanel) {
SqlEditorPanel.currentPanel.panel.reveal();
return SqlEditorPanel.currentPanel;
}
const panel = vscode.window.createWebviewPanel(
'vsdb.sqlEditor',
'VSDB SQL Editor',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
}
);
SqlEditorPanel.currentPanel = new SqlEditorPanel(
panel, extensionUri, ipcManager, connectionManager
);
return SqlEditorPanel.currentPanel;
}
}
Webview 与 React 通信
sequenceDiagram
participant VSCode as VSCode Extension
participant Webview as Webview Panel
participant React as React App
VSCode->>Webview: 初始化 HTML + 加载 React bundle
Webview->>React: 启动 React 应用
React->>VSCode: postMessage({ type: 'ready' })
VSCode->>React: postMessage({ type: 'connections', payload: [...] })
Note over React: 用户输入 SQL
React->>VSCode: postMessage({ type: 'executeQuery', payload: { sql } })
VSCode->>VSCode: ipcManager.sendRequest(query)
VSCode->>React: postMessage({ type: 'queryResult', payload: result })
React->>React: 渲染结果表格
VSCode 消息发送
private updateWebview(): void {
const connections = await this.connectionManager.listConnections();
this.panel.webview.postMessage({
type: 'connections',
payload: connections,
});
}
private handleWebviewMessage(message: any): void {
switch (message.type) {
case 'executeQuery':
this.executeQuery(message.payload);
break;
case 'saveQuery':
this.saveQuery(message.payload);
break;
case 'exportData':
this.exportData(message.payload);
break;
}
}
React 消息接收
// SqlEditor.tsx
useEffect(() => {
const handler = (event: MessageEvent) => {
const message = event.data;
switch (message.type) {
case 'connections':
setConnections(message.payload);
break;
case 'queryResult':
setQueryStatus('success');
addResultTab(message.payload);
break;
case 'queryError':
setQueryStatus('error');
addErrorTab(message.payload.message);
break;
}
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, []);
const executeQuery = () => {
postMessage({
type: 'executeQuery',
payload: { sql, connectionId },
});
};
7.4 SQL Editor 组件
Monaco Editor 配置
<Editor
height="200px"
defaultLanguage="sql"
defaultValue={sql}
theme={monacoTheme}
options={{
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
wordWrap: 'on',
tabSize: 2,
}}
onMount={handleEditorMount}
/>
SQL 自动补全
const handleEditorMount: OnMount = (editor, monaco) => {
monaco.languages.registerCompletionItemProvider('sql', {
provideCompletionItems: (model, position) => {
const keywords = [
'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE',
'JOIN', 'ON', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT',
// ... 更多关键字
];
const suggestions = keywords.map(kw => ({
label: kw,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: kw,
range,
}));
// 添加表名建议
for (const conn of connections) {
suggestions.push({
label: conn.name,
kind: monaco.languages.CompletionItemKind.Reference,
insertText: conn.name,
range,
}));
}
return { suggestions };
},
});
// Ctrl+Enter 执行快捷键
editor.addAction({
id: 'vsdb-execute-query',
label: 'Execute Query',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
run: () => executeQuery(),
});
};
7.5 DataGrid 组件
虚拟滚动
// 使用 react-window 处理大数据
import { FixedSizeList as List } from 'react-window';
const DataGrid: React.FC<{ rows: any[]; columns: string[] }> = ({ rows, columns }) => {
const Row = ({ index, style }) => (
<div style={style} className="grid-row">
{columns.map(col => (
<div key={col} className="grid-cell">
{rows[index][col]}
</div>
))}
</div>
);
return (
<List
height={400}
itemCount={rows.length}
itemSize={35}
width="100%"
>
{Row}
</List>
);
};
内联编辑
// EditManager 管理编辑状态
class EditManager {
private edits = new Map<string, any>(); // rowIndex -> modifiedData
private editMode: 'readonly' | 'inline' | 'form' = 'readonly';
startEdit(rowIndex: number): void {
this.editMode = 'inline';
}
setValue(rowIndex: number, column: string, value: any): void {
// 记录修改
this.edits.set(`${rowIndex}:${column}`, value);
}
getEdits(): Array<{ row: number; column: string; value: any }> {
return Array.from(this.edits.entries()).map(([key, value]) => {
const [row, col] = key.split(':');
return { row: parseInt(row), column: col, value };
});
}
commit(connectionId: string, database: string, table: string): Promise<void> {
// 生成 UPDATE SQL 并发送
for (const edit of this.getEdits()) {
const sql = this.buildUpdateSql(table, edit);
await postMessage({ type: 'executeUpdate', payload: { sql, connectionId } });
}
}
}
7.6 表结构面板
多 Tab 展示
interface TableStructureProps {
connectionId: string;
database: string;
table: string;
}
const TableStructure: React.FC<TableStructureProps> = ({ connectionId, database, table }) => {
const [activeTab, setActiveTab] = useState<'columns' | 'indexes' | 'constraints' | 'triggers' | 'ddl'>('columns');
const [data, setData] = useState<any>(null);
useEffect(() => {
postMessage({
type: 'getTableStructure',
payload: { connectionId, database, table, schemaType: activeTab },
});
const handler = (event: MessageEvent) => {
if (event.data.type === 'schemaResult') {
setData(event.data.payload);
}
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, [activeTab]);
return (
<div>
<TabBar active={activeTab} onChange={setActiveTab}>
<Tab id="columns">Columns</Tab>
<Tab id="indexes">Indexes</Tab>
<Tab id="constraints">Constraints</Tab>
<Tab id="triggers">Triggers</Tab>
<Tab id="ddl">DDL</Tab>
</TabBar>
<TabContent active={activeTab} data={data} />
</div>
);
};
7.7 连接表单
表单验证
interface ConnectionFormData {
name: string;
type: 'mysql' | 'postgresql';
host: string;
port: number;
username: string;
password: string;
database?: string;
scope: 'project' | 'global';
}
const ConnectionForm: React.FC = () => {
const [formData, setFormData] = useState<ConnectionFormData>({
name: '',
type: 'mysql',
host: 'localhost',
port: 3306,
username: '',
password: '',
database: '',
scope: 'project',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = (): boolean => {
const errs: Record<string, string> = {};
if (!formData.name) errs.name = 'Name is required';
if (!formData.host) errs.host = 'Host is required';
if (!formData.username) errs.username = 'Username is required';
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSubmit = () => {
if (!validate()) return;
postMessage({
type: 'saveConnection',
payload: formData,
});
};
// 自动填充默认端口
const handleTypeChange = (type: 'mysql' | 'postgresql') => {
setFormData({
...formData,
type,
port: type === 'mysql' ? 3306 : 5432,
});
};
return (
<form>
<Input label="Name" value={formData.name} error={errors.name} onChange={...} />
<Select label="Type" value={formData.type} onChange={handleTypeChange}>
<option value="mysql">MySQL</option>
<option value="postgresql">PostgreSQL</option>
</Select>
<Input label="Host" value={formData.host} error={errors.host} onChange={...} />
<Input label="Port" type="number" value={formData.port} onChange={...} />
<Input label="Username" value={formData.username} error={errors.username} onChange={...} />
<Input label="Password" type="password" value={formData.password} onChange={...} />
<Input label="Database" value={formData.database} onChange={...} />
<RadioGroup label="Scope" value={formData.scope} onChange={...}>
<Radio value="project">Project (shared)</Radio>
<Radio value="global">Global (private)</Radio>
</RadioGroup>
<Button onClick={handleTestConnection}>Test Connection</Button>
<Button onClick={handleSubmit}>Save</Button>
</form>
);
};
7.8 小结
本章介绍了 VSDB 的 UI 组件实现:
| 组件 | 类型 | 功能 |
|---|---|---|
| TreeProvider | VSCode TreeView | 连接树展示 |
| ConnectionNode | TreeItem | 连接节点 |
| SqlEditorPanel | Webview Panel | SQL 编辑器 |
| SqlEditor.tsx | React Component | Monaco 编辑器集成 |
| DataGrid.tsx | React Component | 数据网格 + 虚拟滚动 |
| ConnectionForm | React Component | 连接表单 |
| TableStructure | React Component | 表结构多 Tab |
关键设计:
- TreeView 自动连接:展开时自动发送 connect 请求
- Webview 双向通信:postMessage 实现消息传递
- 虚拟滚动:react-window 处理大数据集
- 内联编辑:EditManager 管理编辑状态
- Monaco 集成:SQL 语法高亮 + 自动补全
下一章将介绍数据操作功能。
第八章:数据操作功能
本章介绍 VSDB 的数据操作功能,包括查询执行、导入导出、数据编辑等。
8.1 查询执行流程
执行流程概览
sequenceDiagram
participant User as 用户
participant Editor as SqlEditor
participant Panel as SqlEditorPanel
participant IPC as IpcManager
participant Worker as Worker Process
participant Driver as DatabaseDriver
participant DB as Database
User->>Editor: 输入 SQL + Ctrl+Enter
Editor->>Panel: postMessage(executeQuery)
Panel->>Panel: 记录查询历史
Panel->>IPC: sendRequest(type=query)
IPC->>Worker: WorkerRequest
Worker->>Driver: driver.query(sql)
Driver->>DB: 执行 SQL
DB-->>Driver: 结果
Driver-->>Worker: QueryResult
Worker-->>IPC: WorkerResponse
IPC-->>Panel: WorkerResponse
Panel-->>Editor: postMessage(queryResult)
Editor-->>User: 渲染结果表格
查询历史记录
// HistoryManager.ts
export class HistoryManager {
private maxItems = 100;
constructor(private globalState: vscode.Memento) {}
add(item: QueryHistoryItem): void {
const history = this.getList();
// 添加新项
history.unshift(item);
// 清理超出上限的未固定项
while (history.length > this.maxItems) {
const unpinnedIndex = history.findIndex(h => !h.pinned);
if (un pinnedIndex >= 0) {
history.splice(unpinnedIndex, 1);
} else {
break; // 全部固定,不再清理
}
}
this.globalState.update('queryHistory', history);
}
getList(limit?: number): QueryHistoryItem[] {
const history = this.globalState.get<QueryHistoryItem[]>('queryHistory', []);
return limit ? history.slice(0, limit) : history;
}
pin(id: string): void {
const history = this.getList();
const item = history.find(h => h.id === id);
if (item) {
item.pinned = true;
this.globalState.update('queryHistory', history);
}
}
remove(id: string): void {
const history = this.getList().filter(h => h.id !== id);
this.globalState.update('queryHistory', history);
}
clear(): void {
const history = this.getList().filter(h => h.pinned);
this.globalState.update('queryHistory', history);
}
}
执行时间统计
// Driver 端计时
async query(sql: string, params?: any[]): Promise<QueryResult> {
const startTime = Date.now();
const result = await this.connection.execute(sql, params);
const executionTime = Date.now() - startTime;
return {
...result,
executionTime,
};
}
// 历史记录包含执行时间
const historyItem: QueryHistoryItem = {
id: uuid(),
sql,
connectionId,
connectionName: conn.name,
executedAt: new Date(),
rowCount: result.rowCount,
executionTime: result.executionTime,
};
8.2 导入导出功能
导出管理器
export class ExportManager {
async exportToCsv(
result: QueryResult,
filePath: string
): Promise<void> {
const lines: string[] = [];
// 头行:列名
lines.push(result.columns.join(','));
// 数据行
for (const row of result.rows) {
const values = result.columns.map(col => {
const val = row[col];
// 处理特殊字符
if (val === null || val === undefined) return '';
if (typeof val === 'string' && (val.includes(',') || val.includes('"'))) {
return `"${val.replace(/"/g, '""')}"`;
}
return String(val);
});
lines.push(values.join(','));
}
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
}
async exportToJson(
result: QueryResult,
filePath: string
): Promise<void> {
const data = {
columns: result.columns,
rows: result.rows,
rowCount: result.rowCount,
exportedAt: new Date().toISOString(),
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
}
async exportToSql(
result: QueryResult,
filePath: string,
tableName: string
): Promise<void> {
const lines: string[] = [];
for (const row of result.rows) {
const columns = result.columns.map(c => `\`${c}\``).join(', ');
const values = result.columns.map(c => {
const val = row[c];
if (val === null) return 'NULL';
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
if (typeof val === 'number') return String(val);
return `'${String(val)}'`;
}).join(', ');
lines.push(`INSERT INTO \`${tableName}\` (${columns}) VALUES (${values});`);
}
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
}
}
导入管理器
export class ImportManager {
async importFromCsv(
filePath: string,
connectionId: string,
tableName: string
): Promise<{ imported: number; errors: string[] }> {
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
if (lines.length < 2) {
throw new Error('CSV file is empty or has no data');
}
// 第一行是列名
const columns = this.parseCsvLine(lines[0]);
const errors: string[] = [];
let imported = 0;
for (let i = 1; i < lines.length; i++) {
try {
const values = this.parseCsvLine(lines[i]);
const sql = this.buildInsertSql(tableName, columns, values);
await ipcManager.sendRequest({
type: 'query',
connectionId,
payload: { sql },
});
imported++;
} catch (err: any) {
errors.push(`Row ${i}: ${err.message}`);
}
}
return { imported, errors };
}
async importFromJson(
filePath: string,
connectionId: string,
tableName: string
): Promise<{ imported: number; errors: string[] }> {
const content = fs.readFileSync(filePath, 'utf-8');
const data = JSON.parse(content);
const rows = Array.isArray(data.rows) ? data.rows :
Array.isArray(data) ? data : [data];
const errors: string[] = [];
let imported = 0;
for (const row of rows) {
try {
const columns = Object.keys(row);
const values = columns.map(c => row[c]);
const sql = this.buildInsertSql(tableName, columns, values);
await ipcManager.sendRequest({
type: 'query',
connectionId,
payload: { sql },
});
imported++;
} catch (err: any) {
errors.push(`Row ${imported + errors.length + 1}: ${err.message}`);
}
}
return { imported, errors };
}
}
CSV 解析
private parseCsvLine(line: string): string[] {
const values: string[] = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (inQuotes) {
if (char === '"') {
if (line[i + 1] === '"') {
current += '"';
i++;
} else {
inQuotes = false;
}
} else {
current += char;
}
} else {
if (char === '"') {
inQuotes = true;
} else if (char === ',') {
values.push(current);
current = '';
} else {
current += char;
}
}
}
values.push(current);
return values;
}
8.3 数据编辑
内联编辑模式
graph TD
A["查看数据"] --> B["启用编辑模式"]
B --> C["点击单元格"]
C --> D["输入新值"]
D --> E{"确认修改"}
E -->|"Enter"| F["标记为已修改"]
E -->|"Esc"| G["取消修改"]
F --> H["继续编辑或提交"]
H --> I["提交更改"]
I --> J["生成 UPDATE SQL"]
J --> K["执行 SQL"]
K --> L["刷新数据"]
EditManager 实现
class EditManager {
private edits = new Map<string, { original: any; modified: any }>();
private editMode: EditMode = 'readonly';
startEdit(rowIndex: number, column: string, originalValue: any): void {
const key = `${rowIndex}:${column}`;
this.edits.set(key, { original: originalValue, modified: originalValue });
this.editMode = 'inline';
}
updateValue(rowIndex: number, column: string, newValue: any): void {
const key = `${rowIndex}:${column}`;
const edit = this.edits.get(key);
if (edit) {
edit.modified = newValue;
}
}
cancelEdit(rowIndex: number, column: string): void {
const key = `${rowIndex}:${column}`;
this.edits.delete(key);
}
hasEdits(): boolean {
return this.edits.size > 0;
}
getEdits(): Array<{ rowIndex: number; column: string; original: any; modified: any }> {
return Array.from(this.edits.entries())
.filter(([_, edit]) => edit.original !== edit.modified)
.map(([key, edit]) => {
const [rowIndex, column] = key.split(':');
return {
rowIndex: parseInt(rowIndex),
column,
original: edit.original,
modified: edit.modified,
};
});
}
buildUpdateSql(
tableName: string,
primaryKeyColumns: string[],
rows: any[]
): string[] {
const sqls: string[] = [];
for (const edit of this.getEdits()) {
const row = rows[edit.rowIndex];
const whereClause = primaryKeyColumns
.map(pk => `${pk} = ${this.formatValue(row[pk])}`)
.join(' AND ');
sqls.push(
`UPDATE ${tableName} SET ${edit.column} = ${this.formatValue(edit.modified)} WHERE ${whereClause}`
);
}
return sqls;
}
private formatValue(value: any): string {
if (value === null) return 'NULL';
if (typeof value === 'number') return String(value);
return `'${String(value).replace(/'/g, "''")}'`;
}
}
批量编辑提交
const commitEdits = async () => {
if (!editManager.hasEdits()) return;
const edits = editManager.getEdits();
// 确认对话框
const confirm = await vscode.window.showWarningMessage(
`Apply ${edits.length} changes to ${tableName}?`,
{ modal: true },
'Apply',
'Cancel'
);
if (confirm !== 'Apply') return;
// 生成并执行 SQL
const sqls = editManager.buildUpdateSql(tableName, primaryKeyColumns, rows);
for (const sql of sqls) {
try {
await ipcManager.sendRequest({
type: 'query',
connectionId,
payload: { sql },
});
} catch (err) {
// 错误处理
}
}
// 刷新数据
editManager.clearEdits();
refreshData();
};
8.4 数据搜索
SearchEngine 实现
export class SearchEngine {
private schemaCache = new Map<string, SchemaCacheEntry>();
constructor(
private ipcManager: IpcManager,
private connectionManager: ConnectionManager,
) {}
async searchAll(
keyword: string,
options: SearchOptions = {}
): Promise<SearchResult[]> {
const results: SearchResult[] = [];
const connectionIds = options.connectionIds ||
await this.getAllConnectionIds();
for (const connectionId of connectionIds) {
if (options.searchTables) {
const tables = await this.searchTables(connectionId, keyword);
results.push(...tables);
}
if (options.searchColumns) {
const columns = await this.searchColumns(connectionId, keyword);
results.push(...columns);
}
if (options.searchData) {
const data = await this.searchData(connectionId, keyword, options.tables);
results.push(...data);
}
}
return results;
}
async searchTables(connectionId: string, keyword: string): Promise<SearchResult[]> {
const schema = await this.getSchema(connectionId);
const results: SearchResult[] = [];
for (const table of schema.tables) {
if (this.matches(table.name, keyword)) {
results.push({
type: 'table',
connectionId,
connectionName: await this.getConnectionName(connectionId),
database: table.schema || '',
name: table.name,
detail: `Table (${table.rowCount || '?'} rows)`,
});
}
}
return results;
}
async searchColumns(connectionId: string, keyword: string): Promise<SearchResult[]> {
const schema = await this.getSchema(connectionId);
const results: SearchResult[] = [];
for (const column of schema.columns) {
if (this.matches(column.name, keyword)) {
results.push({
type: 'column',
connectionId,
connectionName: await this.getConnectionName(connectionId),
database: '', // 需要关联表获取
name: column.name,
detail: `${column.type} in ${column.tableName}`,
});
}
}
return results;
}
private matches(value: string, keyword: string, caseSensitive = false): boolean {
if (!caseSensitive) {
return value.toLowerCase().includes(keyword.toLowerCase());
}
return value.includes(keyword);
}
private async getSchema(connectionId: string): Promise<SchemaCacheEntry> {
const cached = this.schemaCache.get(connectionId);
if (cached && Date.now() - cached.cachedAt.getTime() < 60000) {
return cached; // 1 分钟缓存
}
// 查询 Schema
const tablesResponse = await this.ipcManager.sendRequest({
type: 'schema',
connectionId,
payload: { schemaType: 'tables' },
});
const columnsResponse = await this.ipcManager.sendRequest({
type: 'schema',
connectionId,
payload: { schemaType: 'columns' },
});
const entry: SchemaCacheEntry = {
tables: tablesResponse.data?.data || [],
columns: columnsResponse.data?.data || [],
cachedAt: new Date(),
};
this.schemaCache.set(connectionId, entry);
return entry;
}
clearCache(): void {
this.schemaCache.clear();
}
}
8.5 小结
本章介绍了 VSDB 的数据操作功能:
| 功能 | 实现模块 | 关键点 |
|---|---|---|
| 查询执行 | SqlEditorPanel + Worker | 历史记录、执行时间统计 |
| 导出 | ExportManager | CSV/JSON/SQL 格式 |
| 导入 | ImportManager | CSV/JSON 解析 |
| 数据编辑 | EditManager | 内联编辑、UPDATE SQL 生成 |
| 数据搜索 | SearchEngine | Schema 缓存、表/列搜索 |
关键设计:
- 查询历史:上限 100 条,支持固定防清理
- 导出格式:CSV(逗号分隔)、JSON(结构化)、SQL(INSERT 语句)
- 内联编辑:标记修改、批量提交、主键定位
- 搜索缓存:Schema 缓存 1 分钟,提升搜索性能
下一章将介绍辅助功能系统。
第九章:辅助功能系统
本章介绍 VSDB 的辅助功能,包括查询历史、书签系统、全局搜索和错误处理。
9.1 查询历史系统
HistoryManager 设计
graph TD
A["执行查询"] --> B["HistoryManager.add()"]
B --> C["添加到历史列表"]
C --> D["检查上限"]
D -->|"超过 100 条"| E["清理未固定项"]
D -->|"未超过"| F["保存到 globalState"]
G["查看历史"] --> H["TreeProvider"]
H --> I["显示历史节点"]
I --> J["点击重用"]
J --> K["填充到 SQL Editor"]
实现细节
export class HistoryManager {
private maxItems = 100;
constructor(private globalState: vscode.Memento) {}
// 添加历史项
add(item: QueryHistoryItem): void {
const history = this.getList();
history.unshift(item); // 新项在前
// 清理策略:先清理未固定,超过上限时清理最旧的
while (history.length > this.maxItems) {
// 从末尾找未固定项
for (let i = history.length - 1; i >= 0; i--) {
if (!history[i].pinned) {
history.splice(i, 1);
break;
}
}
// 全部固定时,清理最旧的固定项
if (history.length > this.maxItems) {
history.pop();
}
}
this.globalState.update('queryHistory', history);
}
// 固定历史项
pin(id: string): void {
const history = this.getList();
const item = history.find(h => h.id === id);
if (item) {
item.pinned = true;
this.globalState.update('queryHistory', history);
}
}
// 取消固定
unpin(id: string): void {
const history = this.getList();
const item = history.find(h => h.id === id);
if (item) {
item.pinned = false;
this.globalState.update('queryHistory', history);
}
}
// 删除单项
remove(id: string): void {
const history = this.getList().filter(h => h.id !== id);
this.globalState.update('queryHistory', history);
}
// 清空未固定
clear(): void {
const pinned = this.getList().filter(h => h.pinned);
this.globalState.update('queryHistory', pinned);
}
// 获取列表
getList(limit?: number): QueryHistoryItem[] {
const history = this.globalState.get<QueryHistoryItem[]>('queryHistory', []);
return limit ? history.slice(0, limit) : history;
}
}
TreeView 历史节点
class HistoryNode extends vscode.TreeItem {
constructor(public item: QueryHistoryItem) {
super(item.sql.substring(0, 50) + '...', vscode.TreeItemCollapsibleState.None);
this.contextValue = item.pinned ? 'history-pinned' : 'history';
this.iconPath = item.pinned
? new vscode.ThemeIcon('pin')
: new vscode.ThemeIcon('history');
this.tooltip = `${item.connectionName}\n${item.sql}\nExecuted: ${item.executedAt}`;
this.description = `${item.executionTime}ms, ${item.rowCount || 0} rows`;
}
}
9.2 书签系统
BookmarkManager 设计
graph TD
A["添加书签"] --> B["BookmarkManager.add()"]
B --> C["保存到 globalState"]
D["查看书签"] --> E["TreeProvider"]
E --> F["显示书签节点"]
F --> G["展开分组"]
H["管理书签"] --> I["移动到分组"]
H --> J["删除书签"]
H --> K["创建分组"]
实现细节
export class BookmarkManager {
constructor(private globalState: vscode.Memento) {}
add(bookmark: Bookmark): void {
const bookmarks = this.getList();
bookmark.id = uuid();
bookmark.createdAt = new Date();
bookmarks.push(bookmark);
this.globalState.update('bookmarks', bookmarks);
}
remove(id: string): void {
const bookmarks = this.getList().filter(b => b.id !== id);
this.globalState.update('bookmarks', bookmarks);
}
getList(): Bookmark[] {
return this.globalState.get<Bookmark[]>('bookmarks', []);
}
// 分组管理
getGroups(): BookmarkGroup[] {
return this.globalState.get<BookmarkGroup[]>('bookmarkGroups', []);
}
createGroup(name: string, color?: string): BookmarkGroup {
const groups = this.getGroups();
const group: BookmarkGroup = {
id: uuid(),
name,
color,
};
groups.push(group);
this.globalState.update('bookmarkGroups', groups);
return group;
}
moveToGroup(bookmarkId: string, groupId?: string): void {
const bookmarks = this.getList();
const bookmark = bookmarks.find(b => b.id === bookmarkId);
if (bookmark) {
bookmark.groupId = groupId;
this.globalState.update('bookmarks', bookmarks);
}
}
// 按分组获取书签
getByGroup(groupId?: string): Bookmark[] {
return this.getList().filter(b => b.groupId === groupId);
}
}
书签节点类型
// 书签分组节点
class BookmarkGroupNode extends vscode.TreeItem {
constructor(public group: BookmarkGroup) {
super(group.name, vscode.TreeItemCollapsibleState.Collapsed);
this.contextValue = 'bookmark-group';
this.iconPath = new vscode.ThemeIcon('folder', new vscode.ThemeColor(group.color));
}
}
// 书签项节点
class BookmarkNode extends vscode.TreeItem {
constructor(public bookmark: Bookmark) {
super(bookmark.name, vscode.TreeItemCollapsibleState.None);
this.contextValue = `bookmark-${bookmark.type}`;
// 根据类型设置图标
switch (bookmark.type) {
case 'connection':
this.iconPath = new vscode.ThemeIcon('plug');
break;
case 'table':
this.iconPath = new vscode.ThemeIcon('table');
break;
case 'query':
this.iconPath = new vscode.ThemeIcon('file-code');
break;
}
this.tooltip = `Type: ${bookmark.type}`;
}
}
9.3 全局搜索
SearchEngine 设计
graph TD
A["用户输入关键词"] --> B["SearchEngine.searchAll()"]
B --> C["遍历活动连接"]
C --> D["searchTables()"]
C --> E["searchColumns()"]
C --> F["searchData()"]
D --> G["查询 Schema Cache"]
E --> G
F --> H["生成 SELECT LIKE SQL"]
G --> I["匹配过滤"]
H --> I
I --> J["合并结果"]
J --> K["返回 SearchResult[]"]
实现细节
export class SearchEngine {
private schemaCache = new Map<string, SchemaCacheEntry>();
private cacheTimeout = 60000; // 1 分钟缓存
async searchAll(keyword: string, options: SearchOptions = {}): Promise<SearchResult[]> {
const results: SearchResult[] = [];
const connectionIds = options.connectionIds || await this.getActiveConnectionIds();
for (const connectionId of connectionIds) {
if (options.searchTables !== false) {
results.push(...await this.searchTables(connectionId, keyword, options));
}
if (options.searchColumns !== false) {
results.push(...await this.searchColumns(connectionId, keyword, options));
}
if (options.searchData) {
results.push(...await this.searchData(connectionId, keyword, options));
}
}
return results;
}
async searchTables(
connectionId: string,
keyword: string,
options: SearchOptions
): Promise<SearchResult[]> {
const schema = await this.getSchemaWithCache(connectionId);
const matcher = this.createMatcher(keyword, options.caseSensitive);
return schema.tables
.filter(t => matcher(t.name))
.map(t => ({
type: 'table',
connectionId,
connectionName: await this.getConnectionName(connectionId),
database: t.schema || 'public',
name: t.name,
detail: `${t.rowCount || '?'} rows`,
}));
}
async searchData(
connectionId: string,
keyword: string,
options: SearchOptions
): Promise<SearchResult[]> {
const results: SearchResult[] = [];
const tables = options.tables || await this.getAllTables(connectionId);
for (const table of tables) {
// 生成搜索 SQL
const columns = await this.getTableColumns(connectionId, table);
const whereClause = columns
.map(c => `${c.name} LIKE '%${keyword}%'`)
.join(' OR ');
const sql = `SELECT * FROM ${table} WHERE ${whereClause} LIMIT 100`;
try {
const response = await this.ipcManager.sendRequest({
type: 'query',
connectionId,
payload: { sql },
});
if (response.data?.rowCount > 0) {
results.push({
type: 'data',
connectionId,
connectionName: await this.getConnectionName(connectionId),
database: '',
name: keyword,
detail: `Found in ${table}: ${response.data.rowCount} rows`,
});
}
} catch {
// 搜索失败跳过
}
}
return results;
}
private createMatcher(keyword: string, caseSensitive?: boolean): (value: string) => boolean {
const k = caseSensitive ? keyword : keyword.toLowerCase();
return (value: string) => {
const v = caseSensitive ? value : value.toLowerCase();
return v.includes(k);
};
}
private async getSchemaWithCache(connectionId: string): Promise<SchemaCacheEntry> {
const cached = this.schemaCache.get(connectionId);
if (cached && Date.now() - cached.cachedAt.getTime() < this.cacheTimeout) {
return cached;
}
// 刷新缓存
const tables = await this.fetchTables(connectionId);
const columns = await this.fetchColumns(connectionId);
const entry: SchemaCacheEntry = {
tables,
columns,
cachedAt: new Date(),
};
this.schemaCache.set(connectionId, entry);
return entry;
}
clearCache(): void {
this.schemaCache.clear();
}
}
搜索命令
const searchCmd = vscode.commands.registerCommand(COMMANDS.SEARCH, async () => {
const keyword = await vscode.window.showInputBox({
prompt: 'Search tables, columns, or data',
placeHolder: 'Enter search keyword...',
});
if (!keyword) return;
vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: `VSDB: Searching for "${keyword}"...`,
}, async () => {
const results = await searchEngine!.searchAll(keyword, {
searchTables: true,
searchColumns: true,
searchData: false, // 数据搜索较慢,默认关闭
});
if (results.length === 0) {
vscode.window.showInformationMessage(`No results found for "${keyword}"`);
return;
}
const items = results.map(r => ({
label: r.name,
description: r.detail || '',
detail: `${r.connectionName} / ${r.database}`,
result: r,
}));
const selected = await vscode.window.showQuickPick(items, {
placeHolder: `Found ${results.length} results`,
matchOnDescription: true,
matchOnDetail: true,
});
if (selected) {
// 导航到结果
navigateToResult(selected.result);
}
});
});
9.4 错误处理系统
ErrorHandler 设计
export class ErrorHandler {
private outputChannel: vscode.OutputChannel;
constructor(outputChannel: vscode.OutputChannel) {
this.outputChannel = outputChannel;
}
handleError(error: Error | unknown, context?: string): void {
const message = this.extractMessage(error);
const code = this.extractCode(error);
// 记录到输出通道
this.outputChannel.appendLine(
`[${new Date().toISOString()}] ERROR: ${context || ''} ${message}`
);
// 根据错误类型决定是否显示用户通知
if (this.shouldNotifyUser(error)) {
vscode.window.showErrorMessage(this.toUserMessage(error));
}
}
extractMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return String(error);
}
extractCode(error: unknown): string {
if (error && typeof error === 'object' && 'code' in error) {
return String((error as any).code);
}
return 'UNKNOWN';
}
shouldNotifyUser(error: unknown): boolean {
const message = this.extractMessage(error);
// 连接相关错误显示通知
if (message.includes('connect') ||
message.includes('ECONNREFUSED') ||
message.includes('timeout')) {
return true;
}
// Worker 崩溃显示通知
if (message.includes('Worker') || message.includes('crash')) {
return true;
}
return false;
}
toUserMessage(error: unknown): string {
const message = this.extractMessage(error);
// 简化技术细节
if (message.includes('ECONNREFUSED')) {
return 'VSDB: Connection refused. Check if database is running.';
}
if (message.includes('ETIMEDOUT')) {
return 'VSDB: Connection timed out. Check network connectivity.';
}
if (message.includes('syntax error')) {
return 'VSDB: SQL syntax error.';
}
if (message.includes('Access denied')) {
return 'VSDB: Access denied. Check username/password.';
}
return `VSDB: ${message}`;
}
}
Worker 崩溃回调
// extension.ts
const ipcManager = new IpcManager({
workerScriptPath: workerPath,
restartOnCrash: true,
callbacks: {
onWorkerCrash: (attempt: number) => {
errorHandler?.handleError(new Error(`Worker crashed, restart attempt ${attempt}`));
vscode.window.showWarningMessage(
`VSDB: Worker process crashed. Restarting (attempt ${attempt})...`
);
},
onWorkerRestarted: () => {
vscode.window.showInformationMessage('VSDB: Worker process restarted successfully');
},
onConnectionsLost: (connectionIds: string[]) => {
errorHandler?.handleError(new Error(`Connections lost: ${connectionIds.join(', ')}`));
treeProvider?.refresh(); // 更新连接状态显示
},
},
});
9.5 小结
本章介绍了 VSDB 的辅助功能系统:
| 功能 | 实现模块 | 关键特性 |
|---|---|---|
| 查询历史 | HistoryManager | 上限 100 条,固定防清理 |
| 书签系统 | BookmarkManager | 分组管理,多种书签类型 |
| 全局搜索 | SearchEngine | Schema 缓存,表/列/数据搜索 |
| 错误处理 | ErrorHandler | 输出通道记录,用户友好提示 |
关键设计:
- 历史清理策略:优先清理未固定项
- 书签分组:支持创建分组、移动书签
- 搜索缓存:Schema 缓存 1 分钟提升性能
- 错误友好化:技术错误转换为用户友好提示
下一章将介绍开发与调试流程。
第十章:开发与调试
本章介绍 VSDB 的开发环境配置、调试流程和测试策略。
10.1 开发环境配置
依赖安装
# 克隆项目
git clone https://github.com/yourname/vsdb.git
cd vsdb
# 安装扩展依赖
npm install --legacy-peer-deps
# 安装 Webview 依赖
cd webview-ui
npm install
cd ..
项目配置
// package.json 关键配置
{
"engines": { "vscode": "^1.85.0" },
"main": "./dist/extension.js",
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "webpack --mode production",
"watch": "webpack --mode development --watch",
"build:webview": "cd webview-ui && npm run build",
"test": "vitest run",
"lint": "eslint src --ext ts"
}
}
TypeScript 配置
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "webview-ui"]
}
10.2 调试配置
VSCode Launch 配置
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "npm: watch"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/dist/test"
],
"preLaunchTask": "npm: test-compile"
}
]
}
Tasks 配置
// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"id": "npm: watch",
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true
},
{
"id": "npm: compile",
"type": "npm",
"script": "compile"
}
]
}
调试流程
graph TD
A["F5 启动调试"] --> B["启动 Extension Development Host"]
B --> C["加载扩展代码"]
C --> D["激活扩展"]
D --> E["测试功能"]
E --> F["发现问题"]
F --> G["修改代码"]
G --> H{"代码类型"}
H -->|"扩展代码"| I["自动重编译"]
I --> J["重启扩展 Host"]
H -->|"Webview 代码"| K["手动 rebuild"]
K --> L["刷新 Webview"]
J --> E
L --> E
10.3 Webview 调试
开发模式构建
# Webview 开发模式(支持热重载)
cd webview-ui
npm run dev
Webview DevTools
// 在 Webview Panel 创建时启用 DevTools
const panel = vscode.window.createWebviewPanel(
'vsdb.sqlEditor',
'VSDB SQL Editor',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
// 启用 DevTools(仅开发时)
enableFindWidget: true,
}
);
// 开发时可以打开 DevTools 检查 React 组件
消息调试
// 在 Webview 端添加调试日志
useEffect(() => {
const handler = (event: MessageEvent) => {
console.log('[Webview] Received:', event.data);
handleExtensionMessage(event.data);
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, []);
// 在扩展端添加调试日志
panel.webview.onDidReceiveMessage(message => {
console.log('[Extension] Received:', message);
handleWebviewMessage(message);
});
10.4 Worker 调试
Worker 日志
// worker.ts 启用详细日志
console.log('[Worker] Started');
process.on('message', (request: WorkerRequest) => {
console.log('[Worker] Request:', request.type, request.id);
handleMessage(request);
});
function send(message: WorkerResponse) {
console.log('[Worker] Response:', message.type, message.id);
process.send?.(message);
}
IPC 调试
// IpcManager.ts 启用详细日志
sendRequest(request): Promise<WorkerResponse> {
console.log('[IPC] Sending:', request.type, request.id);
return new Promise((resolve, reject) => {
// ...
this.worker.on('message', (message) => {
console.log('[IPC] Received:', message.type, message.id);
this.handleMessage(message);
});
});
}
模拟 Worker 崩溃
// 测试崩溃恢复
// 在 Worker 中手动触发崩溃
process.exit(1);
// 或发送无效消息
send({ type: 'invalid' } as any);
10.5 测试策略
测试架构
graph TD
A["测试层级"] --> B["单元测试"]
A --> C["集成测试"]
A --> D["E2E 测试"]
B --> B1["Parser 测试"]
B --> B2["Manager 测试"]
B --> B3["Driver 测试"]
C --> C1["真实数据库连接"]
C --> C2["IPC 通信测试"]
D --> D1["VSCode Extension Tester"]
D --> D2["UI 交互测试"]
单元测试示例
// __tests__/core/ipcManager.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { IpcManager } from '../../src/core/ipcManager';
describe('IpcManager', () => {
let ipcManager: IpcManager;
beforeEach(() => {
ipcManager = new IpcManager({
workerScriptPath: './dist/worker.js',
requestTimeout: 5000,
});
});
afterEach(async () => {
await ipcManager.shutdown();
});
it('should start worker successfully', async () => {
ipcManager.start();
const ready = await ipcManager.waitReady(5000);
expect(ready).toBe(true);
expect(ipcManager.isRunning()).toBe(true);
});
it('should handle connect request', async () => {
ipcManager.start();
await ipcManager.waitReady();
const response = await ipcManager.sendRequest({
type: 'connect',
connectionId: 'test-conn',
payload: {
config: {
id: 'test-conn',
name: 'test',
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '',
scope: 'project',
},
},
});
expect(response.type).toBe('result');
});
it('should restart on crash', async () => {
ipcManager.start();
await ipcManager.waitReady();
// 模拟崩溃
const worker = ipcManager.getWorker();
worker?.kill('SIGKILL');
// 等待重启
await new Promise(resolve => setTimeout(resolve, 2000));
expect(ipcManager.isRunning()).toBe(true);
});
});
Parser 测试示例
// __tests__/scanner/envParser.test.ts
import { describe, it, expect } from 'vitest';
import { EnvParser } from '../../src/scanner/envParser';
import * as fs from 'fs';
import * as path from 'path';
describe('EnvParser', () => {
const parser = new EnvParser();
it('should parse DATABASE_URL', async () => {
// 创建临时测试文件
const tempDir = './temp-test-env';
const envFile = path.join(tempDir, '.env');
fs.mkdirSync(tempDir, { recursive: true });
fs.writeFileSync(envFile, 'DATABASE_URL=postgresql://user:pass@localhost:5432/testdb');
const result = await parser.scan(tempDir);
expect(result.connections.length).toBe(1);
expect(result.connections[0].type).toBe('postgresql');
expect(result.connections[0].host).toBe('localhost');
expect(result.connections[0].port).toBe(5432);
// 清理
fs.rmSync(tempDir, { recursive: true });
});
it('should parse MYSQL_* variables', async () => {
const tempDir = './temp-test-mysql';
const envFile = path.join(tempDir, '.env');
fs.mkdirSync(tempDir, { recursive: true });
fs.writeFileSync(envFile, `
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=secret
MYSQL_DATABASE=myapp
`.trim());
const result = await parser.scan(tempDir);
expect(result.connections.length).toBe(1);
expect(result.connections[0].type).toBe('mysql');
expect(result.connections[0].database).toBe('myapp');
fs.rmSync(tempDir, { recursive: true });
});
});
Vitest 配置
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
threshold: {
lines: 80,
functions: 80,
branches: 80,
},
},
testTimeout: 10000,
hookTimeout: 10000,
},
});
10.6 常用调试技巧
日志级别控制
// 根据环境变量控制日志
const DEBUG = process.env.VSDB_DEBUG === 'true';
function log(...args: any[]) {
if (DEBUG) {
console.log('[VSDB]', ...args);
}
}
断点调试
// 在关键位置设置断点
debugger; // VSCode 会在此暂停
// 或使用条件断点
if (request.type === 'query') {
debugger;
}
状态检查
// 定期检查内部状态
setInterval(() => {
console.log('Pending requests:', ipcManager.pendingRequestCount);
console.log('Active connections:', ipcManager.getActiveConnectionIds());
}, 5000);
10.7 小结
本章介绍了 VSDB 的开发与调试:
| 主题 | 内容 |
|---|---|
| 开发环境 | 依赖安装、项目配置 |
| 调试配置 | launch.json、tasks.json |
| Webview 调试 | DevTools、消息日志 |
| Worker 调试 | IPC 日志、崩溃模拟 |
| 测试策略 | 单元测试、集成测试 |
| 测试工具 | Vitest、覆盖率要求 |
关键配置:
- TypeScript strict:严格类型检查
- 覆盖率阈值:80% lines/functions/branches
- 热重载:扩展代码自动重编译
- Webview 调试:React DevTools 可用
下一章将介绍扩展开发指南。
第十一章:扩展开发
本章介绍如何扩展 VSDB,包括添加新数据库支持、自定义功能和插件机制。
11.1 添加新数据库支持
扩展架构
graph TD
A["新数据库支持"] --> B["驱动实现"]
A --> C["Schema Inspector"]
A --> D["配置解析器"]
B --> B1["实现 Driver 接口"]
B --> B2["connect/disconnect"]
B --> B3["query/streamQuery"]
C --> C1["getDatabases"]
C --> C2["getTables"]
C --> C3["getColumns"]
D --> D1["更新 ScannerEngine"]
D --> D2["添加 Parser"]
Driver 接口实现
以 SQLite 为例:
// src/worker/driver/sqlite.ts
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import type { DbConnection, QueryResult } from '../../shared/types';
export class SqliteDriver {
private db: sqlite.Database | null = null;
async connect(config: DbConnection): Promise<void> {
// SQLite 连接配置使用文件路径
const dbPath = config.host; // SQLite 使用 host 字段存储路径
this.db = await open({
filename: dbPath,
driver: sqlite3.Database,
});
}
async disconnect(): Promise<void> {
if (this.db) {
await this.db.close();
this.db = null;
}
}
async query(sql: string, params?: any[]): Promise<QueryResult> {
if (!this.db) {
throw new Error('SQLite: not connected');
}
const startTime = Date.now();
const result = await this.db.all(sql, params || []);
const executionTime = Date.now() - startTime;
const rows = result as Record<string, unknown>[];
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
return {
columns,
rows,
rowCount: rows.length,
executionTime,
};
}
async *streamQuery(
sql: string,
params?: any[],
chunkSize = 1000
): AsyncGenerator<StreamChunkData> {
if (!this.db) {
throw new Error('SQLite: not connected');
}
const result = await this.db.all(sql, params || []);
const rows = result;
const totalRows = rows.length;
for (let offset = 0; offset < rows.length; offset += chunkSize) {
const chunk = rows.slice(offset, offset + chunkSize);
const isLast = offset + chunkSize >= rows.length;
yield {
chunkIndex: Math.floor(offset / chunkSize),
rows: chunk as Record<string, unknown>[],
...(isLast ? { totalRows } : {}),
};
}
}
getDb(): sqlite.Database | null {
return this.db;
}
}
Schema Inspector 实现
// src/worker/schema/sqliteSchema.ts
export class SqliteSchema {
async getTables(db: sqlite.Database): Promise<SchemaResult> {
const rows = await db.all(
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`
);
return {
type: 'tables',
data: rows.map(r => ({ name: r.name })),
};
}
async getColumns(db: sqlite.Database, table: string): Promise<SchemaResult> {
const rows = await db.all(`PRAGMA table_info(${table})`);
return {
type: 'columns',
data: rows.map(r => ({
name: r.name,
type: r.type,
nullable: r.notnull === 0,
defaultValue: r.dflt_value,
isPrimaryKey: r.pk === 1,
isAutoIncrement: false, // SQLite 无原生 auto_increment 标记
})),
};
}
async getIndexes(db: sqlite.Database, table: string): Promise<SchemaResult> {
const rows = await db.all(`PRAGMA index_list(${table})`);
const indexes: IndexInfo[] = [];
for (const row of rows) {
const indexInfo = await db.all(`PRAGMA index_info(${row.name})`);
indexes.push({
name: row.name,
columns: indexInfo.map(i => i.name),
isUnique: row.unique === 1,
isPrimary: row.origin === 'pk',
});
}
return {
type: 'indexes',
data: indexes,
};
}
async getDDL(db: sqlite.Database, table: string): Promise<SchemaResult> {
const row = await db.get(
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
table
);
return {
type: 'ddl',
data: [{ ddl: row?.sql || '' }],
};
}
}
Worker 路由更新
// worker.ts
import { SqliteDriver } from './driver/sqlite';
import { SqliteSchema } from './schema/sqliteSchema';
async function handleConnect(request: WorkerRequest): Promise<void> {
const config = request.payload.config;
let driver;
switch (config.type) {
case 'mysql':
driver = new MySqlDriver();
break;
case 'postgresql':
driver = new PostgreSqlDriver();
break;
case 'sqlite': // 新增
driver = new SqliteDriver();
break;
default:
sendError(request.id, 'UNSUPPORTED_DATABASE', `Unsupported database type: ${config.type}`);
return;
}
// ... 连接逻辑
}
async function executeSchema(schemaType: string, driver: any, ...) {
// 新增 SQLite 路由
if (config.type === 'sqlite') {
const db = (driver as SqliteDriver).getDb();
return sqliteSchema[schemaType](db, ...);
}
// ...
}
类型定义更新
// types.ts
export interface DbConnection {
// ...
type: 'mysql' | 'postgresql' | 'sqlite' | 'redis' | 'mongodb';
// ...
}
11.2 自定义功能扩展
添加新命令
// extension.ts
const customCommand = vscode.commands.registerCommand('vsdb.customAction', async () => {
// 实现自定义功能
const selected = await vscode.window.showQuickPick([...]);
if (selected) {
// 处理逻辑
}
});
context.subscriptions.push(customCommand);
扩展 TreeView
// treeItems.ts - 添加自定义节点类型
class CustomNode extends vscode.TreeItem {
constructor(public data: any) {
super('Custom Node', vscode.TreeItemCollapsibleState.None);
this.contextValue = 'custom';
this.iconPath = new vscode.ThemeIcon('star');
this.command = {
command: 'vsdb.customAction',
title: 'Custom Action',
};
}
}
// treeProvider.ts - 返回自定义节点
async getChildren(element?: TreeItem): Promise<TreeItem[]> {
if (!element) {
const nodes: TreeItem[] = [];
// 添加标准连接节点
nodes.push(...await this.getConnectionNodes());
// 添加自定义节点
nodes.push(new CustomNode({ ... }));
return nodes;
}
}
扩展 Webview
// 创建新的 Webview Panel
export class CustomPanel {
public static createOrShow(extensionUri: vscode.Uri): CustomPanel {
const panel = vscode.window.createWebviewPanel(
'vsdb.customPanel',
'VSDB Custom Panel',
vscode.ViewColumn.One,
{ enableScripts: true }
);
return new CustomPanel(panel, extensionUri);
}
private getHtml(): string {
return `
<!DOCTYPE html>
<html>
<head>
<script src="${this.panel.webview.asWebviewUri(...)}"></script>
</head>
<body>
<div id="root"></div>
<script>
// React 应用入口
</script>
</body>
</html>
`;
}
}
11.3 配置解析器扩展
添加新 Parser
// src/scanner/customParser.ts
export class CustomParser {
async scan(root: string): Promise<ScannerResult> {
const result: ScannerResult = {
connections: [],
errors: [],
scannedFiles: [],
};
// 查找特定配置文件
const configFiles = this.findConfigFiles(root);
for (const file of configFiles) {
try {
const content = fs.readFileSync(file, 'utf-8');
const connections = this.parseContent(content, file);
result.connections.push(...connections);
result.scannedFiles.push(file);
} catch (err: any) {
result.errors.push({ file, error: err.message });
}
}
return result;
}
private parseContent(content: string, sourceFile: string): ScannedConnection[] {
// 自定义解析逻辑
// ...
return [{
name: 'custom-connection',
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'user',
password: 'pass',
database: 'db',
source: 'custom',
sourceFile,
confidence: 'high',
}];
}
}
注册到 ScannerEngine
// scanner.ts
export class ScannerEngine {
private envParser = new EnvParser();
private dockerComposeParser = new DockerComposeParser();
private frameworkParser = new FrameworkParser();
private customParser = new CustomParser(); // 新增
async scan(root: string, existing: DbConnection[]): Promise<ScannerResult> {
// 执行所有 Parser
const results = [
await this.envParser.scan(root),
await this.dockerComposeParser.scan(root),
await this.frameworkParser.scan(root),
await this.customParser.scan(root), // 新增
];
// 合并结果
// ...
}
}
11.4 扩展最佳实践
代码组织
src/
├── worker/
│ ├── driver/
│ │ ├── mysql.ts # 已有
│ │ ├── postgresql.ts # 已有
│ │ ├── sqlite.ts # 新增
│ │ └── redis.ts # 未来扩展
│ └── schema/
│ ├── mysqlSchema.ts
│ ├── pgSchema.ts
│ ├── sqliteSchema.ts # 新增
├── scanner/
│ ├── envParser.ts
│ ├── dockerComposeParser.ts
│ ├── customParser.ts # 新增
测试覆盖
// __tests__/worker/driver/sqlite.test.ts
describe('SqliteDriver', () => {
it('should connect to sqlite file', async () => {
const driver = new SqliteDriver();
await driver.connect({
type: 'sqlite',
host: './test.db',
// ...
});
expect(driver.isConnected()).toBe(true);
});
it('should execute query', async () => {
// ...
});
});
文档更新
// README.md
## Supported Databases
| Database | Status |
|----------|--------|
| MySQL | ✅ Full support |
| PostgreSQL | ✅ Full support |
| SQLite | ✅ Added in v0.2.0 |
| Redis | 🔜 Planned |
11.5 小结
本章介绍了 VSDB 的扩展开发:
| 扩展类型 | 步骤 |
|---|---|
| 新数据库支持 | Driver + Schema Inspector + Worker 路由 |
| 自定义命令 | registerCommand + 实现 |
| TreeView 扩展 | 自定义节点 + TreeProvider |
| Webview 扩展 | 新 Panel + React 组件 |
| 配置解析器 | Parser + ScannerEngine 注册 |
关键要点:
- Driver 接口:遵循 connect/disconnect/query/streamQuery
- Schema Inspector:实现 getTables/getColumns/getIndexes
- Worker 路由:在 worker.ts 添加新类型判断
- 类型更新:扩展 DbConnection.type
- 测试覆盖:新增模块需要完整测试
下一章将介绍最佳实践。
第十二章:最佳实践
本章总结 VSDB 开发中的最佳实践,包括架构设计、性能优化、安全考虑和代码规范。
12.1 架构设计原则
进程隔离原则
graph TD
A["架构原则"] --> B["进程隔离"]
A --> C["异步优先"]
A --> D["错误边界"]
A --> E["状态持久化"]
B --> B1["数据库操作在 Worker"]
B --> B2["Worker 崩溃不影响 VSCode"]
C --> C1["IPC 消息异步"]
C --> C2["UI 不阻塞"]
D --> D1["错误分类"]
D --> D2["自动恢复"]
E --> E1["连接配置 JSON"]
E --> E2["密码 SecretStorage"]
关键原则总结
| 原则 | 实现 | 原因 |
|---|---|---|
| 进程隔离 | Worker 进程 | 数据库操作异常不影响 VSCode |
| 异步优先 | Promise + AsyncGenerator | UI 保持响应 |
| 错误边界 | try-catch + 错误分类 | 友好错误提示 |
| 状态持久化 | JSON + SecretStorage | 配置不丢失 |
12.2 性能优化
IPC 优化
// 1. 流式传输避免大消息
async *streamQuery(sql: string): AsyncGenerator<StreamChunk> {
for (let offset = 0; offset < rows.length; offset += 1000) {
yield { rows: rows.slice(offset, offset + 1000) };
}
}
// 2. 批量请求合并
async getFullSchema(connectionId: string): Promise<FullSchema> {
// 并行发送多个 schema 请求
const [databases, tables, columns] = await Promise.all([
this.sendRequest({ type: 'schema', payload: { schemaType: 'databases' } }),
this.sendRequest({ type: 'schema', payload: { schemaType: 'tables' } }),
this.sendRequest({ type: 'schema', payload: { schemaType: 'columns' } }),
]);
return { databases, tables, columns };
}
UI 渲染优化
// 1. 虚拟滚动
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={400}
itemCount={rows.length}
itemSize={35}
>
{Row}
</FixedSizeList>
// 2. 惰性加载
async getChildren(element: TreeItem): Promise<TreeItem[]> {
// 仅在展开时加载子节点
if (element.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) {
return []; // 空数组,等待展开时加载
}
return await this.loadChildren(element);
}
// 3. 缓存策略
class SchemaCache {
private cache = new Map<string, { data: any; expireAt: number }>();
private ttl = 60000; // 1 分钟
get(key: string): any | null {
const entry = this.cache.get(key);
if (entry && entry.expireAt > Date.now()) {
return entry.data;
}
this.cache.delete(key);
return null;
}
set(key: string, data: any): void {
this.cache.set(key, {
data,
expireAt: Date.now() + this.ttl,
});
}
}
数据库查询优化
// 1. 连接池配置(PostgreSQL)
const pool = new Pool({
max: 5, // 限制连接数
connectionTimeoutMillis: 10000,
idleTimeoutMillis: 10000,
});
// 2. 查询超时
const queryQueue = new QueryQueue({
maxConcurrency: 5,
defaultTimeout: 30000, // 30 秒超时
});
// 3. 结果限制
const sql = `${baseSql} LIMIT 10000`; // 防止返回过多数据
12.3 安全考虑
密码存储
// 使用 VSCode SecretStorage API
export class Storage {
constructor(
private globalState: vscode.Memento,
private secrets: vscode.SecretStorage,
) {}
async savePassword(connectionId: string, password: string): Promise<void> {
await this.secrets.store(`vsdb.password.${connectionId}`, password);
}
async getPassword(connectionId: string): Promise<string | undefined> {
return await this.secrets.get(`vsdb.password.${connectionId}`);
}
}
SQL 注入防护
// 使用参数化查询
async query(sql: string, params?: any[]): Promise<QueryResult> {
// mysql2 和 pg 都支持参数化查询
await this.connection.execute(sql, params);
}
// 不要拼接 SQL
// 错误示例:
const sql = `SELECT * FROM users WHERE name = '${name}'`; // 危险!
// 正确示例:
const sql = `SELECT * FROM users WHERE name = ?`;
await this.connection.execute(sql, [name]);
连接字符串处理
// 安全解析 DATABASE_URL
parseConnectionString(url: string): ScannedConnection | null {
try {
const parsed = new URL(url);
// 解码用户名和密码
const username = decodeURIComponent(parsed.username || '');
const password = decodeURIComponent(parsed.password || '');
// 不暴露原始 URL
return {
username,
password,
// 不存储原始 URL
};
} catch {
return null;
}
}
12.4 错误处理规范
错误分类
type ErrorClass = 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';
interface ClassifiedError {
errorClass: ErrorClass;
retryable: boolean;
userMessage: string;
}
const ERROR_MESSAGES: Record<ErrorClass, string> = {
connection: 'Connection failed. Check if database is running.',
syntax: 'SQL syntax error.',
timeout: 'Query timed out. Try again or simplify query.',
permission: 'Access denied. Check credentials.',
unknown: 'An unexpected error occurred.',
};
错误恢复策略
| 错误类型 | 策略 |
|---|---|
| connection | 重试 3 次,提示检查服务 |
| syntax | 不重试,显示错误位置 |
| timeout | 提示重试或简化查询 |
| permission | 不重试,提示检查权限 |
| Worker crash | 自动重启,恢复连接 |
用户友好提示
toUserMessage(error: unknown): string {
const message = this.extractMessage(error);
// 技术错误转换为用户友好提示
const mappings: Array<{ pattern: RegExp; message: string }> = [
{ pattern: /ECONNREFUSED/, message: 'Cannot connect to database. Is it running?' },
{ pattern: /ETIMEDOUT/, message: 'Connection timed out. Check network.' },
{ pattern: /syntax error/, message: 'SQL syntax error. Check your query.' },
{ pattern: /Access denied/, message: 'Permission denied. Check username/password.' },
];
for (const { pattern, message } of mappings) {
if (pattern.test(message)) {
return `VSDB: ${message}`;
}
}
return `VSDB: ${message}`;
}
12.5 代码规范
TypeScript 规范
// 1. 严格类型
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
}
}
// 2. 接口优先
interface DbConnection {
id: string;
name: string;
// ...
}
// 3. 枚举替代字符串常量
type DatabaseType = 'mysql' | 'postgresql' | 'sqlite';
// 4. 明确的返回类型
async query(sql: string): Promise<QueryResult> {
// ...
}
命名规范
// 文件命名
connectionManager.ts // Manager 类
envParser.ts // Parser 类
mysqlDriver.ts // Driver 类
// 类命名
class ConnectionManager {}
class EnvParser {}
class MySqlDriver {}
// 接口命名
interface DbConnection {}
interface WorkerRequest {}
// 函数命名
async getConnection() {} // 获取
async addConnection() {} // 添加
async deleteConnection() {} // 删除
async executeQuery() {} // 执行
// 变量命名
const connectionId = '...'; // camelCase
const MAX_CONNECTIONS = 5; // 常量 UPPER_CASE
文档规范
/**
* IPC Manager for communicating with the DB Worker process.
*
* Responsibilities:
* - Spawn and manage Worker process lifecycle
* - Send WorkerRequest messages and match responses
* - Handle Worker crashes and auto-restart
*
* @example
* const ipc = new IpcManager({ workerScriptPath: './worker.js' });
* ipc.start();
* const response = await ipc.sendRequest({ type: 'query', ... });
*/
export class IpcManager {
// ...
}
12.6 测试规范
测试覆盖率要求
// vitest.config.ts
coverage: {
threshold: {
lines: 80,
functions: 80,
branches: 80,
},
}
测试命名
describe('IpcManager', () => {
describe('start', () => {
it('should start worker process', async () => {
// ...
});
it('should throw if worker script not found', async () => {
// ...
});
});
describe('sendRequest', () => {
it('should return response for valid request', async () => {
// ...
});
it('should timeout for long request', async () => {
// ...
});
});
});
测试结构
describe('Module', () => {
let instance: Module;
beforeEach(() => {
instance = new Module();
});
afterEach(() => {
instance.dispose();
});
describe('feature', () => {
it('should do something', async () => {
// 准备
const input = '...';
// 执行
const result = await instance.doSomething(input);
// 验证
expect(result).toBe('...');
});
});
});
12.7 小结
本章总结了 VSDB 开发的最佳实践:
| 主题 | 关键点 |
|---|---|
| 架构设计 | 进程隔离、异步优先、错误边界、状态持久化 |
| 性能优化 | 流式传输、虚拟滚动、缓存、并行请求 |
| 安全考虑 | SecretStorage、参数化查询、不暴露原始 URL |
| 错误处理 | 分类、友好提示、恢复策略 |
| 代码规范 | 严格类型、接口优先、命名规范 |
| 测试规范 | 覆盖率 80%、describe/it 结构 |
这些最佳实践确保 VSDB 的:
- 稳定性:Worker 崩溃不影响 VSCode
- 性能:大数据集流畅渲染
- 安全性:密码加密、防注入
- 可维护性:清晰代码结构、完整测试
附录 A:类型定义速查
本附录汇总 VSDB 所有核心类型定义。
A.1 连接相关类型
// DbConnection - 数据库连接配置
export interface DbConnection {
id: string; // UUID
name: string; // 用户自定义名称
type: 'mysql' | 'postgresql'; // 数据库类型
host: string; // 主机地址
port: number; // 端口号
username: string; // 用户名
password: string; // 密码
database?: string; // 默认数据库
options?: Record<string, any>; // 扩展选项
group?: string; // 分组名称
color?: string; // 颜色标记
scope: 'project' | 'global'; // 存储范围
}
// ConnectionState - 连接状态
export interface ConnectionState {
connectionId: string;
status: 'disconnected' | 'connecting' | 'connected' | 'error';
error?: string;
activeDatabase?: string;
lastQuery?: string;
lastQueryTime?: number;
}
// ScannedConnection - 扫描发现的连接
export interface ScannedConnection {
name: string;
type: 'mysql' | 'postgresql';
host: string;
port: number;
username: string;
password: string;
database?: string;
source: 'env' | 'docker-compose' | 'framework';
sourceFile: string;
confidence: 'high' | 'medium' | 'low';
}
// ScannerResult - 扫描结果
export interface ScannerResult {
connections: ScannedConnection[];
errors: Array<{ file: string; error: string }>;
scannedFiles: string[];
}
A.2 IPC 协议类型
// WorkerRequest - Worker 请求
export interface WorkerRequest {
id: string;
type: 'connect' | 'disconnect' | 'query' | 'streamQuery' |
'schema' | 'cancel' | 'ping' | 'shutdown';
connectionId: string;
payload: {
config?: DbConnection;
sql?: string;
params?: unknown[];
schemaType?: SchemaType;
database?: string;
table?: string;
};
}
// WorkerResponse - Worker 响应
export interface WorkerResponse {
id: string;
type: 'result' | 'stream' | 'streamEnd' | 'error' | 'pong';
data?: QueryResult | SchemaResult;
error?: WorkerError;
}
// StreamChunk - 流式分块
export interface StreamChunk {
requestId: string;
chunkIndex: number;
rows: Record<string, unknown>[];
totalRows?: number;
}
// WorkerError - Worker 错误
export interface WorkerError {
code: string;
message: string;
errorClass?: 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';
retryable?: boolean;
}
type SchemaType = 'databases' | 'tables' | 'columns' | 'views' |
'procedures' | 'indexes' | 'constraints' | 'triggers' | 'ddl';
A.3 查询结果类型
// QueryResult - 查询结果
export interface QueryResult {
columns: string[];
rows: Record<string, unknown>[];
rowCount: number;
affectedRows?: number;
executionTime: number;
}
// SchemaResult - Schema 查询结果
export interface SchemaResult {
type: SchemaType;
data: unknown[];
}
A.4 Schema 信息类型
// TableInfo - 表信息
export interface TableInfo {
name: string;
schema?: string;
columns: ColumnInfo[];
indexes: IndexInfo[];
rowCount?: number;
}
// ColumnInfo - 列信息
export interface ColumnInfo {
name: string;
type: string;
nullable: boolean;
defaultValue?: string;
isPrimaryKey: boolean;
isAutoIncrement: boolean;
}
// IndexInfo - 索引信息
export interface IndexInfo {
name: string;
columns: string[];
isUnique: boolean;
isPrimary: boolean;
}
// ViewInfo - 视图信息
export interface ViewInfo {
name: string;
schema?: string;
definition?: string;
}
// ProcedureInfo - 存储过程信息
export interface ProcedureInfo {
name: string;
schema?: string;
parameters?: any[];
}
// SchemaInfo - 完整 Schema 信息
export interface SchemaInfo {
tables: TableInfo[];
views: ViewInfo[];
procedures: ProcedureInfo[];
}
A.5 辅助功能类型
// QueryHistoryItem - 查询历史项
export interface QueryHistoryItem {
id: string;
sql: string;
connectionId: string;
connectionName: string;
executedAt: Date;
rowCount?: number;
executionTime?: number;
pinned?: boolean;
}
// Bookmark - 书签
export interface Bookmark {
id: string;
type: 'connection' | 'database' | 'table' | 'query';
name: string;
target: {
connectionId?: string;
database?: string;
table?: string;
sql?: string;
};
createdAt: Date;
groupId?: string;
}
// BookmarkGroup - 书签分组
export interface BookmarkGroup {
id: string;
name: string;
color?: string;
}
// SearchResult - 搜索结果
export interface SearchResult {
type: 'table' | 'column' | 'data';
connectionId: string;
connectionName: string;
database: string;
name: string;
detail?: string;
}
// SearchOptions - 搜索选项
export interface SearchOptions {
searchTables?: boolean;
searchColumns?: boolean;
searchData?: boolean;
connectionIds?: string[];
tables?: string[];
caseSensitive?: boolean;
}
// SchemaCacheEntry - Schema 缓存条目
export interface SchemaCacheEntry {
tables: TableInfo[];
columns: ColumnInfo[];
cachedAt: Date;
}
A.6 默认端口常量
// constants.ts
export const DEFAULT_PORTS = {
mysql: 3306,
postgresql: 5432,
sqlite: 0, // SQLite 无端口概念
redis: 6379,
mongodb: 27017,
};
A.7 命令和视图常量
// constants.ts
export const VIEWS = {
CONNECTIONS: 'vsdb.connections',
};
export const COMMANDS = {
ADD_CONNECTION: 'vsdb.addConnection',
SCAN_PROJECT: 'vsdb.scanProject',
REMOVE_CONNECTION: 'vsdb.removeConnection',
CONNECT: 'vsdb.connect',
DISCONNECT: 'vsdb.disconnect',
NEW_QUERY: 'vsdb.newQuery',
SEARCH: 'vsdb.search',
EXPORT_DATA: 'vsdb.exportData',
IMPORT_DATA: 'vsdb.importData',
};
export const TREE_COMMANDS = {
CONNECT: 'vsdb.tree.connect',
DISCONNECT: 'vsdb.tree.disconnect',
NEW_QUERY: 'vsdb.tree.newQuery',
COPY_CONNECTION_STRING: 'vsdb.tree.copyConnectionString',
EDIT_CONNECTION: 'vsdb.tree.editConnection',
DELETE_CONNECTION: 'vsdb.tree.deleteConnection',
VIEW_TABLE_STRUCTURE: 'vsdb.viewTableStructure',
REFRESH: 'vsdb.tree.refresh',
};
附录 B:IPC协议参考
本附录详细描述 VSDB IPC 通信协议。
B.1 协议概述
VSDB 使用 JSON 序列化的消息通过 child_process.send() 进行 IPC 通信。
graph LR
A["Extension Host"] -->|"JSON over IPC"| B["Worker Process"]
B -->|"JSON over IPC"| A
B.2 请求类型
connect
建立数据库连接。
{
"id": "uuid-request",
"type": "connect",
"connectionId": "uuid-connection",
"payload": {
"config": {
"id": "uuid-connection",
"name": "dev-mysql",
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "secret",
"database": "myapp",
"scope": "project"
}
}
}
disconnect
断开数据库连接。
{
"id": "uuid-request",
"type": "disconnect",
"connectionId": "uuid-connection",
"payload": {}
}
query
执行 SQL 查询。
{
"id": "uuid-request",
"type": "query",
"connectionId": "uuid-connection",
"payload": {
"sql": "SELECT * FROM users LIMIT 10",
"params": []
}
}
streamQuery
执行流式查询(大数据集)。
{
"id": "uuid-request",
"type": "streamQuery",
"connectionId": "uuid-connection",
"payload": {
"sql": "SELECT * FROM large_table",
"params": []
}
}
schema
查询数据库元信息。
{
"id": "uuid-request",
"type": "schema",
"connectionId": "uuid-connection",
"payload": {
"schemaType": "tables",
"database": "myapp"
}
}
schemaType 可选值:
databases- 数据库列表tables- 表列表columns- 列信息(需指定 table)views- 视图列表procedures- 存储过程列表indexes- 索引信息(需指定 table)constraints- 约束信息(需指定 table)triggers- 触发器信息(需指定 table)ddl- DDL 语句(需指定 table)
cancel
取消流式查询。
{
"id": "uuid-stream-request",
"type": "cancel",
"connectionId": "",
"payload": {}
}
ping
心跳检测。
{
"id": "heartbeat",
"type": "ping",
"connectionId": "",
"payload": {}
}
shutdown
关闭 Worker。
{
"id": "",
"type": "shutdown",
"connectionId": "",
"payload": {}
}
B.3 响应类型
result
成功响应。
{
"id": "uuid-request",
"type": "result",
"data": {
"columns": ["id", "name", "email"],
"rows": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
],
"rowCount": 2,
"executionTime": 15
}
}
streamEnd
流式查询结束标记。
{
"id": "uuid-request",
"type": "streamEnd"
}
error
错误响应。
{
"id": "uuid-request",
"type": "error",
"error": {
"code": "QUERY_ERROR",
"message": "Syntax error near 'SELCT'",
"errorClass": "syntax",
"retryable": false
}
}
错误码列表:
CONNECT_FAILED- 连接失败NOT_CONNECTED- 未连接MISSING_SQL- 缺少 SQLMISSING_CONFIG- 缺少配置QUERY_ERROR- 查询错误STREAM_ERROR- 流式查询错误SCHEMA_ERROR- Schema 查询错误UNKNOWN_REQUEST_TYPE- 未知的请求类型WORKER_MEMORY_EXCEEDED- 内存超限
pong
心跳响应。
{
"id": "heartbeat",
"type": "pong"
}
B.4 流式分块
StreamChunk 格式:
{
"requestId": "uuid-request",
"chunkIndex": 0,
"rows": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" },
// ... 最多 1000 行
],
"totalRows": 2500 // 仅最后一块包含
}
B.5 时序图示例
普通查询
sequenceDiagram
participant Host as Extension Host
participant Worker as Worker
Host->>Worker: { type: "query", sql: "SELECT ..." }
Worker-->>Host: { type: "result", data: { rows: [...] } }
流式查询
sequenceDiagram
participant Host as Extension Host
participant Worker as Worker
Host->>Worker: { type: "streamQuery", sql: "SELECT ..." }
Worker-->>Host: { requestId, chunkIndex: 0, rows: [...] }
Worker-->>Host: { requestId, chunkIndex: 1, rows: [...] }
Worker-->>Host: { requestId, chunkIndex: 2, rows: [...], totalRows: 2500 }
Worker-->>Host: { type: "streamEnd" }
连接失败
sequenceDiagram
participant Host as Extension Host
participant Worker as Worker
Host->>Worker: { type: "connect", config: {...} }
Worker-->>Host: { type: "error", error: { code: "CONNECT_FAILED" } }
B.6 超时配置
| 操作类型 | 默认超时 | 说明 |
|---|---|---|
| connect | 60s | 连接建立 |
| query | 60s | 普通查询 |
| streamQuery | 120s | 流式查询(较长) |
| streamChunk | 30s | 分块间隔超时 |
| heartbeat | 30s | 心跳超时 |
B.7 错误分类
| errorClass | 典型错误 | retryable |
|---|---|---|
| connection | ECONNREFUSED, CONNECTION_LOST | ✓ |
| syntax | syntax error, ER_PARSE_ERROR | ✗ |
| timeout | ETIMEDOUT, query timeout | ✓ |
| permission | Access denied, permission denied | ✗ |
| unknown | 其他 | ✗ |
附录 C:命令与菜单
本附录列出 VSDB 所有命令、菜单和快捷键。
C.1 全局命令
| 命令 ID | 标题 | 功能 |
|---|---|---|
vsdb.addConnection | VSDB: Add Connection | 打开连接表单添加新连接 |
vsdb.scanProject | VSDB: Scan Project | 扫描项目配置发现连接 |
vsdb.removeConnection | VSDB: Remove Connection | 选择并删除连接 |
vsdb.connect | VSDB: Connect | 连接选中的数据库 |
vsdb.disconnect | VSDB: Disconnect | 断开选中连接 |
vsdb.newQuery | VSDB: New Query | 打开 SQL 编辑器 |
vsdb.search | VSDB: Search | 打开搜索对话框 |
vsdb.exportData | VSDB: Export Data | 导出当前结果 |
vsdb.importData | VSDB: Import Data | 导入数据 |
C.2 树节点命令
| 命令 ID | 标题 | 适用节点 | 功能 |
|---|---|---|---|
vsdb.tree.connect | Connect | connection | 连接数据库 |
vsdb.tree.disconnect | Disconnect | connection-connected | 断开连接 |
vsdb.tree.newQuery | New Query | connection-connected, database, table | 打开 SQL 编辑器 |
vsdb.tree.copyConnectionString | Copy Connection String | connection, database | 复制连接字符串 |
vsdb.tree.editConnection | Edit Connection | connection | 编辑连接配置 |
vsdb.tree.deleteConnection | Delete Connection | connection | 删除连接 |
vsdb.tree.refresh | Refresh | connection, database | 刷新节点 |
vsdb.viewTableStructure | View Table Structure | table | 查看表结构 |
C.3 视图工具栏
| 命令 | 图标 | 位置 | 功能 |
|---|---|---|---|
vsdb.addConnection | $(add) | navigation@1 | 添加连接 |
vsdb.tree.refresh | $(refresh) | navigation@2 | 刷新树 |
vsdb.scanProject | $(search) | navigation@3 | 扫描项目 |
C.4 右键菜单
view/title(视图标题栏)
{
"view/title": [
{
"command": "vsdb.addConnection",
"when": "view == vsdb.connections",
"group": "navigation@1"
},
{
"command": "vsdb.tree.refresh",
"when": "view == vsdb.connections",
"group": "navigation@2"
},
{
"command": "vsdb.scanProject",
"when": "view == vsdb.connections",
"group": "navigation@3"
}
]
}
view/item/context(节点右键)
{
"view/item/context": [
// 连接节点内联按钮
{
"command": "vsdb.tree.connect",
"when": "view == vsdb.connections && viewItem == connection",
"group": "inline@1"
},
{
"command": "vsdb.tree.disconnect",
"when": "view == vsdb.connections && viewItem == connection-connected",
"group": "inline@1"
},
// 查询相关
{
"command": "vsdb.tree.newQuery",
"when": "view == vsdb.connections && viewItem == connection-connected",
"group": "1_query@1"
},
// 表结构
{
"command": "vsdb.viewTableStructure",
"when": "view == vsdb.connections && viewItem == table",
"group": "1_actions@1"
},
// 管理操作
{
"command": "vsdb.tree.copyConnectionString",
"when": "view == vsdb.connections && viewItem =~ /^connection/",
"group": "2_manage@1"
},
{
"command": "vsdb.tree.editConnection",
"when": "view == vsdb.connections && viewItem =~ /^connection/",
"group": "3_edit@1"
},
{
"command": "vsdb.tree.deleteConnection",
"when": "view == vsdb.connections && viewItem =~ /^connection/",
"group": "3_edit@2"
},
// 刷新
{
"command": "vsdb.tree.refresh",
"when": "view == vsdb.connections && viewItem =~ /^(connection|database)/",
"group": "4_refresh@1"
}
]
}
C.5 viewItem 类型
| viewItem 值 | 节点类型 | 说明 |
|---|---|---|
connection | ConnectionNode (disconnected) | 未连接的连接节点 |
connection-connected | ConnectionNode (connected) | 已连接的连接节点 |
connection-connecting | ConnectionNode (connecting) | 正在连接 |
connection-error | ConnectionNode (error) | 连接错误 |
database | DatabaseNode | 数据库节点 |
table | TableNode | 表节点 |
column | ColumnNode | 列节点 |
history | HistoryNode | 历史记录节点 |
history-pinned | HistoryNode (pinned) | 固定的历史记录 |
bookmark | BookmarkNode | 书签节点 |
C.6 SQL Editor 快捷键
| 快捷键 | 功能 |
|---|---|
Ctrl+Enter / Cmd+Enter | 执行当前 SQL |
Ctrl+S / Cmd+S | 保存查询 |
C.7 树节点图标
| 状态 | 图标 | 颜色 |
|---|---|---|
| connected | $(database) | charts.green |
| connecting | $(loading~spin) | - |
| disconnected | $(database) | - |
| error | $(error) | charts.red |
| database | $(folder) | - |
| table | $(table) | - |
| column | $(symbol-field) | - |
| primary key | $(key) | - |
| index | $(list-unordered) | - |
C.8 命令注册示例
// extension.ts
const addConnectionCmd = vscode.commands.registerCommand(
'vsdb.addConnection',
() => {
ConnectionFormPanel.show(connectionManager, ipcManager, undefined, () => {
treeProvider?.refresh();
});
}
);
const scanProjectCmd = vscode.commands.registerCommand(
'vsdb.scanProject',
async () => {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
if (!workspaceRoot) {
vscode.window.showWarningMessage('No workspace folder open.');
return;
}
vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'VSDB: Scanning project...',
}, async () => {
const result = await scannerEngine!.scan(workspaceRoot, existingConnections);
// 处理结果...
});
}
);
context.subscriptions.push(
addConnectionCmd,
scanProjectCmd,
// ...
);
附录 D:配置参考
本附录列出 VSDB 的配置文件和选项。
D.1 连接存储位置
项目级连接
.vsdb/connections.json
示例内容:
{
"connections": [
{
"id": "uuid-1",
"name": "dev-mysql",
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"database": "myapp",
"scope": "project",
"group": "development",
"color": "blue"
}
]
}
全局连接
~/.vsdb/connections.json
格式与项目级相同,scope 为 "global"。
密码存储
密码通过 VSCode SecretStorage API 加密存储,键名格式:
vsdb.password.{connectionId}
D.2 IpcManager 配置
interface IpcManagerOptions {
workerScriptPath: string; // Worker 脚本路径(必填)
restartOnCrash?: boolean; // 崩溃时重启(默认 true)
maxRestartAttempts?: number; // 最大重启次数(默认 3)
requestTimeout?: number; // 请求超时(默认 60000ms)
heartbeatInterval?: number; // 心跳间隔(默认 15000ms)
heartbeatTimeout?: number; // 心跳超时(默认 30000ms)
streamChunkTimeout?: number; // 分块超时(默认 30000ms)
gracefulShutdownTimeout?: number; // 关闭超时(默认 10000ms)
callbacks?: IpcManagerCallbacks; // 事件回调
}
默认值:
const DEFAULT_REQUEST_TIMEOUT = 60000; // 60 秒
const DEFAULT_MAX_RESTART_ATTEMPTS = 3;
const DEFAULT_HEARTBEAT_INTERVAL = 15000; // 15 秒
const DEFAULT_HEARTBEAT_TIMEOUT = 30000; // 30 秒
const DEFAULT_STREAM_CHUNK_TIMEOUT = 30000; // 30 秒
const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT = 10000; // 10 秒
D.3 QueryQueue 配置
interface QueryQueueOptions {
maxConcurrency?: number; // 最大并发(默认 5)
defaultTimeout?: number; // 默认超时(默认 30000ms)
}
D.4 连接选项
DbConnection 的 options 字段支持:
MySQL 选项
{
charset?: string; // 字符集(如 'utf8mb4')
timezone?: string; // 时区(如 '+08:00'')
ssl?: { // SSL 配置
rejectUnauthorized?: boolean;
ca?: string;
cert?: string;
key?: string;
};
multipleStatements?: boolean; // 多语句支持(默认 false)
}
PostgreSQL 选项
{
ssl?: boolean | { // SSL 配置
rejectUnauthorized?: boolean;
ca?: string;
cert?: string;
key?: string;
};
applicationName?: string; // 应用名
connectionTimeoutMillis?: number; // 连接超时
idleTimeoutMillis?: number; // 空闲超时
}
D.5 扫描配置
EnvParser
const ENV_FILE_PATTERNS = [
'.env',
'.env.local',
'.env.development',
'.env.production',
'.env.test',
'.env.staging',
];
const MAX_DEPTH = 3; // 最大搜索深度
解析的环境变量:
| 变量 | 类型 | 说明 |
|---|---|---|
DATABASE_URL | PostgreSQL/MySQL URL | 完整连接字符串 |
DB_URL | PostgreSQL/MySQL URL | 同 DATABASE_URL |
DB_HOST | Host | 需配合 DB_PORT 等 |
DB_PORT | Port | 默认 PostgreSQL |
DB_USER | Username | - |
DB_PASSWORD | Password | - |
DB_NAME | Database | - |
MYSQL_HOST | Host (MySQL) | - |
MYSQL_PORT | Port (MySQL) | 默认 3306 |
MYSQL_USER | Username | - |
MYSQL_PASSWORD | Password | - |
MYSQL_DATABASE | Database | - |
PG_HOST | Host (PostgreSQL) | - |
PG_PORT | Port (PostgreSQL) | 默认 5432 |
PG_USER | Username | - |
PG_PASSWORD | Password | - |
PG_DATABASE | Database | - |
DockerComposeParser
识别的镜像:
- 包含
mysql或mariadb→ MySQL - 包含
postgres或postgresql→ PostgreSQL
解析的环境变量:
| 变量 | 类型 | 说明 |
|---|---|---|
MYSQL_ROOT_PASSWORD | Password | MySQL root 密码 |
MYSQL_USER | Username | MySQL 用户 |
MYSQL_PASSWORD | Password | MySQL 用户密码 |
MYSQL_DATABASE | Database | MySQL 数据库 |
POSTGRES_USER | Username | PostgreSQL 用户 |
POSTGRES_PASSWORD | Password | PostgreSQL 密码 |
POSTGRES_DB | Database | PostgreSQL 数据库 |
D.6 HistoryManager 配置
const maxItems = 100; // 最大历史记录数
清理策略:
- 超过上限时优先清理未固定项
- 全部固定时清理最旧的固定项
D.7 SearchEngine 配置
const cacheTimeout = 60000; // Schema 缓存时间(1 分钟)
搜索选项默认值:
| 选项 | 默认值 |
|---|---|
searchTables | true |
searchColumns | true |
searchData | false(较慢) |
caseSensitive | false |
D.8 扩展 manifest 配置
// package.json
{
"name": "vsdb",
"displayName": "VSDB - Database Client",
"version": "0.1.0",
"engines": {
"vscode": "^1.85.0"
},
"categories": ["Database", "Other"],
"activationEvents": ["onView:vsdb.connections"],
"main": "./dist/extension.js"
}
D.9 TypeScript 配置
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
D.10 Vitest 配置
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
threshold: {
lines: 80,
functions: 80,
branches: 80,
},
},
},
});
附录 E:常见问题
本附录解答 VSDB 开发和使用中的常见问题。
E.1 开发问题
Q: Worker 启动后立即退出
原因:Worker 脚本路径错误或未正确编译。
解决方案:
- 确保
npm run compile已执行 - 检查
worker.js是否存在于dist/目录 - 检查 IpcManager 的
workerScriptPath配置
const workerPath = vscode.extensions.getExtension('vsdb.vsdb')?.extensionPath
? path.join(extensionPath, 'dist', 'worker.js')
: path.join(__dirname, 'worker.js');
Q: IPC 消息无响应
原因:Worker 未就绪或请求超时。
解决方案:
- 使用
waitReady()等待 Worker 就绪 - 检查超时配置是否合理
- 检查 Worker 日志是否有错误
ipcManager.start();
await ipcManager.waitReady(5000);
const response = await ipcManager.sendRequest({ ... });
Q: Webview 无法接收消息
原因:Webview 未正确初始化或消息监听未设置。
解决方案:
- 确保 Webview 启用了
enableScripts - 确保 React 组件正确监听
window.message
// extension 端
panel.webview.postMessage({ type: 'connections', payload: [...] });
// React 端
window.addEventListener('message', (event) => {
handleExtensionMessage(event.data);
});
Q: Monaco Editor 加载缓慢
原因:Monaco 从 CDN 加载需要网络。
解决方案:
- 使用本地 Monaco 资源
- 或配置 CDN 加载路径
loader.config({
paths: {
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs'
}
});
E.2 连接问题
Q: 连接数据库失败
常见原因:
- 数据库服务未启动
- 网络不通或防火墙阻止
- 用户名/密码错误
- 端口配置错误
排查步骤:
- 检查数据库服务是否运行
- 使用
telnet host port测试网络 - 检查连接配置是否正确
- 查看 VSDB 输出通道日志
Q: MySQL 连接超时
解决方案:
- 增加
connectTimeout配置 - 检查 MySQL 是否允许远程连接
- 检查防火墙规则
await mysql.createConnection({
host: config.host,
connectTimeout: 20000, // 20 秒
});
Q: PostgreSQL SSL 连接失败
解决方案:
- 配置 SSL 选项
- 或禁用 SSL(仅开发环境)
const pool = new Pool({
host: config.host,
ssl: config.ssl || false, // 开发环境可禁用
});
E.3 性能问题
Q: 大数据查询卡顿
解决方案:
- 使用流式查询
streamQuery - 前端使用虚拟滚动
- 添加 LIMIT 限制
// 使用流式查询
await ipcManager.sendRequest({
type: 'streamQuery',
payload: { sql: 'SELECT * FROM large_table' },
});
// 前端虚拟滚动
<FixedSizeList height={400} itemCount={rows.length} itemSize={35}>
{Row}
</FixedSizeList>
Q: TreeView 展开缓慢
原因:每次展开都查询 Schema。
解决方案:
- 使用 Schema 缓存
- 预加载常用节点
// 缓存 Schema
const cached = schemaCache.get(connectionId);
if (cached) {
return cached.tables;
}
E.4 扩展问题
Q: 扩展激活失败
常见原因:
- VSCode 版本不满足
- 依赖未安装
- 入口文件路径错误
排查步骤:
- 检查
engines.vscode配置 - 执行
npm install - 检查
main配置路径
{
"engines": { "vscode": "^1.85.0" },
"main": "./dist/extension.js"
}
Q: 打包扩展体积过大
解决方案:
- 使用
.vscodeignore排除不必要文件 - 避免打包测试文件
- 使用 webpack 优化
.vscodeignore:
__tests__/**
node_modules/@types/**
*.test.ts
E.5 测试问题
Q: 测试数据库连接失败
解决方案:
- 使用 Docker 启动测试数据库
- 或使用 mock 驱动
# docker-compose.yaml
services:
mysql-test:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: test
Q: Vitest 测试超时
解决方案:
- 增加测试超时配置
- 或优化测试代码
// vitest.config.ts
test: {
testTimeout: 30000, // 30 秒
}
E.6 使用问题
Q: 扫描未发现连接
原因:
- 配置文件格式不正确
- 扫描深度限制
- 文件被跳过
解决方案:
- 检查配置文件语法
- 确保配置文件在项目根目录附近
- 检查
.env文件命名
有效的文件名:
.env
.env.local
.env.development
无效的文件名:
.env.prod(不在标准列表)
.env.backup(扩展名不匹配)
Q: 密码无法保存
原因:SecretStorage API 问题。
解决方案:
- 确保 VSCode 版本 >= 1.85.0
- 检查 SecretStorage 权限
await context.secrets.store(`vsdb.password.${id}`, password);
Q: 查询历史丢失
原因:
- 全局状态未持久化
- 清理策略删除了历史
解决方案:
- 固定重要查询
- 检查 globalState 存储
// 固定查询防止被清理
historyManager.pin(historyId);
E.7 其他问题
Q: 如何调试 Worker
解决方案:
- 在 Worker 中添加日志
- 使用 VSCode Attach to Process
// worker.ts
console.log('[Worker] Request:', request);
Q: 如何添加新数据库支持
参考第十一章扩展开发。
Q: 如何贡献代码
- Fork 项目
- 创建功能分支
- 提交 PR
git checkout -b feature/new-database
npm run test
npm run lint
git push origin feature/new-database