适配场景:UniApp 商城 / 点餐系统 | 领域驱动设计 | Vue2/Vue3+Vite/Webpack
核心技术:抽象工厂 + 策略模式 + 装饰器 + 自动化扫描注册
🔖 目录
- 引言|为什么要重构购物车?
- 架构设计图|一眼看懂核心流程
- 背景与痛点|传统写法的致命问题
- 设计目标|DDD架构改造标准
- 核心代码实现|全量可复制
- 领域模型重构|纯净DDD落地
- 实战使用示例|开箱即用
- 方案优势总结|工业级架构价值
- 常见问题FAQ|避坑指南
1. 引言|为什么要重构购物车?
在 UniApp 多端商城、外卖点餐类项目开发中,购物车是承载核心业务的领域模型。随着业务迭代,商品类型越来越丰富:普通单品、特价商品、规格商品(辣度/甜度/尺寸)、组合套餐、预售商品等。
传统开发模式下,购物车添加商品充斥着大量 if-else/switch 硬编码,不仅代码臃肿、难以维护,更违反开闭原则和单一职责,每次新增商品类型都要改动购物车核心代码,极易引发线上故障。
本篇基于领域驱动设计 ( DDD )思想,结合抽象工厂+策略模式+装饰器+自动化扫描,打造一套可无限扩展、零耦合、自动注册的购物车架构,真正实现新增商品=新建文件,无需改动一行旧代码。
2. 架构设计图|一眼看懂核心流程
核心流程泳道图
graph TD
A[业务层:ICart购物车] -->|调用create| B[CartItemFactory工厂]
B -->|遍历规则| C[策略匹配:按优先级筛选]
C -->|匹配成功| D[自动创建对应子类:ICartItem/SkuCartItem...]
C -->|无匹配| E[抛出异常提示]
F[子类装饰器@Register] -->|自动注册| G[工厂规则队列]
H[自动化扫描] -->|加载文件| F
style A fill:#f9f,stroke:#333
style B fill:#9ff,stroke:#333
style G fill:#ff9,stroke:#333
UML类图(核心模块)
classDiagram
class IComponent{
<<基础组件>>
}
class IFactory{
#_rules: FactoryRule[]
+register()
+create()
}
class CartItemFactory{
<<单例工厂>>
}
class ICart{
#_cartItemMap
+addGoodsItem()
+removeGoodsItem()
}
class ICartItem{
#_price
#_num
+match()
+getTotalPrice()
}
class SkuCartItem{
#_sku
+match()
}
IComponent <|-- IFactory
IFactory <|-- CartItemFactory
IComponent <|-- ICart
IComponent <|-- ICartItem
ICartItem <|-- SkuCartItem
ICart --> CartItemFactory: 依赖
3. 背景与痛点|传统写法的致命问题
业务场景
购物车需支持多种商品类型,不同商品的价格计算、匹配规则、UI逻辑完全不同,规格商品还需携带额外参数(如辣度、配料)。
传统坏代码示例
// 坏味道:大量if-else,违反开闭原则
addGoodsItem(goodsItem: any, num: number = 1, options?: any): void {
let cartItem;
// 判定规格商品
if (options) {
cartItem = new SkuCartItem(goodsItem, options);
}
// 判定组合商品
else if (goodsItem.type === 'combo') {
cartItem = new ComboCartItem(goodsItem, options);
}
// 判定特价商品
else if (goodsItem.isSpecial) {
cartItem = new SpecialCartItem(goodsItem, options);
}
// 默认普通商品
else {
cartItem = new ICartItem(goodsItem, options);
}
}
核心痛点
- 硬编码严重,新增商品必须修改购物车源码
- 逻辑嵌套复杂,排查bug成本极高
- 违背DDD理念,领域模型不纯净
- 扩展性极差,多人协作易冲突
- 无统一规范,代码难以维护
4. 设计目标|DDD架构改造标准
✅ 完全解耦:购物车不依赖任何具体CartItem子类
✅ 无限扩展:新增商品仅需新建文件+装饰器
✅ 无if-else:策略自动匹配,优先级可控
✅ 自动注册:Vite/Webpack自动扫描,零手动配置
✅ 运行时动态:支持热插拔、动态替换
✅ 遵循开闭原则:对扩展开放,对修改关闭
✅ 跨平台兼容:UniApp全端、Vue2/Vue3通用
5. 核心代码实现|全量可复制
5.1 抽象工厂基类 IFactory
import { IComponent } from "@/th4/mall/core/IComponent";
/**
* 工厂规则类型定义
*/
export interface FactoryRule<T> {
rule: (data: any, options?: any) => boolean;
constructor: new (data: any, options?: any) => T;
order: number;
}
/**
* 通用抽象工厂基类
* 支持策略注册、优先级排序、自动去重
*/
export class IFactory<T extends mall.Component> extends IComponent implements mall.Factory<T> {
protected _rules: FactoryRule<T>[] = [];
// 内置日志,便于调试
protected _logger = {
info: console.info,
debug: console.debug,
warn: console.warn,
error: console.error,
};
constructor() {
super({});
this._rules = [];
}
/**
* 注册子类构造器+匹配规则+优先级
* @param ctor 子类构造函数
* @param rule 匹配规则函数
* @param order 优先级,数字越小越先匹配
*/
register(
ctor: new (data: any, options?: any) => T,
rule: (data: any, options?: any) => boolean,
order: number = 999
): void {
// 自动去重,避免重复注册
const exist = this._rules.some((x) => x.constructor === ctor);
if (exist) return;
this._rules.push({ rule, constructor: ctor, order });
// 按优先级升序排序
this._rules.sort((a, b) => a.order - b.order);
this._logger.debug(`[Factory] 注册成功: ${ctor.name}`);
}
/**
* 统一创建入口,自动匹配子类
*/
create(data: any, options?: any): T {
for (const rule of this._rules) {
if (rule.rule(data, options)) {
return new rule.constructor(data, options);
}
}
throw new Error(`[Factory] 无匹配的CartItem类型,请检查注册规则`);
}
}
5.2 通用注册装饰器 Register
/**
* 通用自动注册装饰器
* 修饰子类,自动将规则注册到对应工厂
* @param factory 目标工厂实例
* @param rule 匹配规则
* @param order 优先级
*/
export const Register = <T>(
factory: mall.Factory<T>,
rule?: (data: any, options?: any) => boolean,
order?: number
) => {
return <C extends new (...args: any[]) => T>(constructor: C) => {
factory.register(constructor, rule || (() => true), order);
};
}
5.3 购物车商品工厂(单例)
import { IFactory } from "@/th4/mall/core/IFactory";
/**
* 购物车商品专属工厂
* 继承通用抽象工厂,无额外逻辑,职责单一
*/
class CartItemFactory extends IFactory<mall.CartItem> {}
// 全局单例,保证唯一注册入口
export const cartItemFactory = new CartItemFactory();
5.4 自动化扫描启动器(全平台适配)
5.4.1 Vite / UniApp Vue3 版本
// 文件路径:src/th4/mall/starter.ts
/**
* Vite自动扫描:匹配所有CartItem结尾的ts文件
* 自动加载并触发@Register装饰器
*/
(() => {
try {
const modules = import.meta.glob("./cart/**/*CartItem.ts", { eager: true });
console.log(`✅ 商城购物车自动扫描完成,共加载 ${Object.keys(modules).length} 个组件`);
} catch (err) {
console.error("❌ 购物车自动扫描失败", err);
}
})();
5.4.2 Webpack / Vue CLI / UniApp Vue2 版本
// 文件路径:src/th4/mall/starter.ts
/**
* Webpack自动扫描:require.context实现
*/
(() => {
try {
const ctx = require.context("./cart/", true, /CartItem.ts$/);
ctx.keys().forEach(ctx);
console.log(`✅ 商城购物车自动扫描完成,共加载 ${ctx.keys().length} 个组件`);
} catch (err) {
console.error("❌ 购物车自动扫描失败", err);
}
})();
入口引入(main.ts)
// 项目启动时自动执行扫描
import "@/th4/mall/starter";
6. 领域模型重构|纯净DDD落地
6.1 购物车领域模型 ICart
import { IComponent } from "@/th4/mall/core/IComponent";
import { cartItemFactory } from "@/th4/mall/cart/factory/CartItemFactory";
/**
* 购物车事件常量
*/
export const CartEvent = {
CalculatePrice: "CalculatePrice",
};
/**
* 购物车领域核心类
* 无任何具体子类依赖,完全解耦
*/
export class ICart extends IComponent implements mall.Cart {
protected _cartItemMap: Record<string, mall.CartItem> = {};
protected _totalPrice: number = 0;
get totalPrice(): number {
return this._totalPrice;
}
/**
* 添加商品:统一入口,无if-else
*/
addGoodsItem(goodsItem: any, num: number = 1, options?: any): void {
let cartItem = this.loadCartItem(goodsItem, options);
if (!cartItem) {
// 工厂自动创建对应子类,屏蔽创建细节
cartItem = cartItemFactory.create(goodsItem, options);
this._cartItemMap[cartItem.id] = cartItem;
}
cartItem.plus(num);
this._logger.debug("购物车添购商品", goodsItem, cartItem);
// 重新计算总价
this._calculatePrice();
}
/**
* 移除商品
*/
removeGoodsItem(goodsItem: any, num: number = 1, options?: any): void {
const cartItem = this.loadCartItem(goodsItem, options);
if (!cartItem) return;
cartItem.minus(num);
if (cartItem.num <= 0) {
delete this._cartItemMap[cartItem.id];
}
this._logger.debug("购物车减购商品", goodsItem, cartItem);
this._calculatePrice();
}
/**
* 加载已存在的购物车商品
*/
loadCartItem(goodsItem: any, options?: any): mall.CartItem | null {
return Object.values(this._cartItemMap).find((item) =>
item.match(goodsItem, options)
) || null;
}
/**
* 计算总价并派发事件
*/
protected _calculatePrice(): void {
this._totalPrice = Object.values(this._cartItemMap).reduce(
(total, item) => total + item.getTotalPrice(),
0
);
this.emit(CartEvent.CalculatePrice, this._totalPrice, this);
}
// 省略:清空购物车、获取数量、获取列表等方法
}
6.2 基础购物车商品 ICartItem
import { Register } from "@/th4/mall/core/Register";
import { cartItemFactory } from "./factory/CartItemFactory";
/**
* 默认购物车商品:兜底策略
* 优先级999,最后匹配
*/
@Register<mall.CartItem>(cartItemFactory, () => true, 999)
export class ICartItem extends IComponent implements mall.CartItem {
protected _price: number;
protected _num: number;
constructor(data: any, options?: any) {
super(data, options);
this._price = data.price || 0;
this._num = 0;
}
get price(): number {
return this._price;
}
get num(): number {
return this._num;
}
/**
* 计算总价
*/
getTotalPrice(): number {
return this._price * this._num;
}
/**
* 分组匹配
*/
group(goodsItem: any): boolean {
return this._data.id === goodsItem.id;
}
/**
* 唯一匹配规则
*/
match(goodsItem: any, options?: any): boolean {
return this._data.id === goodsItem.id;
}
/**
* 减少数量
*/
minus(num: number = 1): void {
this._num = Math.max(0, this._num - num);
}
/**
* 增加数量
*/
plus(num: number = 1): void {
this._num += num;
}
// 折扣相关:预留扩展
unUseDiscount(discount?: any): void {}
useDiscount(discount: any): void {}
}
6.3 规格商品 SkuCartItem
import { Register } from "@/th4/mall/core/Register";
import { cartItemFactory } from "./factory/CartItemFactory";
import { ICartItem } from "./ICartItem";
/**
* 规格购物车商品
* 优先级1,优先匹配(存在options则命中)
*/
@Register<mall.CartItem>(
cartItemFactory,
(data: any, options: any) => !!options,
1
)
export class SkuCartItem extends ICartItem {
protected _sku: string;
constructor(data: any, options?: any) {
super(data, options);
// 序列化规格参数,用于唯一匹配
this._sku = JSON.stringify(options || {});
}
/**
* 重写匹配规则:商品ID+规格参数双重匹配
*/
match(goodsItem: any, options?: any): boolean {
const baseMatch = super.match(goodsItem, options);
const skuMatch = options && JSON.stringify(options) === this._sku;
return baseMatch && skuMatch;
}
}
7. 实战使用示例|开箱即用
// 初始化购物车
const cart = new ICart();
// 1. 添加普通商品(自动匹配 ICartItem)
cart.addGoodsItem({ id: 1001, price: 18 });
// 2. 添加规格商品(自动匹配 SkuCartItem)
cart.addGoodsItem(
{ id: 1002, price: 22 },
1,
{ 辣度: "中辣", 甜度: "七分糖", 配料: "加珍珠" }
);
// 3. 获取购物车商品列表
const cartItems = cart.getCartItems();
console.log("购物车列表", cartItems);
// 4. 获取总价
console.log("购物车总价", cart.totalPrice);
8. 方案优势总结|工业级架构价值
- 彻底 解耦:购物车领域完全独立,不依赖任何子类
- 极致扩展:新增商品仅需2步(新建类+加装饰器),零旧代码改动
- 代码整洁:彻底消灭if-else,可读性、可维护性拉满
- 自动运维:扫描注册全自动化,无需手动维护导入
- 优先级可控:精确规则优先,兜底规则保障,适配复杂场景
- DDD 规范:领域边界清晰,职责单一,符合大型项目架构标准
- 全端兼容:UniApp、Vue2/Vue3、Vite/Webpack无缝适配
9. 常见问题FAQ|避坑指南
❓ 1. 装饰器不生效,子类没有自动注册?
- 检查
tsconfig.json是否开启:
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
- 确认
starter.ts已在main.ts引入 - 检查文件命名是否符合扫描规则(
*CartItem.ts)
❓ 2. 多个子类规则冲突,匹配错误?
调整 @Register 装饰器的 order 参数,数字越小优先级越高,精确匹配规则设小值,兜底规则设999。
❓ 3. Webpack环境报错:require.context未定义?
- 确认是Vue CLI/Webpack工程,不要混用Vite语法
- 检查扫描路径是否正确,建议使用相对路径
- 重启项目重新编译
❓ 4. 新增子类后,控制台提示未找到匹配类型?
- 检查子类文件是否放在
cart/**目录下 - 确认装饰器参数正确,绑定了
cartItemFactory - 重启项目,让自动扫描重新加载文件
❓ 5. 如何实现动态注销/替换子类?
在 IFactory 中新增 unregister 方法,通过构造函数移除对应规则即可,支持运行时动态调整。
❓ 6. 这套架构能扩展到其他模块吗?
完全可以,商品、优惠券、订单、营销活动等模块,均可复用 IFactory 和 @Register,只需新建对应工厂和扫描规则即可。
📢 文末互动 + 引流
本文已收录于个人专栏 《UniApp实战架构进阶》 ,持续输出UniApp/DDD/前端架构干货,欢迎关注~
✅ 觉得有用记得点赞、收藏、关注三连,防止迷路!
💬 评论区留下你的实战场景/遇到的问题,看到都会一一回复