文章首發於個人博客 https://github.com/mcuking/bl...
最近筆者在使用 DDD / Clean Architecture 思想開發公司內部使用的 CRM,以爲這種分層架構能夠解決目前遇到的問題,因此決定對目前開源的移動端最佳實踐項目進行重構,下面是該項目關於分層架構方面的說明。想了解更多內容請查看源碼:
https://github.com/mcuking/mo...
目前前端開發主要是以單頁應用爲主,當應用的業務邏輯足夠複雜的時候,總會遇到相似下面的問題:html
針對上面所遇到的問題,筆者學習了一些關於 DDD(領域驅動設計)、Clean Architecture 等知識,並收集了相似思想在前端方面的實踐資料,造成了下面這種前端分層架構:前端
其中 View 層想必你們都很瞭解,就不在這裏介紹了,重點介紹下下面三個層的含義:vue
Services 層是用來對底層技術進行操做的,例如封裝 AJAX 請求,操做瀏覽器 cookie、locaStorage、indexDB,操做 native 提供的能力(如調用攝像頭等),以及創建 Websocket 與後端進行交互等。react
其中 Services 層又可細分出 request 層和 translator 層, request 層主要是實現 Services 的大部分功能。而 translator 層主要用於清洗從服務端或客戶端接口返回的數據:刪除部分數據、修改屬性名、轉化部分數據等,通常可定義成純函數形式。下面以本項目實際代碼爲例進行講解。git
從後端獲取 quote 數據:github
export class CommonService implements ICommonService { @m({ maxAge: 60 * 1000 }) public async getQuoteList(): Promise<IQuote[]> { const { data: { list } } = await http({ method: 'post', url: '/quote/getList', data: {} }); return list; } }
向客戶端日曆中同步 Note 數據:web
export class NativeService implements INativeService { // 同步到日曆 @p() public syncCalendar(params: SyncCalendarParams, onSuccess: () => void): void { const cb = async (errCode: number) => { const msg = NATIVE_ERROR_CODE_MAP[errCode]; Vue.prototype.$toast(msg); if (errCode !== 6000) { this.errorReport(msg, 'syncCalendar', params); } else { await onSuccess(); } }; dsbridge.call('syncCalendar', params, cb); } ... }
從 indexDB 讀取某個 Note 詳情數據:後端
import { noteTranslator } from './translators'; export class NoteService implements INoteService { public async get(id: number): Promise<INotebook | undefined> { const db = await createDB(); const notebook = await db.getFromIndex('notebooks', 'id', id); return noteTranslator(notebook!); } }
其中,noteTranslator 就屬於 translator 層,用於訂正接口返回的 note 數據,定義以下:瀏覽器
export function noteTranslator(item: INotebook) { // item.themeColor = item.color; return item; }
另外咱們能夠拓寬下思路,當後端 API 仍在開發的時候,咱們可使用 indexDB 等本地存儲技術進行模擬,創建一個 note-indexDB 服務,先提供給上層 Interactors 層進行調用,當後端 API 開發好後,就能夠建立一個 note-server 服務,來替換以前的服務。只要保證先後兩個服務對外暴露的接口一致,另外與上層的 Interactors 層沒有過分耦合,便可實現快速切換。前端框架
實體 Entity 是領域驅動設計的核心概念,它是領域服務的載體,它定義了業務中某個個體的屬性和方法。例如本項目中 Note 和 Notebook 都是實體。區分一個對象是不是實體,主要是看他是否有惟一的標誌符(例如 id)。下面是本項目的實體 Note:
export default class Note { public id: number; public name: string; public deadline: Date | undefined; ... constructor(note: INote) { this.id = note.id; this.name = note.name; this.deadline = note.deadline; ... } public get isExpire() { if (this.deadline) { return this.deadline.getTime() < new Date().getTime(); } } public get deadlineStr() { if (this.deadline) { return formatTime(this.deadline); } } }
經過上面的代碼能夠看到,這裏主要是以實體自己的屬性以及派生屬性爲主,固然實體自己也能夠具備方法,用於實現屬於實體自身的業務邏輯(筆者認爲業務邏輯能夠分爲兩部分,一部分業務邏輯屬於跟實體強相關的,應該經過在實體類中的方法實現。另外一部分業務邏輯則更多的是實體之間的業務,則能夠放在 Interactors 層中實現)。只是本項目中尚未涉及,在這裏就不做更多說明了,有興趣的能夠參考下面列出來的筆者翻譯的文章:可擴展的前端#2--常見模式(譯)。
另外筆者認爲並非全部的實體都應該按上面那樣封裝成一個類,若是某個實體自己業務邏輯很簡單,就沒有必要進行封裝,例如本項目中 Notebook 實體就沒有作任何封裝,而是直接在 Interactors 層調用 Services 層提供的 API。畢竟咱們作這些分層最終的目的就是理順業務邏輯,提高開發效率,因此沒有必要過於死板。
Interactors 層是負責處理業務邏輯的層,主要是由業務用例組成。通常狀況下 Interactor 是一個單例,它使咱們可以存儲一些狀態並避免沒必要要的 HTTP 調用,提供一種重置應用程序狀態屬性的方法(例如:在失去修改記錄時恢復數據),決定何時應該加載新的數據。
下面是本項目中 Common 的 Interactors 層提供的公共調用的業務:
class CommonInteractor { public static getInstance() { return this._instance; } private static _instance = new CommonInteractor(new CommonService()); private _quotes: any; constructor(private _service: ICommonService) {} public async getQuoteList() { // 單例模式下,將一些基本固定不變的接口數據保存在內存中,避免重複調用 // 但要注意避免內存泄露 if (this._quotes !== undefined) { return this._quotes; } let response; try { response = await this._service.getQuoteList(); } catch (error) { throw error; } this._quotes = response; return this._quotes; } }
經過上面的代碼能夠看到,Sevices 層提供的類的實例主要是經過 Interactors 層的類的構造函數獲取到,這樣就能夠達到兩層之間解耦,實現快速切換 service 的目的了,固然這個和依賴注入 DI 仍是有些差距的,不過已經知足了咱們的需求。
另外 Interactors 層還能夠獲取 Entities 層提供的實體類,將實體類提供的與實體強相關的業務邏輯和 Interactors 層的業務邏輯融合到一塊兒提供給 View 層,例如 Note 的 Interactors 層部分代碼以下:
class NoteInteractor { public static getInstance() { return this._instance; } private static _instance = new NoteInteractor( new NoteService(), new NativeService() ); constructor( private _service: INoteService, private _service2: INativeService ) {} public async getNote(notebookId: number, id: number) { try { const note = await this._service.get(notebookId, id); if (note) { return new Note(note); } } catch (error) { throw error; } } }
固然這種分層架構並非銀彈,其主要適用的場景是:實體關係複雜,而交互相對模式化,例如企業軟件領域。相反實體關係簡單而交互複雜多變就不適合這種分層架構了。
在具體業務開發實踐中,這種領域模型以及實體通常都是有後端同窗肯定的,咱們須要作的是,和後端的領域模型保持一致,但不是同樣。例如同一個功能,在前端只是一個簡單的按鈕,而在後端則可能至關複雜。
另外須要明確的是,架構和項目文件結構並非等同的,文件結構是你從視覺上分離應用程序各部分的方式,而架構是從概念上分離應用程序的方式。你能夠在很好地保持相同架構的同時,選擇不一樣的文件結構方式。沒有完美的文件結構,所以請根據項目的不一樣選擇適合你的文件結構。
最後引用螞蟻金服數據體驗技術的《前端開發-領域驅動設計》文章中的總結做爲結尾:
要明白,驅動領域層分離的目的並非頁面被複用,這一點在思想上必定要轉化過來。領域層並非由於被多個地方複用而被抽離。它被抽離的緣由是:
- 領域層是穩定的(頁面以及與頁面綁定的模塊都是不穩定的)
- 領域層是解耦的(頁面是會耦合的,頁面的數據會來自多個接口,多個領域)
- 領域層具備極高複雜度,值得單獨管理(view 層處理頁面渲染以及頁面邏輯控制,複雜度已經夠高,領域層解耦能夠輕 view 層。view 層儘量輕量是咱們架構師 cnfi 主推的思路)
- 領域層以層爲單位是能夠被複用的(你的代碼可能會拋棄某個技術體系,從 vue 轉成 react,或者可能會推出一個移動版,在這些狀況下,領域層這一層都是能夠直接複用)
- 爲了領域模型的持續衍進(模型存在的目的是讓人們聚焦,聚焦的好處是增強了前端團隊對於業務的理解,思考業務的過程才能讓業務前進)
推薦幾個相關的類庫:
推薦幾篇相關文章: