服务层

VextJS 采用 分层架构,将业务逻辑集中在服务层(Service Layer)。服务文件放在 src/services/ 目录下,由框架自动扫描、实例化并注入到 app.services,路由 handler 中通过 app.services.xxx 访问。

设计理念

路由层 (routes)    ← 参数提取 + 响应返回(薄层)

服务层 (services)  ← 业务逻辑(核心)

数据层 (models)    ← 数据访问(通过插件提供)
  • 路由 handler 只负责从请求中提取参数、调用 service、返回响应
  • 服务层 承载所有业务逻辑,不感知 HTTP 协议(不访问 req / res
  • 数据层 由插件提供(如数据库 ORM),通过 app 对象访问

这种分层使得:

  • 业务逻辑可以在不同路由间复用
  • 服务层可以独立进行单元测试(不依赖 HTTP)
  • 切换底层 Adapter 不影响业务代码

基本写法

服务类

每个服务文件导出一个 class,构造函数接收 app 参数:

// src/services/user.ts
import type { VextApp } from 'vextjs';

export default class UserService {
  private app: VextApp;

  constructor(app: VextApp) {
    this.app = app;
  }

  async findAll(options?: { page?: number; limit?: number }) {
    const { page = 1, limit = 20 } = options ?? {};
    // 业务逻辑...
    return {
      items: [],
      total: 0,
      page,
      limit,
    };
  }

  async findById(id: string) {
    // 业务逻辑...
    const user = { id, name: 'Alice', email: 'alice@example.com' };
    return user;
  }

  async create(data: { name: string; email: string }) {
    this.app.logger.info({ data }, 'Creating user');
    // 业务逻辑...
    return { id: crypto.randomUUID(), ...data };
  }

  async update(id: string, data: Partial<{ name: string; email: string }>) {
    this.app.logger.info({ id, data }, 'Updating user');
    return { id, ...data };
  }

  async delete(id: string) {
    this.app.logger.info({ id }, 'Deleting user');
  }
}

在路由中使用

// src/routes/users.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  app.get('/', async (_req, res) => {
    // 通过 app.services 访问已注入的服务实例
    const users = await app.services.user.findAll();
    res.json(users);
  });

  app.get('/:id', {
    validate: { param: { id: 'string!' } },
  }, async (req, res) => {
    const { id } = req.valid('param');
    const user = await app.services.user.findById(id);
    if (!user) app.throw(404, 'user.not_found');
    res.json(user);
  });

  app.post('/', {
    validate: {
      body: { name: 'string:1-50!', email: 'email!' },
    },
    middlewares: ['auth'],
  }, async (req, res) => {
    const data = req.valid('body');
    const user = await app.services.user.create(data);
    res.json(user, 201);
  });
});

文件命名与映射

service-loader 按文件路径自动将服务实例挂载到 app.services 的对应属性上。

映射规则

文件路径访问方式说明
services/user.tsapp.services.user扁平命名
services/order.tsapp.services.order扁平命名
services/user-profile.tsapp.services.userProfilekebab-case → camelCase
services/payment/stripe.tsapp.services.payment.stripe嵌套命名空间
services/payment/alipay.tsapp.services.payment.alipay嵌套命名空间
services/admin/user-manage.tsapp.services.admin.userManage嵌套 + 驼峰转换

转换规则:

  1. 文件路径相对于 services/ 目录,去除扩展名
  2. 文件名自动从 kebab-case 转换为 camelCase
  3. 子目录映射为嵌套对象

嵌套服务示例

src/services/
├── user.ts                    → app.services.user
├── order.ts                   → app.services.order
└── payment/
    ├── stripe.ts              → app.services.payment.stripe
    └── wechat-pay.ts          → app.services.payment.wechatPay
// src/services/payment/stripe.ts
import type { VextApp } from 'vextjs';

export default class StripeService {
  private app: VextApp;

  constructor(app: VextApp) {
    this.app = app;
  }

  async createPayment(amount: number, currency: string) {
    this.app.logger.info({ amount, currency }, 'Creating Stripe payment');
    // Stripe API 调用...
    return { paymentId: 'pi_xxx', status: 'pending' };
  }

  async refund(paymentId: string) {
    this.app.logger.info({ paymentId }, 'Refunding Stripe payment');
    return { refundId: 're_xxx', status: 'refunded' };
  }
}
// 在路由中使用嵌套服务
app.post('/pay', async (req, res) => {
  const result = await app.services.payment.stripe.createPayment(100, 'usd');
  res.json(result);
});

服务间调用

服务之间可以相互调用。推荐通过 this.app.services方法中按需访问(延迟访问),而非在构造函数中直接引用:

// src/services/order.ts
import type { VextApp } from 'vextjs';

export default class OrderService {
  private app: VextApp;

  constructor(app: VextApp) {
    this.app = app;
  }

  async createOrder(userId: string, items: Array<{ productId: string; quantity: number }>) {
    // 调用其他 service — 通过 this.app.services 延迟访问
    const user = await this.app.services.user.findById(userId);
    if (!user) {
      this.app.throw(404, 'user.not_found');
    }

    // 计算价格
    const total = await this.calculateTotal(items);

    // 调用支付服务
    const payment = await this.app.services.payment.stripe.createPayment(total, 'usd');

    return {
      orderId: crypto.randomUUID(),
      userId,
      items,
      total,
      paymentId: payment.paymentId,
      status: 'created',
    };
  }

  private async calculateTotal(items: Array<{ productId: string; quantity: number }>) {
    // 业务逻辑...
    return items.reduce((sum, item) => sum + item.quantity * 10, 0);
  }
}
避免循环依赖

service-loader 内置循环依赖检测。如果 ServiceAServiceB 相互依赖,框架会在启动时报错。

✅ 正确做法 — 在方法中延迟访问:

export default class OrderService {
  constructor(private app: VextApp) {}

  async createOrder() {
    // ✅ 方法调用时 user service 已经初始化完成
    const user = await this.app.services.user.findById('123');
  }
}

❌ 错误做法 — 在构造函数中直接引用:

export default class OrderService {
  private userService: UserService;

  constructor(app: VextApp) {
    // ❌ 构造函数执行时 user service 可能尚未初始化
    this.userService = app.services.user;
  }
}

使用插件提供的能力

插件通过 app.extend() 注入的能力,在服务中通过 this.app 访问:

// 假设 redis 插件已通过 app.extend('cache', redis) 注入
// src/services/user.ts
import type { VextApp } from 'vextjs';

export default class UserService {
  private app: VextApp;

  constructor(app: VextApp) {
    this.app = app;
  }

  async findById(id: string) {
    // 先查缓存
    const cached = await (this.app as any).cache.get(`user:${id}`);
    if (cached) return cached;

    // 缓存未命中,查数据库
    const user = await this.queryDatabase(id);

    // 写入缓存
    if (user) {
      await (this.app as any).cache.set(`user:${id}`, JSON.stringify(user));
    }

    return user;
  }

  private async queryDatabase(id: string) {
    // 数据库查询逻辑...
    return { id, name: 'Alice' };
  }
}
类型提示

使用 declare module 扩展 VextApp 接口可获得完整的类型提示:

// src/types/extensions.d.ts
declare module 'vextjs' {
  interface VextApp {
    cache: {
      get(key: string): Promise<string | null>;
      set(key: string, value: string, ttl?: number): Promise<void>;
    };
  }
}

扩展后 this.app.cache 即可获得 IDE 自动补全。

使用 app.throw() 抛出错误

服务层中可以通过 this.app.throw() 抛出 HTTP 错误。框架会自动捕获并转化为统一的错误响应,无需在路由层手动 try-catch:

export default class UserService {
  constructor(private app: VextApp) {}

  async findById(id: string) {
    const user = await this.queryDatabase(id);
    if (!user) {
      // 直接在 service 中抛出,框架统一处理
      this.app.throw(404, 'user.not_found');
    }
    return user;
  }

  async create(data: { name: string; email: string }) {
    const existing = await this.findByEmail(data.email);
    if (existing) {
      this.app.throw(409, '邮箱已注册', 10001);
    }
    // 创建逻辑...
    return { id: crypto.randomUUID(), ...data };
  }

  private async queryDatabase(id: string) {
    return null; // 模拟
  }

  private async findByEmail(email: string) {
    return null; // 模拟
  }
}

使用 app.logger 记录日志

服务层推荐通过 this.app.logger 记录结构化日志。日志自动携带 requestId(通过 AsyncLocalStorage 上下文传播):

export default class PaymentService {
  constructor(private app: VextApp) {}

  async processPayment(orderId: string, amount: number) {
    this.app.logger.info({ orderId, amount }, 'Processing payment');

    try {
      // 调用外部支付 API...
      const result = { transactionId: 'txn_xxx' };
      this.app.logger.info({ orderId, transactionId: result.transactionId }, 'Payment successful');
      return result;
    } catch (err) {
      this.app.logger.error({ orderId, err }, 'Payment failed');
      this.app.throw(500, 'payment.failed');
    }
  }
}

加载顺序与生命周期

加载时机

bootstrap 启动流程中,service-loader 在以下阶段执行:

1. config    → 加载配置
2. locales   → 加载语言包
3. plugins   → 执行插件 setup()
4. middlewares → 扫描中间件
5. services  → ⭐ 实例化服务(此处)
6. routes    → 注册路由(handler 中可安全访问 app.services)

这意味着:

  • ✅ 服务构造函数中可以访问 app.config(已加载)
  • ✅ 服务构造函数中可以访问 app.logger(已初始化)
  • ✅ 服务构造函数中可以访问插件注入的能力(插件已 setup)
  • ⚠️ 服务构造函数中访问 app.services 需注意顺序(见循环依赖章节)
  • ✅ 路由 handler 中可以安全访问所有 app.services(已全部注入完成)

实例化过程

  1. 扫描 — 递归扫描 src/services/ 目录下所有 .ts / .js 文件
  2. 排序 — 按文件路径字母序排序(确保加载顺序确定性)
  3. 实例化 — 逐个 new ServiceClass(app) 创建实例
  4. 挂载 — 将实例挂载到 app.services 的对应属性
  5. 检测 — 执行循环依赖检测(可选,默认开启)

排除规则

以下文件会被自动跳过:

  • 测试文件:*.test.ts*.spec.ts
  • _. 开头的文件/目录
  • node_modules 目录

可以利用 _ 前缀创建服务共享的工具模块:

src/services/
├── _base.ts          # 基类,不会被当作服务加载
├── _types.ts         # 共享类型
├── user.ts
└── order.ts

服务层最佳实践

1. 保持服务层的 HTTP 无关性

服务层不应直接操作 req / res 对象。如果需要请求上下文信息(如当前用户),作为参数传入:

// ✅ 正确 — 参数传入
async createOrder(userId: string, items: OrderItem[]) {
  // ...
}

// ❌ 错误 — 直接操作请求对象
async createOrder(req: VextRequest, res: VextResponse) {
  // service 不应感知 HTTP
}

2. 单一职责

每个服务对应一个业务领域。避免将不同领域的逻辑放在同一个服务中:

services/
├── user.ts           # 用户管理
├── order.ts          # 订单管理
├── notification.ts   # 通知服务
└── payment/
    ├── stripe.ts     # Stripe 支付
    └── wechat-pay.ts # 微信支付

3. 使用基类共享通用逻辑

对于有共同行为的服务,可以创建基类(以 _ 前缀防止被当作服务加载):

// src/services/_base.ts(不会被自动加载)
import type { VextApp } from 'vextjs';

export abstract class BaseService {
  protected app: VextApp;

  constructor(app: VextApp) {
    this.app = app;
  }

  protected async paginate<T>(
    queryFn: (offset: number, limit: number) => Promise<T[]>,
    countFn: () => Promise<number>,
    page: number,
    limit: number,
  ) {
    const offset = (page - 1) * limit;
    const [items, total] = await Promise.all([
      queryFn(offset, limit),
      countFn(),
    ]);
    return { items, total, page, limit, pages: Math.ceil(total / limit) };
  }
}
// src/services/user.ts
import { BaseService } from './_base.js';

export default class UserService extends BaseService {
  async findAll(page = 1, limit = 20) {
    return this.paginate(
      (offset, limit) => this.queryUsers(offset, limit),
      () => this.countUsers(),
      page,
      limit,
    );
  }

  private async queryUsers(offset: number, limit: number) {
    return []; // 数据库查询
  }

  private async countUsers() {
    return 0; // 计数查询
  }
}

4. TypeScript 类型声明

app.services 添加类型声明,获得完整的 IDE 支持:

// src/types/services.d.ts
import type UserService from '../services/user.js';
import type OrderService from '../services/order.js';

declare module 'vextjs' {
  interface VextServices {
    user: UserService;
    order: OrderService;
    payment: {
      stripe: import('../services/payment/stripe.js').default;
    };
  }
}

添加后,app.services.user.findById() 等调用将获得完整的方法签名提示和类型检查。

下一步

  • 了解 中间件 如何拦截和处理请求
  • 学习 插件 如何扩展框架能力
  • 查看 测试 如何对服务层进行单元测试