本文首發於 歐雷流。因爲我會時不時對文章進行補充、修正和潤色,爲了保證所看到的是最新版本,請閱讀 原文。
若是把「客戶端」想成是樓,把「數據」想成是水——「Model」就是這幢樓的蓄水池,提供充足的水源;「ViewModel」是將蓄水池裏的水進行淨化等加工的地方,而後輸送給挨家挨戶;「View」部分的每一個 UI 組件就是「挨家挨戶」,對水進行消費的地方。前端
模型是人們根據事物特徵將它們分類並抽象後的結果,建模是人們認知世界的一種方式。web
數字世界這種虛擬空間,裏面本無一物,是個須要被人開墾的空虛的世界。那麼人該如何打造數字世界呢?數據庫
就像《聖經》裏描述的——上帝按照本身的樣子創造了亞當這個世上第一我的類,又從他身上取下一根肋骨創造了夏娃這個世界上第二我的類。在這裏,上帝將本身做爲參照提取特徵抽象出祂所認爲的「人」的模型,並根據這個模型創造出「亞當」和「夏娃」。編程
人在打造數字世界時必然會參照本身所存在的而且是本身所認知的世界,由於人不可能想像出本身沒法認知的事物。人們所抽象的現實世界的事物的模型,就成了建設數字世界的基礎,而數據則爲構造數字世界的基本單元,數字世界成了現實世界的映射。小程序
模型是數字世界萬物的概念,程序是將概念具像化的工具,打造數字世界需從建模開始。後端
上面說了在打造數字世界時首先要創建模型,而後以模型爲中心開始建造。那麼要怎樣進行建模呢?微信小程序
至今爲止,軟件工程發展這麼多年,產生了不少方法論,其中「領域驅動設計」在構建大型軟件時是被普遍採納的實踐方法。它的核心就是針對問題域分析並創建領域模型,理出模型間的關係及業務邏輯。瀏覽器
領域驅動設計最經常使用在商業層面的模型上,如:包含名稱、編號、規格、出廠日期等信息的商品模型;同時也能夠用在技術層面的模型上,如:包含名稱、編碼、字段、關係、約束等用來描述模型的信息的模型。前者稱之爲「業務模型」,後者則是「元模型」。業務模型能夠被元模型描述。緩存
若是把模型映射爲數據庫表,那麼元模型所對應的表中的每條記錄都是元數據,業務模型所對應的表中的每條記錄都是業務數據。微信
標準的 MVVM 架構是 Model-View-ViewModel 三部分:
而這裏所說的以下圖所示:
從圖中能夠看到,多了個「Action」,因此實際上應該是 Model-View-ViewModel-Action 四部分。它們之間彼此分離,以組合的方式協同工做。
爲了講究對稱美,將這種架構簡稱爲「MVAVM」。
模型的主要職責是前、後端協議處理,以及對數據進行讀寫操做。
前、後端協議的處理包括元數據適配和 HTTP 請求構造。與後端對接的工做都控制在這一層,其餘層的運做都基於這層適配後的結果。
在這層中進行讀寫的數據,既有業務數據又有元數據。元數據只加載一次,將適配後的結果進行緩存;業務數據只暫時緩存還沒有持久化的處於草稿狀態的記錄,持久化以後會將其刪除。
VM 的職責很單純,就是處理業務數據流轉相關的邏輯,即數據的分發、彙總與聯動。理論上,在這層不直接進行任何與請求服務、執行動做相關的處理。
正如文章開頭所說——在一個應用中,數據是像水同樣不斷流動的,在此過程當中,VM 應該起到鋪設輸送管線與在特定節點對數據進行處理的做用。根據這一特色,能夠考慮採用管道和過濾器模式:
每一個 VM 實例都來源於數據,是數據的變形,是具有能力的數據。
根據數據源的形態,VM 實例大體分爲列表、對象和值三種。若是值是布爾、數字、字符串等簡單類型,那就即刻終止;若值爲對象、列表等複雜類型,則要遞歸下去,直到末端爲簡單類型。
須要注意的是,VM 實例與數據一一對應,其實質就是數據自己,而不是數據的容器。也就是說,VM 實例不是裝水的瓶子,不能把已經裝的水倒掉換些水進來,而是一塊兒丟棄。
任何對象的生命週期均可粗略地分爲初始化、活動中與銷燬三個階段。
在初始化時根據策略獲取自身數據源,與上級 VM 實例建立的流進行對接造成數據管道,而後建立向外推送自身變化的流。
活動期間就是不斷地與外界進行數據交換:
在處理被提交的輸入數據時會對其進行保留,併發出有數據提交的信號
銷燬時作些清理、善後的工做,如:移除子 VM 引用,取消訂閱等。
在活動期間,數據在各層 VM 實例所連通的數據管道中流轉時會發生變化,爲了方便在不一樣場景下對數據進行處理,須要在初始化 VM 實例時將數據源進行備份,並生成幾個拷貝:初始值(initial value)、默認值(default value)、原始值(data source)和當前值(current value)。
其中,初始值是獲取到數據源那一刻的值,默認值在沒有指定的狀況下與初始值相同,它們都是一經初始化就不會改變的;當前值是自身一段時間內的數據變動,是最新的但不肯定的值,能夠理解爲是一種草稿狀態的值;原始值只有在上級當前值變更,接收到下級提交的數據或強制更新時纔會更新,它是階段性的肯定值,能夠看做是可靠的數據。
「原始值」中的「原始」也許會容易讓人誤解。在這裏,它的含義是相對於「當前值」來講,它是「原始」的,能夠拿來做爲參考的,而不是「最初的值」。表達「最初的值」的含義的是「初始值」。
原始值與當前值的區別與特色是:
數據在流轉時遵循如下幾個原則:
總的來講,只有在上級引起數據變更的狀況下,纔會發生上到下的數據流動。
各層級 VM 實例之間數據的傳遞過程大體以下:
在數據經過上下級 VM 實例之間所連通的數據管道,即數據的分發與彙總時,會通過一系列相對獨立的邏輯的處理,如:數據的裁剪、變形、校驗等。每一段處理邏輯就是一個「過濾器」,每一個過濾器均可以拋出異常終止後續的操做。
每一個 VM 實例都會提供一些供視圖進行狀態同步、數據聯動等的接口:
interface IViewModel<ValueType> { // 獲取原始值 getDataSource(): ValueType; // 設置原始值 setDataSource(value: ValueType): void; // 獲取當前值 getCurrentValue(): ValueType; // 設置當前值 setCurrentValue(value: ValueType): void; // 監聽當前值變化 watch(handler: Function): Subscription; // 監聽提交等事件 on(handlers: {[key: string]: Function}): void; // 在分發數據的過濾器隊列頭部添加一個過濾器 prependDispatchFilter(filter: Function): void; // 在分發數據的過濾器隊列尾部添加一個過濾器 appendDispatchFilter(filter: Function): void; // 在提交數據的過濾器隊列頭部添加一個過濾器 prependCommitFilter(filter: Function): void; // 在提交數據的過濾器隊列尾部添加一個過濾器 appendCommitFilter(filter: Function): void; // 獲取上級 VM 實例 getParent(): IViewModel; // 獲取下級 VM 實例 getChildren(): IViewModel[]; // 獲取模型,返回值包含發請求的 API getModel(): IModel; // 執行動做,不指定 VM 實例的話使用當前 VM 實例 call(action: IAction, vm?: IViewModel): Promise<void>; }
關於「動做」是什麼,在以前的文章《我來聊聊配置驅動的視圖開發》中已經說起——
「動做」是一段完整邏輯的抽象,與函數至關,用來描述且只描述「作什麼事」,不描述「長什麼樣」。一個可複用的動做應該是原子化的。
根據邏輯的定義、執行所在位置,能夠分爲客戶端動做(廣義)與服務端動做:客戶端動做(廣義)是定義而且執行在前端;服務端動做是定義而且執行在後端。
客戶端動做(廣義)根據具體場景的用途及特性,又可分爲如下幾種動做:
- 路由動做
- CRUD 動做
- 客戶端動做(狹義)
- 組合動做
其中,路由動做的做用是進行頁面跳轉;CRUD 動做是對數據進行操做;客戶端動做(狹義)是單純的一段邏輯,能夠簡單理解爲是一個 JS 函數;組合動做用於將其餘類型的動做「打包」處理,就像一個調用了其餘函數的函數。
服務端動做能夠簡單粗暴地理解爲是很是規 CRUD 的後端接口。
——歐雷《我來聊聊配置驅動的視圖開發》
除了客戶端動做(狹義)須要本身寫邏輯以外,其餘的都是徹底根據元數據執行。
路由動做是進行頁面跳轉的動做,這裏的「頁面」是廣義的,根據情景,能夠理解爲是瀏覽器窗口中的整個頁面,也能夠理解爲是某個視圖所在的宿主。在這個體系裏,將視圖跳轉的動做稱爲「視圖動做」,跳轉到當前應用以外的頁面的叫作「頁面動做」。
既然組合動做是將其餘類型的動做「打包」處理的動做,那麼它就得具有調整被「打包」的動做的執行順序及若是某個動做執行失敗要終止後續處理等的控制能力。實現方式能夠參考 continuation 在 JS 中的實踐應用。
解析視圖描述信息,並根據注入的 VM 實例所攜帶的數據進行渲染。
視圖中能夠本身發請求,但理論上只能發獲取數據的請求,不能發修改數據的,修改數據須要經過 VM 實例或動做去處理。
視圖這部分又細分爲描述層、包裝層和渲染層:
「描述層」即「DSL 層」,經過內部定義的 XML 標籤集去描述一個界面中的 UI 元素、數據等信息,是一種相較於 JSON 來講更符合直覺,更容易理解的界面配置。
包裝層的做用是將描述層的標籤轉換爲實際渲染的部件,渲染層則是具體的運行時環境。不像描述層那樣相對獨立,包裝層和描述層能夠說是不能分離的,包裝層在將描述層的標籤轉換爲實際渲染的部件時須要渲染層的支撐。
包裝層的包裝器與描述層的標籤集裏的標籤能夠說是一一對應的,標籤經過包裝器轉換爲部件集裏的部件,但部件卻不必定與包裝器一一對應,極可能一個包裝器對應多個同類別的部件。
在 web 前端開發中,HTML 是一種 DSL,CSS 也是一種 DSL。在這個模型驅動的體系裏,內部定義的用來描述一個界面中的 UI 元素、數據等信息的 XML 標籤集就是 DSL。
描述層是運行時無關的,可以在任何平臺及運行時庫中運行。
平常工做交流中常會說到「模板」,這個詞在不一樣語境中表明着不一樣的東西。在這個體系中,當在開發的語境裏時,若是沒帶任何修飾詞,應該就是指「一段描述界面配置的標籤」,如:
<view widget="form"> <group title="基本信息" widget="fieldset"> <field name="name" label="姓名" widget="input" /> <field name="gender" label="性別" widget="radio" /> <field name="age" label="年齡" widget="number" /> <field name="birthday" label="生日" widget="date-picker" /> </group> <group title="寵物" widget="fieldset"> <field name="dogs" label="🐶" widget="select" /> <field name="cats" label="🐱" widget="select" /> </group> <action ref="submit" text="提交" widget="button" /> <action ref="reset" text="重置" widget="button" /> <action ref="cancel" text="取消" widget="button" /> </view>
模板若是不去解析,它就只是一段普通的文本,沒有任何做用。
要對模板進行解析,得有一套對應模板上標籤的標籤集,還需有能將純文本的模板藉助標籤集轉換成 JS 對象的解析器。
標籤集中的每一個標籤,也能夠稱爲「元素」。考慮到擴展性,須要有元素註冊的機制,這有助於元素屬性等的規範和管理。
在註冊元素時,須要指定一些關鍵信息,如:元素名、標籤名、屬性描述符、行爲。「屬性描述符」主要是用來聲明該元素所支持的屬性及其值的類型;「行爲」則用來告知該元素在解析後是做爲父節點的子節點仍是屬性存在。
全部做爲子節點存在的元素,基本都對應一個具體的部件。從表意上來講,這些元素分爲兩類:一類是較爲抽象的,另外一類是較爲具象的。較爲抽象的元素只有一個,它僅單純地表達是「部件」這個含義,並無更具體地體現出是幹嗎的;其餘的元素都是具象的,像 <view>
、<field>
等,從命名就知道是用於哪方面的。
所謂「節點」,就是將模板中的元素編譯解析後所轉換成的 JS 對象。整個模板會解析成一個樹狀結構的 JS 對象,也就是「節點樹」。每一個節點能夠有一些方法,用來新增子節點、刪除自身、獲取或修改自身信息等。
包裝層的做用是將描述層的產物,即節點,轉換爲部件。在 DSL 節點與部件之間起到橋樑做用的,就是「包裝器」。
包裝器裏面聚集了描述層所產出的一些信息,如:要生成到界面中的節點的屬性及其對應部件的配置等。會根據節點所對應的元素所引用的部件的標識符去查找相應的部件,若是沒指定引用則使用默認的,並將其餘屬性及相關聯部件的配置做爲部件的屬性進行傳遞。
簡單來講,渲染層就是像 Vue、React、iOS、Android、微信小程序之類的庫/框架、平臺的運行時環境。進行實際渲染的組件、部件及做爲橋樑的包裝器都對其依賴,這就須要在每一個運行環境下都得有一套包裝器、組件和部件的封裝。
模型驅動架構正符合我在《前端有架構嗎?》所提到的架構設計的首要核心原則——以不變爲中心。
在這個體系中,根據不一樣層、不一樣角色的設計目標,須要採用適合的編程範式,而不侷限於一種。如:模型主要用 OOP,VM 使用 OOP 和 FRP,動做用到 FP。
合理且完善的模型驅動架構的設計與實現,可以很好地支撐企業業務的變化,快速搭建新的應用。
數據處理相關的架構設計就到這裏。
以上。
歡迎關注微信公衆號以及時閱讀最新的技術文章: