博客 / 詳情

返回

DDD 在前端領域的思考和落地

DDD 在前端領域的思考和落地

實踐落地可見:https://github.com/cklwblove/domain-front/

現狀

  • 問題域本身錯綜複雜
  • 知識的丟失
  • 視圖層代碼分層模糊,過重
  • 違反了代碼重複原則,後期需要統一修改時,涉及文件多成本大
  • 團隊中各成員形成"知識不同步",同樣的功能 A B 都實現了,但是互相卻不知道
  • C 端的 UI 層複用性比較差,需求個性化嚴重
  • 數據層的邏輯代碼複用性不高

達到的目的

  • 沉澱業務:充分抽離業務邏輯代碼,充分為視圖服務。保證視圖層足夠"薄"。不寫重複邏輯。
  • 穩定模型:統一且預處理好的數據邏輯,開發者可以直接使用數據而無需關注處理過程。前端字段不受後端影響。
  • 關注點分離:將屬於不同模塊的功能分散到合適的位置中,同時儘量降低各個模塊的相互依賴並且減少需要聯繫的膠水代碼,從而降低前端的複雜性。

前端流程

數據層 -> 邏輯層 -> 視圖層

數據層生產數據,視圖層消費數據。邏輯層的主要作用是調度,將數據層的結果實例化為運行時數據,這些運行時數據將被作為視圖狀態,用於渲染到界面中。同時,它接收人機交互信號,調度狀態變化,協調視圖層各個部分做出響應。於此同時,它還可能將狀態數據轉化為實體數據,通過調用數據層通道,將數據發送回源頭,並獲取新數據,以再次完成向視圖層輸送數據原料的任務。所以,數據層處理原始數據,邏輯層生產運行時數據,視圖層消費運行時數據。邏輯層的處理,引用 領域(domain) 的概念來實現:

  1. Domain:處理業務數據邏輯,最大程度提高代碼的複用性
  2. View:處理視圖層的交互邏輯,比如,路由跳轉,事件等

domain-flow.jpeg

後端數據(Data) -> Repository 數據映射層(Model-Mapper) -> 前端領域對象 FDO(FrontEnd Domain Object) -> 數據轉換層(Adapter)->提供給業務視圖(Business View Model)使用

架構圖

domain-architecture.jpeg

技術架構

整體架構設計

本項目採用 領域驅動設計(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 數據
  • 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);
  });
});

最佳實踐

需要不斷總結,通過這部分代碼評審獲得

通用字段處理

  1. 值為數字類型的 YYYYMMDD 時間字段格式統一做 字符串 格式處理
  2. 數據字典轉義
  3. 前端頁面描述語言(字段)(與接口沒有必然聯繫)
  4. 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
}
  1. 視圖使用時,無需關注使用字段的邊界情況(統一在領域層處理)
  2. 不在template模板裏使用 filter方法
  3. 列表數據要給個唯一值,如果接口有返回的話,記得添加下這個字段,不管視圖有沒有使用。作為 for 循環的時候 key
  4. 消滅魔法值
  5. 系統中相同語義的字段保持同名(例如 productCode,productName 等)
  6. 返回的字段,尤其是處理過的,比如保留位數的 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);
  }
}

預期達到的目標

  1. 視圖層儘可能薄: 獲得的數據能夠直接使用到視圖層中,禁止在視圖層中對數據進行轉換、篩選、計算等邏輯操作。
  2. 不寫重複邏輯:遇到相同的邏輯儘可能複用而不是重寫,邏輯函數儘可能寫成可拓展可維護,暴露給團隊其他成員。
  3. 不同職責的代碼進行分層:將不同職責代碼合理分層,每層儘可能純淨,互不影響。
  4. 前端字段不受後端影響: 返回字段進行糾正,字段含義儘可能直觀,在視圖層使用時,能夠更清晰地描述視圖結構。
  5. 可縱觀全局領域: 前端進行領域模塊結構設計時,能夠縱覽整個項目下所有的領域,以及每個領域下具有的邏輯功能。
  6. 更為健壯、安全的 Model
  7. 統一結構
  8. 可以被複用或繼承的
  9. 可以預處理好所有數據邏輯,開發者可以直接使用數據而無需關注處理過程
  10. 有清晰的成功失敗判定邏輯
  11. 提供安全的獲取數據的邏輯
  12. 視圖使用 Model 時,無需關注使用字段的邊界情況,如字段不存在,處理 null 等的情況
  13. Logic(邏輯層)沉澱下來,View(視圖層)穩定下來
  14. 更方便的 Mock
  15. 無論接口以何種形式返回數據、以何種數據格式返回數據,都不會間接對咱們的模型層代碼進行毀壞,更不可能將這種破壞性穿透到 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,或者可能會推出一個移動版,在這些情況下,領域層這一層都是可以直接複用)
  • 為了領域模型的持續衍進(模型存在的目的是讓人們聚焦,聚焦的好處是加強了前端團隊對於業務的理解,思考業務的過程才能讓業務前進)

總結

通過領域驅動設計在前端領域的實踐,我們構建了一個完整的技術架構體系:

  1. 分層清晰:通過明確的分層架構,實現了關注點分離
  2. 技術無關:領域層與具體技術框架解耦,提高了複用性
  3. 類型安全:完整的 TypeScript 支持,提高了代碼質量
  4. 工具完備:完整的開發工具鏈,提高了開發效率
  5. 規範統一:統一的代碼規範和最佳實踐,提高了團隊協作效率

這種架構設計不僅解決了當前面臨的技術問題,更為未來的技術演進和業務發展奠定了堅實的基礎。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.