核心架构:DDD 与 SOLID 指导下的分层、模块化与插件化
这套架构的核心思想是**“关注点分离” (Separation of Concerns)**,确保系统的每一部分都只做一件事,并把它做好。
1. DDD (Domain-Driven Design) 领域驱动设计
在前端应用,尤其是复杂的业务型应用(如 B 端系统)中,引入 DDD 思想能极大地提升代码的可维护性和业务表达能力。 我们不再将前端仅仅视为“视图层”,而是看作一个拥有自身领域模型的独立应用。
-
分层架构:前端应用可以借鉴后端的洋葱圈或六边形架构,划分为以下几层:
- Domain Layer (领域层):最核心的一层,纯粹的业务逻辑和状态,与任何框架(如 React/Vue)无关。这里定义了业务实体 (Entities)、值对象 (Value Objects) 和领域服务 (Domain Services)。 例如,“订单”的创建、状态流转等核心逻辑就属于这一层。
- Application Layer (应用层):负责编排领域层的服务,响应用户的操作(即 Use Cases)。它处理应用的具体流程,比如“用户点击下单按钮后,调用领域层的创建订单服务,并处理成功或失败的后续逻辑”。这一层也不应依赖具体 UI 框架。
- Infrastructure Layer (基础设施层):提供与外部系统交互的能力,如 API 请求、本地存储、WebSocket 等。它通过“适配器”和“端口”的模式,将具体实现与应用层解耦。 例如,定义一个
OrderRepository接口,然后实现一个基于fetch的OrderRepositoryAPI。 - Presentation Layer (表现层):即我们最熟悉的 UI 层,由 React、Vue 等框架构建。它负责渲染界面、响应用户输入,并通过调用应用层来执行业务逻辑。这一层应尽可能地“薄”,只做 UI 相关的事情。
-
优势:
- 业务逻辑与UI框架解耦:核心业务逻辑不再散落在各个组件中,而是集中在领域层和应用层,易于测试和维护。
- 代码清晰:代码结构直接反映了业务模型,新成员能更快地理解业务。
- 技术栈迁移成本低:更换 UI 框架时,大部分核心代码(领域层、应用层)可以复用。
2. SOLID 原则
SOLID 是面向对象设计的五个基本原则,同样适用于现代前端开发,尤其是在 TypeScript 项目中,它们能帮助我们写出更健壮、更灵活的代码。
- 单一职责原则 (SRP):一个模块或组件只负责一项功能。例如,一个组件只管渲染 UI,数据获取和处理逻辑则抽离到 Hook 或服务中。
- 开闭原则 (OCP):对扩展开放,对修改关闭。我们可以通过插件化或组合的方式添加新功能,而不是修改现有核心代码。
- 里氏替换原则 (LSP):子类应该可以替换父类。在组件继承或组合时,保证接口和行为的一致性。
- 接口隔离原则 (ISP):不应强迫用户依赖他们不需要的接口。例如,设计组件 Props 或 Hook 的 API 时,应保持其精简和专注。
- 依赖倒置原则 (DIP):高层模块不应依赖底层模块,两者都应依赖抽象。这正是 DDD 分层架构的核心,应用层依赖的是抽象的仓储(Repository)接口,而非具体的 API 实现。
3. 模块化与插件化
在 DDD 和 SOLID 的基础上,我们可以实现高度的模块化和插件化,让应用像乐高一样灵活拼装。
- 业务模块化:将应用按业务领域(如订单模块、用户模块、商品模块)进行划分。 每个模块都是一个独立的单元,包含自己的领域逻辑、应用服务和 UI 组件。
- 插件化架构:对于通用功能或可扩展点,可以设计成插件。 例如,一个表单渲染器,不同的表单项类型(输入框、选择器、上传组件)可以作为插件动态注册和加载。Webpack 5 的 Module Federation 技术是实现微前端和插件化架构的利器,它允许应用在运行时动态加载来自其他独立部署应用的代码,实现真正的插件化集成。
配套工程化:保障研发效率、质量与稳定性
有了优秀的架构,还需要一整套工程化体系来保驾护航,确保项目从开发到上线的全流程都处于高效、可控的状态。
1. 脚手架与开发模板
- 脚手架 (Scaffolding):为了避免每次都从零开始配置项目,我们会开发内部的脚手架工具。 这个工具(如一个 Node.js CLI)可以一键生成集成了上述架构、Linter 规则、测试框架等最佳实践的项目模板。 像社区的
create-react-app或Vite初始化项目一样,但更贴合自己团队的规范。 - 开发模板 (Templates):脚手架会根据不同的项目类型(如中后台、H5、可视化大屏)提供不同的模板。模板中已经预设好目录结构、基础依赖和配置文件。
2. 代码 Lint (静态代码检查)
Linting 是保证代码质量和一致性的第一道防线,它能在开发阶段就发现潜在错误。
- ESLint: 用于检查 JavaScript 和 TypeScript 代码的语法和风格问题。
- StyleLint: 专注于 CSS、SCSS 等样式文件的规范检查。
- Prettier: 用于代码格式化,解决缩进、分号等风格争议,通常与 ESLint 集成。
- Commit Lint & Hooks: 使用
husky和lint-staged等工具,在代码提交(git commit)时自动运行 Lint 检查和格式化,不符合规范的代码将被禁止提交。
3. 自动化测试
自动化测试是确保代码质量和重构信心的关键。 根据测试金字塔模型,我们会配置不同层次的测试:
- 单元测试 (Unit Tests):使用 Jest、Vitest 等框架,专注于测试最小的功能单元,如独立的函数、组件或领域层的业务逻辑。
- 集成测试 (Integration Tests):测试多个单元协同工作的场景,例如一个包含表单和按钮的组件,在点击按钮后是否能正确调用业务逻辑。
- 端到端测试 (E2E Tests):使用 Cypress、Playwright 等工具,模拟真实用户在浏览器中的完整操作流程,确保整个应用功能正常。
4. CI/CD (持续集成/持续部署)
CI/CD 能够自动化构建、测试、部署流程,实现快速、可靠的软件交付。
- 持续集成 (CI):当代码推送到仓库后,CI 服务器(如 Jenkins、GitHub Actions、GitLab CI)会自动执行以下任务:
- 安装依赖。
- 运行 Lint 检查。
- 运行所有自动化测试。
- 执行生产环境构建 (Build)。
- 将构建产物打包。
- 持续部署 (CD):CI 流程成功后,CD 环节会自动将构建产物部署到不同环境:
- 开发分支 (develop) -> 开发环境 (DEV)
- 发布分支 (release) -> 测试环境 (QA)
- 主分支 (main/master) -> 生产环境 (PROD)(通常会引入灰度发布)
5. 灰度发布 (Grayscale Release / Canary Deployment)
直接将新版本全量发布到生产环境风险很高。 灰度发布是一种平滑过渡的发布策略,先让一小部分用户使用新版本,验证其稳定性,然后逐步扩大覆盖范围,直到全量上线。
- 实现方式:
- Nginx/Gateway 层:通过配置流量分发策略,根据用户 ID、Cookie、IP 地址等将一部分流量导向部署了新版本代码的服务器。
- CDN/Serverless:利用边缘计算的能力,根据规则为不同用户返回不同版本的静态资源。
- 前端应用内:通过“功能开关”(Feature Flag),在代码中控制新功能的可见范围。
6. 监控与告警
应用上线后,我们需要实时了解其运行状况,以便在问题发生时快速响应。
- 性能监控 (Performance Monitoring):
- 核心 Web 指标 (Core Web Vitals):监控 LCP、FID、CLS 等关键性能指标。
- 资源加载:监控 JS/CSS/图片等资源的加载时间和成功率。
- API 性能:监控接口请求的响应时间、成功率和状态码。
- 错误监控 (Error Tracking):
- JS 错误:捕获并上报应用运行时发生的 JavaScript 错误。
- 资源加载错误:捕获静态资源加载失败的情况。
- 用户行为监控 (User Behavior Analytics):
- 会话重放 (Session Replay):录制用户操作过程,便于复现和定位问题。
- 告警 (Alerting):当监控数据达到预设阈值(如错误率突增、性能指标急剧下降)时,通过邮件、钉钉、Slack 等方式立即通知开发团队。
- 常用工具:Sentry、LogRocket、Datadog、AppSignal 等。
总结
您所描述的这套前端工程化体系,是一个现代化、工业级的开发范式。它将 DDD 与 SOLID 作为架构设计的指导思想,构建出分层清晰、模块化、可插拔的健壮应用;同时,通过脚手架、自动化测试、CI/CD、灰度发布和监控告警等一系列工程化手段,全方位地保障了项目的开发效率、代码质量、交付速度和线上稳定性。
作为前端工程师,掌握并实践这套体系,能够帮助我们从容应对日益复杂的业务需求和技术挑战,打造出真正优秀的用户界面和产品。
我们将构建一个非常简单的电子商务功能:“商品列表页,用户可以浏览商品并将其添加到购物车”。
这个例子将清晰地展示各层职责、代码如何组织,以及它们之间如何解耦和协作。
1. 文件夹结构
这是我们项目的核心结构,它直观地反映了 DDD 的分层和模块化思想。
src
├── modules # 业务模块化
│ └── product # 商品模块 (一个独立的业务领域)
│ ├── application # ┌─ 应用层 (Use Cases)
│ │ └── cart.service.ts
│ │
│ ├── domain # ├─ 领域层 (核心业务逻辑, 与框架无关)
│ │ ├── entities
│ │ │ ├── cart.entity.ts
│ │ │ └── product.entity.ts
│ │ └── repositories
│ │ └── product.repository.ts # 定义接口 (Port)
│ │
│ ├── infrastructure # ├─ 基础设施层 (具体实现)
│ │ ├── V1
│ │ │ └── product.repository.ts # 接口的具体API实现 (Adapter)
│ │ └── state
│ │ └── cart.store.ts # UI状态管理
│ │
│ └── presentation # └─ 表现层 (UI)
│ ├── components
│ │ ├── ProductCard.tsx
│ │ └── ShoppingCart.tsx
│ ├── hooks
│ │ └── useProducts.ts
│ └── pages
│ └── ProductListPage.tsx
│
├── shared # 全局共享模块
│ ├── api # 通用API客户端
│ ├── components # 通用UI组件 (如 Button, Modal)
│ └── utils # 通用工具函数
│
└── main.tsx # 应用入口 & 依赖注入配置
2. 代码实现
第1步:领域层 (Domain) - 核心业务逻辑
这部分代码是纯粹的 TypeScript,不依赖任何 UI 框架。
src/modules/product/domain/entities/product.entity.ts
// 定义了 "商品" 这个实体
export interface Product {
id: string;
name: string;
price: number;
stock: number;
}
// 可以在这里附加纯粹的领域逻辑
export function isOutOfStock(product: Product): boolean {
return product.stock <= 0;
}
src/modules/product/domain/entities/cart.entity.ts
import { Product } from './product.entity';
// 定义了 "购物车项"
export interface CartItem {
product: Product;
quantity: number;
}
// 定义了 "购物车" 实体,并封装了核心业务规则
export class Cart {
public items: CartItem[] = [];
// 规则1:添加商品到购物车
addItem(product: Product, quantity: number): void {
if (product.stock < quantity) {
throw new Error('Not enough stock available.');
}
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
}
// 规则2:计算总价
getTotalPrice(): number {
return this.items.reduce((total, item) => total + item.product.price * item.quantity, 0);
}
}
src/modules/product/domain/repositories/product.repository.ts
import { Product } from '../entities/product.entity';
// 定义了一个 "端口" (Port),应用层将依赖这个抽象接口,而不是具体实现
export interface IProductRepository {
fetchAll(): Promise<Product[]>;
}
第2步:基础设施层 (Infrastructure) - 实现外部依赖
这里我们提供 IProductRepository 接口的具体实现,例如通过 API 获取数据。
src/modules/product/infrastructure/V1/product.repository.ts
import { IProductRepository } from '../../domain/repositories/product.repository';
import { Product } from '../../domain/entities/product.entity';
// 这是 "适配器" (Adapter),它实现了端口,并封装了数据获取的细节
export class ApiV1ProductRepository implements IProductRepository {
async fetchAll(): Promise<Product[]> {
// 在真实世界中, 这里会使用 axios 或 fetch 与后端 API 通信
// const response = await fetch('/api/v1/products');
// const data = await response.json();
// return data;
// 为了演示, 我们返回模拟数据
console.log('Fetching products from API V1...');
return Promise.resolve([
{ id: '1', name: 'Vue.js Mastery', price: 49.9, stock: 10 },
{ id: '2', name: 'React for Pro', price: 59.9, stock: 5 },
{ id: '3', name: 'DDD in Frontend', price: 79.9, stock: 0 },
]);
}
}
第3步:应用层 (Application) - 编排用例
应用层连接了领域和外部世界,它定义了用户可以执行的“用例”(Use Case)。
src/modules/product/application/cart.service.ts
import { Cart } from '../domain/entities/cart.entity';
import { Product } from '../domain/entities/product.entity';
// 这个服务是纯粹的,它接收一个领域实体 Cart,并对其执行操作
// 它不知道 React 的 state,只关心业务流程
export class CartService {
addToCart(cart: Cart, product: Product, quantity: number): Cart {
// 创建一个新的 Cart 实例以保持不可变性
const newCart = new Cart();
newCart.items = [...cart.items]; // 复制现有项
// 使用领域实体的方法来执行业务规则
newCart.addItem(product, quantity);
return newCart;
}
}
第4步:表现层 (Presentation) - UI 组件和 Hooks
这是用户直接交互的部分,由 React 构建。
src/modules/product/infrastructure/state/cart.store.ts
// 这个 Store 是 Infrastructure 和 Presentation 的桥梁,用于管理 UI 状态
import { useState } from 'react';
import { Cart, CartItem } from '../../domain/entities/cart.entity';
// 在真实项目中,可能会使用 Zustand, Redux Toolkit 等状态管理库
export const useCartStore = () => {
// 状态的唯一来源是一个领域实体
const [cart, setCart] = useState<Cart>(new Cart());
return {
cart,
setCart,
// 衍生状态可以直接从领域实体获取
totalPrice: cart.getTotalPrice(),
items: cart.items,
};
};
src/modules/product/presentation/components/ProductCard.tsx
import React from 'react';
import { Product, isOutOfStock } from '../../domain/entities/product.entity';
interface Props {
product: Product;
onAddToCart: (product: Product) => void;
}
export const ProductCard: React.FC<Props> = ({ product, onAddToCart }) => {
const outOfStock = isOutOfStock(product);
return (
<div style={{ border: '1px solid #ccc', padding: '16px', margin: '8px' }}>
<h3>{product.name}</h3>
<p>Price: ${product.price}</p>
<p>Stock: {product.stock}</p>
<button onClick={() => onAddToCart(product)} disabled={outOfStock}>
{outOfStock ? 'Out of Stock' : 'Add to Cart'}
</button>
</div>
);
};
src/modules/product/presentation/hooks/useProducts.ts
import { useEffect, useState } from 'react';
import { Product } from '../../domain/entities/product.entity';
import { IProductRepository } from '../../domain/repositories/product.repository';
// 这个 Hook 封装了获取产品的逻辑,但它依赖于抽象的 Repository
export const useProducts = (productRepository: IProductRepository) => {
const [products, setProducts] = useState<Product[]>