服务层
VextJS 采用 分层架构,将业务逻辑集中在服务层(Service Layer)。服务文件放在 src/services/ 目录下,由框架自动扫描、实例化并注入到 app.services,路由 handler 中通过 app.services.xxx 访问。
设计理念
- 路由 handler 只负责从请求中提取参数、调用 service、返回响应
- 服务层 承载所有业务逻辑,不感知 HTTP 协议(不访问
req/res) - 数据层 由插件提供(如数据库 ORM),通过
app对象访问
这种分层使得:
- 业务逻辑可以在不同路由间复用
- 服务层可以独立进行单元测试(不依赖 HTTP)
- 切换底层 Adapter 不影响业务代码
基本写法
服务类
每个服务文件导出一个 class,构造函数接收 app 参数:
在路由中使用
文件命名与映射
service-loader 按文件路径自动将服务实例挂载到 app.services 的对应属性上。
映射规则
转换规则:
- 文件路径相对于
services/目录,去除扩展名 - 文件名自动从
kebab-case转换为camelCase - 子目录映射为嵌套对象
嵌套服务示例
服务间调用
服务之间可以相互调用。推荐通过 this.app.services 在方法中按需访问(延迟访问),而非在构造函数中直接引用:
service-loader 内置循环依赖检测。如果 ServiceA 和 ServiceB 相互依赖,框架会在启动时报错。
✅ 正确做法 — 在方法中延迟访问:
❌ 错误做法 — 在构造函数中直接引用:
使用插件提供的能力
插件通过 app.extend() 注入的能力,在服务中通过 this.app 访问:
使用 declare module 扩展 VextApp 接口可获得完整的类型提示:
扩展后 this.app.cache 即可获得 IDE 自动补全。
使用 app.throw() 抛出错误
服务层中可以通过 this.app.throw() 抛出 HTTP 错误。框架会自动捕获并转化为统一的错误响应,无需在路由层手动 try-catch:
使用 app.logger 记录日志
服务层推荐通过 this.app.logger 记录结构化日志。日志自动携带 requestId(通过 AsyncLocalStorage 上下文传播):
加载顺序与生命周期
加载时机
在 bootstrap 启动流程中,service-loader 在以下阶段执行:
这意味着:
- ✅ 服务构造函数中可以访问
app.config(已加载) - ✅ 服务构造函数中可以访问
app.logger(已初始化) - ✅ 服务构造函数中可以访问插件注入的能力(插件已 setup)
- ⚠️ 服务构造函数中访问
app.services需注意顺序(见循环依赖章节) - ✅ 路由 handler 中可以安全访问所有
app.services(已全部注入完成)
实例化过程
- 扫描 — 递归扫描
src/services/目录下所有.ts/.js文件 - 排序 — 按文件路径字母序排序(确保加载顺序确定性)
- 实例化 — 逐个
new ServiceClass(app)创建实例 - 挂载 — 将实例挂载到
app.services的对应属性 - 检测 — 执行循环依赖检测(可选,默认开启)
排除规则
以下文件会被自动跳过:
- 测试文件:
*.test.ts、*.spec.ts - 以
_或.开头的文件/目录 node_modules目录
可以利用 _ 前缀创建服务共享的工具模块:
服务层最佳实践
1. 保持服务层的 HTTP 无关性
服务层不应直接操作 req / res 对象。如果需要请求上下文信息(如当前用户),作为参数传入:
2. 单一职责
每个服务对应一个业务领域。避免将不同领域的逻辑放在同一个服务中:
3. 使用基类共享通用逻辑
对于有共同行为的服务,可以创建基类(以 _ 前缀防止被当作服务加载):
4. TypeScript 类型声明
为 app.services 添加类型声明,获得完整的 IDE 支持:
添加后,app.services.user.findById() 等调用将获得完整的方法签名提示和类型检查。