DDD 在前端領域的思考和落地
實踐落地可見:https://github.com/cklwblove/domain-front/
現狀
- 問題域本身錯綜複雜
- 知識的丟失
- 視圖層代碼分層模糊,過重
- 違反了代碼重複原則,後期需要統一修改時,涉及文件多成本大
- 團隊中各成員形成"知識不同步",同樣的功能 A B 都實現了,但是互相卻不知道
- C 端的 UI 層複用性比較差,需求個性化嚴重
- 數據層的邏輯代碼複用性不高
達到的目的
- 沉澱業務:充分抽離業務邏輯代碼,充分為視圖服務。保證視圖層足夠"薄"。不寫重複邏輯。
- 穩定模型:統一且預處理好的數據邏輯,開發者可以直接使用數據而無需關注處理過程。前端字段不受後端影響。
- 關注點分離:將屬於不同模塊的功能分散到合適的位置中,同時儘量降低各個模塊的相互依賴並且減少需要聯繫的膠水代碼,從而降低前端的複雜性。
前端流程
數據層 -> 邏輯層 -> 視圖層
數據層生產數據,視圖層消費數據。邏輯層的主要作用是調度,將數據層的結果實例化為運行時數據,這些運行時數據將被作為視圖狀態,用於渲染到界面中。同時,它接收人機交互信號,調度狀態變化,協調視圖層各個部分做出響應。於此同時,它還可能將狀態數據轉化為實體數據,通過調用數據層通道,將數據發送回源頭,並獲取新數據,以再次完成向視圖層輸送數據原料的任務。所以,數據層處理原始數據,邏輯層生產運行時數據,視圖層消費運行時數據。邏輯層的處理,引用 領域(domain) 的概念來實現:
- Domain:處理業務數據邏輯,最大程度提高代碼的複用性
- View:處理視圖層的交互邏輯,比如,路由跳轉,事件等
後端數據(Data) -> Repository 數據映射層(Model-Mapper) -> 前端領域對象 FDO(FrontEnd Domain Object) -> 數據轉換層(Adapter)->提供給業務視圖(Business View Model)使用
架構圖
技術架構
整體架構設計
本項目採用 領域驅動設計(DDD) 在前端領域的實踐,構建了一個完整的技術架構體系:
domain-front/
├── packages/
│ ├── domain-pet-store/ # 領域業務包
│ ├── interface-pet-store/ # 接口 SDK 包
│ └── interface-utils/ # 工具包
└── playground/ # 應用示例
核心包説明
1. @domain/domain-pet-store
領域業務包 - 實現具體的業務邏輯和領域模型
- 領域模型:定義業務實體和值對象
- 倉儲層:數據訪問抽象,隔離外部數據源
- 服務層:業務邏輯編排和協調
- 適配器層:對外接口適配,處理數據轉換
2. @domain/interface-pet-store
接口 SDK 包 - 基於 Swagger 自動生成的 API 接口層
- 自動生成:基於 Swagger 規範自動生成 TypeScript 接口
- 類型安全:完整的 TypeScript 類型支持
- 攔截器:請求/響應攔截器支持
- Mock 支持:開發環境 Mock 數據支持
3. @domain/interface-utils
工具包 - 提供通用的工具函數和類型判斷
- 類型判斷:完整的 JavaScript 類型判斷工具
- 數據處理:安全的 JSON 解析、對象扁平化等
- HTTP 工具:狀態碼檢查、響應日誌等
- Tree-shaking:支持按需引入,優化打包體積
特性
- 技術框架無關性,和採用什麼技術框架沒有什麼關係
- UI 無關性
- 邏輯代碼可測試
- 目錄即分層
作為一個普通(不分前後端)的開發人員,我們關注於業務邏輯的抽離,讓業務邏輯獨立於框架。而在前端的實現,則是讓前端的業務邏輯,可以獨立於框架,只讓 UI(即表現層)與框架綁定。一旦,我們更換框架的時候,只需要替換這部分的業務邏輯即可。
領域層分層
.
├── pet
│ ├── adapter
│ │ └── pet.adapter.js
│ ├── service
│ │ └── pet.service.js
│ ├── model
│ │ └── pet.entity.js
│ └── repository
│ ├── pet.repository.js
│ └── mapper
│ └── pet.mapper.js
└── sharedUtils
├── const.js
└── index.js
- pet: 核心業務模型,這裏以 pet 為例
- adapter: 做數據清洗。邏輯判斷、數據篩選、數據轉換等。供視圖層調用。業務的緩衝層,第二層防腐層(主適配器)
- service:放置 http 請求,本地存儲/讀取數據等,處理成 Result Pattern 結構
- model:數據實體,簡單的數據模型,用來表示核心的業務邏輯,為視圖提供所需字段,全局統一
-
repository:Repository,用於讀取和存儲數據。隔離後端服務與模型,第一層防腐層(次適配器)。暴露當處理異常時,默認使用的數據
- mapper:映射層,用於核心實體層映射,或映射到核心實體層,異構轉換。默認數據,及字段處理。使用者主要關注此。負責將接口請求到的
entity轉換成UI所需要的model數據
- mapper:映射層,用於核心實體層映射,或映射到核心實體層,異構轉換。默認數據,及字段處理。使用者主要關注此。負責將接口請求到的
- sharedUtils:各模型共享常量、字典項和工具類函數
備註: 防腐層 Anti Corruption Layer 是領域驅動設計中的一個概念,用於隔離兩個系統,允許兩個系統之間在不知道對方領域知識的情況下進行集成。主要進行的是兩個系統之間的模型(model)或者協議(protocol)的轉換,並且最終目的是為了系統使用者的方便而不是系統提供者的方便,進一步的解釋就是把系統提供者的模型轉換為系統使用者的模型。在前端落地的話,就是上面提到的 mapper 映射層。
實現細節
1. 實體模型設計
// pet.entity.js
export class PetEntity {
constructor(params = {}) {
this.id = params.id || 0;
this.category = params.category || new CategoryEntity();
this.name = params.name || '';
this.photoUrls = params.photoUrls || [];
this.tags = params.tags || [];
this.status = params.status || 'available';
}
}
設計原則:
- 不可變性:實體對象一旦創建,其核心屬性不應被修改
- 完整性:實體包含完整的業務狀態
- 一致性:實體內部狀態保持一致
2. 映射層實現
// pet.mapper.js
export class PetMapper {
mapFrom(params) {
const categoryMapper = new CategoryMapper();
const tagMapper = new TagMapper();
return new PetEntity({
id: params.id,
category: params.category ? categoryMapper.mapFrom(params.category) : new CategoryEntity(),
name: params.name,
photoUrls: params.photoUrls || [],
tags: params.tags ? params.tags.map((tag) => tagMapper.mapFrom(tag)) : [],
status: params.status
});
}
}
映射職責:
- 數據轉換:將外部數據格式轉換為內部實體格式
- 默認值處理:為缺失字段提供合理的默認值
- 類型安全:確保數據類型的正確性
3. 倉儲層設計
// pet.repository.js
export async function getPetByIdRepository(params) {
const mapper = new PetMapper();
const [error, result] = await getPetByIdService(params);
if (error) {
return [error, mapper.mapFrom({})];
}
return [null, mapper.mapFrom(result)];
}
倉儲模式優勢:
- 數據訪問抽象:隱藏具體的數據源實現
- 錯誤處理統一:統一的錯誤處理和默認值返回
- 緩存策略:可以在倉儲層實現緩存邏輯
4. 適配器層實現
// pet.adapter.js
export async function getPetByIdAdapter(params) {
if (isMockMode()) {
return [null, await fetchMockJson('pet/getPetByIdAdapter')];
}
return await getPetByIdRepository(params);
}
適配器職責:
- 環境適配:根據運行環境選擇不同的數據源
- 接口統一:為視圖層提供統一的接口
- Mock 支持:開發環境支持 Mock 數據
實踐
請求接口統一處理
HTTP 接口返回統一的數據結構
// 業務異常及http協議異常
interface IError {
errorMsg: string;
errorCode: number | string;
}
// 數據返回結構
interface IReq {
data: any; // Array, Set, Map, 類數組等
error: IError;
}
引入 nemo-engine生成的接口,比如
import { petstore } from '@domain/pet-store-sdk/src/services/mods';
import to from 'await-to-js';
import { formatResponse } from '../../sharedUtils';
const $httpApi = petstore.pet;
/**
* 添加新的寵物
* @param params
* @returns {Promise<*[]>}
*/
export async function addPetService(params) {
const [error, result] = await to($httpApi.addPet(params));
return formatResponse(error, result);
}
異常的處理
- HTTP 請求異常:(status >= 200 && status < 300) || status === 304(status 服務器響應的 HTTP 狀態碼)
- 業務數據異常:接口請求成功,但業務報異常,比如入參不對等。使用
error字段即可。 UI 層在處理異常時,使用 Result Pattern,提供了一個輔助方法來幫助使用該模式。通過[await-to-js](https://github.com/scopsy/await-to-js)來轉換。
import to from 'await-to-js';
export async function result<T, U = Error>(promise: Promise<T>): Promise<[U | null, T | null]> {
try {
const data = await to(httpRequestPromise);
return [null, data];
} catch (err) {
return [err];
}
}
工具鏈與工程化
1. 代碼生成工具
Nemo Engine - 基於 Swagger 規範的接口代碼生成工具
# 生成 API 代碼
npm run gen:nemo
# 掃描 API 變更
npm run scan:nemo
# 生成文檔
npm run gen:docs
特性:
- 自動生成:基於 Swagger 規範自動生成 TypeScript 接口
- 類型安全:完整的類型定義和類型檢查
- 版本管理:支持 API 版本變更檢測
- 文檔生成:自動生成 API 文檔
2. 開發工具鏈
Monorepo 架構 - 使用 pnpm workspace 管理多包項目
// pnpm-workspace.yaml
packages:
- 'packages/*'
- 'playground'
構建工具鏈:
- Vite:現代化的構建工具,支持快速熱更新
- TypeScript:類型安全的 JavaScript 超集
- ESLint + Prettier:代碼規範和格式化
- Husky + lint-staged:Git hooks 和提交前檢查
3. 測試策略
分層測試 - 針對不同層級採用不同的測試策略
// 單元測試 - 測試業務邏輯
describe('PetMapper', () => {
it('should map pet data correctly', () => {
const mapper = new PetMapper();
const result = mapper.mapFrom(mockPetData);
expect(result).toBeInstanceOf(PetEntity);
});
});
// 集成測試 - 測試數據流
describe('PetRepository', () => {
it('should handle API errors gracefully', async () => {
const [error, result] = await getPetByIdRepository({ petId: 999 });
expect(error).toBeTruthy();
expect(result).toBeInstanceOf(PetEntity);
});
});
最佳實踐
需要不斷總結,通過這部分代碼評審獲得
通用字段處理
- 值為數字類型的
YYYYMMDD時間字段格式統一做字符串格式處理 - 數據字典轉義
- 前端頁面描述語言(字段)(與接口沒有必然聯繫)
- Array 列表
// 列表數據
Array<any>;
注意:如果是 Map 和 Array 結合的,還是需要保留 records。列表的 key 統一為 records。如
{
"records": [
{
"id": 1,
"category": {
"id": 1,
"name": "Dogs"
},
"name": "doggie",
"photoUrls": ["http://example.com/photo1.jpg"],
"tags": [
{
"id": 1,
"name": "friendly"
}
],
"status": "available"
},
{
"id": 2,
"category": {
"id": 2,
"name": "Cats"
},
"name": "kitty",
"photoUrls": ["http://example.com/photo2.jpg"],
"tags": [
{
"id": 2,
"name": "cute"
}
],
"status": "available"
}
]
}
注意:如果是返回是狀態相關的,key 統一重命名為 state,默認值統一為 -1。如
{
"state": 0
}
- 視圖使用時,無需關注使用字段的邊界情況(統一在領域層處理)
- 不在
template模板裏使用filter方法 - 列表數據要給個唯一值,如果接口有返回的話,記得添加下這個字段,不管視圖有沒有使用。作為 for 循環的時候 key
- 消滅魔法值
- 系統中相同語義的字段保持同名(例如 productCode,productName 等)
- 返回的字段,尤其是處理過的,比如保留位數的 fixedNum(params.unit_nv, 4) 。如果出現了別家券商保留位數不一致,可以再實體增加一個新的字段,並以 Original 結尾,如 unitNvOriginal
設計原則
1. 單一職責原則
每個類和方法只負責一個明確的職責
// ✅ 好的設計
class PetMapper {
mapFrom(params) {
/* 只負責數據映射 */
}
}
class PetValidator {
validate(params) {
/* 只負責數據驗證 */
}
}
// ❌ 不好的設計
class PetManager {
mapFrom(params) {
/* 數據映射 */
}
validate(params) {
/* 數據驗證 */
}
saveToCache(params) {
/* 緩存處理 */
}
}
2. 開閉原則
對擴展開放,對修改關閉
// ✅ 可擴展的設計
class BaseMapper {
mapFrom(params) {
return this.transform(params);
}
transform(params) {
throw new Error('transform method must be implemented');
}
}
class PetMapper extends BaseMapper {
transform(params) {
return new PetEntity(params);
}
}
3. 依賴倒置原則
依賴抽象而不是具體實現
// ✅ 依賴抽象
class PetRepository {
constructor(dataSource) {
this.dataSource = dataSource;
}
async getPetById(id) {
return await this.dataSource.fetch(`/pets/${id}`);
}
}
// 可以注入不同的數據源
const httpRepository = new PetRepository(httpDataSource);
const mockRepository = new PetRepository(mockDataSource);
性能優化
1. 懶加載
按需加載領域模塊
// 動態導入領域模塊
const PetModule = await import('@domain/domain-pet-store/pet');
const { getPetByIdAdapter } = PetModule;
2. 緩存策略
在倉儲層實現緩存邏輯
class CachedPetRepository {
constructor(repository, cache) {
this.repository = repository;
this.cache = cache;
}
async getPetById(id) {
const cached = this.cache.get(`pet:${id}`);
if (cached) return cached;
const result = await this.repository.getPetById(id);
this.cache.set(`pet:${id}`, result);
return result;
}
}
3. 批量處理
減少網絡請求次數
class BatchPetRepository {
constructor(repository) {
this.repository = repository;
this.batchQueue = [];
this.batchTimer = null;
}
async getPetById(id) {
return new Promise((resolve) => {
this.batchQueue.push({ id, resolve });
this.scheduleBatch();
});
}
scheduleBatch() {
if (this.batchTimer) return;
this.batchTimer = setTimeout(() => {
this.processBatch();
}, 10);
}
}
預期達到的目標
- 視圖層儘可能薄: 獲得的數據能夠直接使用到視圖層中,禁止在視圖層中對數據進行轉換、篩選、計算等邏輯操作。
- 不寫重複邏輯:遇到相同的邏輯儘可能複用而不是重寫,邏輯函數儘可能寫成可拓展可維護,暴露給團隊其他成員。
- 不同職責的代碼進行分層:將不同職責代碼合理分層,每層儘可能純淨,互不影響。
- 前端字段不受後端影響: 返回字段進行糾正,字段含義儘可能直觀,在視圖層使用時,能夠更清晰地描述視圖結構。
- 可縱觀全局領域: 前端進行領域模塊結構設計時,能夠縱覽整個項目下所有的領域,以及每個領域下具有的邏輯功能。
- 更為健壯、安全的 Model
- 統一結構
- 可以被複用或繼承的
- 可以預處理好所有數據邏輯,開發者可以直接使用數據而無需關注處理過程
- 有清晰的成功失敗判定邏輯
- 提供安全的獲取數據的邏輯
- 視圖使用 Model 時,無需關注使用字段的邊界情況,如字段不存在,處理 null 等的情況
- Logic(邏輯層)沉澱下來,View(視圖層)穩定下來
- 更方便的 Mock
- 無論接口以何種形式返回數據、以何種數據格式返回數據,都不會間接對咱們的模型層代碼進行毀壞,更不可能將這種破壞性穿透到 UI 層代碼中
部署與運維
1. 構建優化
多環境構建 - 支持開發、測試、生產環境
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'domain-pet-store': ['@domain/domain-pet-store'],
'interface-utils': ['@domain/interface-utils']
}
}
}
}
});
代碼分割策略:
- 領域包分離:將不同領域包分別打包
- 工具庫分離:將通用工具庫獨立打包
- 動態導入:按需加載領域模塊
2. 監控與日誌
性能監控 - 監控領域層的性能表現
// 性能監控裝飾器
function performanceMonitor(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
// 記錄性能指標
console.log(`${propertyKey} took ${duration}ms`);
return result;
} catch (error) {
const duration = performance.now() - start;
console.error(`${propertyKey} failed after ${duration}ms:`, error);
throw error;
}
};
return descriptor;
}
3. 錯誤處理
全局錯誤處理 - 統一的錯誤處理機制
// 錯誤邊界
class DomainErrorBoundary {
static handle(error, context) {
// 記錄錯誤日誌
console.error('Domain Error:', {
error: error.message,
stack: error.stack,
context
});
// 返回默認值
return this.getDefaultValue(context);
}
static getDefaultValue(context) {
switch (context.type) {
case 'pet':
return new PetEntity();
case 'list':
return [];
default:
return null;
}
}
}
團隊協作
1. 代碼規範
統一的代碼風格 - 確保團隊代碼一致性
// .eslintrc.js
module.exports = {
extends: ['@winner-fed/eslint-config-vue', '@winner-fed/eslint-config-typescript'],
rules: {
// 領域層特定規則
'no-magic-numbers': 'error',
'prefer-const': 'error',
'no-var': 'error'
}
};
2. 文檔規範
API 文檔 - 自動生成和維護文檔
// JSDoc 註釋規範
/**
* 獲取寵物信息
* @param {Object} params - 請求參數
* @param {number} params.petId - 寵物ID
* @returns {Promise<[Error|null, PetEntity]>} 返回寵物實體或錯誤
* @example
* const [error, pet] = await getPetByIdAdapter({ petId: 123 });
* if (error) {
* console.error('獲取寵物失敗:', error);
* return;
* }
* console.log('寵物名稱:', pet.name);
*/
export async function getPetByIdAdapter(params) {
// 實現邏輯
}
3. 版本管理
語義化版本 - 遵循語義化版本規範
{
"name": "@domain/domain-pet-store",
"version": "1.2.3",
"description": "寵物商店領域包"
}
版本規則:
- 主版本號:不兼容的 API 修改
- 次版本號:向下兼容的功能性新增
- 修訂號:向下兼容的問題修正
注意
領域層並不是因為被多個地方複用而被抽離。它被抽離的原因是:
- 領域層是穩定的(頁面以及與頁面綁定的模塊都是不穩定的)
- 領域層是解耦的(頁面是會耦合的,頁面的數據會來自多個接口,多個領域)
- 領域層具有極高複雜度,值得單獨管理(view 層處理頁面渲染以及頁面邏輯控制,複雜度已經夠高,領域層解耦可以輕 view 層)
- 領域層以層為單位,可以複用(你的代碼可能會拋棄某個技術體系,從 vue 轉成 react,或者可能會推出一個移動版,在這些情況下,領域層這一層都是可以直接複用)
- 為了領域模型的持續衍進(模型存在的目的是讓人們聚焦,聚焦的好處是加強了前端團隊對於業務的理解,思考業務的過程才能讓業務前進)
總結
通過領域驅動設計在前端領域的實踐,我們構建了一個完整的技術架構體系:
- 分層清晰:通過明確的分層架構,實現了關注點分離
- 技術無關:領域層與具體技術框架解耦,提高了複用性
- 類型安全:完整的 TypeScript 支持,提高了代碼質量
- 工具完備:完整的開發工具鏈,提高了開發效率
- 規範統一:統一的代碼規範和最佳實踐,提高了團隊協作效率
這種架構設計不僅解決了當前面臨的技術問題,更為未來的技術演進和業務發展奠定了堅實的基礎。