外观
Watch(文件监视)
watch 插件让页面跟随应用允许它们监视的目录和文件的变化,例如编辑器中 打开的项目。Go 代码和原生 UI 不需要它:它们直接调用 mygo.WatchFiles (参见监视文件),插件也使用它。
设置
在 Go 中使用该插件,带上页面可以监视的目录和文件,每个都有一个名字:
go
import (
"github.com/egoist/mygo"
"github.com/egoist/mygo/plugins/watch"
)
func main() {
mygo.Use(watch.New(watch.Options{
Roots: map[string]watch.Root{
"project": {Path: projectDir, Recursive: true},
"settings": {Path: settingsFile},
},
}))
// ...
}并在前端添加它的包:
sh
bun add @mygo-plugins/watch页面用名字来指定根,绝不用路径,因此它们无法监视其他任何东西。名字由 小写字母、数字和短横线组成,以字母开头。路径必须是绝对路径,并且在 mygo.Use 运行时必须是目录或文件,否则会 panic;它会解析路径的符号链接, 因为 macOS 的 /tmp 就是一个。不带根的 watch.Plugin 让页面什么也不能 监视。
Recursive 让页面也可以监视目录中的目录及其中的内容;不带它,它们只能 监视目录本身和其中的文件。Ignore 按相对根的路径排除文件和目录,正如 mygo.FileWatchOptions.Ignore 所做的那样:node_modules 这样被忽略的 目录永远不会被打开,也不计入一次监视可覆盖的条目数(MaxEntries:8192, macOS 上为 65536)。
go
"project": {Path: projectDir, Recursive: true, Ignore: func(p string, isDir bool) bool {
return isDir && (path.Base(p) == "node_modules" || path.Base(p) == ".git")
}},根也可以在应用运行时授予,例如用户选择的文件夹:当某个名字不在 Roots 中时,会询问 Resolve 该名字的根,带着调用的 context,对它不 认识的名字返回 false。
go
var granted sync.Map // 名字 → watch.Root
watch.New(watch.Options{
Resolve: func(ctx context.Context, name string) (watch.Root, bool) {
root, ok := granted.Load(name)
if !ok {
return watch.Root{}, false
}
return root.(watch.Root), true
},
})
// 用户在对话框中选好 dir 之后,一个绑定方法返回页面随后要监视的名字。
granted.Store("picked-1", watch.Root{Path: dir, Recursive: true})Allow(如果设置)会在每次监视之前被询问,带着调用的 context (mygo.CallerWindow(ctx) 是发出询问的窗口)和 watch.Request:根的 名字、其中的路径以及是否递归。它可以拒绝:
go
watch.New(watch.Options{
Roots: roots,
Allow: func(ctx context.Context, r watch.Request) bool {
return mygo.CallerWindow(ctx) == editorWindow
},
})每个页面一次可以监视 8 个目录和文件(MaxWatchesPerPage),所有页面共 128 个(MaxWatches)。
监视
watch 启动一次监视,并在一切监视完成后 resolve。迭代它来获取变化, 每次变化停止时得到一批事件:
ts
import { watch } from "@mygo-plugins/watch";
const watcher = await watch("project", { recursive: true });
for await (const events of watcher) {
for (const event of events) console.log(event.op, event.path, event.oldPath);
}path 监视根中的某个目录或文件而不是根本身,相对于根并以斜杠分隔 ("src"、"package.json");它不能离开根,也不能穿过符号链接。
事件带有 op,是 create、write、remove 和 rename 之一;path, 相对于被监视的内容并以斜杠分隔(. 是被监视的内容本身);oldPath, 重命名前的位置;以及 isDir。监视开始时已经存在的内容不会被报告。被 监视的文件通过它的目录来监视:被替换的文件(编辑器保存时就是这样)是 . 的一次 write,被移除的是 remove,重新出现的是一次 create。当 被监视的内容,或被监视文件的目录,被移除、重命名或替换时,最后一批会 包含一次 . 的 remove,迭代随之结束。
变化在报告之前等待 50 毫秒的安静期,并且当变化持续到来时最多等待 500 毫秒;debounceMs 设置安静期,正如 Go 的 Debounce:0 是 50 毫秒, 负数立即报告。监视把它看到的与它之前看到的做比较,因此事件说的是有 什么不同,而不是造成不同的每一步:一个文件连续写入多次算一次 write, 一个文件在其路径上被另一个文件替换(原子保存就是这样替换的)也算一次 write,一个文件在中间创建又删除则什么都不算。把它们当作重新读取文件的 提示。在页面取走一批之前,Go 不会再次扫描,因此页面忙碌时不会丢失任何 东西。
watcher.close()、从循环中 break,或作为 signal 传入的 AbortSignal 会停止监视,页面导航离开或关闭同样如此。watcher.closed 在 Go 停止监视后落定(settle),并以停止它的错误拒绝。错误是 WatchError,其 code 为:
denied:名字不是根之一,Allow拒绝,路径不在根内,或者页面对没有Recursive的根请求了递归监视,或其中某个目录的监视;not-found:路径不存在;limit:页面或应用监视了太多,或者目录的文件和目录数超过MaxEntries(8192,macOS 上为 65536);unsupported:平台无法监视文件;path-encoding:文件的名称不是有效的 UTF-8,无法提供给页面;io:其他一切,例如某个根自应用启动后被替换。
平台
监视跟随目录中的内容,而不是其中符号链接和 Windows junction 指向的 位置。只暴露应用可控的目录:名字可以防止页面选择路径,但另一个程序仍然 可以移动或替换目录中的内容。
- macOS 用 FSEvents 监视:一次监视是一个事件流,覆盖其根之下的一切, 因此每个文件不占用文件描述符,被监视的文件由其目录的流覆盖。只有当 FSEvents 说内容已改变时才报告一次 write,因为 APFS 也会把 O_TRUNC 写入称作创建;监视的比较负责报告其余部分。
- Linux 用 inotify 监视每个目录,通过应用的一个实例,其实例上限是系统 的上限(
fs.inotify.max_user_watches)。 - Windows 用一个
ReadDirectoryChangesW递归监视目录,因此其中的目录 可以被重命名,同时它也监视其父目录以得知它何时被重命名。目录被监视 期间,Windows 不允许其父目录及更上层目录被重命名,这对每个监视文件的 程序都一样。驱动器的根可以监视,云文件的文件夹(例如 OneDrive 的) 也可以。