ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

node-crawler 仓库开发与测试指南:从 AGENTS.md 读懂构建、测试与架构约定

node-crawler 仓库开发与测试指南:从 AGENTS.md 读懂构建、测试与架构约定 网页爬虫【免费下载链接】node-crawlerWeb Crawler/Spider for NodeJS server-side jQuery ;-)项目地址https://gitcode.com/gh_mirrors/no/node-crawler点击查看免费下载导读AGENTS.md是本仓库面向 AI Agent以及所有后续开发者的操作手册它浓缩了项目最核心的工程约定测试必须跑在编译产物dist/上、测试框架是 ava nock、代码走 TypeScript strict ESM ESLint并明确了got懒加载、Cheerio 解析、限速器集群等关键架构决策。本文将逐条拆解这些约定并结合仓库内的 package.json、tsconfig.json、eslint.config.js、src/crawler.ts 与测试用例讲清楚为什么这么约定以及如何照着做让读者既能快速上手参与开发也能理解 node-crawler 底层的设计思路。一、先看全局这份文档在讲什么AGENTS.md 的定位很明确——它不是给最终用户看的 API 文档那是 README.md 的职责而是给在仓库里改代码的人人类开发者或 AI Agent看的工程规约。它只用了五个小节就覆盖了一个 TypeScript 库项目最需要被交代的四件事构建与测试的先后关系测试基于编译产物而非源码测试的具体写法约束ava 框架、nock 拦截、testCb包装器、20 秒超时代码风格红线strict、ESM、双引号、分号、禁console架构层面的雷区got必须懒加载、jsdom 不支持、限速器是集群且只能全局配置。下面逐一展开并结合源码验证每一项约定背后的理由。二、构建与测试先编译再测试2.1 为什么测试要跑在dist/上AGENTS.md 开篇就给出了一条硬性规则Tests run against compiled output indist/, not source. Always build before testing.这是 node-crawler 测试体系与一般项目最大的不同测试文件 import 的是编译产物不是 TypeScript 源码。查看 test/errorHandling.js 的头部即可验证import test from ava; import { testCb } from ./lib/avaTestCb.js; import nock from nock; import Crawler from ../dist/index.js;所有测试都从../dist/index.js引入Crawler。这样做的直接收益是测试环境与发布环境完全一致——你在测试里验证的正是用户npm install后拿到的那份代码天然规避了源码能跑、打包后崩掉的经典翻车场景。2.2 两条核心命令npm run build # tsup → dist/index.js dist/index.cjs npm test # NODE_ENVtest ava运行 test/*.js排除 test/*test.js对照 package.json 中的 scripts 定义scripts: { build: tsup src/index.ts --format cjs,esm --clean, test: NODE_ENVtest ava, cover: NODE_ENVtest c8 ava }build用tsup把 src/index.ts 打包为cjs,esm两种格式--clean会先清空dist/。这与 package.json 的exports字段一一对应require走./dist/index.cjsimport走./dist/index.js。test显式设置NODE_ENVtest再执行 ava这是为了让代码在测试环境下走特定分支例如避免真实网络调用。另有cover命令NODE_ENVtest c8 ava用 c8 收集覆盖率其报告器配置lcov/html/text同样在 package.json 的c8字段中。2.3 CI 流水线AGENTS.md 明确 CI 的完整链路为npx eslint npm run build npm test顺序是有讲究的先静态检查eslint、再构建build、最后跑测试test。由于测试依赖dist/产物build 必须位于 test 之前而 eslint 检查源码不需要产物故放在最前任何一步失败都会让流水线提前中断节省资源。三、测试约定ava nock纯离线单测3.1 框架选型与文件命名测试框架是 avaHTTP 层用nock 模拟No real network calls——测试完全不发真实网络请求。nock 在测试中扮演假服务器拦截特定 host 的请求并直接返回预设响应。测试文件是纯.js且必须从../dist/index.js导入编译产物。命名红线ava 配置排除了test/*test.js新测试文件不要使用.test.js后缀。从 package.json 的 ava 配置可以看到这层映射ava: { files: [test/*.js, !test/*test.js], timeout: 20s, extensions: [js], verbose: true }也就是说test/*.js全部纳入但test/*test.js被显式排除。仓库现有测试文件如 test/errorHandling.js、test/rateLimit.js、test/priority.js、test/requests.js都遵循了xxx.js的命名规范。3.2testCb包装器让回调式异步可被 ava 感知node-crawler 的 API 是回调风格的callback(error, response, done)中必须手动调用done()来通知任务完成。但 ava 本身是 Promise/async 导向的二者如何对接答案是仓库自带的 test/lib/avaTestCb.js。export const testCbAsync (test, description, assertions) { test(description, async t { await new Promise(resolve { t.end () { resolve(undefined); }; assertions(t); }); }); }; export const testCbSync (test, description, assertions) { test.serial(description, async t { await new Promise(resolve { t.end () { resolve(undefined); }; assertions(t); }); }); }; export const testCb testCbSync;它的原理很精巧把t.end重写为 resolve Promise 的函数测试体内部通过await new Promise(...)挂起直到业务回调里调用t.end()才放行。于是回调驱动的异步测试被无缝转成了 ava 能识别的 async 测试。两种导出有明确分工testCbAsync→ 普通test()可并行执行testCbSync→test.serial()串行执行适合有共享状态的用例默认导出testCb testCbSync即默认串行。在 test/errorHandling.js 中可以看到典型用法——用例内声明callback断言完成后调用t.end()结束测试testCb(test, should retry after timeout, async t { let options { url: http://test.crawler.com/delay/1, callback: (error, response, done) { t.truthy(error); t.is(response.options.retries, 0); t.end(); }, }; t.context.crawler.add(options); t.is(options.retries, 2); });3.3 nock 模拟 HTTP 的实战写法以 test/errorHandling.js 的test.before为例它用 nock 一次性注册了各种状态码的假接口nock(http://test.crawler.com).get(/delay/1).delay(1000).reply(200, ok).persist(); nock(http://test.crawler.com).get(/status/400).reply(400, Bad Request).persist(); nock(http://test.crawler.com).get(/status/401).reply(401, Unauthorized).persist(); // ... 404 / 500 / 204 等persist()让拦截规则可被多次命中。测试timeout 后重试时只需设置timeout: 500, retryInterval: 500, retries: 2然后请求一个delay(1000)的接口断言最终收到ETIMEDOUT/ESOCKETTIMEDOUT错误即可——全程零真实网络。test/rateLimit.js 则展示了如何验证限速行为在request事件里记录时间戳断言相邻两次请求的时间差不小于或近似等于配置的 rateLimit 值testCb(test, Interval of two requests should be no less than 500ms, async t { nock(http://nockHost).get(url url.includes(status)).times(2).delay(500).reply(200, Yes); t.context.c.add({ url: http://nockHost/status/200 }); // ... t.true(t.context.tsArrs[1] - t.context.tsArrs[0] 500); done(); });这套事件时间戳 drain 收尾的写法是验证限速、优先级等时序类功能的标准姿势。四、代码风格TypeScript strict ESM ESLint 红线4.1 类型与模块体系AGENTS.md 声明TypeScript strict mode、ESM throughouttype: module。在 tsconfig.json 中可以逐项对上号strict: true—— 开启全部严格类型检查target: es2020、module: es2020—— 目标 ES2020、ESM 模块moduleResolution: bundler—— 与打包器tsup配套的模块解析策略declaration: true/declarationMap: true/sourceMap: true—— 产出.d.ts与源码映射方便消费者获得类型提示。同时 package.json 里type: module配合exports的双格式入口构成了原生 ESM、附赠 CJS 产物的完整发布方案。注意strict是全局开关项目并没有依赖noImplicitAny等单项开关——它们由strict: true统一覆盖tsconfig.json 中这些选项均处于注释状态。4.2 ESLint 规则清单与落地位置AGENTS.md 列举的红线在 eslint.config.js 的Crawler规则块中逐一落实仅作用于src/**/*.ts规则配置含义quotes[error, double, { avoidEscape: true }]强制双引号必要时可转义semi[error, always]分号必填no-consoleerror禁止console出错即报no-emptyerror禁止空代码块typescript-eslint/no-unused-varserror且argsIgnorePattern/varsIgnorePattern/caughtErrorsIgnorePattern均为^_未使用变量报错但_前缀放行typescript-eslint/no-explicit-anywarn显式any仅警告另外两个细节值得注意配置中ignores: [test/*, dist/*, **/*.config.js]——测试目录、编译产物和配置文件被整体豁免这正是 AGENTS.mdESLint 只检查src/**/*.ts的落地方式no-console是 error 级别意味着调试时不能用console.log而应使用仓库自带的日志器见 src/logger.ts底层为 tslog。这也是为什么 README 示例里的console.log只出现在用户代码中而非库实现里。五、架构约定四个必须知道的雷区5.1got必须懒加载动态 import禁止顶层静态导入AGENTS.md 明确got是在首次使用时通过动态import(got)懒加载的禁止添加顶层静态导入。这一点在 src/crawler.ts 中得到了最直观的印证let gotInstance: Got | null null; async function loadGot() { if (!gotInstance) { gotInstance (await import(got)).default; // 动态 import首次调用才加载 } return gotInstance; }模块顶层只用import type { Got } from got引用类型编译期擦除不产生运行时依赖实际请求发起时才通过loadGot()取实例。结合 package.json 的依赖声明got: ^15.0.5可以推断懒加载是为了控制启动成本——让只做队列管理、不发请求的场景不必付出加载 got 及其依赖的代价。5.2 Cheerio 是默认解析器res.$jsdom 不受支持AGENTS.md 写明cheerio 是默认 HTML 解析器暴露为res.$jsdom 不被支持。源码层面src/crawler.ts 顶层import { load } from cheerio默认选项jQuery: trueres.$即 cheerio 加载后的选择器对象支持$(title).text()这类服务端 jQuery 用法。若在某次请求中传jQuery: false则跳过解析、直接拿到原始res.body如 README.md 中下载图片、PDF 等二进制文件的示例配合encoding: null使用。README 也明确说明We are temporarily no longer supporting jsdom for certain reasons, may be later——当前版本不支持 jsdom 是既定事实迁移老代码时不要指望它。5.3 限速器是独立限速器集群且 rateLimit 全局唯一AGENTS.md 的一句话是整个限速体系的纲领Rate limiter uses a cluster of independent limiters.rateLimitis global-only — modify per-limiter viacrawler.setLimiter(), not per-request options.代码侧证据链很完整构造时创建集群this._limiters new Cluster({ maxConnections, rateLimit, priorityLevels, defaultPriority, homogeneous })src/crawler.ts每个任务通过options.rateLimiterId路由到集群中某个限速器rateLimit 强制降级 maxConnections构造函数中if (this.options.rateLimit 0) this.options.maxConnections 1;——设置非零 rateLimit 时并发被强制为 1这正是rateLimit 是两次任务间最小时间间隔这一语义的物理保证动态修改只能走setLimitersrc/crawler.ts 的setLimiter(rateLimiterId, property, value)目前只支持rateLimit属性文档注明Only supportrateLimitchangecrawler.setLimiter(0, rateLimit, 1000)即可修改默认限速器id 为 0选项白名单印证全局专属src/options.ts 的globalOnlyOptions数组包含maxConnections、rateLimit、priorityLevels、skipDuplicates、homogeneous、userAgents、silence而rateLimiterId属于可按请求指定的选项——用哪个限速器是局部的限速参数是全局的两者边界分明。test/rateLimit.js 中有两个用例直接验证了这套机制默认 500ms 间隔时相邻请求间隔不小于 500ms调用c.setLimiter(0, rateLimit, 300)后间隔变为约 300ms断言Math.abs(interval - 300) 30。这证明setLimiter的改动会实时作用于集群。实务建议与 AGENTS.md 一致限速器在实例化后尽量保持不变若确需调整只通过crawler.setLimiter()不要在单次crawler.add()里塞rateLimit。5.4 回调必须调用done()释放任务槽位的唯一通道AGENTS.md 最后一条架构约定callback 模式必须调用done()来标记任务完成并释放槽位。从 src/crawler.ts 的调度代码可以看出底层机制this._limiters.getRateLimiter(options.rateLimiterId).submit(options.priority, (done, rateLimiterId) { options.release () { done(); // 归还限速器/连接池的槽位 this.emit(_release); // 触发队列收尾检查 }; options.callback options.callback || options.release; // ... });done来自限速器options.release是它的包装既归还槽位又触发_release事件。构造函数里_release的监听器会检查this._limiters.empty为空则emit(drain)。因此可以串起一条完整链路callback 调用done()→ release → 限速器释放槽位 →_release→ 若队列清空则触发drain事件反过来若回调里忘记调用done()任务槽位永不释放队列会卡死drain永远不会触发——这正是 AGENTS.md 用must be called强调的原因。README 中几乎所有示例含错误分支if (error)都无一例外地调用done()可作为最佳实践模板。六、把约定串起来一次完整的开发-验证循环综合以上所有信息一个符合仓库规约的工作流如下改源码只改src/**/*.ts遵守 strict TS、ESM、双引号、分号、无console的风格约束静态检查npx eslint只扫src/**/*.ts测试目录豁免构建npm run buildtsup 产出dist/index.jsESM与dist/index.cjsCJS同时生成类型声明写测试新建test/xxx.js不要用.test.js后缀从../dist/index.js导入Crawler用 nock 拦截请求回调用例通过testCbt.end()收尾必要时用crawler.on(drain, t.end)等待队列清空跑测试npm test即NODE_ENVtest ava单测超时上限 20 秒验证覆盖率可选npm run coverc8 产出 lcov/html/text 报告。这条链路与 AGENTS.md 开篇的 CI 流水线npx eslint npm run build npm test完全同构照着做即可保证改动在合并前通过全部关卡。七、延伸阅读在仓库中继续深挖完整用户文档用法、Options 全表、Crawler 事件与 APIREADME.md构建脚本与 ava/c8 配置package.jsonTypeScript 编译目标与严格模式tsconfig.jsonESLint 规则src/**/*.ts专属块eslint.config.js回调式异步测试的 ava 适配层test/lib/avaTestCb.js核心入口懒加载 got、限速器集群、任务调度与 release 链路src/crawler.ts选项白名单与全局/局部选项划分src/options.ts行为验证范例超时重试与错误码断言见 test/errorHandling.js限速间隔与setLimiter验证见 test/rateLimit.js结语AGENTS.md 篇幅虽短却是 node-crawler 工程质量的浓缩写照测试对着发布产物跑、HTTP 全部离线模拟、风格红线交给 ESLint、关键架构决策用注释与源码双重固化。对任何想参与本项目或借鉴其工程实践的开发者与 AI Agent 来说先读透这五条约定再动手改代码就能避免绝大多数测试过不了、架构被破坏的返工。它本身就是一份值得所有 Node 库作者参考的Agent 友好型仓库协作规范。赞分享网页爬虫【免费下载链接】node-crawlerWeb Crawler/Spider for NodeJS server-side jQuery ;-)项目地址https://gitcode.com/gh_mirrors/no/node-crawler点击查看免费下载相关推荐ClawX 工程开发指南读懂 AGENTS.md 中的构建、测试与架构约束ClawX 工程开发指南读懂 AGENTS.md 中的构建、测试与架构约束 本文面向在 ClawX 仓库中开展开发、审查或 AI 辅助编码工作的工程师与 Ag人工智能AI 应用桌面应用交互助手Trigger.dev 仓库开发协作指南读懂 AGENTS.md 与 CLAUDE.md 分层的构建、测试与架构体系Trigger.dev 仓库开发协作指南读懂 AGENTS.md 与 CLAUDE.md 分层的构建、测试与架构体系 本指南以 Trigger.dev 开源仓AI Agent后端任务调度开发工具可观测性AI 应用Helm 源码仓库开发指南从 AGENTS.md 读懂代码结构、构建测试与贡献规范Helm 源码仓库开发指南从 AGENTS.md 读懂代码结构、构建测试与贡献规范 Helm 是使用 Go 编写的 Kubernetes 包管理器它通过 C云原生容器编排CLI运维上一篇CodeIgniter 3配置与路由系统详解下一篇深入理解百度amis项目中的表达式系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表