跳转到内容
搜索文档

监视文件系统变更

最后更新 查看 MarkdownAgent 设置

本指南介绍如何使用 Sandbox SDK 的文件监视 API 实时监视文件系统变更。文件监视适用于构建开发工具、自动化工作流,以及随文件变更即时响应的应用。

watch() 方法返回 SSE(Server-Sent Events)流,你可使用 parseSSEStream() 消费。流中的每个事件描述一次文件系统变更。

基本文件监视

首先监视目录中的任何变更:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		console.log(`Is directory: ${event.isDirectory}`);
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		console.log(`Is directory: ${event.isDirectory}`);
	}
}

流会发出四种生命周期事件类型:

  • watching — 监视已建立,包含 watchId
  • event — 发生了文件系统变更
  • error — 监视遇到错误
  • stopped — 监视已停止

文件系统变更事件(event.eventType)包括:

  • create — 创建了文件或目录
  • modify — 文件内容已变更
  • delete — 删除了文件或目录
  • move_from / move_to — 移动或重命名了文件或目录
  • attrib — 文件属性已变更(权限、时间戳)

按文件类型筛选

使用 include 模式仅监视特定文件类型:

import { parseSSEStream } from "@cloudflare/sandbox";

// Only watch TypeScript and JavaScript files
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx", "*.js", "*.jsx"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

// Only watch TypeScript and JavaScript files
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx", "*.js", "*.jsx"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}

常见 include 模式:

  • *.ts — TypeScript 文件
  • *.js — JavaScript 文件
  • *.json — JSON 配置文件
  • *.md — Markdown 文档
  • package*.json — 特定的 package 文件

排除目录

使用 exclude 模式跳过某些目录或文件:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace", {
	exclude: ["node_modules", "dist", "*.log", ".git", "*.tmp"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`Change detected: ${event.path}`);
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace", {
	exclude: ["node_modules", "dist", "*.log", ".git", "*.tmp"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`Change detected: ${event.path}`);
	}
}

构建响应式开发工具

变更时自动重新构建

在源文件被修改时自动触发构建:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx"],
});

let buildInProgress = false;

for await (const event of parseSSEStream(stream)) {
	if (
		event.type === "event" &&
		event.eventType === "modify" &&
		!buildInProgress
	) {
		buildInProgress = true;
		console.log(`File changed: ${event.path}, rebuilding...`);

		try {
			const result = await sandbox.exec("npm run build");
			if (result.success) {
				console.log("Build completed successfully");
			} else {
				console.error("Build failed:", result.stderr);
			}
		} catch (error) {
			console.error("Build error:", error);
		} finally {
			buildInProgress = false;
		}
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts", "*.tsx"],
});

let buildInProgress = false;

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (
		event.type === "event" &&
		event.eventType === "modify" &&
		!buildInProgress
	) {
		buildInProgress = true;
		console.log(`File changed: ${event.path}, rebuilding...`);

		try {
			const result = await sandbox.exec("npm run build");
			if (result.success) {
				console.log("Build completed successfully");
			} else {
				console.error("Build failed:", result.stderr);
			}
		} catch (error) {
			console.error("Build error:", error);
		} finally {
			buildInProgress = false;
		}
	}
}

变更时自动运行测试

在测试文件被修改时重新运行测试:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/tests", {
	include: ["*.test.ts", "*.spec.ts"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event" && event.eventType === "modify") {
		console.log(`Test file changed: ${event.path}`);
		const result = await sandbox.exec(`npm test -- ${event.path}`);
		console.log(result.success ? "Tests passed" : "Tests failed");
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/tests", {
	include: ["*.test.ts", "*.spec.ts"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event" && event.eventType === "modify") {
		console.log(`Test file changed: ${event.path}`);
		const result = await sandbox.exec(`npm test -- ${event.path}`);
		console.log(result.success ? "Tests passed" : "Tests failed");
	}
}

增量索引

仅对已变更的文件重新建立索引,而不是重新扫描整个目录树:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/docs", {
	include: ["*.md", "*.mdx"],
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		switch (event.eventType) {
			case "create":
			case "modify":
				console.log(`Indexing ${event.path}...`);
				await indexFile(event.path);
				break;
			case "delete":
				console.log(`Removing ${event.path} from index...`);
				await removeFromIndex(event.path);
				break;
		}
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/docs", {
	include: ["*.md", "*.mdx"],
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		switch (event.eventType) {
			case "create":
			case "modify":
				console.log(`Indexing ${event.path}...`);
				await indexFile(event.path);
				break;
			case "delete":
				console.log(`Removing ${event.path} from index...`);
				await removeFromIndex(event.path);
				break;
		}
	}
}

高级模式

使用辅助函数处理事件

将事件处理提取到可复用函数中,以处理流生命周期:

import { parseSSEStream } from "@cloudflare/sandbox";

async function watchFiles(sandbox, path, options, handler) {
	const stream = await sandbox.watch(path, options);

	for await (const event of parseSSEStream(stream)) {
		switch (event.type) {
			case "watching":
				console.log(`Watching ${event.path}`);
				break;
			case "event":
				await handler(event.eventType, event.path, event.isDirectory);
				break;
			case "error":
				console.error(`Watch error: ${event.error}`);
				break;
			case "stopped":
				console.log(`Watch stopped: ${event.reason}`);
				return;
		}
	}
}

// Usage
await watchFiles(
	sandbox,
	"/workspace/src",
	{ include: ["*.ts"] },
	async (eventType, filePath) => {
		console.log(`${eventType}: ${filePath}`);
	},
);
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

async function watchFiles(
	sandbox: any,
	path: string,
	options: { include?: string[]; exclude?: string[] },
	handler: (
		eventType: string,
		filePath: string,
		isDirectory: boolean,
	) => Promise<void>,
) {
	const stream = await sandbox.watch(path, options);

	for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
		switch (event.type) {
			case "watching":
				console.log(`Watching ${event.path}`);
				break;
			case "event":
				await handler(event.eventType, event.path, event.isDirectory);
				break;
			case "error":
				console.error(`Watch error: ${event.error}`);
				break;
			case "stopped":
				console.log(`Watch stopped: ${event.reason}`);
				return;
		}
	}
}

// Usage
await watchFiles(
	sandbox,
	"/workspace/src",
	{ include: ["*.ts"] },
	async (eventType, filePath) => {
		console.log(`${eventType}: ${filePath}`);
	},
);

防抖文件操作

通过先收集变更再处理,避免过多操作:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const changedFiles = new Set();
let debounceTimeout = null;

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		changedFiles.add(event.path);

		if (debounceTimeout) {
			clearTimeout(debounceTimeout);
		}

		debounceTimeout = setTimeout(async () => {
			console.log(`Processing ${changedFiles.size} changed files...`);
			for (const filePath of changedFiles) {
				await processFile(filePath);
			}
			changedFiles.clear();
			debounceTimeout = null;
		}, 1000);
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const changedFiles = new Set<string>();
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		changedFiles.add(event.path);

		if (debounceTimeout) {
			clearTimeout(debounceTimeout);
		}

		debounceTimeout = setTimeout(async () => {
			console.log(`Processing ${changedFiles.size} changed files...`);
			for (const filePath of changedFiles) {
				await processFile(filePath);
			}
			changedFiles.clear();
			debounceTimeout = null;
		}, 1000);
	}
}

非递归监视

仅监视目录的顶层,不进入子目录:

import { parseSSEStream } from "@cloudflare/sandbox";

// Only watch root-level config files
const stream = await sandbox.watch("/workspace", {
	include: ["package.json", "tsconfig.json", "vite.config.ts"],
	recursive: false,
});

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log("Configuration changed, rebuilding project...");
		await sandbox.exec("npm run build");
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

// Only watch root-level config files
const stream = await sandbox.watch("/workspace", {
	include: ["package.json", "tsconfig.json", "vite.config.ts"],
	recursive: false,
});

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log("Configuration changed, rebuilding project...");
		await sandbox.exec("npm run build");
	}
}

停止监视

当 container 休眠或关闭时,流会自然结束。有两种方式可提前停止监视:

使用 AbortController

parseSSEStream 传入 AbortSignal。中止该信号会取消流读取器,并将清理传播到服务器。当你需要在消费循环外部取消监视时,推荐此方法:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const controller = new AbortController();

// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000);

for await (const event of parseSSEStream(stream, controller.signal)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}

console.log("Watch stopped");
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
const controller = new AbortController();

// Cancel after 60 seconds
setTimeout(() => controller.abort(), 60_000);

for await (const event of parseSSEStream<FileWatchSSEEvent>(
	stream,
	controller.signal,
)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
	}
}

console.log("Watch stopped");

跳出循环

跳出 for await 循环也会取消流:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
let eventCount = 0;

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		eventCount++;

		// Stop after 100 events
		if (eventCount >= 100) {
			break; // Breaking out of the loop cancels the stream
		}
	}
}

console.log("Watch stopped");
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");
let eventCount = 0;

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		console.log(`${event.eventType}: ${event.path}`);
		eventCount++;

		// Stop after 100 events
		if (eventCount >= 100) {
			break; // Breaking out of the loop cancels the stream
		}
	}
}

console.log("Watch stopped");

最佳实践

使用服务端筛选

使用 includeexclude 模式进行筛选,而不是在 JavaScript 中筛选事件。服务端筛选发生在 inotify 级别,可减少通过网络发送的事件数量。

import { parseSSEStream } from "@cloudflare/sandbox";

// Efficient: filtering happens at the inotify level
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts"],
});

// Less efficient: all events are sent and then filtered in JavaScript
const stream2 = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream(stream2)) {
	if (event.type === "event") {
		if (!event.path.endsWith(".ts")) continue;
		// Handle event
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

// Efficient: filtering happens at the inotify level
const stream = await sandbox.watch("/workspace/src", {
	include: ["*.ts"],
});

// Less efficient: all events are sent and then filtered in JavaScript
const stream2 = await sandbox.watch("/workspace/src");
for await (const event of parseSSEStream<FileWatchSSEEvent>(stream2)) {
	if (event.type === "event") {
		if (!event.path.endsWith(".ts")) continue;
		// Handle event
	}
}

在事件处理中处理错误

事件处理程序中的错误不会停止监视流。用 try...catch 包装处理逻辑,以防止未处理的异常:

import { parseSSEStream } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream(stream)) {
	if (event.type === "event") {
		try {
			await handleFileChange(event.eventType, event.path);
		} catch (error) {
			console.error(
				`Failed to handle ${event.eventType} for ${event.path}:`,
				error,
			);
			// Continue processing events
		}
	}

	if (event.type === "error") {
		console.error("Watch error:", event.error);
	}
}
import { parseSSEStream } from "@cloudflare/sandbox";
import type { FileWatchSSEEvent } from "@cloudflare/sandbox";

const stream = await sandbox.watch("/workspace/src");

for await (const event of parseSSEStream<FileWatchSSEEvent>(stream)) {
	if (event.type === "event") {
		try {
			await handleFileChange(event.eventType, event.path);
		} catch (error) {
			console.error(
				`Failed to handle ${event.eventType} for ${event.path}:`,
				error,
			);
			// Continue processing events
		}
	}

	if (event.type === "error") {
		console.error("Watch error:", event.error);
	}
}

监视前确保目录存在

监视不存在的路径会返回错误。在开始监视前验证路径是否存在:

const watchPath = "/workspace/src";
const result = await sandbox.exists(watchPath);

if (!result.exists) {
	await sandbox.mkdir(watchPath, { recursive: true });
}

const stream = await sandbox.watch(watchPath, {
	include: ["*.ts"],
});
const watchPath = "/workspace/src";
const result = await sandbox.exists(watchPath);

if (!result.exists) {
	await sandbox.mkdir(watchPath, { recursive: true });
}

const stream = await sandbox.watch(watchPath, {
	include: ["*.ts"],
});

故障排除

CPU 使用率过高

如果监视大型目录导致性能问题:

  1. 使用具体的 include 模式,而不是监视所有内容
  2. 排除 node_modulesdist 等大型目录
  3. 监视特定子目录,而不是整个项目
  4. 使用 recursive: false 进行浅层监视

路径未找到错误

所有路径必须存在,并解析到 /workspace 内。相对路径相对于 /workspace 解析。

相关资源

这篇文档对您有帮助吗?