Task 任务系统
Task 是 VS Code 中"可追踪的进程":它比 sendText 更正式,有明确定义、执行类型、生命周期事件,还能与问题匹配器联动把输出转成诊断。构建、测试、打包这类可重复操作都应该做成 Task。
任务是什么
Task 的核心价值在于把"命令"提升为"一等公民":
| 能力 | sendText 终端 | Task |
|---|---|---|
| 执行定义 | 纯文本命令 | 结构化的 Task 对象 |
| 完成通知 | 需自行监听 | onDidEndTask 事件 |
| 输出解析 | 手动 | ProblemMatcher 自动转诊断 |
| 用户可复用 | 否 | 用户可直接在任务菜单中运行 |
| 多执行器 | 手动管理 | 内置复用/排队的执行策略 |
注册自定义任务
vscode.tasks.registerTaskProvider(type, provider) 注册一类任务的提供者,provideTasks 返回当前工作区下可用的任务列表:
import * as vscode from 'vscode';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
const provider: vscode.TaskProvider = {
provideTasks(token: vscode.CancellationToken): vscode.Task[] {
const root = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!root) return [];
// 一个"构建"任务
const buildTask = new vscode.Task(
{ type: 'mytool', task: 'build' }, // definition
root, // scope: 工作区
'构建', // name
'mytool', // source
new vscode.ShellExecution('npm run build') // execution
);
buildTask.group = vscode.TaskGroup.Build;
buildTask.problemMatchers = ['$tsc'];
// 一个"测试"任务
const testTask = new vscode.Task(
{ type: 'mytool', task: 'test' },
root,
'测试',
'mytool',
new vscode.ShellExecution('npm test')
);
testTask.group = vscode.TaskGroup.Test;
return [buildTask, testTask];
},
// 用户手动创建的任务(resolve 非必需)
resolveTask(task: vscode.Task): vscode.Task | undefined {
return task;
}
};
context.subscriptions.push(
vscode.tasks.registerTaskProvider('mytool', provider)
);
}registerTaskProvider 返回一个 Disposable,放入 context.subscriptions 即可随插件停用自动注销。
Task 定义结构
Task 构造函数的第一个参数是 TaskDefinition,它描述任务"身份",在任务列表中去重:
interface TaskDefinition {
type: string; // 必须:任务类型,与 registerTaskProvider 的 type 对应
[key: string]: any; // 自定义字段,用于区分同类型下的不同任务
}任务属性速览
| 字段 | 类型 | 作用 |
|---|---|---|
name | string | 显示名称 |
source | string | 来源标识(如插件名) |
execution | ShellExecution 或 ProcessExecution | 执行方式 |
group | TaskGroup | 任务分组(build/test/clean/rebuild) |
problemMatchers | string[] 或 ProblemMatcher[] | 输出问题匹配器 |
runOptions | RunOptions | 运行策略(复用/清除/后台) |
presentationOptions | TaskPresentationOptions | 终端显示行为 |
isBackground | boolean | 是否为后台长驻任务 |
detail | string | 任务列表中显示的说明文字 |
scope 参数
构造函数的第二个参数是任务的作用域,决定任务归属的工作区:
// 单根工作区:直接用文件夹 URI
const task = new vscode.Task(def, folderUri, '构建', 'mytool', execution);
// 多根工作区:可以为每个文件夹各建一套任务
for (const folder of vscode.workspace.workspaceFolders ?? []) {
tasks.push(new vscode.Task(def, folder.uri, `构建(${folder.name})`, 'mytool', execution));
}
// 全局任务:scope 为 vscode.TaskScope.Global(不依赖工作区)
const globalTask = new vscode.Task(
{ type: 'mytool', task: 'healthcheck' },
vscode.TaskScope.Global,
'健康检查',
'mytool',
new vscode.ShellExecution('curl -s http://localhost:3000/health')
);两种执行类型
ShellExecution 与 ProcessExecution 是任务的两类执行器,本质区别在"谁负责解析命令"。
ShellExecution
命令交给 shell(bash/cmd/powershell)解析执行,支持管道、重定向、环境变量展开:
import * as vscode from 'vscode';
// 单命令形式
const shellExec = new vscode.ShellExecution('npm run build');
// 参数数组形式:命令与参数分离,避免引号转义问题
const shellExec2 = new vscode.ShellExecution('node', ['scripts/build.js', '--prod']);
// 自定义 shell 与工作目录
const shellExec3 = new vscode.ShellExecution('make clean', {
cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
env: { CI: 'true' },
executable: '/usr/bin/zsh',
shellArgs: ['--login'],
shellQuoting: vscode.ShellQuoting.Strong
});| 选项 | 说明 |
|---|---|
cwd | 执行目录 |
env | 附加环境变量 |
executable / shellArgs | 指定 shell 及其参数 |
shellQuoting | 参数引号策略(Strong/Weak/Escape) |
ProcessExecution
直接启动一个可执行文件,不经过 shell:参数按数组原样传递,不存在引号与通配符解析,更安全、跨平台表现一致:
import * as vscode from 'vscode';
import * as path from 'path';
// 直接执行 node 脚本,参数逐项给出
const procExec = new vscode.ProcessExecution(
'node',
[path.join(__dirname, '../tools/build.js'), '--prod'],
{ cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath }
);
// 也可以指定可执行文件路径
const procExec2 = new vscode.ProcessExecution('/usr/bin/python3', ['tool.py']);如何选择
| 场景 | 推荐 | 原因 |
|---|---|---|
需要管道、重定向、&& 串联 | ShellExecution | shell 原生支持 |
| 参数含空格或特殊字符 | ProcessExecution | 避免引号地狱 |
| 需要跨平台一致 | ProcessExecution | 不依赖 shell 差异 |
| 用户手动运行任务 | ShellExecution | 与用户敲命令的直觉一致 |
任务生命周期事件
vscode.tasks 提供四个生命周期事件:
| 事件 | 触发时机 |
|---|---|
onDidStartTask | 任务开始执行 |
onDidEndTask | 任务执行结束(含退出码) |
onDidStartTaskProcess | 底层进程已启动(可拿 PID) |
onDidEndTaskProcess | 底层进程结束(可拿退出码) |
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 任务开始
context.subscriptions.push(
vscode.tasks.onDidStartTask((e) => {
console.log(`任务开始: ${e.execution.task.name} (${e.execution.task.source})`);
})
);
// 任务结束
context.subscriptions.push(
vscode.tasks.onDidEndTask((e) => {
const task = e.execution.task;
console.log(`任务结束: ${task.name}`);
})
);
// 进程已启动:拿到 PID
context.subscriptions.push(
vscode.tasks.onDidStartTaskProcess((e) => {
console.log(`进程 ${e.processId} 已启动,任务: ${e.execution.task.name}`);
})
);
// 进程结束:拿到退出码
context.subscriptions.push(
vscode.tasks.onDidEndTaskProcess((e) => {
const code = e.exitCode;
const taskName = e.execution.task.name;
if (code === 0) {
vscode.window.showInformationMessage(`${taskName} 执行成功`);
} else {
vscode.window.showErrorMessage(`${taskName} 失败,退出码 ${code}`);
}
})
);
}任务执行追踪器
配合 vscode.tasks.taskExecutions 可以查看当前运行中的任务:
function listRunningTasks(): string[] {
return vscode.tasks.taskExecutions.map((e) => e.task.name);
}
// 终止所有运行中的任务
export function cancelAllTasks() {
for (const execution of vscode.tasks.taskExecutions) {
execution.terminate();
}
}执行任务
vscode.tasks.executeTask(task) 启动任务,返回 Thenable<TaskExecution>:
import * as vscode from 'vscode';
async function runMyBuild() {
const root = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!root) return;
const task = new vscode.Task(
{ type: 'mytool', task: 'build' },
root,
'构建',
'mytool',
new vscode.ShellExecution('npm run build')
);
task.group = vscode.TaskGroup.Build;
try {
const execution = await vscode.tasks.executeTask(task);
// 拿到 execution 后可以监听其结束(onDidEndTask 全局事件)
} catch (err) {
vscode.window.showErrorMessage('任务启动失败: ' + err);
}
}runOptions 运行策略
Task.runOptions 控制重复执行时的行为:
import * as vscode from 'vscode';
// 复用已有运行实例(如果任务在跑,不重复启动)
const reuseTask = new vscode.Task(
{ type: 'mytool', task: 'watch' },
vscode.workspace.workspaceFolders?.[0]?.uri!,
'监听构建',
'mytool',
new vscode.ShellExecution('tsc -w')
);
reuseTask.runOptions = {
reevaluateOnRerun: true, // 重跑时重新解析 execution
rerunStrategy: vscode.TaskReRunStrategy.ReuseExisting // 复用已有实例
};rerunStrategy | 行为 |
|---|---|
ReuseExisting | 若任务已在运行,直接复用,不启动新进程 |
Rerun | 终止旧实例后重新启动 |
presentationOptions 终端表现
控制任务运行时的终端展示方式:
const task = new vscode.Task(
{ type: 'mytool', task: 'lint' },
vscode.workspace.workspaceFolders?.[0]?.uri!,
'Lint 检查',
'mytool',
new vscode.ShellExecution('npm run lint')
);
task.presentationOptions = {
reveal: vscode.TaskRevealKind.Always, // 总是显示终端
panel: vscode.TaskPanelKind.Dedicated, // 专用面板(每次复用同一面板)
clear: true, // 运行前清空终端
focus: false, // 不抢焦点
showReuseMessage: true, // 复用面板时提示
echo: true // 回显命令本身
};| 字段 | 可选值 | 说明 |
|---|---|---|
reveal | Always / Silent / Never | 何时显示终端 |
panel | Shared / Dedicated / New | 面板复用策略 |
clear | boolean | 运行前是否清屏 |
focus | boolean | 是否抢占焦点 |
echo | boolean | 是否回显命令 |
showReuseMessage | boolean | 复用面板时是否提示 |
任务触发方式
命令面板触发
任务会自动出现在命令面板的"任务"菜单中,也可以自定义命令触发:
context.subscriptions.push(
vscode.commands.registerCommand('mytool.runBuild', async () => {
const tasks = await vscode.tasks.fetchTasks({ type: 'mytool' });
const buildTask = tasks.find((t) => t.name === '构建');
if (buildTask) {
await vscode.tasks.executeTask(buildTask);
}
})
);vscode.tasks.fetchTasks 拉取全部已注册任务(含用户 tasks.json 中定义的),可以按类型过滤。
快捷键触发
在 package.json 中把命令绑定快捷键:
{
"contributes": {
"commands": [
{ "command": "mytool.runBuild", "title": "mytool: 构建" },
{ "command": "mytool.runTest", "title": "mytool: 测试" }
],
"keybindings": [
{
"command": "mytool.runBuild",
"key": "ctrl+alt+b",
"mac": "cmd+alt+b",
"when": "editorTextFocus"
},
{
"command": "mytool.runTest",
"key": "ctrl+alt+t",
"mac": "cmd+alt+t",
"when": "editorTextFocus"
}
]
}
}任务分组菜单
任务在"终端"菜单和任务快速选择中按 group 分组展示,内置分组:
TaskGroup | 含义 |
|---|---|
Build | 构建类任务 |
Test | 测试类任务 |
Clean | 清理类任务 |
Rebuild | 重建类任务 |
group.isDefault 标记默认任务,按 Ctrl+Shift+B(默认构建快捷键)时直接运行默认构建任务:
const buildTask = new vscode.Task(
{ type: 'mytool', task: 'build' },
root,
'构建',
'mytool',
new vscode.ShellExecution('npm run build')
);
// 标记为默认构建任务:Ctrl+Shift+B 直接触发
buildTask.group = { id: vscode.TaskGroup.Build.id, isDefault: true };综合示例:一键"构建+测试"工作流
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const provider: vscode.TaskProvider = {
provideTasks(): vscode.Task[] {
const folder = vscode.workspace.workspaceFolders?.[0]?.uri;
if (!folder) return [];
const root = folder.fsPath;
const build = new vscode.Task(
{ type: 'demo', task: 'build' },
folder, '构建', 'demo',
new vscode.ShellExecution('npm run build', { cwd: root })
);
build.group = { id: vscode.TaskGroup.Build.id, isDefault: true };
build.problemMatchers = ['$tsc'];
const test = new vscode.Task(
{ type: 'demo', task: 'test' },
folder, '测试', 'demo',
new vscode.ProcessExecution('node', ['--test'], { cwd: root })
);
test.group = vscode.TaskGroup.Test;
const clean = new vscode.Task(
{ type: 'demo', task: 'clean' },
folder, '清理', 'demo',
new vscode.ShellExecution('npm run clean', { cwd: root })
);
clean.group = vscode.TaskGroup.Clean;
return [build, test, clean];
}
};
context.subscriptions.push(
vscode.tasks.registerTaskProvider('demo', provider)
);
// 一键"构建+测试"
context.subscriptions.push(
vscode.commands.registerCommand('demo.buildAndTest', async () => {
const tasks = await vscode.tasks.fetchTasks({ type: 'demo' });
const build = tasks.find((t) => t.name === '构建');
const test = tasks.find((t) => t.name === '测试');
if (build) await vscode.tasks.executeTask(build);
if (test) await vscode.tasks.executeTask(test);
vscode.window.setStatusBarMessage('$(sync~spin) 构建与测试已提交', 3000);
})
);
// 结束事件:统一提示
context.subscriptions.push(
vscode.tasks.onDidEndTaskProcess((e) => {
const name = e.execution.task.name;
const ok = e.exitCode === 0;
if (ok) {
vscode.window.setStatusBarMessage(`$(check) ${name} 完成`, 5000);
} else {
vscode.window.showWarningMessage(`${name} 失败(退出码 ${e.exitCode})`);
}
})
);
}
export function deactivate() {}任务系统把"命令"结构化:定义(Task)、执行(ShellExecution/ProcessExecution)、生命周期(四个事件)、触发(命令面板/快捷键/分组)。用户可以在任务菜单中看到插件提供的任务并自由触发,这是比裸终端命令更规范的集成方式。