Vue 3 CQRS模式应用深度指南

概述

CQRS(Command Query Responsibility Segregation,命令查询责任分离)是一种架构模式,它将数据的写操作(命令)和读操作(查询)分离,使用不同的模型和处理管道。本集将深入探讨CQRS模式的核心概念、设计原则,以及如何在 Vue 3 项目中实现这一模式,包括命令处理、查询处理、事件驱动的更新机制以及与状态管理的结合。

一、CQRS核心概念

1. 什么是CQRS

CQRS 是由 Greg Young 提出的一种架构模式,它的核心思想是将系统的操作分为两类:

  • 命令(Command):用于修改数据的操作,不返回结果(或只返回操作状态)
  • 查询(Query):用于读取数据的操作,不修改数据,返回查询结果

2. 核心组成部分

  • 命令(Command):表示要执行的操作,包含执行操作所需的数据
  • 命令处理器(Command Handler):处理命令,执行相应的业务逻辑,修改数据
  • 查询(Query):表示要执行的查询,包含查询条件
  • 查询处理器(Query Handler):处理查询,从数据存储中获取数据并返回结果
  • 事件(Event):表示系统中发生的重要事件,用于通知其他组件
  • 事件处理器(Event Handler):处理事件,执行相应的业务逻辑
  • 命令模型(Command Model):用于处理命令的数据模型
  • 查询模型(Query Model):用于处理查询的数据模型

3. CQRS与传统架构的对比

传统架构 CQRS架构
单一数据模型 分离的命令模型和查询模型
读写操作使用相同的接口 读写操作使用不同的接口
难以优化读写性能 可以独立优化读写性能
难以扩展 可以独立扩展读写组件
事务管理复杂 命令处理可以使用事件溯源,简化事务管理

4. CQRS架构模式

CQRS 可以与其他架构模式结合使用,如:

  • 事件驱动架构:使用事件来同步命令模型和查询模型
  • 事件溯源:将所有状态变更记录为事件,通过重放事件来重建系统状态
  • DDD:与领域驱动设计结合,使用领域模型处理命令
  • 微服务:在微服务架构中使用 CQRS,提高系统的可扩展性

二、Vue 3 项目中的 CQRS 设计

1. 项目目录结构

src/
├── core/                      # 核心层
│   ├── application/           # 应用服务
│   │   ├── commands/          # 命令定义
│   │   │   ├── handlers/      # 命令处理器
│   │   │   └── models/        # 命令模型
│   │   ├── queries/           # 查询定义
│   │   │   ├── handlers/      # 查询处理器
│   │   │   └── models/        # 查询模型
│   │   └── events/            # 事件定义
│   ├── domain/                # 领域模型
│   │   ├── entities/          # 实体
│   │   ├── value-objects/     # 值对象
│   │   └── services/          # 领域服务
│   └── ports/                 # 端口定义
│       ├── inbound/           # 输入端口
│       └── outbound/          # 输出端口
├── adapters/                  # 适配器层
│   ├── primary/               # 主适配器
│   │   ├── ui/                # UI 适配器
│   │   │   ├── components/    # Vue 组件
│   │   │   ├── pages/         # 页面组件
│   │   │   └── stores/        # 状态管理
│   │   └── api/               # API 适配器
│   └── secondary/             # 从适配器
│       ├── persistence/       # 持久化适配器
│       └── http/              # HTTP 客户端适配器
├── shared/                    # 共享代码
└── main.ts                    # 应用入口

2. 命令和查询的定义

命令定义

// core/application/commands/models/create-product-command.ts
export class CreateProductCommand {
  constructor(
    public readonly name: string,
    public readonly description: string,
    public readonly price: number,
    public readonly category: string,
    public readonly stock: number
  ) {}
}

// core/application/commands/models/update-product-command.ts
export class UpdateProductCommand {
  constructor(
    public readonly id: string,
    public readonly name?: string,
    public readonly description?: string,
    public readonly price?: number,
    public readonly category?: string,
    public readonly stock?: number
  ) {}
}

// core/application/commands/models/delete-product-command.ts
export class DeleteProductCommand {
  constructor(
    public readonly id: string
  ) {}
}

查询定义

// core/application/queries/models/get-product-query.ts
export class GetProductQuery {
  constructor(
    public readonly id: string
  ) {}
}

// core/application/queries/models/get-products-query.ts
export class GetProductsQuery {
  constructor(
    public readonly page?: number,
    public readonly pageSize?: number,
    public readonly category?: string,
    public readonly minPrice?: number,
    public readonly maxPrice?: number
  ) {}
}

三、命令处理实现

1. 命令处理器接口

// core/ports/inbound/command-handler.ts
export interface CommandHandler<TCommand, TResult = void> {
  /**
   * 处理命令
   * @param command 命令对象
   * @returns 处理结果
   */
  handle(command: TCommand): Promise<TResult>;
}

2. 命令处理器实现

// core/application/commands/handlers/create-product-command-handler.ts
import { CommandHandler } from '../../../ports/inbound/command-handler';
import { CreateProductCommand } from '../models/create-product-command';
import { ProductRepositoryPort } from '../../../ports/outbound/product-repository-port';
import { Product } from '../../../domain/entities/product';
import { Money } from '../../../domain/value-objects/money';
import { EventBusPort } from '../../../ports/outbound/event-bus-port';
import { ProductCreatedEvent } from '../../events/models/product-created-event';

export class CreateProductCommandHandler implements CommandHandler<CreateProductCommand, string> {
  constructor(
    private readonly productRepository: ProductRepositoryPort,
    private readonly eventBus: EventBusPort
  ) {}

  public async handle(command: CreateProductCommand): Promise<string> {
    // 创建领域实体
    const product = new Product(
      `product-${Date.now()}`, // 实际项目中应使用 UUID
      command.name,
      command.description,
      new Money(command.price, 'CNY'),
      command.category,
      command.stock
    );

    // 保存产品
    await this.productRepository.save(product);

    // 发布产品创建事件
    const event = new ProductCreatedEvent(
      product.getId(),
      product.getName(),
      product.getPrice().getAmount(),
      product.getCategory(),
      product.getStock()
    );
    await this.eventBus.publish(event);

    // 返回产品 ID
    return product.getId();
  }
}

四、查询处理实现

1. 查询处理器接口

// core/ports/inbound/query-handler.ts
export interface QueryHandler<TQuery, TResult> {
  /**
   * 处理查询
   * @param query 查询对象
   * @returns 查询结果
   */
  handle(query: TQuery): Promise<TResult>;
}

2. 查询处理器实现

// core/application/queries/handlers/get-products-query-handler.ts
import { QueryHandler } from '../../../ports/inbound/query-handler';
import { GetProductsQuery } from '../models/get-products-query';
import { ProductReadModel } from '../models/product-read-model';
import { ProductQueryRepositoryPort } from '../../../ports/outbound/product-query-repository-port';

export class GetProductsQueryHandler implements QueryHandler<GetProductsQuery, ProductReadModel[]> {
  constructor(
    private readonly productQueryRepository: ProductQueryRepositoryPort
  ) {}

  public async handle(query: GetProductsQuery): Promise<ProductReadModel[]> {
    // 构建查询条件
    const filter: any = {};
    if (query.category) {
      filter.category = query.category;
    }
    if (query.minPrice !== undefined) {
      filter.price = { $gte: query.minPrice };
    }
    if (query.maxPrice !== undefined) {
      filter.price = { ...filter.price, $lte: query.maxPrice };
    }

    // 设置分页参数
    const page = query.page || 1;
    const pageSize = query.pageSize || 10;
    const skip = (page - 1) * pageSize;

    // 执行查询
    return this.productQueryRepository.find(filter, skip, pageSize);
  }
}

3. 查询模型

// core/application/queries/models/product-read-model.ts
export class ProductReadModel {
  constructor(
    public readonly id: string,
    public readonly name: string,
    public readonly description: string,
    public readonly price: number,
    public readonly currency: string,
    public readonly category: string,
    public readonly stock: number,
    public readonly isActive: boolean,
    public readonly createdAt: Date,
    public readonly updatedAt: Date
  ) {}
}

五、事件驱动的更新机制

1. 事件定义

// core/application/events/models/product-created-event.ts
export class ProductCreatedEvent {
  constructor(
    public readonly productId: string,
    public readonly name: string,
    public readonly price: number,
    public readonly category: string,
    public readonly stock: number,
    public readonly timestamp: Date = new Date()
  ) {}
}

// core/application/events/models/product-updated-event.ts
export class ProductUpdatedEvent {
  constructor(
    public readonly productId: string,
    public readonly updates: Partial<{
      name: string;
description: string;
      price: number;
      category: string;
      stock: number;
    }>,
    public readonly timestamp: Date = new Date()
  ) {}
}

// core/application/events/models/product-deleted-event.ts
export class ProductDeletedEvent {
  constructor(
    public readonly productId: string,
    public readonly timestamp: Date = new Date()
  ) {}
}

2. 事件处理器实现

// core/application/events/handlers/product-created-event-handler.ts
import { EventHandler } from '../../../ports/inbound/event-handler';
import { ProductCreatedEvent } from '../models/product-created-event';
import { ProductQueryRepositoryPort } from '../../../ports/outbound/product-query-repository-port';
import { ProductReadModel } from '../../queries/models/product-read-model';

export class ProductCreatedEventHandler implements EventHandler<ProductCreatedEvent> {
  constructor(
    private readonly productQueryRepository: ProductQueryRepositoryPort
  ) {}

  public async handle(event: ProductCreatedEvent): Promise<void> {
    // 创建查询模型
    const productReadModel = new ProductReadModel(
      event.productId,
      event.name,
      '', // 描述在事件中未提供,实际项目中应包含
      event.price,
      'CNY',
      event.category,
      event.stock,
      true,
      event.timestamp,
      event.timestamp
    );

    // 更新查询模型存储
    await this.productQueryRepository.save(productReadModel);
  }
}

六、与 Pinia 状态管理结合

1. 创建 CQRS Store

// adapters/primary/ui/stores/cqrs-store.js
import { defineStore } from 'pinia';
import { CreateProductCommand } from '@/core/application/commands/models/create-product-command';
import { CreateProductCommandHandler } from '@/core/application/commands/handlers/create-product-command-handler';
import { GetProductsQuery } from '@/core/application/queries/models/get-products-query';
import { GetProductsQueryHandler } from '@/core/application/queries/handlers/get-products-query-handler';
import { ProductRepository } from '@/adapters/secondary/persistence/repositories/product-repository';
import { ProductQueryRepository } from '@/adapters/secondary/persistence/repositories/product-query-repository';
import { EventBus } from '@/utils/event-bus';

// 创建适配器实例
const productRepository = new ProductRepository();
const productQueryRepository = new ProductQueryRepository();
const eventBus = new EventBus();

// 创建命令和查询处理器
const createProductCommandHandler = new CreateProductCommandHandler(
  productRepository,
  eventBus
);

const getProductsQueryHandler = new GetProductsQueryHandler(
  productQueryRepository
);

export const useCqrsStore = defineStore('cqrs', {
  state: () => ({
    products: [],
    isLoading: false,
    error: null
  }),

  actions: {
    // 处理创建产品命令
    async createProduct(productData) {
      try {
        this.isLoading = true;
        this.error = null;

        // 创建命令
        const command = new CreateProductCommand(
          productData.name,
          productData.description,
          productData.price,
          productData.category,
          productData.stock
        );

        // 执行命令
        const productId = await createProductCommandHandler.handle(command);

        // 重新加载产品列表
        await this.loadProducts();

        return productId;
      } catch (error) {
        this.error = error.message;
        throw error;
      } finally {
        this.isLoading = false;
      }
    },

    // 处理获取产品查询
    async loadProducts(filters = {}) {
      try {
        this.isLoading = true;
        this.error = null;

        // 创建查询
        const query = new GetProductsQuery(
          filters.page,
          filters.pageSize,
          filters.category,
          filters.minPrice,
          filters.maxPrice
        );

        // 执行查询
        this.products = await getProductsQueryHandler.handle(query);
      } catch (error) {
        this.error = error.message;
        throw error;
      } finally {
        this.isLoading = false;
      }
    }
  }
});

2. 在 Vue 组件中使用 CQRS Store

<template>
  <div class="product-management">
    <h1>产品管理</h1>
    
    <!-- 创建产品表单 -->
    <div class="create-form">
      <h2>创建产品</h2>
      
      <form @submit.prevent="handleCreateProduct">
        <div class="form-group">
          <label for="name">产品名称</label>
          <input v-model="productForm.name" id="name" type="text" required>
        </div>
        
        <div class="form-group">
          <label for="description">产品描述</label>
          <textarea v-model="productForm.description" id="description"></textarea>
        </div>
        
        <div class="form-group">
          <label for="price">价格</label>
          <input v-model.number="productForm.price" id="price" type="number" min="0" step="0.01" required>
        </div>
        
        <div class="form-group">
          <label for="category">分类</label>
          <input v-model="productForm.category" id="category" type="text" required>
        </div>
        
        <div class="form-group">
          <label for="stock">库存</label>
          <input v-model.number="productForm.stock" id="stock" type="number" min="0" required>
        </div>
        
        <button type="submit" :disabled="cqrsStore.isLoading">
          {{ cqrsStore.isLoading ? '创建中...' : '创建产品' }}
        </button>
      </form>
    </div>
    
    <!-- 产品列表 -->
    <div class="product-list">
      <h2>产品列表</h2>
      
      <div v-if="cqrsStore.isLoading" class="loading">
        加载中...
      </div>
      
      <div v-else-if="cqrsStore.error" class="error">
        {{ cqrsStore.error }}
      </div>
      
      <div v-else>
        <ul>
          <li v-for="product in cqrsStore.products" :key="product.id" class="product-item">
            <div class="product-info">
              <h3>{{ product.name }}</h3>
              <p>{{ product.description }}</p>
              <div class="product-meta">
                <span class="price">¥{{ product.price }}</span>
                <span class="category">{{ product.category }}</span>
                <span class="stock" :class="product.stock > 0 ? 'in-stock' : 'out-of-stock'">
                  {{ product.stock > 0 ? `库存: ${product.stock}` : '缺货' }}
                </span>
              </div>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useCqrsStore } from '@/adapters/primary/ui/stores/cqrs-store';

const cqrsStore = useCqrsStore();

// 产品表单数据
const productForm = reactive({
  name: '',
description: '',
  price: 0,
  category: '',
  stock: 0
});

// 处理创建产品
const handleCreateProduct = async () => {
  try {
    await cqrsStore.createProduct(productForm);
    
    // 重置表单
    Object.assign(productForm, {
      name: '',
description: '',
      price: 0,
      category: '',
      stock: 0
    });
  } catch (error) {
    console.error('创建产品失败:', error);
  }
};

// 组件挂载时加载产品列表
onMounted(() => {
  cqrsStore.loadProducts();
});
</script>

<style scoped>
.product-management {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  color: #333;
  margin-bottom: 30px;
}

.create-form {
  background-color: #f5f5f5;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 30px;
}

.create-form h2 {
  margin-bottom: 20px;
  color: #409eff;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: 500;
}

input, textarea {
  width: 100%;
  padding: 10px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}

textarea {
  resize: vertical;
  min-height: 100px;
}

button {
  padding: 10px 20px;
  background-color: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

button:disabled {
  background-color: #c6e2ff;
  cursor: not-allowed;
}

.product-list h2 {
  margin-bottom: 20px;
  color: #67c23a;
}

.loading, .error {
  padding: 20px;
  text-align: center;
  border-radius: 4px;
  margin-bottom: 20px;
}

.loading {
  background-color: #e3f2fd;
  color: #1976d2;
}

.error {
  background-color: #ffebee;
  color: #d32f2f;
}

.product-item {
  background-color: #f5f5f5;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 15px;
}

.product-info h3 {
  margin-bottom: 10px;
  color: #333;
}

.product-info p {
  margin-bottom: 15px;
  color: #666;
}

.product-meta {
  display: flex;
  gap: 15px;
  font-size: 14px;
}

.price {
  font-weight: 500;
  color: #f56c6c;
}

.category {
  color: #409eff;
}

.stock {
  font-weight: 500;
}

.stock.in-stock {
  color: #67c23a;
}

.stock.out-of-stock {
  color: #f56c6c;
}
</style>

七、API 适配器实现

1. 命令 API 实现

// adapters/primary/api/controllers/command-controller.ts
import { Router } from 'express';
import { CreateProductCommand } from '../../../core/application/commands/models/create-product-command';
import { CreateProductCommandHandler } from '../../../core/application/commands/handlers/create-product-command-handler';
import { ProductRepository } from '../../secondary/persistence/repositories/product-repository';
import { EventBus } from '@/utils/event-bus';

// 创建适配器实例
const productRepository = new ProductRepository();
const eventBus = new EventBus();

// 创建命令处理器
const createProductCommandHandler = new CreateProductCommandHandler(
  productRepository,
  eventBus
);

const router = Router();

// 创建产品
router.post('/products', async (req, res) => {
  try {
    const { name, description, price, category, stock } = req.body;
    
    // 创建命令
    const command = new CreateProductCommand(
      name,
      description,
      price,
      category,
      stock
    );
    
    // 执行命令
    const productId = await createProductCommandHandler.handle(command);
    
    res.status(201).json({ id: productId });
  } catch (error) {
    res.status(400).json({ error: error instanceof Error ? error.message : '创建产品失败' });
  }
});

export { router as commandRoutes };

2. 查询 API 实现

// adapters/primary/api/controllers/query-controller.ts
import { Router } from 'express';
import { GetProductsQuery } from '../../../core/application/queries/models/get-products-query';
import { GetProductsQueryHandler } from '../../../core/application/queries/handlers/get-products-query-handler';
import { ProductQueryRepository } from '../../secondary/persistence/repositories/product-query-repository';

// 创建适配器实例
const productQueryRepository = new ProductQueryRepository();

// 创建查询处理器
const getProductsQueryHandler = new GetProductsQueryHandler(
  productQueryRepository
);

const router = Router();

// 获取产品列表
router.get('/products', async (req, res) => {
  try {
    const { page, pageSize, category, minPrice, maxPrice } = req.query;
    
    // 创建查询
    const query = new GetProductsQuery(
      page ? parseInt(page as string) : undefined,
      pageSize ? parseInt(pageSize as string) : undefined,
      category as string,
      minPrice ? parseFloat(minPrice as string) : undefined,
      maxPrice ? parseFloat(maxPrice as string) : undefined
    );
    
    // 执行查询
    const products = await getProductsQueryHandler.handle(query);
    
    res.json(products);
  } catch (error) {
    res.status(500).json({ error: error instanceof Error ? error.message : '获取产品列表失败' });
  }
});

export { router as queryRoutes };

八、CQRS 最佳实践

1. 命令设计原则

  • 命令命名:使用动词开头,清晰表达意图,如 CreateProductCommandUpdateUserCommand
  • 命令数据:只包含执行命令所需的必要数据
  • 命令验证:在命令处理器中验证命令的有效性
  • 命令幂等性:设计命令处理逻辑时,确保幂等性,避免重复执行导致的问题
  • 命令响应:命令通常不返回数据,或只返回操作状态和 ID

2. 查询设计原则

  • 查询命名:使用 GetFind 开头,清晰表达意图,如 GetProductsQueryFindUserByIdQuery
  • 查询数据:只返回客户端需要的数据,避免过度获取
  • 查询优化:优化查询性能,如使用索引、缓存等
  • 查询验证:验证查询参数的有效性
  • 查询响应:返回结构化的数据,便于客户端处理

3. 事件设计原则

  • 事件命名:使用过去式,清晰表达发生的事件,如 ProductCreatedEventUserUpdatedEvent
  • 事件数据:包含事件发生时的必要数据,便于重建状态
  • 事件版本:考虑事件版本管理,以便向后兼容
  • 事件幂等性:设计事件处理逻辑时,确保幂等性

4. 架构最佳实践

  • 独立扩展:独立扩展命令和查询组件,根据负载调整资源
  • 异步处理:命令处理可以异步执行,提高系统的响应性
  • 事件溯源:考虑使用事件溯源,简化命令处理和状态管理
  • 缓存策略:为查询结果实现缓存,提高查询性能
  • 监控和日志:监控命令和查询的执行情况,记录详细日志

九、CQRS 的优势与适用场景

1. 优势

  • 性能优化:可以独立优化读写性能,如为查询添加缓存,为命令使用异步处理
  • 可扩展性:可以独立扩展读写组件,根据负载调整资源
  • 安全性:可以为命令和查询设置不同的安全策略
  • 简化设计:命令和查询使用不同的模型,简化了设计和实现
  • 支持复杂业务逻辑:命令处理可以使用领域模型,支持复杂的业务逻辑
  • 事件驱动:可以与事件驱动架构结合,提高系统的响应性和可扩展性

2. 适用场景

  • 高并发应用:如电商网站、社交媒体平台等,读写负载不平衡的应用
  • 复杂业务逻辑:需要处理复杂业务规则的应用
  • 需要审计和追溯的应用:如金融系统、医疗系统等
  • 微服务架构:在微服务架构中使用 CQRS,提高系统的可扩展性
  • 事件驱动应用:与事件驱动架构结合,使用事件同步命令模型和查询模型

3. 不适用场景

  • 简单应用:对于简单的 CRUD 应用,CQRS 可能会增加不必要的复杂性
  • 读写比例均衡的应用:如果读写比例相近,CQRS 可能不会带来明显的性能提升
  • 资源受限的应用:CQRS 需要更多的资源来维护分离的命令模型和查询模型

十、总结

CQRS 模式为 Vue 3 应用提供了一种强大的架构设计方式,它通过分离命令和查询,提高了系统的性能、可扩展性和安全性。通过本文的学习,您应该掌握了:

  1. CQRS 的核心概念和设计原则
  2. Vue 3 项目中实现 CQRS 的目录结构和代码组织
  3. 命令处理的实现方法,包括命令定义、命令处理器和事件发布
  4. 查询处理的实现方法,包括查询定义、查询处理器和查询模型
  5. 事件驱动的更新机制,用于同步命令模型和查询模型
  6. 与 Pinia 状态管理的结合,实现前端的 CQRS 模式
  7. API 适配器的实现,支持外部系统的命令和查询
  8. CQRS 的最佳实践、优势和适用场景

CQRS 代表了现代软件设计的发展方向,它强调分离关注点,提高系统的可扩展性和性能。在实际项目中,建议根据具体需求灵活应用 CQRS 模式,结合其他架构模式(如 DDD、事件驱动架构),构建可扩展、易维护的现代化 Vue 3 应用。

下集预告

下一集将深入探讨领域事件与消息队列的集成,包括领域事件的定义、发布和处理,以及与消息队列(如 RabbitMQ、Kafka)的结合使用。敬请期待!

« 上一篇 Vue 3 事件驱动架构深度指南:以事件为核心的系统设计 下一篇 » Vue 3 领域事件与消息队列深度指南:构建可靠的事件驱动系统