ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Watchman Node.js 客户端(fb-watchman)完整实战指南:安装、能力检测、watch-project 与实时订阅

Watchman Node.js 客户端(fb-watchman)完整实战指南:安装、能力检测、watch-project 与实时订阅 后端开发工具【免费下载链接】watchmanWatches files and records, or triggers actions, when they change.项目地址https://gitcode.com/gh_mirrors/watchm/watchman点击查看免费下载本文以 Watchman 官方文档 nodejs.md 为核心系统讲解如何通过 npm 包fb-watchman在 Node.js 应用中接入 Watchman 文件监控服务。你将掌握客户端的安装与初始化、能力capability检测、watch-project建立监视、subscribe实时订阅文件变更、基于抽象时钟clock的时间约束订阅以及客户端全部方法Methods与事件Events的 API 细节并深入了解这些 API 背后的底层实现原理。1. 安装与初始化1.1 安装 npm 包Node.js 客户端的包名是fb-watchman通过 npm 直接安装即可$ npm install fb-watchman从仓库中的 watchman/node/package.json 可以看到该包的核心依赖是bser版本2.1.1它负责实现 Watchman 客户端与服务端之间的 BSER 二进制协议编解码{ name: fb-watchman, version: 2.0.2, main: index.js, dependencies: { bser: 2.1.1 } }1.2 导入并创建客户端实例var watchman require(fb-watchman); var client new watchman.Client();这里创建出的Client实例继承自 Node.js 的EventEmitter因此它同时具备了方法调用与事件订阅两种能力。从 watchman/node/index.js 的源码结构看Client内部维护了一个命令队列commands数组所有通过client.command()发起的调用都会被异步排队并在连接建立后按 FIFO先进先出顺序依次发送。如果你希望指定 watchman 可执行文件的路径可以在构造函数中传入选项var client new watchman.Client({ watchmanBinaryPath: /usr/local/bin/watchman // 绝对路径默认为 watchman从 PATH 中查找 });本文档的示例假定你使用的是 npm 仓库中发布的最新版fb-watchman包。2. 底层连接原理客户端是如何找到 watchman 服务的在深入 API 之前先理解客户端与服务端的连接建立方式这对排查问题很有帮助。从 watchman/node/index.js 的connect()实现可以看到两条路径环境变量直连如果环境变量WATCHMAN_SOCK已设置例如由 Watchman 的 trigger 命令导出客户端会直接使用该 socket 路径建立连接避免额外启动子进程if (process.env.WATCHMAN_SOCK) { makeSock(process.env.WATCHMAN_SOCK); return; }询问 CLI 获取 socket 路径否则客户端会通过child_process.spawn执行watchman --no-pretty get-sockname命令从返回的 JSON 中解析出sockname字段再建立连接。如果服务尚未运行这一步会顺带把 watchman 服务启动起来const args [--no-pretty, get-sockname]; // ... const obj JSON.parse(stdout.join()); makeSock(obj.sockname);值得注意的两个细节均有源码依据若watchman二进制不在PATH中spawn 会失败并抛出ENOENT错误客户端会将错误信息改写为提示安装 watchman并通过error事件暴露给上层。建立 socket 连接后收到的所有字节流都交给bser模块的BunserBuf见 watchman/node/bser/index.js做流式 BSER 解码。解码出的每个对象会判断是否属于unilateralTags [subscription, log]这两种单边unilateral响应——如果是就直接作为同名事件发出否则视为请求-响应序列中的响应回调对应的命令回调函数。理解了这条链路就能明白为什么客户端可以单独安装而不需要 watchman 服务已经运行——连接是惰性建立的第一次调用命令时才会触发。3. 检查 watchman 是否可用capabilityCheck客户端包可以在未安装 watchman 服务的情况下先行安装因此应用必须自行处理服务不可用的情况同时检测服务端是否支持你的应用所需的能力capabilities。capabilityCheck方法会向服务端发出一个 version 命令以此查询服务端的能力列表。var watchman require(fb-watchman); var client new watchman.Client(); client.capabilityCheck({optional:[], required:[relative_root]}, function (error, resp) { if (error) { // error 是一个 Error 对象可能因为 watchman 服务未安装 // 或 required 数组中列出的某个能力不被服务端支持 console.error(error); } // resp 是扩展后的 version 响应 // {version: 3.8.0, capabilities: {relative_root: true}} console.log(resp); });关于能力检测的底层逻辑源码中还有两点值得了解见 watchman/node/index.js响应结构resp.capabilities是一个对象其键是optional与required能力名的并集值取决于该能力是否被服务端支持。版本回退模拟对于不支持 capabilities 协议的旧版服务端_synthesizeCapabilityCheck()会基于服务端报告的版本号模拟出一份能力表。仓库内置的版本对照表如下cap_versions能力名引入版本cmd-watch-del-all3.1.1cmd-watch-project3.1relative_root3.3term-dirname3.1term-idirname3.1wildmatch3.7如果required中的能力不被支持回调的error参数会被设置并带有含义明确的错误信息。更多能力列表参见 capabilities.md。4. 建立监视watch-project几乎每个 watchman 操作都围绕监视某个目录树展开。你可以反复请求监视同一个目录而不会出错——watchman 会复用已有的 watch。var watchman require(fb-watchman); var client new watchman.Client(); var dir_of_interest /some/path; client.capabilityCheck({optional:[], required:[relative_root]}, function (error, resp) { if (error) { console.log(error); client.end(); return; } // 发起监视 client.command([watch-project, dir_of_interest], function (error, resp) { if (error) { console.error(Error initiating watch:, error); return; } // 最佳实践把响应中的 warning 或 error 信息展示给用户 // 它们可能提示了需要采取的补救措施 if (warning in resp) { console.log(warning: , resp.warning); } // watch-project 可能把你的 dir_of_interest 与树中更高层级 // 的另一个 watch 合并复用因此务必记录响应中的 relative_path console.log(watch established on , resp.watch, relative_path, resp.relative_path); }); });为什么relative_path如此重要这是watch-project与旧的watch命令最核心的区别当目标目录位于某个已被监视的项目根目录之下时watchman 不会为它单独建立一个新的根而是复用上层的 watch并通过resp.relative_path告诉你目标目录相对该 watch 根的位置。后续的订阅、查询都必须带上这个relative_path作为relative_root否则结果集会错误地覆盖整个上层根目录。仓库中的 watchman/node/example.js 对此做了同样的强调并把resp.watch与resp.relative_path分别保存在root与path_prefix变量中供后续使用。5. 订阅实时变更subscribe大多数 Node 应用关心的是实时文件变更通知。在 watchman 中这通过发出 subscribe 命令来配置。一个订阅在客户端连接存续期间一直有效直到你用 unsubscribe 命令取消它。下面的例子会先为树中所有匹配查询表达式的文件生成一轮订阅结果建立订阅时的初始快照之后随着文件变化持续生成订阅结果// watch 取自 watch-project 响应中的 resp.watch // relative_path 取自 watch-project 响应中的 resp.relative_path function make_subscription(client, watch, relative_path) { sub { // 匹配 dir_of_interest 下的任意 .js 文件 expression: [allof, [match, *.js]], // 我们关心的字段 fields: [name, size, mtime_ms, exists, type] }; if (relative_path) { sub.relative_root relative_path; } client.command([subscribe, watch, mysubscription, sub], function (error, resp) { if (error) { // 多半是订阅条件写错了 console.error(failed to subscribe: , error); return; } console.log(subscription resp.subscribe established); }); // 订阅结果通过 subscription 事件发出。 // 注意该事件对所有订阅统一触发。如果不同订阅使用了不同的 fields // 你需要自行检查订阅名并按需处理不同的数据结构。 // 实际运行中 resp 形如 // // { root: /private/tmp/foo, // subscription: mysubscription, // files: [ { name: node_modules/fb-watchman/index.js, // size: 4768, // exists: true, // type: f } ] } client.on(subscription, function (resp) { if (resp.subscription ! mysubscription) return; resp.files.forEach(function (file) { // 把 Int64 实例转换为 javascript 整数 const mtime_ms file.mtime_ms; console.log(file changed: file.name, mtime_ms); }); }); }几点关键说明expression表达式[allof, [match, *.js]]表示同时满足所有子条件这里即匹配任意.js文件名。完整的表达式语法可参考 file-query.md。fields字段列表指定每个文件记录里包含哪些字段。注意像mtime_ms这类字段在 BSER 协议中可能以 Int64 形式返回BSER 使用本地字节序见 watchman/node/bser/index.js示例中用一元加号file.mtime_ms将其安全转换为 JS 数字。订阅名作用域订阅名绑定的是你的客户端连接不同的客户端可以放心使用相同的订阅名而不会冲突。初始快照行为默认情况下watchman 在建立订阅时会先投递一遍当前所有匹配文件包括已删除文件的exists: false记录应用因此无需在启动时自行遍历目录树——仓库中的 watchman/node/example.js 注释明确指出了这一点。5.1 只订阅变化之后的文件上面这个例子会在订阅建立的那一刻为现存以及已删除的文件生成一轮结果。某些应用不希望这样。下面的例子演示如何加上逻辑时间约束。watchman 使用抽象时钟abstract clock跟踪变更。我们可以在发起监视的同时取得当前时钟然后把它作为订阅的时间约束function make_time_constrained_subscription(client, watch, relative_path) { client.command([clock, watch], function (error, resp) { if (error) { console.error(Failed to query clock:, error); return; } sub { // 匹配 dir_of_interest 下的任意 .js 文件 expression: [allof, [match, *.js]], // 我们关心的字段 fields: [name, size, exists, type], // 加上时间约束 since: resp.clock }; if (relative_path) { sub.relative_root relative_path; } client.command([subscribe, watch, mysubscription, sub], function (error, resp) { // 在这里处理订阅结果 }); }); }since字段接受一个 clockspec例如c:12345:6789这种形式它让订阅只报告时钟之后发生的变更从而跳过建立订阅时的存量文件。6. NodeJS API 参考方法Methods6.1 client.capabilityCheck(options, done)capabilityCheck方法向服务端发出 version 命令以查询其能力。如果服务端不支持 capabilities 协议capabilityCheck会基于服务端报告的版本号为一组重要能力模拟出能力响应对应源码_synthesizeCapabilityCheck与cap_versions表见上文第 3 节。options参数可包含以下属性optional可选能力名的数组required必需能力名的数组这些属性会原样透传给底层的version命令。done是命令完成时的回调签名为(error, result)。单独调用capabilityCheck而不提供done回调没有意义。响应对象含有一个capabilities属性其键为optional与required能力名的并集值为true或false取决于能力是否可用。如果required中的任一能力不被服务端支持done回调的error参数会被设置并附带明确的错误消息。client.capabilityCheck({optional:[], required:[relative_root]}, function (error, resp) { if (error) { // error 是一个 Error 对象watchman 服务未安装 // 或 required 中的能力不被服务端支持 console.error(error); } // resp 是扩展后的 version 响应 // {version: 3.8.0, capabilities: {relative_root: true}} console.log(resp); });6.2 client.command(args [, done])向 watchman 服务发送一条命令。args是数组第一个元素为命令名后续元素为命令参数。命令会被排队并异步分发——你可以在连接建立前就连续command()多条命令它们会在连接就绪后按 FIFO 顺序依次发送对应源码中的commands队列与sendNextCommand()。done是命令完成时的回调(error, result)如果对结果不感兴趣可以省略。client.command([watch-project, process.cwd()], function(error, resp) { if (error) { console.log(watch failed: , error); return; } if (warning in resp) { console.log(warning: , resp.warning); } if (relative_path in resp) { // 我们需要记住并针对 relative_path 做调整 console.log(watching project , resp.watch, relative path to cwd is , resp.relative_path); } else { console.log(watching , resp.watch); } });关于warning字段如果resp中存在warning字段说明 watchman 服务正在传达一个用户应当看到并处理的问题。例如当系统 watch 资源需要调整时watchman 会给出相关信息以及补救建议。建议所有基于本库构建的工具把 warning 消息向上传递给用户仓库示例 watchman/node/example.js 也遵循了这一最佳实践。6.3 client.end()终止与 watchman 服务的连接。注意它不会等待任何已排队但尚未发送的命令。从源码看end()会先以 The client was ended 为原因取消所有待处理命令逐个以错误回调收尾再关闭 socket。7. NodeJS API 参考事件Events客户端对象会发出以下事件7.1 事件connect客户端成功连接到 watchman 服务时触发。7.2 事件error到 watchman 服务的 socket 遇到错误时触发。它也可能在连接建立之前触发——例如无法成功执行 watchman CLI 二进制来确定与服务端的通信方式时对应第 2 节中 spawnget-sockname失败的分支。回调会收到一个封装了错误信息的变量。7.3 事件end到 watchman 服务的 socket 关闭时触发。结合源码可见socket 的end事件处理中会先把socket、bunser置空取消所有待处理命令错误信息为 The watchman connection was closed最后才发出end事件。7.4 事件log响应 watchman 服务发来的单边logPDU 时触发。要启用它需要先向服务发送log-level命令// 这非常冗长通常你不应该这么做 client.command([log-level, debug]); client.on(log, function(info) { console.log(info); });7.5 事件subscription响应 watchman 服务发来的单边subscriptionPDU 时触发。要启用它需要先向服务发送subscribe命令// 订阅关于 .js 文件的通知 client.command([subscribe, process.cwd(), mysubscription, { expression: [match, *.js] }], function(error, resp) { if (error) { // 多半是订阅条件写错了 console.log(failed to subscribe: , error); return; } console.log(subscription resp.subscribe established); } ); // 订阅结果通过 subscription 事件发出。 // 注意watchman 在首次订阅时会先投递当前所有文件的列表 // 因此启动时你无需自行遍历目录树 client.on(subscription, function(resp) { console.log(resp.root, resp.subscription, resp.files); });取消订阅使用unsubscribe命令并传入要取消的订阅名client.command([unsubscribe, process.cwd(), mysubscription]);再次强调订阅名的作用域是你的连接不同客户端可以使用相同的订阅名而互不干扰。8. 仓库中的完整示例与集成测试8.1 开箱即用的完整示例仓库中的 watchman/node/example.js 是一个可以直接运行的端到端示例覆盖了上述全部流程监听end与error事件capabilityCheck({required:[relative_root]})检测服务可用性故意发送一条无效命令[invalid-command-never-will-work]演示错误回调watch-project建立监视并妥善处理resp.watch/resp.relative_path建立mysubscription订阅expression: [allof, [match, *.js]]配合relative_root再通过clocksince建立只关注之后变化的sincesub订阅。其中sincesub示例还演示了一个实用细节当订阅只请求fields: [name]一个字段时订阅响应中的files会退化为文件名组成的字符串数组而非对象数组。8.2 自动化集成测试watchman 的集成测试通过 Python 的 unittest 驱动 Node.js 脚本来验证客户端行为见 watchman/integration/test_nodejs.pyfind_js_tests装饰器会扫描integration目录下所有*.js文件为每个脚本动态生成一个测试类用node_bin执行脚本测试会先通过 yarn 在临时目录安装fb-watchman含其bser依赖设置WATCHMAN_SOCK环境变量指向测试实例的 socket然后运行脚本并校验退出状态码相关的 JS 测试脚本包括 watchman/integration/node_basic.js 等它们构成了 Node.js 客户端在真实 watchman 服务上的行为回归保障。如果你要为自己的项目编写类似测试可以直接复用这套模式把 watchman/node/index.js 与 watchman/node/bser 作为依赖通过WATCHMAN_SOCK注入 socket 路径即可在测试环境中驱动真实服务。9. 实践要点小结关注点关键做法服务可用性任何命令之前先做capabilityCheck并处理error监视目录使用watch-project而非旧watch务必记录resp.relative_path实时通知subscribesubscription事件用resp.subscription区分多个订阅初始快照默认首次订阅会返回存量文件不需要时用clocksince约束字段与类型fields决定记录内容Int64 字段用转成 JS 数字资源释放退出时调用client.end()连接关闭前排队中的命令会被取消错误处理关注resp.warning它可能是系统 watch 资源不足等问题的提示赞分享后端开发工具【免费下载链接】watchmanWatches files and records, or triggers actions, when they change.项目地址https://gitcode.com/gh_mirrors/watchm/watchman点击查看免费下载相关推荐fb-watchman 使用指南在 Node.js 中接入 Watchman 实现高效文件监听与变更订阅fb watchman 使用指南在 Node.js 中接入 Watchman 实现高效文件监听与变更订阅 fb watchman 是 Watchman 文件监后端开发工具Watchman C 客户端库实战指南连接、命令与订阅的完整用法Watchman C 客户端库实战指南连接、命令与订阅的完整用法 Watchman 官方提供了面向 C 应用的高层客户端库封装了本地 Watchma后端开发工具Watchman Java 客户端库从构建、BSER 协议到订阅使用的完整指南Watchman Java 客户端库从构建、BSER 协议到订阅使用的完整指南 本指南以仓库内 watchman/java/README.md https:/后端开发工具上一篇LeetCode-Book 图解 LCR 141「训练计划 III」双指针迭代与递归两种方式反转链表下一篇酷安桌面版安装指南5 分钟跑起来大屏刷动态更顺手创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表