自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

基于 RxJs 的前端數(shù)據(jù)層實(shí)踐

企業(yè)動(dòng)態(tài)
近來(lái)前端社區(qū)有越來(lái)越多的人開(kāi)始關(guān)注前端數(shù)據(jù)層的設(shè)計(jì)。DaoCloud 也遇到了這方面的問(wèn)題。我們調(diào)研了很多種解決方案,最終采用 RxJs 來(lái)設(shè)計(jì)一套數(shù)據(jù)層

近來(lái)前端社區(qū)有越來(lái)越多的人開(kāi)始關(guān)注前端數(shù)據(jù)層的設(shè)計(jì)。DaoCloud 也遇到了這方面的問(wèn)題。我們調(diào)研了很多種解決方案,最終采用 RxJs 來(lái)設(shè)計(jì)一套數(shù)據(jù)層。這一想法并非我們的首創(chuàng),社區(qū)里已有很多前輩、大牛分享過(guò)關(guān)于用 RxJs 設(shè)計(jì)數(shù)據(jù)層的構(gòu)想和實(shí)踐。站在巨人的肩膀上,才能走得更遠(yuǎn)。因此我們也打算把我們的經(jīng)驗(yàn)公布給大家,也算是對(duì)社區(qū)的回饋吧。

[[202018]]

作者簡(jiǎn)介

[[202019]]

DaoCloud 前端工程師  瞬光

一名中文系畢業(yè)的非典型程序員

一、我們遇到了什么困難

DaoCloud Enterprise(下文簡(jiǎn)稱 DCE) 是 DaoCloud 的主要產(chǎn)品,它是一個(gè)應(yīng)用云平臺(tái),也是一個(gè)非常復(fù)雜的單頁(yè)面應(yīng)用。它的復(fù)雜性主要體現(xiàn)在數(shù)據(jù)和交互邏輯兩方面上。在數(shù)據(jù)方面,DCE 需要展示大量數(shù)據(jù),數(shù)據(jù)之間依賴關(guān)系繁雜。在交互邏輯方面,DCE 中有著大量的交互操作,而且?guī)缀趺恳粋€(gè)操作幾乎都是牽一發(fā)而動(dòng)全身。但是交互邏輯的復(fù)雜最終還是會(huì)表現(xiàn)為數(shù)據(jù)的復(fù)雜。因?yàn)槊恳淮谓换?,本質(zhì)上都是在處理數(shù)據(jù)。一開(kāi)始的時(shí)候,為了保證數(shù)據(jù)的正確性,DCE 里寫(xiě)了很多處理數(shù)據(jù)、檢測(cè)數(shù)據(jù)變化的代碼,結(jié)果導(dǎo)致應(yīng)用非常地卡頓,而且代碼非常難以維護(hù)。

在整理了應(yīng)用數(shù)據(jù)層的邏輯后,我們總結(jié)出了以下幾個(gè)難點(diǎn)。本文會(huì)用較大的篇幅來(lái)描述我們所遇到的場(chǎng)景,這是因?yàn)槿绱藦?fù)雜的前端場(chǎng)景比較少見(jiàn),只有充分理解我們所遇到的場(chǎng)景,才能充分理解我們使用這一套設(shè)計(jì)的原因,以及這一套設(shè)計(jì)的優(yōu)勢(shì)所在。

二、應(yīng)用的難點(diǎn)

1. 數(shù)據(jù)來(lái)源多

DCE 的獲取數(shù)據(jù)的來(lái)源很多,主要有以下幾種:

(1) 后端、 Docker 和 Kubernetes 的 API

API 是數(shù)據(jù)的主要來(lái)源,應(yīng)用、服務(wù)、容器、存儲(chǔ)、租戶等等信息都是通過(guò) API 獲取的。

(2) WebSocket

后端通過(guò) WebSocket 來(lái)通知應(yīng)用等數(shù)據(jù)的狀態(tài)的變化。

(3) LocalStorage

保存用戶信息、租戶等信息。

(4) 用戶操作

用戶操作最終也會(huì)反應(yīng)為數(shù)據(jù)的變化,因此也是一個(gè)數(shù)據(jù)的來(lái)源。

數(shù)據(jù)來(lái)源多導(dǎo)致了兩個(gè)問(wèn)題:

(1) 復(fù)用處理數(shù)據(jù)的邏輯比較困難

由于數(shù)據(jù)來(lái)源多,因此獲取數(shù)據(jù)的邏輯常常分布在代碼各處。比如說(shuō)容器列表,展示它的時(shí)候我們需要一段代碼來(lái)格式化容器列表。但是容器列表之后還會(huì)更新,由于更新的邏輯和獲取的邏輯不一樣,所以就很難再?gòu)?fù)用之前所使用的格式化代碼。

(2) 獲取數(shù)據(jù)的接口形式不統(tǒng)一

如今我們調(diào)用 API 時(shí),都會(huì)返回一個(gè) Promise。但并不是所有的數(shù)據(jù)來(lái)源都能轉(zhuǎn)換成 Promise,比如 WebSocket 怎么轉(zhuǎn)換成 Promise 呢?結(jié)果就是在獲取數(shù)據(jù)的時(shí)候,要先調(diào)用 API,然后再監(jiān)聽(tīng) WebSocket 的事件?;蛟S還要同時(shí)再去監(jiān)聽(tīng)用戶的點(diǎn)擊事件等等。等于說(shuō)有多個(gè)數(shù)據(jù)源影響同一個(gè)數(shù)據(jù),對(duì)每一個(gè)數(shù)據(jù)源都要分別寫(xiě)一套對(duì)應(yīng)的邏輯,十分啰嗦。

聰明的讀者可能會(huì)想到:只要把處理數(shù)據(jù)的邏輯和獲取數(shù)據(jù)的邏輯解耦不就可以了嗎?很好,現(xiàn)在我們有兩個(gè)問(wèn)題了。

2. 數(shù)據(jù)復(fù)雜

DCE 數(shù)據(jù)的復(fù)雜主要體現(xiàn)在下面三個(gè)方面:

  • 從后端獲取的數(shù)據(jù)不能直接展示,要經(jīng)過(guò)一系列復(fù)雜邏輯的格式化。
  • 其中部分格式化邏輯還包括發(fā)送請(qǐng)求。
  • 數(shù)據(jù)之間存在著復(fù)雜的依賴關(guān)系。所謂依賴關(guān)系是指,必須要有 B 數(shù)據(jù)才能格式化 A 數(shù)據(jù)。

下圖是 DCE 數(shù)據(jù)依賴關(guān)系的大體示意圖。

DCE 數(shù)據(jù)依賴關(guān)系的大體示意圖

以格式化應(yīng)用列表為例,總共有這么幾個(gè)步驟。讀者不需要完全搞清楚,領(lǐng)會(huì)大意即可:

  1. 獲取應(yīng)用列表的數(shù)據(jù)
  2. 獲取服務(wù)列表的數(shù)據(jù)。這是因?yàn)閼?yīng)用是由服務(wù)組成的,應(yīng)用的狀態(tài)取決于服務(wù)的狀態(tài),因此要格式化應(yīng)用的狀態(tài),就必須獲取服務(wù)列表的數(shù)據(jù)。
  3. 獲取任務(wù)列表的數(shù)據(jù)。服務(wù)列表里其實(shí)也不包含服務(wù)的狀態(tài),服務(wù)的狀態(tài)取決于服務(wù)的任務(wù)的狀態(tài),因此要格式化服務(wù)的狀態(tài),就必須獲取任務(wù)列表的數(shù)據(jù)。
  4. 格式化任務(wù)列表。
  5. 根據(jù)服務(wù)的 id 從任務(wù)列表中找到服務(wù)所對(duì)應(yīng)的任務(wù),然后根據(jù)任務(wù)的狀態(tài),得出服務(wù)的狀態(tài)。
  6. 格式化 服務(wù)列表。
  7. 根據(jù)應(yīng)用的 id 從服務(wù)列表中找到應(yīng)用所對(duì)應(yīng)的服務(wù),然后根據(jù)服務(wù)的狀態(tài),得出應(yīng)用的狀態(tài)。順便還要把每個(gè)應(yīng)用的服務(wù)的數(shù)據(jù)塞到每個(gè)應(yīng)用里,因?yàn)橹筮€要用到。
  8. 格式化應(yīng)用列表。
  9. 完成!

這其中摻雜了同步和異步的邏輯,非常繁瑣,非常難以維護(hù)(肺腑之言)。況且,這還只是處理應(yīng)用列表的邏輯,服務(wù)、容器、存儲(chǔ)、網(wǎng)絡(luò)等等列表需要獲取呢,并且邏輯也不比應(yīng)用列表簡(jiǎn)單。所以說(shuō),要想解耦獲取和處理數(shù)據(jù)的邏輯并不容易。因?yàn)樘幚頂?shù)據(jù)這件事本身,就包括了獲取數(shù)據(jù)的邏輯。

如此復(fù)雜的依賴關(guān)系,經(jīng)常會(huì)發(fā)送重復(fù)的請(qǐng)求。比如說(shuō)我之前格式化應(yīng)用列表的時(shí)候請(qǐng)求過(guò)服務(wù)列表了,下次要獲取服務(wù)列表的時(shí)候又得再請(qǐng)求一次服務(wù)列表。

聰明的讀者會(huì)想:我把數(shù)據(jù)緩存起來(lái)保管到一個(gè)地方,每次要格式化數(shù)據(jù)的時(shí)候,不要重新去請(qǐng)求依賴的數(shù)據(jù),而是從緩存里讀取數(shù)據(jù),然后一股腦傳給格式化函數(shù),這樣不就可以了嗎?很好!現(xiàn)在我們有三個(gè)問(wèn)題了!

3. 數(shù)據(jù)更新困難

緩存是個(gè)很好的想法。但是在 DCE 里很難做,DCE 是一個(gè)對(duì)數(shù)據(jù)的實(shí)時(shí)性和一致性要求非常高的應(yīng)用。

DCE 中幾乎所有數(shù)據(jù)都是會(huì)被全局使用到的。比如說(shuō)應(yīng)用列表的數(shù)據(jù),不僅要在應(yīng)用列表中顯示,側(cè)邊欄里也會(huì)顯示應(yīng)用的數(shù)量,還有很多下拉菜單里面也會(huì)出現(xiàn)它。所以如果一處數(shù)據(jù)更新了,另一處沒(méi)更新,那就非常尷尬了。

還有就是之前提到的應(yīng)用和服務(wù)的依賴關(guān)系。由于應(yīng)用是依賴服務(wù)的,理論上來(lái)說(shuō)服務(wù)變了,應(yīng)用也是要變的,這個(gè)時(shí)候也要更新應(yīng)用的緩存數(shù)據(jù)。但事實(shí)上,因?yàn)閿?shù)據(jù)的依賴樹(shù)實(shí)在是太深了(比如上圖中的應(yīng)用和主機(jī)),有些依賴關(guān)系不那么明顯,結(jié)果就會(huì)忘記更新緩存,數(shù)據(jù)就會(huì)不一致。

什么時(shí)候要使用緩存、緩存保存在哪里、何時(shí)更新緩存,這些是都是非常棘手的問(wèn)題。

聰明讀者又會(huì)想:我用 redux 之類的庫(kù),弄個(gè)全局的狀態(tài)樹(shù),各個(gè)組件使用全局的狀態(tài),這樣不就能保證數(shù)據(jù)的一致了嗎?這個(gè)想法很好的,但是會(huì)遇到上面兩個(gè)難點(diǎn)的阻礙。redux 在面對(duì)復(fù)雜的異步邏輯時(shí)就無(wú)能為力了。

三、結(jié)論

結(jié)果我們會(huì)發(fā)現(xiàn)這三個(gè)難點(diǎn)每個(gè)單獨(dú)看起來(lái)都有辦法可以解決,但是合在一起似乎就成了無(wú)解死循環(huán)。因此,在經(jīng)過(guò)廣泛調(diào)研之后,我們選擇了 RxJs。

1. 為什么 RxJs 可以解決我們的困難

在說(shuō)明我們?nèi)绾斡?RxJs 解決上面三個(gè)難題之前,首先要說(shuō)明 RxJs 的特性。畢竟 RxJs 目前還是個(gè)比較新的技術(shù),大部分人可能還沒(méi)有接觸過(guò),所以有必要給大家普及一下 RxJs。

(1) 統(tǒng)一了數(shù)據(jù)來(lái)源

RxJs ***的特點(diǎn)就是可以把所有的事件封裝成一個(gè) Observable,翻譯過(guò)來(lái)就是可觀察對(duì)象。只要訂閱這個(gè)可觀察對(duì)象,就可以獲取到事件源所產(chǎn)生的所有事件。想象一下,所有的 DOM 事件、ajax 請(qǐng)求、WebSocket、數(shù)組等等數(shù)據(jù),統(tǒng)統(tǒng)可以封裝成同一種數(shù)據(jù)類型。這就意味著,對(duì)于有多個(gè)來(lái)源的數(shù)據(jù),我們可以每個(gè)數(shù)據(jù)來(lái)源都包裝成 Observable,統(tǒng)一給視圖層去訂閱,這樣就抹平了數(shù)據(jù)源的差異,解決了***個(gè)難題。

(2) 強(qiáng)大的異步同步處理能力

RxJs 還提供了功能非常強(qiáng)大且復(fù)雜的操作符( Operator) 用來(lái)處理、組合 Observable,因此 RxJs 擁有十分強(qiáng)大的異步處理能力,幾乎可以滿足任何異步邏輯的需求,同步邏輯更不在話下。它也抹平了同步和異步之間的鴻溝,解決了第二個(gè)難題。

(3) 數(shù)據(jù)推送的機(jī)制把拉取的操作變成了推送的操作

RxJs 傳遞數(shù)據(jù)的方式和傳統(tǒng)的方式有很大不同,那就是改“拉取”為“推送”。原本一個(gè)組件如果需要請(qǐng)求數(shù)據(jù),那它必須主動(dòng)去發(fā)送請(qǐng)求才能獲得數(shù)據(jù),這稱為“拉取”。如果像 WebSocket 那樣被動(dòng)地接受數(shù)據(jù),這稱為“推送”。如果這個(gè)數(shù)據(jù)只要請(qǐng)求一次,那么采用“拉取”的形式獲取數(shù)據(jù)就沒(méi)什么問(wèn)題。但是如果這個(gè)數(shù)據(jù)之后需要更新,那么“拉取”就無(wú)能為力了,開(kāi)發(fā)者不得不在代碼里再寫(xiě)一段代碼來(lái)處理更新。

但是 RxJs 則不同。RxJs 的精髓在于推送數(shù)據(jù)。組件不需要寫(xiě)請(qǐng)求數(shù)據(jù)和更新數(shù)據(jù)的兩套邏輯,只要訂閱一次,就能得到現(xiàn)在和將來(lái)的數(shù)據(jù)。這一點(diǎn)改變了我們寫(xiě)代碼的思路。我們?cè)谀脭?shù)據(jù)的時(shí)候,不是拿到了數(shù)據(jù)就萬(wàn)事大吉了,還需要考慮未來(lái)的數(shù)據(jù)何時(shí)獲取、如何獲取。如果不考慮這一點(diǎn),就很難開(kāi)發(fā)出具備實(shí)時(shí)性的應(yīng)用。

如此一來(lái),就能更好地解耦視圖層和數(shù)據(jù)層的邏輯。視圖層從此不用再操心任何有關(guān)獲取數(shù)據(jù)和更新數(shù)據(jù)的邏輯,只要從數(shù)據(jù)層訂閱一次就可以獲取到所有數(shù)據(jù),從而可以只專注于視圖層本身的邏輯。

(4) BehaviorSubject 可以緩存數(shù)據(jù)。

BehaviorSubject 是一種特殊的 Observable。如果 BehaviorSubject 已經(jīng)產(chǎn)生過(guò)一次數(shù)據(jù),那么當(dāng)它再一次被訂閱的時(shí)候,就可以直接產(chǎn)生上次所緩存的數(shù)據(jù)。比起使用一個(gè)全局變量或?qū)傩詠?lái)緩存數(shù)據(jù),BehaviorSubject 的好處在于它本身也是 Observable,所以異步邏輯對(duì)于它來(lái)說(shuō)根本不是問(wèn)題。這樣一來(lái)第三個(gè)難題也解決了。

這樣一來(lái)三個(gè)問(wèn)題是不是都沒(méi)有了呢?不,這下其實(shí)我們有了四個(gè)問(wèn)題。

2. 我們是怎么用 RxJs 解決困難的

相信讀者看到這里肯定是一臉懵逼。這就是第四個(gè)問(wèn)題。RxJs 學(xué)習(xí)曲線非常陡峭,能參考的資料也很少。我們?cè)陂_(kāi)發(fā)的時(shí)候,甚至都不確定怎么做才是***實(shí)踐,可以說(shuō)是摸著石頭過(guò)河。建議大家閱讀下文之前先看一下 RxJs 的文檔,不然接下來(lái)肯定十臉懵逼。

RxJs 真是太 TM 難啦!Observable、Subject、Scheduler 都是什么鬼啦!Operator 怎么有這么多啊!每個(gè) Operator 后面只是加個(gè) Map 怎么變化這么大啊!都是 map,為什么這個(gè) map和_.map 還不一樣啦!文檔還只有英文噠(現(xiàn)在有中文了)!我昨天還在寫(xiě) jQuery,怎么一下子就要寫(xiě)這么難的東西啊啊啊!!!(劃掉)

——來(lái)自實(shí)習(xí)生的吐槽

首先,給大家看一個(gè)整體的數(shù)據(jù)層的設(shè)計(jì)。熟悉單向數(shù)據(jù)流的讀者應(yīng)該不會(huì)覺(jué)得太陌生。

整體的數(shù)據(jù)層的設(shè)計(jì)

  1. 從 API 獲取一些必須的數(shù)據(jù)
  2. 由事件分發(fā)器來(lái)分發(fā)事件
  3. 事件分發(fā)器觸發(fā)控制各個(gè)數(shù)據(jù)管道
  4. 視圖層拼接數(shù)據(jù)管道,獲得用來(lái)展示的數(shù)據(jù)
  5. 視圖層通過(guò)事件分發(fā)器來(lái)更新數(shù)據(jù)管道
  6. 形成閉環(huán)

可以看到,我們的數(shù)據(jù)層設(shè)計(jì)基本上是一個(gè)單向數(shù)據(jù)流,確切地說(shuō)是“單向數(shù)據(jù)樹(shù)”。

樹(shù)的最上面是樹(shù)根。樹(shù)根會(huì)從各個(gè) API 獲得數(shù)據(jù)。樹(shù)根的下面是樹(shù)干。從樹(shù)干分岔出一個(gè)個(gè)樹(shù)枝。每個(gè)樹(shù)枝的終點(diǎn)都是一個(gè)可以供視圖層訂閱的 BehaviorSubject,每個(gè)視圖層組件可以按自己的需求來(lái)訂閱各個(gè)數(shù)據(jù)。數(shù)據(jù)和數(shù)據(jù)之間也可以互相訂閱。這樣一來(lái),當(dāng)一個(gè)數(shù)據(jù)變化的時(shí)候,依賴它的數(shù)據(jù)也會(huì)跟著變化,最終將會(huì)反應(yīng)到視圖層上。

四、設(shè)計(jì)詳細(xì)說(shuō)明

1. root(樹(shù)根)

root 是樹(shù)根。樹(shù)根有許多觸須,用來(lái)吸收養(yǎng)分。我們的 root 也差不多。一個(gè)應(yīng)用總有一些數(shù)據(jù)是關(guān)鍵的數(shù)據(jù),比如說(shuō)認(rèn)證信息、許可證信息、用戶信息。要使用我們的應(yīng)用,我們首先得知道你登錄沒(méi)登錄,付沒(méi)付過(guò)錢(qián)對(duì)不對(duì)?所以,這一部分?jǐn)?shù)據(jù)是***層數(shù)據(jù),如果不先獲取這些數(shù)據(jù),其他的數(shù)據(jù)便無(wú)法獲取。而這些數(shù)據(jù)一旦改變,整個(gè)應(yīng)用其他的數(shù)據(jù)也會(huì)發(fā)生根本的變化。比方說(shuō),如果登錄的用戶改變了,整個(gè)應(yīng)用展示的數(shù)據(jù)肯定也會(huì)大變樣。

在具體的實(shí)現(xiàn)中,root 通過(guò) zip 操作符匯總所有的 api 的數(shù)據(jù)。為了方便理解,本文中的代碼都有所簡(jiǎn)化,實(shí)際場(chǎng)景肯定遠(yuǎn)比這個(gè)復(fù)雜。

  1. // 從各個(gè) API 獲取數(shù)據(jù) 
  2. const license$ = Rx.Observable.fromPromise(getLicense()); 
  3. const auth$ = Rx.Observable.fromPromise(getAuth()); 
  4. const systemInfo$ = Rx.Observable.fromPromise(getSystemInfo()); 
  5. // 通過(guò) zip 拼接三個(gè)數(shù)據(jù),當(dāng)三個(gè) API 全部返回時(shí),root$ 將會(huì)發(fā)出這三個(gè)數(shù)據(jù) 
  6. const root$ = Rx.Observable.zip(license$, auth$, systemInfo$); 

當(dāng)所有必須的的數(shù)據(jù)都獲取到了,就可以進(jìn)入到樹(shù)干的部分了。

2. trunk(樹(shù)干)

trunk 是我們的樹(shù)干,所有的數(shù)據(jù)都首先流到 trunk ,trunk 會(huì)根據(jù)數(shù)據(jù)的種類,來(lái)決定這個(gè)數(shù)據(jù)需要流到哪一個(gè)樹(shù)枝中。簡(jiǎn)而言之,trunk 是一個(gè)事件分發(fā)器。所有事件首先都匯總到 trunk 中。然后由 trunk 根據(jù)事件的類型,來(lái)決定哪些數(shù)據(jù)需要更新。有點(diǎn)類似于 redux 中根據(jù) action 來(lái)觸發(fā)相應(yīng) reducer 的概念。

之所以要有這么一個(gè)事件分發(fā)器,是因?yàn)?DCE 的數(shù)據(jù)都是牽一發(fā)而動(dòng)全身的,一個(gè)事件發(fā)生時(shí),往往需要觸發(fā)多個(gè)數(shù)據(jù)的更新。此時(shí)有一個(gè)統(tǒng)一的地方來(lái)管理事件和數(shù)據(jù)之間的對(duì)應(yīng)關(guān)系就會(huì)非常方便。一個(gè)統(tǒng)一的事件的入口,可以大大降低未來(lái)追蹤數(shù)據(jù)更新過(guò)程的難度。

在具體的實(shí)現(xiàn)中,trunk 是一個(gè) Subject。因?yàn)?trunk 不但要訂閱 WebSocket,同時(shí)還要允許視圖層手動(dòng)地發(fā)布一些事件。當(dāng)有事件發(fā)生時(shí),無(wú)論是 WebSocket 事件還是視圖層發(fā)布的事件,經(jīng)過(guò) trunk 的處理后,我們都可以一視同仁。

  1. //一個(gè)產(chǎn)生 WebSocket 事件的 Observable 
  2. const socket$ = Observable.webSocket('ws://localhost:8081'); 
  3. // trunk 是一個(gè) Subject 
  4. const trunk$ = new Rx.Subject() 
  5.  // 在 root 產(chǎn)生數(shù)據(jù)之前,trunk 不會(huì)發(fā)布任何值。trunk 之后的所有邏輯也都不會(huì)運(yùn)行。 
  6.  .skipUntil(root$) 
  7.  // 把 WebSocket 推送過(guò)來(lái)的事件,合并到 trunk 中 
  8.  .merge(socket$) 
  9.  .map(event => { 
  10.    // 在實(shí)際開(kāi)發(fā)過(guò)程中,trunk 可能會(huì)接受來(lái)自各種事件源的事件 
  11.    // 這些事件的數(shù)據(jù)格式可能會(huì)大不相同,所以一般在這里還需要一些格式化事件的數(shù)據(jù)格式的邏輯。 
  12.  }); 

3. branch(樹(shù)枝)

trunk 的數(shù)據(jù)最終會(huì)流到各個(gè) branch。branch 究竟是什么,下面就會(huì)提到。

在具體的實(shí)現(xiàn)中,我們?cè)?trunk 的基礎(chǔ)上,用操作符對(duì) trunk 所分發(fā)的事件進(jìn)行過(guò)濾,從而創(chuàng)建出各個(gè)數(shù)據(jù)的 Observable,就像從樹(shù)干中分出的樹(shù)枝一樣。

  1. // trunk 格式化好的事件的數(shù)據(jù)格式是一個(gè)數(shù)組,其中是需要更新的數(shù)據(jù)的名稱 
  2. // 這里通過(guò) filter 操作符來(lái)過(guò)濾事件,給每個(gè)數(shù)據(jù)創(chuàng)建一個(gè) Observable。相當(dāng)于于從 trunk 分岔出多條樹(shù)枝。 
  3. // 比如說(shuō) trunk 發(fā)布了一個(gè) ['app', 'services'] 的事件,那么 apps$ 和 services$ 就能得到通知 
  4. const apps$ = trunk$.filter(events => events.includes('app')); 
  5. const services$ = trunk$.filter(events => events.includes('service')); 
  6. const containers$ = trunk$.filter(events => events.includes('container')); 
  7. const nodes$ = trunk$.filter(events => events.includes('node')); 

僅僅如此,我們的 branch 還沒(méi)有什么實(shí)質(zhì)性的內(nèi)容,它僅僅能接受到數(shù)據(jù)更新的通知而已,后面還需要加上具體的獲取和處理數(shù)據(jù)的邏輯,下面就是一個(gè)容器列表的 branch 的例子。

  1. // containers$ 就是從 trunk 分出來(lái)的一個(gè) branch。 
  2. // 當(dāng) containers$ 收到來(lái)自 trunk 的通知的時(shí)候,containers$ 后面的邏輯就會(huì)開(kāi)始執(zhí)行 
  3. containers$ 
  4.  // 當(dāng)收到通知后,首先調(diào)用 API 獲取容器列表 
  5.  .switchMap(() => Rx.Observable.fromPromise(containerApi.list())) 
  6.  // 獲取到容器列表后,對(duì)每個(gè)容器分別進(jìn)行格式化。 
  7.  // 每個(gè)容器都是作為參數(shù)傳遞給格式化函數(shù)的。格式化函數(shù)中不包含任何異步的邏輯。 
  8.  .map(containers => containers.map(container, container => formatContainer(container))); 

現(xiàn)在我們就有了一個(gè)能夠產(chǎn)生容器列表的數(shù)據(jù)的 containers$。我們只要訂閱 containers$就可以獲得***的容器列表數(shù)據(jù),并且當(dāng) trunk 發(fā)出更新通知的時(shí)候,數(shù)據(jù)還能夠自動(dòng)更新。這是巨大的進(jìn)步。

現(xiàn)在還有一個(gè)問(wèn)題,那就是如何處理數(shù)據(jù)之間的依賴關(guān)系呢?比如說(shuō),格式化應(yīng)用列表的時(shí)候假如需要格式化好的容器列表和服務(wù)列表應(yīng)該怎么做呢?這個(gè)步驟在以前一直都十分麻煩,寫(xiě)出來(lái)的代碼猶如意大利面。因?yàn)檫@個(gè)步驟需要處理不少的異步和同步邏輯,這其中的順序還不能出錯(cuò),否則可能就會(huì)因?yàn)殛P(guān)鍵數(shù)據(jù)還沒(méi)有拿到導(dǎo)致格式化時(shí)報(bào)錯(cuò)。

實(shí)際上,我們可以把 branch 想象成一個(gè)“管道”,或者“流”。這兩個(gè)概念都不是新東西,大家應(yīng)該比較熟悉。

We should have some ways of connecting programs like garden hose—screw in another segment when it becomes necessary to massage data in another way.

——Douglas McIlroy

如果數(shù)據(jù)是以管道的形式存在的,那么當(dāng)一個(gè)數(shù)據(jù)需要另一個(gè)數(shù)據(jù)的時(shí)候,只要把管道接起來(lái)不就可以了嗎?幸運(yùn)的是,借助 RxJs 的 Operator,我們可以非常輕松地拼接數(shù)據(jù)管道。下面就是一個(gè)應(yīng)用列表拼接容器列表的例子。

  1. // apps$ 也是從 trunk 分出來(lái)的一個(gè) branch 
  2. apps$ 
  3.  // 同樣也從 API 獲取數(shù)據(jù) 
  4.  .switchMap(() => Rx.Observable.fromPromise(appApi.list())) 
  5.  // 這里使用 combineLatest 操作符來(lái)把容器列表和服務(wù)列表的數(shù)據(jù)拼接到應(yīng)用列表中 
  6.  // 當(dāng)容器或服務(wù)的數(shù)據(jù)更新時(shí),combineLatest 之后的代碼也會(huì)執(zhí)行,應(yīng)用的數(shù)據(jù)也能得到更新。 
  7.  .combineLatest(containers$, services$) 
  8.    // 把這三個(gè)數(shù)據(jù)一起作為參數(shù)傳遞給格式化函數(shù)。 
  9.    // 注意,格式化函數(shù)中還是沒(méi)有任何異步邏輯,因?yàn)樾枰惒将@取的數(shù)據(jù)已經(jīng)在上面的 combineLatest 操作符中得到了。 
  10.  .map(([apps, containers, services]) => apps.map(app => formatApp(app, containers, services))); 

4. 格式化函數(shù)

格式化函數(shù)就是上文中的 formatApp 和 formatContainer。它沒(méi)有什么特別的,和 RxJs 沒(méi)什么關(guān)系。

唯一值得一提的是,以前我們的格式化函數(shù)中充斥著異步邏輯,很難維護(hù)。所以在用 RxJs 設(shè)計(jì)數(shù)據(jù)層的時(shí)候我們刻意地保證了格式化函數(shù)中沒(méi)有任何異步邏輯。即使有的格式化步驟需要異步獲取數(shù)據(jù),也是在 branch 中通過(guò)數(shù)據(jù)管道的拼接獲取,再以參數(shù)的形式統(tǒng)一傳遞給格式化函數(shù)。這么做的目的就是為了將異步和同步解耦,畢竟異步的邏輯由 RxJs 處理更加合適,也更便于理解。

5. fruit

現(xiàn)在我們只差緩存沒(méi)有做了。雖然我們現(xiàn)在只要訂閱 apps$ 和 containers$ 就能獲取到相應(yīng)的數(shù)據(jù),但是前提是 trunk 必需要發(fā)布事件才行。這是因?yàn)?trunk 是一個(gè) Subject,假如 trunk 不發(fā)布事件,那么所有訂閱者都獲取不到數(shù)據(jù)。所以,我們必須要把 branch 吐出來(lái)的數(shù)據(jù)緩存起來(lái)。 RxJs 中的 BehaviorSubject 就非常適合承擔(dān)這個(gè)任務(wù)。

BehaviorSubject 可以緩存每次產(chǎn)生的數(shù)據(jù)。當(dāng)有新的訂閱者訂閱它時(shí),它就會(huì)立刻提供最近一次所產(chǎn)生的數(shù)據(jù),這就是我們要的緩存功能。所以對(duì)于每個(gè) branch,還需要用 BehaviorSubject 包裝一下。數(shù)據(jù)層最終對(duì)外暴露的接口實(shí)際上是 BehaviorSubject,視圖層所訂閱的也是 BehaviorSubject。在我們的設(shè)計(jì)中,BehaviorSubject 叫作 fruit,這些經(jīng)過(guò)層層格式化的數(shù)據(jù),就好像果實(shí)一樣。

具體的實(shí)現(xiàn)并不復(fù)雜,下面是一個(gè)容器列表的例子。

  1. // 每個(gè)數(shù)據(jù)流對(duì)外暴露的一個(gè)借口是 BehaviorSubject,我們?cè)谧兞磕┪灿?$,表示這是一個(gè)BehaviorSubject 
  2. const containers$$ = new Rx.BehaviorSubject(); 
  3. // 用 BehaviorSubject 去訂閱 containers$ 這個(gè) branch 
  4. // 這樣 BehaviorSubject 就能緩存***的容器列表數(shù)據(jù),同時(shí)當(dāng)有新數(shù)據(jù)的時(shí)它也能產(chǎn)生新的數(shù)據(jù) 
  5. containers$.subscribe(containers$$); 

6. 視圖層

整個(gè)數(shù)據(jù)層到上面為止就完成了,但是在我們用視圖層對(duì)接數(shù)據(jù)層的時(shí)候,也走了一些彎路。一般情況下,我們只需要用 vue-rx 所提供的 subscriptions 來(lái)訂閱 fruit 就可以了。

  1. <template> 
  2.  <app-list :data="apps"></app-list> 
  3. </template> 
  4.  
  5. <script> 
  6. import app$$ from '../branch/app.branch'; 
  7.  
  8. export default { 
  9.  name: 'app', 
  10.  subscriptions: { 
  11.    apps: app$$, 
  12.  }, 
  13. }; 
  14. </script> 

但有些時(shí)候,有些頁(yè)面的數(shù)據(jù)很復(fù)雜,需要進(jìn)一步處理數(shù)據(jù)。遇到這種情況,那就要考慮兩點(diǎn)。一是這個(gè)數(shù)據(jù)是否在別的頁(yè)面或組件中也要用,如果是的話,那么就應(yīng)該考慮把它做進(jìn)數(shù)據(jù)層中。如果不是的話,那其實(shí)可以考慮在頁(yè)面中單獨(dú)再創(chuàng)建一個(gè) Observable,然后用 vue-rx 去訂閱這個(gè) Observable。

還有一個(gè)問(wèn)題就是,假如視圖層需要更新數(shù)據(jù)怎么辦?之前已經(jīng)提到過(guò),整個(gè)數(shù)據(jù)層的事件分發(fā)是由 trunk 來(lái)管理的。因此,視圖層如果想要更新數(shù)據(jù),也必須取道 trunk。這樣一來(lái),數(shù)據(jù)層和視圖層就形成了一個(gè)閉環(huán)。視圖層根本不用擔(dān)心數(shù)據(jù)怎么處理,只要向數(shù)據(jù)層發(fā)布一個(gè)事件就能全部搞定。

  1. methods: { 
  2.  updateApp(app) { 
  3.    appApi.update(app) 
  4.      .then(() => { 
  5.        trunk$.next(['app']) 
  6.      }) 
  7.  }, 
  8. }, 

下面是整個(gè)數(shù)據(jù)層設(shè)計(jì)的全貌,供大家參考。

整個(gè)數(shù)據(jù)層設(shè)計(jì)的全貌

總結(jié)

之后的開(kāi)發(fā)過(guò)程證明,這一套數(shù)據(jù)層很大程度上解決了我們的問(wèn)題。它***的好處在于提高了代碼的可維護(hù)性,從而使得開(kāi)發(fā)效率大大提高,bug 也大大減少。

【本文是51CTO專欄機(jī)構(gòu)“道客船長(zhǎng)”的原創(chuàng)文章,轉(zhuǎn)載請(qǐng)通過(guò)微信公眾號(hào)(daocloudpublic)聯(lián)系原作者】

戳這里,看該作者更多好文

責(zé)任編輯:趙寧寧 來(lái)源: 51CTO專欄
相關(guān)推薦

2022-05-26 21:33:09

業(yè)務(wù)前端測(cè)試

2022-05-26 10:12:21

前端優(yōu)化測(cè)試

2022-06-27 09:36:29

攜程度假GraphQL多端開(kāi)發(fā)

2023-06-12 21:32:56

卡口服務(wù)系統(tǒng)

2019-06-19 16:01:14

Spark數(shù)據(jù)分析SparkSQL

2024-11-15 08:00:00

2021-02-20 10:26:00

前端

2022-07-27 22:56:45

前端應(yīng)用緩存qiankun

2021-04-15 08:08:48

微前端Web開(kāi)發(fā)

2023-06-14 08:25:18

2022-02-13 23:00:48

前端微前端qiankun

2016-10-28 15:01:35

Cookie前端實(shí)踐

2023-04-17 07:32:41

2016-09-04 15:14:09

攜程實(shí)時(shí)數(shù)據(jù)數(shù)據(jù)平臺(tái)

2023-09-07 20:04:06

前后端趨勢(shì)Node.js

2015-07-17 10:25:43

kubernetesDocker集群系統(tǒng)

2022-08-08 07:05:36

KubeSphere分級(jí)管理

2023-03-01 18:32:16

系統(tǒng)監(jiān)控數(shù)據(jù)

2021-10-29 21:26:39

前端引擎層類型

2023-11-24 09:44:07

數(shù)據(jù)攜程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)