Unleash 代码库架构指南:特性开关平台的 CSR 分层、组合根模式与 AI 协作开发规范
【免费下载链接】unleashOpen-source feature management platform项目地址: https://gitcode.com/GitHub_Trending/un/unleash
本文面向希望理解并参与 Unleash 开源特性开关(Feature Flag)平台开发的工程师与 AI 编码助手,系统梳理当前仓库的整体架构、分层约定、关键设计模式与工程规范。读完本文,你将掌握后端 Controller/Service/Store 分层与前端 React 数据流组织方式,理解 Enterprise 如何通过preRouterHook钩子无 Fork 扩展 OSS 功能,并能遵循组合根(Composition Root)、读模型/写模型分离、迁移与测试等约定,快速定位代码、提交高质量的变更。
项目概览:前后端同仓的特性开关平台
Unleash 是一个开源特性开关管理平台,本仓库(OSS 版本)是一个 monorepo,包含两大组成部分(见 AGENTS.md):
- 后端:基于 Node.js/TypeScript 的 REST API,位于 src 目录;
- 前端:基于 React/TypeScript 的单页应用(SPA),位于 frontend 目录。
从 README.md 可以看出,Unleash 的产品形态围绕“特性开关”展开:开发者把新功能用开关包裹起来,在生产环境以小批量、可控的方式逐步放量;后端 SDK 通过unleash.isEnabled("AwesomeFeature")之类的调用在客户端本地评估开关状态,而管理端(Admin UI)通过 Admin API 完成开关的创建、激活与策略配置。
需要特别强调的是 Enterprise 与 OSS 的关系:Enterprise 并不 Fork OSS,而是通过独立的unleash-enterprise仓库,基于钩子架构向 OSS 注入额外功能。因此,当用户提到某个功能属于 Enterprise 特性时,需要先确认 Enterprise 仓库的位置,并同时在这两个仓库中开展工作。这是理解 Unleash 工程体系的第一条原则。
上图展示了 Unleash 的系统级架构:应用端通过 Backend SDK 与 Frontend SDK 本地评估特性开关,Unleash 服务端通过 Client API(供 SDK 拉取开关配置)与 Admin API(供管理界面配置)对外提供服务。后文讨论的后端分层、路由与数据访问,正是实现这套 API 的代码级骨架。
后端架构:CSR(Controller-Service-Repository)分层
后端整体遵循CSR(Controller、Service、Repository/Store)模式。虽然新代码提倡按功能领域(Feature)而不是按层打包模块(详见下文),但经典的按层组织在传统组件中依然保留:
| 分层 | 目录 | 职责 |
|---|---|---|
| Controller | src/lib/routes | 处理 HTTP 请求、校验输入、把业务逻辑委托给 Service、划定事务边界 |
| Service | src/lib/services | 业务逻辑层,负责发出事件、管理事务 |
| Store | src/lib/db | 数据访问层,使用 Knex 查询构建器操作 PostgreSQL |
从入口的调用链可以验证这一分层确实被严格执行。以 src/lib/server-impl.ts 中的createApp(第 279-402 行)为例,启动流程依次为:创建数据库连接createDb→ 实例化全部 Store(createStores)→ 实例化全部 Service(createServices)→ 组装 Express 应用(getApp)。Controller 永远通过 Service 访问数据,绝不会直接触碰 Store。
按功能领域打包的 Feature 模块
与按层组织的传统组件不同,Feature-based modules位于 src/lib/features,每个领域内部自带自己的 controller、service、store 与类型定义,例如feature-toggle、project、segment、change-request、release-plans等。
以最核心的feature-toggle模块为例,源码结构完整呈现了 CSR 的落地方式:
- feature-toggle-controller.ts:定义 HTTP 路由与 OpenAPI 请求/响应 Schema,调用 Service 完成创建、更新、删除开关等操作;
- feature-toggle-service.ts:承载业务规则,例如校验开关命名、处理变更事件、协调跨模块协作;
- feature-toggle-store.ts:封装开关的读写,从源码可以看到
getAll(第 242 行)、create(第 469 行)、update(第 493 行)等基础 CRUD 方法。
关键设计模式
AGENTS.md 明确列出了四条贯穿后端的关键模式,它们也都能在源码中得到印证:
- 审计日志(Audit-log):Service 层发出类型化事件(如
FeatureCreatedEvent),既用于审计追踪,也用于驱动读模型更新。事件在业务逻辑中被集中发出,保证"谁在什么时候改了什么"可追溯。 - 事务包装(Transaction wrapper):使用
withTransactional()实现跨 Service 的原子操作,常见约定是在 Controller 层发起事务。该工具函数在 src/lib/services/index.ts 中被大量使用,例如withTransactional((db) => createAccessService(db, config), db)(见createServices,第 198 行)。 - Fake 实现:每个 Store/Service 都配有 Fake 变体用于测试,优先使用 Fake 而不是 Mock,从而保证单元测试不依赖真实数据库。
- 内部特性开关:
flagResolver.isEnabled()用于控制产品自身的运维级功能开关,是 Unleash“用特性开关管理自身”的体现。
技术栈:Express + PostgreSQL(Knex)+ TypeScript(ES Modules)。
前端架构:React SPA 的数据流与路由组织
前端是一个通过 REST API 与后端通信的 React SPA,代码集中在 frontend/src:
- 组件:frontend/src/component 按功能领域组织的 React 组件;
- Hooks:frontend/src/hooks 包含 71+ 个自定义 Hook,负责数据获取与变更;
- Contexts:
AccessContext(权限)、UIContext(Toast 提示、主题)。
前端同样有一组约定俗成的关键模式:
- 数据获取:基于 SWR 的
useApiGetter系列 Hook 处理 GET 请求并带缓存,避免重复请求; - 数据变更:
useApiHook 封装 POST/PUT/DELETE 并统一处理错误; - 路由门控:路由支持
flag、enterprise、configFlag属性,实现按内部开关、企业版能力或配置动态控制页面可见性; - 样式:基于 emotion 的 MUI
styled()组件,一次性样式使用sx。
技术栈:React 18+、Vite、Material-UI(MUI)、SWR 管理服务端状态。
Enterprise 集成:通过 Hook 扩展而非 Fork
Enterprise 与 OSS 的集成方式是理解整个平台扩展性的关键,其机制可以概括为四条(详见 AGENTS.md):
- 入口:
unleash-enterprise/src/index.ts包装 OSS 的start()/create(); - 钩子:
preRouterHook在 OSS 初始化完成之后、路由绑定之前执行; - 扩展:新增 50+ Service、30+ Store、50+ Controller;
- 门控:通过 License 中间件限制 Enterprise 特性。
在 OSS 源码中可以直接看到钩子的调用位置。src/lib/app.ts 在完成鉴权、RBAC、维护模式等中间件装配后、注册IndexRouter之前,执行了:
if (typeof config.preRouterHook === 'function') { config.preRouterHook(app, config, services, stores, db); }(见 src/lib/app.ts 第 203-205 行)。而钩子的注入点位于 src/lib/create-config.ts(第 870 行附近,preRouterHook: options.preRouterHook),它作为IUnleashConfig的一部分贯穿整个启动流程。在 src/lib/app.test.ts 中也能找到 "should call preRouterHook" 的测试用例,验证该钩子在应用装配过程中的行为。
Enterprise 专属特性包括:Change Requests(变更请求)、SSO(SAML/OIDC)、Service Accounts、Signals & Actions、Insights、SCIM、Private Projects、Release Plans、Safeguards 等。
接口合并:通过IEnterpriseServices extends IUnleashServices、IUnleashEnterpriseStores extends IUnleashStores实现类型层面的无缝扩展,让 Enterprise 代码可以像使用 OSS 类型一样使用合并后的依赖。
组合根模式(Composition Root)
Unleash 遵循组合根模式:所有依赖在应用启动时一次性装配完成,而不是散落在代码库各处。每个 Service 都配有专门的组合根函数来负责自身及依赖的创建。
OSS 组合根:
- src/lib/db/index.ts →
createStores()(第 75 行)使用 Knex 连接实例化全部 Store; - src/lib/services/index.ts →
createServices()(第 198 行)使用 Store + 配置实例化全部 Service; - src/lib/server-impl.ts → 编排整体顺序:DB → Stores → Services → App。
Enterprise 组合根:
enterprise/src/util/setup-stores.ts:创建 Enterprise Store 并与 OSS Store 合并;enterprise/src/util/setup-services.ts:使用合并后的 Store 创建 Enterprise Service;enterprise/src/create-enterprise-routes.ts:在preRouterHook中把所有东西接线完成。
这条规则为什么重要:永远不要在代码行内new一个 Service 或 Store,而应通过构造函数注入接收依赖。这样做一方面让依赖图显式化、可测试(可以用 Fake 注入),另一方面避免隐式耦合。从createServices的实现可以看到,大量依赖(如AccessService、ApiTokenService、LastSeenService)都是先构造局部变量再以构造参数形式传递,这正是组合根模式的直接体现。
读模型 vs 写模型:读写分离的实践
为了避免 Store 被复杂查询压垮,Unleash 将读写关注点分离:
写模型(Stores)负责单个实体的 CRUD 操作:
- 保持查询简单:insert、update、delete、getById;
- 位于 src/lib/db 或各 feature 目录;
- 典型例子:
FeatureToggleStore只负责开关的基础增删改查。
读模型(Read Models)负责复杂查询、聚合、跨领域查询与反规范化视图:
- 针对特定读取场景优化(看板、列表、报表);
- 位于各 feature 目录下的
read-models/子目录; - 典型例子:
FeatureStrategiesReadModel、ProjectOwnersReadModel、FeatureSearchReadModel。在 src/lib/features/feature-toggle 中可以看到 features-read-model.ts 与 feature-strategies-read-model.ts 等实现。
何时应该使用读模型:
- 查询跨多张表、需要复杂 JOIN;
- 需要反规范化数据以提升性能;
- 正在构建看板/概览类端点;
- 查询无法对应到单个实体的生命周期;
- 不想暴露整个写模型,只需要其他模块的某个值。
约定模式:Service 在 Store(写)与读模型(读)之间协调;Controller 只能调用 Service 或读模型,绝不能直接调用 Store。这条边界保证了分层清晰,也让读模型可以独立优化而不影响写路径。
开发哲学与编码规范
AGENTS.md 强调三条核心原则:
- 始终测试代码:优先自动化测试而非手工测试;
- 编写可维护的代码:代码即沟通,清晰与可读性至关重要;
- 提交前三思。
详细的编码标准以架构决策记录(ADR)的形式沉淀在 contributing/ADRs 目录下,分为三组:
- 后端:contributing/ADRs/back-end(如 REST API 规范、SQL 标准、正确类型依赖等);
- 前端:contributing/ADRs/front-end(如组件命名、数据获取方式、表单架构、样式方案等);
- 全局:contributing/ADRs/overarching(如领域语言、日志规范、请求/响应 Schema 分离等)。
此外还有一条直接可执行的编码约定:优先使用Boolean(someVariable)而不是!!someVariable。
数据库迁移规范
- 迁移文件统一存放在 src/migrations,从仓库内容可以看到从 2014 年初始 Schema 到近期功能(如 Safeguards、API Tokens v2、Edge 可观测性)的数百个迁移文件;
- 绝不修改已经合并的迁移,需要变更时创建新的迁移;
- 每个迁移必须包含
up和down两个方法; - 使用
pnpm db-migrate create <name>创建新迁移。
测试策略
Unleash 采用分层测试策略,工具与范围如下:
| 层级 | 工具 | 说明 |
|---|---|---|
| 后端 | Vitest + Supertest | API 测试,使用 Fake Store 实现隔离 |
| 前端 | Vitest + Testing Library | 组件与 Hook 测试 |
| E2E | Cypress(frontend/cypress) | 端到端流程验证 |
运行测试的命令:
pnpm test # 全部测试 pnpm test:frontend # 仅前端 pnpm test:backend # 仅后端Fake 实现是后端测试的基石:每个 Store/Service 都有对应 Fake(例如src/test/fixtures/下的各类 fake store),测试优先注入 Fake 而非使用 Mock 库,保证测试环境与真实行为高度一致。
关键文件速查表
OSS 入口与装配
| 文件 | 用途 |
|---|---|
| src/server.ts | 主入口 |
| src/lib/app.ts | Express 应用装配、中间件栈 |
| src/lib/routes/index.ts | 路由注册 |
| src/lib/services/index.ts | Service 工厂(createServices) |
| src/lib/db/index.ts | Store 工厂(createStores) |
模式参考(Pattern References)
| 模式 | 示例位置 |
|---|---|
| Controller | src/lib/features/feature-toggle/feature-toggle-controller.ts |
| Service | src/lib/features/feature-toggle/feature-toggle-service.ts |
| Store(写模型) | src/lib/features/feature-toggle/feature-toggle-store.ts |
| 读模型 | src/lib/features/feature-toggle/features-read-model.ts 等 |
| 组合根 | src/lib/services/index.ts |
| API Hook(GET) | frontend/src/hooks/api/getters/useFeature/useFeature.ts |
| API Hook(变更) | frontend/src/hooks/api/actions/useFeatureApi.ts |
| Fake Store | src/test/fixtures/fake-feature-toggle-store.ts |
结语:给 AI 编码助手的行动清单
对于希望在本仓库中高效工作的 AI 编码助手或开发者,可以把 AGENTS.md 的规范浓缩为以下操作清单:
- 先定位领域再写代码:新功能优先放进 src/lib/features 下对应领域的 controller/service/store,而不是散落到按层组织的旧目录;
- 遵守分层边界:Controller 调 Service,Service 协调 Store(写)与读模型(读),永不直接使用 Store;
- 依赖走构造注入:不
new服务,通过组合根函数装配,测试时注入 Fake; - Enterprise 功能跨仓库协作:涉及 Enterprise 特性时,必须同时打开
unleash-enterprise仓库,通过preRouterHook扩展而非修改 OSS; - 数据库变更新建迁移:不改旧迁移,用
pnpm db-migrate create生成新迁移并实现up/down; - 提交前跑测试:用
pnpm test验证全量测试,遵循 ADR 中记录的编码规范。
【免费下载链接】unleashOpen-source feature management platform项目地址: https://gitcode.com/GitHub_Trending/un/unleash
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考