什么是前端數(shù)據(jù)建模?
提到數(shù)據(jù)建模,大多數(shù)人第一時(shí)間想到的都是和后端、數(shù)據(jù)這些方面相關(guān)的內(nèi)容。而前端數(shù)據(jù)建模,似乎讓人感到陌生。
那么我通過(guò)回答下面幾個(gè)問(wèn)題,來(lái)統(tǒng)一一下我們對(duì)前端數(shù)據(jù)建模概念的理解。
什么是數(shù)據(jù)建模?
數(shù)據(jù)建模是對(duì)業(yè)務(wù)邏輯所使用的數(shù)據(jù)以及這些數(shù)據(jù)之間的關(guān)系進(jìn)行分析和定義的過(guò)程。
數(shù)據(jù)建模有何意義?
- 為團(tuán)隊(duì)成員之間的協(xié)作創(chuàng)建一種統(tǒng)一的模式。
- 通過(guò)定義數(shù)據(jù)需求和使用情況來(lái)發(fā)現(xiàn)改進(jìn)業(yè)務(wù)流程的機(jī)會(huì)。
- 節(jié)省代碼維護(hù)成本。
- 減少錯(cuò)誤,改進(jìn)數(shù)據(jù)完整性。
前端有必要進(jìn)行數(shù)據(jù)建模嗎?
在我的理念里,前端應(yīng)該始終以用戶(hù)體驗(yàn)為核心。但是事實(shí)上,前端無(wú)法完全脫離業(yè)務(wù)邏輯。那既然涉及到業(yè)務(wù),那就應(yīng)該構(gòu)建模型。
前端數(shù)據(jù)建模有何優(yōu)缺點(diǎn)?
研發(fā)階段:需要花費(fèi)時(shí)間和精力進(jìn)行數(shù)據(jù)和關(guān)系的定義。
維護(hù)階段:節(jié)省邏輯變更后的修改時(shí)間,讓頁(yè)面保持相對(duì)穩(wěn)定。
綜上所述,在業(yè)務(wù)邏輯相對(duì)復(fù)雜的系統(tǒng)中,前端數(shù)據(jù)建模是很有必要的。而在一些臨時(shí)性的項(xiàng)目中,則沒(méi)有必要進(jìn)行數(shù)據(jù)建模。
頁(yè)面與模型
數(shù)據(jù)建模,就是圍繞數(shù)據(jù)建立一個(gè)模型。
在大多數(shù)前端研發(fā)人員眼里,通常都會(huì)以頁(yè)面(Page)為緯度來(lái)切割業(yè)務(wù)內(nèi)容。而不會(huì)以模型(Model)為緯度來(lái)切割業(yè)務(wù)內(nèi)容。這會(huì)導(dǎo)致一個(gè)問(wèn)題,前端難以自己形成一個(gè)具有內(nèi)聚力的系統(tǒng)。
和頁(yè)面功能相關(guān)的內(nèi)容都會(huì)存到該頁(yè)面中。如果其他頁(yè)面需要依賴(lài)這個(gè)頁(yè)面中的某些數(shù)據(jù)或者某些功能,很多人會(huì)選擇復(fù)制一份對(duì)應(yīng)的代碼來(lái)完成功能。因?yàn)檫@種做法趨近于人類(lèi)的原始本能-「拒絕思考」。稍微有點(diǎn)追求的人,就會(huì)把多個(gè)頁(yè)面中會(huì)共用的數(shù)據(jù)和函數(shù)提取到某個(gè)獨(dú)立的位置,然后由這兩個(gè)頁(yè)面單獨(dú)引用。這就是所謂的「封裝」。但這種封裝并沒(méi)有邏輯性,它只是遵循了「多個(gè)頁(yè)面引用了一個(gè)頁(yè)面中的某個(gè)相同的功能,就要把這個(gè)功能抽離出去,封裝成一個(gè)獨(dú)立的函數(shù)」。這種做法并沒(méi)有任何問(wèn)題。但是,做的程度還是不夠。
舉個(gè)例子,在開(kāi)發(fā)階段,頁(yè)面 Page A、B、C 都依賴(lài)了功能 Func X。于是你封裝了 Func X。但是后面業(yè)務(wù)邏輯發(fā)生了變化,Page A、B 和 Page C 所需要處理的邏輯不同了。這時(shí)你要么在 Page C 中把對(duì) Func X 的依賴(lài)全部移除,在 Page C 中寫(xiě)自己的邏輯?;蛘呤窃?Func X 中增加一段判斷代碼,將 Page C 和 Page A、B 進(jìn)行區(qū)分,然后調(diào)用不同的邏輯。
無(wú)論哪種做法,在代碼的維護(hù)上都不算簡(jiǎn)單。因?yàn)槟愕南到y(tǒng)是完全不穩(wěn)定的、是易變的。為什么會(huì)這樣呢?是因?yàn)槿狈σ粋€(gè)模型。
以目前的技術(shù)角度分析,一個(gè)前端頁(yè)面,可以拆分為幾個(gè)部分:UI、路由、接口、數(shù)據(jù)和業(yè)務(wù)邏輯。其中 UI 是純前端層面的東西,復(fù)用是通過(guò)組件化或者 CSS 來(lái)實(shí)現(xiàn)的。路由是頁(yè)面的一部分,不可復(fù)用。剩下的接口、數(shù)據(jù)和業(yè)務(wù)邏輯,都是在一定程度上可以復(fù)用的。
而一個(gè)模型,主要是由數(shù)據(jù)和業(yè)務(wù)邏輯組成的。
數(shù)據(jù)建模該做什么?
目錄結(jié)構(gòu)劃分
一個(gè)好的目錄結(jié)構(gòu)應(yīng)該是高內(nèi)聚、低耦合的。
很多人喜歡將文件夾根據(jù)技術(shù)職責(zé)進(jìn)行區(qū)分,如下:

這并不是我推薦的做法。我認(rèn)為一個(gè)高內(nèi)聚的目錄結(jié)構(gòu)應(yīng)該是根據(jù)模塊區(qū)分的,如下:

文件夾的層次應(yīng)該盡量扁平,而不是深層。
定義接口
定義接口的過(guò)程,基本上就是定義數(shù)據(jù)模型的過(guò)程。
因?yàn)榻涌谑欠€(wěn)定的,而實(shí)現(xiàn)是易變的。
實(shí)現(xiàn)一個(gè)業(yè)務(wù)功能,隨便是一個(gè)程序員都可以做到,這并沒(méi)有什么門(mén)檻。真正難的是如何對(duì)業(yè)務(wù)邏輯進(jìn)行抽象,定義成接口。這存在一定門(mén)檻。
拿購(gòu)物車(chē)這個(gè)場(chǎng)景舉例。

購(gòu)物車(chē)的數(shù)據(jù)分為如下幾部分:
- 商品排序條件。
- 商品過(guò)濾條件。
- 圖片顯示狀態(tài)。
- 商品列表加載狀態(tài)。
- 購(gòu)物車(chē)商品列表加載狀態(tài)。
- 商品列表。
- 購(gòu)物車(chē)商品列表。
- 商品總數(shù)。
- 商品總價(jià)。
改變購(gòu)物車(chē)數(shù)據(jù)的途徑有如下幾個(gè)操作:
- 初始化購(gòu)物車(chē)。
- 添加商品到購(gòu)物車(chē)。
- 在購(gòu)物車(chē)刪除商品。
- 增加商品。
- 減少商品。
- 提交下單。
根據(jù)上述的文字定義,我們可以利用 TypeScript 來(lái)定義出一個(gè)接口。
interface IGoodscartModel {
filter: Filter;
order: string;
imageVisible: boolean;
goodsList: GoodsList;
goodscartList: GoodscartList;
total: () => number;
quantity: () => number;
initialGoodsList: () => Promise<void>;
initialGoodscart:() => Promise<void>;
addToCart: (goodsId: string) => void;
plusGoods: (goodsId: string) => void;
minusGoods: (goodsId: string) => void;
removeGoods: (goodsId: string) => void;
submitOrder: () => Promise<void>;
}
一旦定義出這個(gè)接口,實(shí)現(xiàn)也就無(wú)關(guān)緊要了。因?yàn)闃I(yè)務(wù)邏輯的細(xì)節(jié),隨便一個(gè)思維邏輯能力正常的人都可以寫(xiě)得出來(lái)。
對(duì)邏輯而言,接口就像是一張?jiān)O(shè)計(jì)圖紙。有了這張圖紙,即使是最廉價(jià)的苦力一樣可以建造出瓊樓玉宇。
當(dāng)然,其中有一些細(xì)節(jié)。
比如購(gòu)物車(chē)商品總量和購(gòu)物車(chē)商品總價(jià),是通過(guò)計(jì)算而得到的,屬于計(jì)算屬性,所以在模型的定義中,將其定義為函數(shù),而非基礎(chǔ)類(lèi)型。
比如初始化商品列表和購(gòu)物車(chē)商品列表,是一種異步操作,所以定義為返回 Promise 的函數(shù)。
你可能會(huì)發(fā)現(xiàn),這不就是面向?qū)ο缶幊讨械闹R(shí)嗎?沒(méi)錯(cuò),這就是面向?qū)ο笾械姆庋b、繼承、多態(tài)和組合。
區(qū)分?jǐn)?shù)據(jù)的性質(zhì)
雖然在上面的模型中定義了很多個(gè)數(shù)據(jù)字段,但其實(shí)并不是所有的數(shù)據(jù)都一定屬于模型。有一些臨時(shí)性的數(shù)據(jù),是應(yīng)該存儲(chǔ)在頁(yè)面中的。
區(qū)分?jǐn)?shù)據(jù)性質(zhì)的方法有很多,但我認(rèn)為,對(duì)前端而言并不需要過(guò)度復(fù)雜,只需要根據(jù)一個(gè)條件來(lái)區(qū)分即可-數(shù)據(jù)從何而來(lái)?
因此,可以區(qū)分為系統(tǒng)外部數(shù)據(jù)和系統(tǒng)內(nèi)部數(shù)據(jù)。
由于 Web 應(yīng)用的特殊性,基本上所有的 Web 應(yīng)用都依賴(lài)系統(tǒng)外部數(shù)據(jù)。而這些數(shù)據(jù)通常都依賴(lài)接口提供。這種系統(tǒng)外部數(shù)據(jù)相對(duì)穩(wěn)定和純凈。
而系統(tǒng)內(nèi)部數(shù)據(jù),又可以根據(jù)業(yè)務(wù)的重要程度來(lái)區(qū)分為多種類(lèi)型。比如存儲(chǔ)在內(nèi)存中的值,可以稱(chēng)之為臨時(shí)值。頁(yè)面刷新時(shí),這些值就會(huì)被重置。而重置的值,通常來(lái)自于兩個(gè)位置,一個(gè)是存儲(chǔ)在本地存儲(chǔ)(WebStorage)中的值,可以稱(chēng)之為緩存值。另一個(gè)是寫(xiě)在代碼中的值,可以稱(chēng)之為默認(rèn)值。
這幾類(lèi)數(shù)據(jù)之間是存在流轉(zhuǎn)關(guān)系的,也就是意味著不同性質(zhì)的值,在系統(tǒng)運(yùn)行的不同階段會(huì)相互轉(zhuǎn)換。
比如頁(yè)面加載時(shí),先調(diào)用接口,獲取系統(tǒng)外部數(shù)據(jù),獲取成功后,系統(tǒng)外部數(shù)據(jù)轉(zhuǎn)化為臨時(shí)值,用戶(hù)對(duì)臨時(shí)值進(jìn)行操作變更后,再對(duì)數(shù)據(jù)進(jìn)行某種提交操作,將臨時(shí)值同步到系統(tǒng)外部。
用戶(hù)的某些操作也可以將臨時(shí)值存儲(chǔ)到本地緩存,變成緩存值。
用戶(hù)離開(kāi)網(wǎng)站,再次回歸時(shí),會(huì)讀取緩存值替換為臨時(shí)值。
總結(jié)而言,數(shù)據(jù)的類(lèi)型分為如下幾類(lèi):
- 系統(tǒng)外部數(shù)據(jù)(通常來(lái)自于接口)
- 系統(tǒng)內(nèi)部數(shù)據(jù)
- 默認(rèn)值(通常硬編碼在代碼中)
- 臨時(shí)值(通常存儲(chǔ)在內(nèi)存中)
- 緩存值(通常存儲(chǔ)在本地緩存中)
這幾類(lèi)數(shù)據(jù)之間又可以相互同步與轉(zhuǎn)換。
根據(jù)上面的歸類(lèi),我們可以發(fā)現(xiàn),排序、過(guò)濾與圖片顯示隱藏狀態(tài)應(yīng)該歸屬于頁(yè)面,而不是模塊。
因?yàn)檫@些數(shù)據(jù)的功能是和頁(yè)面強(qiáng)綁定的,或者說(shuō),這些功能本身就屬于頁(yè)面,不應(yīng)該再割裂到模塊中。
與框架結(jié)合實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式
數(shù)據(jù)模型的定義和框架并沒(méi)有關(guān)系。但我們通常需要配合數(shù)據(jù)響應(yīng)式來(lái)實(shí)現(xiàn)操作數(shù)據(jù)自動(dòng)更新 UI 的功能。否則就需要在模型中處理 UI 的變化,會(huì)導(dǎo)致模型依賴(lài) UI,這不是我們想要的。
目前最主流的 UI 框架,都具有數(shù)據(jù)的響應(yīng)式。但我更推薦和一些狀態(tài)管理庫(kù)進(jìn)行結(jié)合。
因?yàn)榍懊嬗刑岬?,只有相?duì)有規(guī)模的項(xiàng)目才會(huì)用到數(shù)據(jù)建模。而在具備相對(duì)規(guī)模的項(xiàng)目中,使用狀態(tài)管理庫(kù)可以讓我們的開(kāi)發(fā)事半功倍。
下面是使用 pinia 的代碼示例:
import { defineStore } from "pinia";
import { GoodscartModel } from "./model";
const goodscart = new GoodscartModel();
const {
goodsList,
goodscartList,
goodsListLoading,
total,
quantity,
initialGoodsList,
plusGoods,
minusGoods,
addToCart,
removeGoods,
order,
} = goodscart;
export const useGoodscartStore = defineStore("goodscart", {
state() {
return {
goodscartList,
goodsList,
goodsListLoading,
};
},
actions: {
initialGoodsList,
plusGoods,
minusGoods,
addToCart,
removeGoods,
order,
},
getters: {
total,
quantity,
},
});
增加接口的防腐層
定義模型,對(duì)前端而言還有另一層意義。就是可以起到隔離外部和內(nèi)部的關(guān)聯(lián)、提高系統(tǒng)穩(wěn)定性的作用。
我們把前端想像成一個(gè)系統(tǒng),系統(tǒng)中存在很多外部的資源,圖片、視頻、字體、圖標(biāo)、樣式、腳本等等,以及非常重要的接口。
正常情況下,在開(kāi)發(fā)過(guò)程中,接口中的字段名稱(chēng)、字段類(lèi)型以及數(shù)據(jù)結(jié)構(gòu)都是非常易變的,特別是遇到不專(zhuān)業(yè)或者對(duì)代碼追求較低的后端人員時(shí),對(duì)接接口將會(huì)是一件非常令人感到煩惱和痛苦的事情。
從根本上看,造成這個(gè)現(xiàn)象的原因就是接口的變動(dòng)不在我們的掌控之中,它是系統(tǒng)外部的不可控的事物。
那么該如何減少和降低接口變化導(dǎo)致的前端變動(dòng)呢?
答案很簡(jiǎn)單,增加防腐層。
防腐層 Anti Corruption Layer 是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中的一個(gè)概念,用于隔離兩個(gè)系統(tǒng),允許兩個(gè)系統(tǒng)之間在不知道對(duì)方領(lǐng)域知識(shí)的情況下進(jìn)行集成。
主要進(jìn)行的是兩個(gè)系統(tǒng)之間的模型(model)或者協(xié)議(protocol)的轉(zhuǎn)換,并且最終目的是為了系統(tǒng)使用者的方便而不是系統(tǒng)提供者的方便,進(jìn)一步的解釋就是把系統(tǒng)提供者的模型轉(zhuǎn)換為系統(tǒng)使用者的模型。
這個(gè)概念在前端落地到代碼中非常簡(jiǎn)單,可能就是一個(gè)關(guān)系映射函數(shù)而已。
function dataMap(rawData) {
// ...logic
return formatData;
}
現(xiàn)在,按照我們上述理論構(gòu)建的系統(tǒng)中,是存在三個(gè)層級(jí)的,自下而上分別是防腐層、模型層和 UI 層,如圖所示:

采用這種方式構(gòu)建的系統(tǒng),無(wú)論接口以何種方式返回?cái)?shù)據(jù)、以何種數(shù)據(jù)格式返回?cái)?shù)據(jù),都不會(huì)直接對(duì)我們的模型層代碼進(jìn)行破壞,更不可能將這種破壞性穿透到 UI 層代碼中。
當(dāng)然,如果是整條業(yè)務(wù)流程都改變了,那這種破壞性會(huì)影響到所有層級(jí),這是不可避免的。
總結(jié)
前端數(shù)據(jù)建模是一種研發(fā)中大型規(guī)模項(xiàng)目和產(chǎn)品的可實(shí)踐方法論,它通過(guò)設(shè)計(jì)一種模型來(lái)幫助我們降低系統(tǒng)的復(fù)雜程度,這看上去很吸引人。
但我們不能否認(rèn)的是,從前后端分離這種架構(gòu)在業(yè)界真正流行開(kāi)始(以 2016 年計(jì)算),距今已經(jīng)有六七年的時(shí)間了,前端數(shù)據(jù)建模從未被統(tǒng)一或者被作為某種開(kāi)發(fā)模式廣泛應(yīng)用。
那我們不禁要思考了,前端數(shù)據(jù)建模到底是不是一件正確的事?
根據(jù)我的經(jīng)歷來(lái)看,有幾個(gè)原因:
- 宏觀(guān)業(yè)態(tài):
- 前端程序員跳槽頻率高。根據(jù)個(gè)人招聘經(jīng)歷和國(guó)內(nèi)幾個(gè)招聘大平臺(tái)的數(shù)據(jù)來(lái)看,前端程序員的平均跳槽頻率不足 1 年。既然大家都是短工,那么為什么要思考那么長(zhǎng)遠(yuǎn)的事情呢?代碼被搞成了「屎山」,我跳槽便是,為什么要死磕呢?又不是找不到工作。
- 大多數(shù)公司不是特別重視前端質(zhì)量。因?yàn)闊o(wú)論是大公司還是小公司,無(wú)論是傳統(tǒng)行業(yè)還是互聯(lián)網(wǎng)行業(yè),都會(huì)更看重?cái)?shù)據(jù)的重要性。因?yàn)榫W(wǎng)站只是展示數(shù)據(jù)、操作數(shù)據(jù)的一種手段。數(shù)據(jù)才能決定流程和金錢(qián)。甚至很多傳統(tǒng)行業(yè)的公司,連 UI 都沒(méi)有。只有中大型的互聯(lián)網(wǎng)企業(yè),才會(huì)有用戶(hù)體驗(yàn)部門(mén)。既然公司都不重視,為什么要做呢?
- 微觀(guān)業(yè)態(tài):
- 從業(yè)人員水平低。前端的門(mén)檻相對(duì)較低,很多人還處于實(shí)現(xiàn)某個(gè) UI 效果而抓耳撓腮的狀態(tài),哪還有什么精力投入到業(yè)務(wù)上來(lái)?流程跑不通?找后端哇。連問(wèn)題都意識(shí)不到的人,怎么可能會(huì)去解決問(wèn)題呢?
- 高手大多不屑于碰業(yè)務(wù)。編譯構(gòu)建、框架、組件化、數(shù)據(jù)可視化、WebGL、CI/CD、LowCode/NoCode、工具鏈......前端可以深挖的方向太多太多了,但談?wù)摰秸l(shuí)技術(shù)好,基本上都是上面這些,很少有人會(huì)說(shuō),某人業(yè)務(wù)代碼寫(xiě)得好,技術(shù)好牛啊。因?yàn)闃I(yè)務(wù)在大多數(shù)人眼里只是基本操作而已。再者,我前面也有說(shuō),前端的未來(lái)應(yīng)該是交互和體驗(yàn),而非業(yè)務(wù)。
但無(wú)論何種原因,都不能否認(rèn)前端數(shù)據(jù)建模對(duì)業(yè)務(wù)的意義和價(jià)值。
時(shí)刻記住,我們要成為工程師,而不是碼農(nóng)。