尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Cypress 的集中式数据访问层:深入解析 @packages/data-context 的 Data / Sources / Actions 架构

Cypress 的集中式数据访问层:深入解析 @packages/data-context 的 Data / Sources / Actions 架构 Cypress 的集中式数据访问层深入解析 packages/data-context 的 Data / Sources / Actions 架构【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypresspackages/data-context是 Cypress 桌面应用launchpad与app两个前端背后唯一的集中式数据访问层Centralized data access for the Cypress application。它把整个应用的状态存储、派生查询与变更操作统一收敛到Data、Sources、Actions三个清晰的概念之下并通过 DataContext 与 GraphQL 面向 UI 提供服务。读完本文你将掌握该包的三层架构模型、状态写入为何必须经过 Action、如何新增一种核心数据并把它一步步暴露到 GraphQL 查询中以及该包为何用 jest 而非 vitest 做测试。1. 包定位它解决什么问题在 Cypress 桌面应用里从选择项目到运行测试再到浏览器管理、登录鉴权、设置读写有大量跨模块共享的状态与操作。如果每个 UI 层各自维护状态、直接操作文件或进程很容易产生数据不一致与难以追踪的副作用。packages/data-context正是为此而生它定义了一套统一的读数据、写数据的边界规则让应用前端只依赖一个DataContext入口。查看其 package.json该包的描述只有一句话Centralized data access for the Cypress application从包结构上可以看得很清楚它同时托管了状态定义src/data、只读派生数据src/sources、变更操作src/actions、GraphQL 层graphql/以及运行时代码生成产物src/gen是连接 Cypress 内部各能力项目生命周期、浏览器探测、本地设置、云服务、登录态等与前端 UI 的枢纽。2. 目录结构与核心概念packages/data-context/src下按职责划分为若干目录。对照源码树可以确认除了actions、codegen、data、gen、sources、util这些目录外DataContext.ts与DataActions.ts位于src根层src/actions —— 所有变更mutation操作类例如ProjectActions、AuthActions、BrowserActions、LocalSettingsActions、FileActions等以及聚合入口 DataActions.tssrc/codegen —— 与脚手架、代码生成相关的逻辑如组件/E2E 空模板生成src/data —— 顶层核心数据coreData的类型定义与初始值工厂src/gen —— 生成的 GraphQL/nexus 相关类型如nxs.gensrc/sources —— 只读、派生的数据源按领域命名空间组织src/util —— 各种工具函数DataContext.ts —— 整个包的中枢聚合了 Sources、Actions、coreData并向上层暴露DataActions.ts —— 在构造阶段一次性实例化全部 Action 类并提供 getter。其中开发者真正需要掌握的是data、sources 与 actions三个部分。它们的协作关系可以用一句话概括Data 存状态Sources 读与推导状态Actions 负责改状态。app测试结果界面与launchpad项目启动器都通过DataContext访问这套能力。2.1 Data被 UI 消费的顶层状态Data对应接口CoreDataShape见 coreDataShape.ts。它是暴露给 launchpad 与 app 的顶层数据快照字段按领域分块组织如cliBrowser/activeBrowser/allBrowsers浏览器、servers各类 HTTP/Socket 服务器句柄、currentProject当前项目根路径、currentTestingType、wizard组件测试向导状态、user/authState登录、electronElectron 句柄、localSettings编辑器与偏好设置、versionData、cloudProject、diagnostics错误与警告等。从源码注释可以看出它的设计意图All state for the app should live here for now见 coreDataShape.ts——应用级状态统一收拢于此真实值由makeCoreData工厂函数创建见 coreDataShape.ts它接收PartialAllModeOptions作为可选初始参数例如 CLI 传入的browser、testingType、projectRoot、global等会被映射进初始值并为其余字段填充默认值。需要记住的规则凡是临时状态、内部标志、不对外暴露的次级数据一般放进 Source 而非 Data对 Data 的任何修改必须通过 Action见下文。2.2 Sources只读与派生的数据源Sources目录中存放的是可以被理解为只读和派生的数据。每一个 Source 按它关联的数据类型做命名空间划分例如ProjectDataSource—— 项目相关spec 解析、配置文件监听等BrowserDataSource—— 浏览器探测与状态WizardDataSource—— 脚手架/向导相关LocalSettings、Versions、Cloud、Git、Error、Html、Env、File等领域均有对应 Source。从真实源码可以看到ProjectDataSource见 ProjectDataSource.ts会引用packages/config的defaultSpecPattern、micromatch等进行 spec 匹配与目录推导并向上层返回SpecWithRelativeRoot[]这类只读结构。Sources 的共同特点是通过构造器拿到ctx类型为DataContext内部使用this.ctx读取coreData、调用其它 Source或者访问this.ctx.actions触发的派生流程但不直接承担状态写入职责。需要记住的规则若想修改某个 Source 里的内容或coreData同样应当经由 Action 完成。2.3 Actions变更与破坏性操作的家Actions是各类可变/破坏性操作的归宿——文件写入、进程与服务器启停、设置更新、登出等都被归到这里。这样做的核心目的是让变更可预测、可追踪。仓库现有 Action 类见 src/actions 与 DataActions.ts覆盖了ProjectActions、AppActions、AuthActions、BrowserActions、ElectronActions、FileActions、LocalSettingsActions、DevActions、ServersActions、VersionsActions、WizardActions、CodegenActions、CohortsActions、EventCollectorActions、NotificationActions、CloudProjectActions、CurrentRecordingActions、DataEmitterActions、ErrorActions等。与 Source 相同每个 Action 类以constructor (private ctx: DataContext) {}方式接收上下文不同之处在于Action 内部可以通过this.ctx.update拿到当前coreData并加以修改// 约定更新 this.ctx.coreData 应经由 Action // 并通过 this.ctx.update 完成update 的第一个参数即为当前的 coreData this.ctx.update((coreData) { coreData.specs specs })ctx.update的真实定义位于 DataContext.ts其 updater 类型为(proj: CoreDataShape) void | undefined | CoreDataShape注释还说明未来将迁移到 Immer 做不可变更新。DataContext.onError/onWarning等内部方法也正是通过this.update把诊断信息写入coreData.diagnostics见 DataContext.ts随后通过 emitter 通知 UI是这套模式的典型用例。而所有 Action 类都由 DataActions.ts 在构造时一次性实例化并暴露成 getter如ctx.actions.project、ctx.actions.authUI 与其它模块只需访问this.ctx.actions.xxx即可安全地发起变更。3. 端到端示例从定义 Data 到 GraphQL 暴露下面沿用官方指南中的教学场景把四个环节完整走一遍加载某个项目的 specs 并持久化再用 Source 过滤出文件名含foo的 spec最终通过 GraphQL 提供给前端。这也是理解 Data、Sources、Actions 三者如何串联的最佳路径。3.1 第一步在 coreData 中定义 Data在 data/coreDataShape.ts 中定义类型CoreDataShape并用makeCoreData提供初始值。这一步决定了真实值被保存在哪里export interface CoreDataShape { specs: string[] } // ... export function makeCoreData (modeOptions: PartialAllModeOptions {}): CoreDataShape { return { // ... specs: [], } }真实代码库中的makeCoreDatacoreDataShape.ts正是采用同一模式为几十个字段逐一给出默认值部分字段直接消费modeOptionsCLI 选项异步字段如machineId则返回 Promise。3.2 第二步用 Action 更新 specscoreData.specs不能由 Source 或 UI 直接改需要定义 Action。在actions目录下新增SpecActions类示意真实项目中可参考同目录既有 Action 类的写法并用this.ctx.update写入import type { DataContext } from .. import globby from globby export class SpecActions { constructor (private ctx: DataContext) {} async findSpecs () { const specs await globby(./**/*.spec.js) this.ctx.update(coreData { coreData.specs specs }) } }注意如果你新增了一个 Action 文件还需要把它挂到聚合类 DataActions.ts 上这种操作并不常见因为多数场景可以复用既有 Action即在该类构造器中实例化并通过 getter 暴露例如import type { DataContext } from . import { // ... SpecActions } from ./actions export class DataActions { constructor (private ctx: DataContext) {} // ... get specs () { return new SpecActions(this.ctx) } }结合当前源码可以确认DataActions的构造函数会依次new出全部 Action 子类并保存到私有字段getter 返回这些实例见 DataActions.ts。README 中建议用cached之类的机制缓存每次 getter 的实例避免重复实例化这与DataActions在构造期统一实例化的思路目标一致——保证动作执行者与coreData之间只有一套稳定入口。3.3 第三步用 Source 派生数据场景需求是只暴露文件名含foo的 specs。派生逻辑应放在 Source 中。新建SpecDataSource示意或者直接扩展语义合适的既有 Source如ProjectDataSourceimport type { DataContext } from .. export class SpecDataSource { constructor (private ctx: DataContext) {} fooSpecs () { return this.ctx.coreData.specs.filter(spec spec.includes(foo)) } }提示源码中spec.includes这类判断放在.find/.filter上更合理示例意在展示派生读的来源是this.ctx.coreData而this.ctx同时还可访问其它 Source如this.ctx.project、this.ctx.browser与工具方法。新增的 Source 需要注册到 DataContext.ts在构造器中实例化并暴露 getter。真实代码库就是这样做的——DataContext的构造函数会依次创建GraphQLDataSource、RemoteRequestDataSource、FileDataSource、VersionsDataSource、BrowserDataSource、DataActions、WizardDataSource、ProjectDataSource等最后才初始化依赖上述一切的ProjectLifecycleManager见 DataContext.ts。示意如下import { SpecDataSource } from ./sources/SpecDataSource export class DataContext { // ... cached get specs () { return new SpecDataSource(this) } }3.4 第四步加分项通过 GraphQL 暴露 Data / Source如果你希望 launchpad 或 app 前端真正看得见这些数据最直接的方式是通过 GraphQL 暴露它们——GraphQL resolver 的第三个参数就是ctx即DataContext所以 resolver 里可以直接读写coreData或调用 Source 方法。例如在 gql-Query.ts 中新增两个查询字段export const Query objectType({ definition (t) { // ... t.list.string(specs, { description: A list of specs, resolve: (source, args, ctx) { return ctx.coreData.specs }, }) t.list.string(fooSpecs, { description: A list of specs containing foo, resolve: (source, args, ctx) { return ctx.specs.fooSpecs() }, }) } })这样一来读coreDataData→ 由 Source 派生Sources→ 由 Action 变更Actions的完整链路就被打通了UI 发起 GraphQL mutation/query → resolver 通过ctx.actions写状态或通过ctx.xxxSource读派生结果 → 状态变更通过订阅/推送机制回到 UI。4. DataContext一切访问的枢纽与生命周期DataContextDataContext.ts是包内单例式的上帝入口它通过 getter 将各个领域暴露出来包括coreData、actions、file、versions、browser、wizard、project、cloud、env、emitter、html、error、util等并声明了一个重要约定见源码注释 DataContext.ts所有 mutation更新/删除/创建与文件系统写入都应经由actions命名空间其余一切应当是 getter。同时DataContext还负责应用生命周期管理initializeModeDataContext.ts根据config.mode是run还是open调用ProjectLifecycleManager分别初始化运行模式或开放模式reinitializeCypress/_resetDataContext.ts重建coreData、销毁旧的生命周期管理器并发出reset:data-context事件供测试场景或模式切换复用destroyDataContext.ts先关闭 GraphQL/graphql-ws 服务器再拆除生命周期避免销毁过程中对 socket 的写入竞态。此外 src/index.ts 提供了setCtx/getCtx/clearCtx等模块级函数方便旧式 server 代码在不深挖类继承链的情况下访问当前唯一的DataContext注释明确说明这在 Electron 应用生命周期内假设只有一个实例。5. 测试策略为什么用 jest --runInBand该包对自身的测试约定也很特殊见 packages/data-context/README.md 的 Testing 一节与 jest.config.ts不使用 vitest而使用 jest。原因在于本包中的graphql被加载进 vitest 的 service worker 时存在问题很可能是因为存在多个 GraphQL server 实例而 jest 运行在共享进程中因此这里选择 jest依赖中可见jest^30、ts-jest、types/jest等。必须串行运行--runInBand。ProjectLifecycleManager见 data/ProjectLifecycleManager.ts会改变进程的当前工作目录cwd因此测试无法并行执行。package.json 中的测试脚本即体现了这一约束test-unit: yarn node --experimental-vm-modules node_modules/.bin/jest --runInBand, test-debug: node --experimental-vm-modules --inspect-brk node_modules/.bin/jest --runInBand --testTimeout600000jest.config.ts使用 ts-jest 的默认 presettestMatch为[rootDir/test/**/*.spec.ts]测试环境为 node。测试用例分布在 test/unit再按actions、sources、data、graphql、util、polling等领域划分以及test/graphql中。6. 给开发者的快速定位地图如果你要在本仓库中实践上述模式下面这些文件是最高频的参照点关注点推荐阅读路径核心状态类型与默认值src/data/coreDataShape.tsDataContext 入口与生命周期src/DataContext.tsAction 聚合与注册方式src/DataActions.ts各类 Action 实现src/actions各类 Source 实现src/sourcesGraphQL 对象类型与 schemagraphql/schemaTypes测试脚本与配置package.json、jest.config.ts实践要点回顾想增加一块对外可见的状态先在CoreDataShape加字段、在makeCoreData给默认值想修改这块状态新增或复用 Action在DataActions中注册并通过this.ctx.update写回coreData想做派生/只读查询在合适的 Source 中新增方法通过this.ctx.coreData读取并在DataContext上暴露 getter想让 UI 可见在 GraphQL 对象类型如Query中新增字段resolver 直接使用第三参数ctx别忘了测试新逻辑落在 test/unit 对应目录且运行单元测试时保持--runInBand避免ProjectLifecycleManager修改 cwd 带来的并发干扰。【免费下载链接】cypressFast, easy and reliable testing for anything that runs in a browser.项目地址: https://gitcode.com/GitHub_Trending/cy/cypress创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表