Flux用過了,Redux也用過了,仍是以爲不順手?要不要本身造一個?

1 前言

不少同窗用過了Flux,也用過了Redux,但仍是以爲不趁心?要不要本身造一個?一百行來代碼就基本搞定,So easy, so good!git

其實,本身造的框架實不實用,並不重要,重要的是思想。有了設計框架的思想後,再去看人家的框架,就會更多地關注人家爲什麼要這麼設計?好處在哪?弊端在哪?是否有改進的地方?明白了框架設計者的想法,才能更好地使用框架。github

如今,我們就一塊兒來設計一個React框架,這個框架具有如下幾個的特色:瀏覽器

  1. 單向數據流:業務數據從UI層觸發,經處理到Module層便結束,再也不須要人爲地將數據反映到UI層。mvc

  2. 消息機制:組件與服務之間經過消息總線完成,包括組件與組件之間的嵌套關係。框架

我們給這個框架起個響亮的名字——Rebus(React-Bus)。這裏的Bus不是公交車的Bus,是計算機基礎原理中「Bus」(總線)。很顯然,我們要用「消息總線」這樣的思想,實現ReactJs的單向數據流開發模式。一句話歸納我們的框架:Rebus是一個基於消息總線的,單向數據流的,ReactJs開發框架dom

這裏是用Rebus寫的一個TodoMVC實例:https://github.com/odebo/todomvc-rebus(看在個人代碼寫得如此粗糙的份上,大蝦們賞顆星星鼓勵鼓勵下唄)。異步

clipboard.png

2 什麼是單向數據流模式

什麼是「單向數據流模式」?這個概念對不少人來講可能有點陌生。下面是Facebook的Flux官網(http://facebook.github.io/flux/)提供的說明圖:函數

clipboard.png

好像有點抽象?那我們先補補腦,看看什麼是雙向數據流模式。工具

什麼是雙向數據模式?簡單地說就是UI層的一個操做通過UI層(View)、控制層(Control)、模式層(Model),作完增、刪、改、查等處理後,還得反過來,手動地將增刪改查後的數據反映到UI層上。這就雙向數據流模式。單元測試

而Flux中所謂的單向數據流模式是指:UI層監聽應用的「狀態」,當一個操做(Action)通過Dispatch(分發器)、Store(狀態容器),最後更新了「狀態」,UI層自動根據「狀態」的變化而更新界面。

這裏的「狀態」是指一個應用某個時刻的某個狀態:好比左側菜單欄展開與否——狀態;導航中高亮項是誰——狀態;用戶是否登陸,用戶是誰——狀態;Table中多少個Item,分別是什麼內容——狀態。

簡單地說,單向數據流就是單向綁定,UI層與狀態綁定,當狀態發生變化,UI層自動更新。

可能有的同窗會問,既然有AngularJs這樣的雙向綁定的MVVM模式,還搞什麼單向綁定模式,聽起來弱爆了。雙向綁定確定比單向綁定高大上得多。

這個問題不太好下結論,雙向綁定當然有雙向綁定的好處,但也有它的弊端。而相比單向數據流的邏輯處理思路更加單純清晰。

3 Rebus 框架的數據流模型

理解了單向數據流後,我們給出Rebus框架的數據流模式(以下圖)。歸納起來就三個步驟:

  1. UI層觸發的一個Action。

  2. Rebus總線根據Action路由表選擇對應的Service進行處理。

  3. Service處理後,更新狀態(State),結束。

clipboard.png

這裏的Services層指的是業務服務層,提供業務處理接口,包括對狀態的修改,對後臺數據的異步處理等等。若是以爲這一層太厚,能夠分離出專門的Modle接口層。但無論怎樣,一個業務操做從UI層到最後修改狀態便結束,數據流方向只有一個。

但光這麼說仍是太抽象了,我們直接上代碼,看看在TodoMVC這個例子中,添加一個新的Todo這個操做是怎麼被處理的。

clipboard.png

clipboard.png

clipboard.png

是否是挺簡單,簡潔的三層結構,清晰的數據流:

  1. ReactJs組件只負責渲染和觸發Action,具體誰來響應Action,它無論。

  2. Rebus總線根據Action路由表,調用對應的Service進行處理。

  3. Service層進行完邏輯處理後,經過Rebus.setState()方法更新狀態。

但你必定會問:React組件是怎麼監聽狀態的變化的?其實很簡單,直接看代碼:好比我們但願添加新的Todo後,TodoBody組件會自動更新。因此TodoBody組件應該監聽狀態「todos」的變化。

clipboard.png

4 Rebus中的action

用過Flux的同窗都知道Flux中有個叫Dispatch的模塊,用來dispatch各類Action。而我們的Rebus.execute()的做用與Dispatch.dispatch()差很少(以下圖)。

clipboard.png

clipboard.png

不同的是Rebus.execute(actionHead, arg1,arg2,…)的第一個參數是action頭,其它參數直接跟在action頭後面。Action頭中包含兩個信息:要作什麼?從哪裏來?

「從哪裏來」這個參數很重要,由於它給我們開發、調試提供了極大的便利。試想下,在Action路由表中,我們可以很清晰地看出一個Action將會到哪一個Service處理,但無法直觀地看出一個Action是從哪裏觸發的,並且一樣的Action可能由不一樣的組件觸發,這是無法從Action路由表中直觀看出來的。

因此,我們給Rebus增長了一個調試功能,只要打開這個功能,即可以打印Action信息。

clipboard.png

clipboard.png

另外,若是一個Action被觸發,卻沒在路由表中找到這個Action的路由,Rebus會經過打印錯誤信息的方式提醒開發者。

clipboard.png

自從Action有了源信息,領導不再用擔憂我找不到代碼的出處了,歐耶!

5 Rebus中的Action路由表

Action路由表這個概念在Flux與Redux中沒有,但也很好理解,就是一個很直觀的路由配置信息表。它是在Web應用開始初始化時,加載進來的。

clipboard.png

在這張Action路由表中,你能夠直觀地添加、修改、跟蹤一個Action會被哪一個Service處理。當你但願某個Action被另外一個Service處理時,直接在這個Action路由表中進行修改即是。

clipboard.png

另外,在這個Action路由表中,我們能夠經過and()讓一個Action觸發多個service,如上圖的第29行。我們寫了一個日誌服務TodoLog.logAddTodo,但願系統處理ADD_TODO的同時也記錄這個事件。我們就能夠經過and()函數將這個服務綁定到ADD_TODO這條路由後面,and()的參數是一個數據,意思能夠綁定多個服務。

可是,必須提醒的是,不建議and()中的服務也修改State,除非你確定and()中的服務修改的State與Rebus.connet()中的服務修改的State的監聽者沒有任何交集。因此,再三提醒and()中只綁定跟State無關的服務,好比一些日誌服務、系通通計服務等。

可能你會問,一個Web應用就一張Action路由表嗎?是的,也許在後續的版本中我們能夠支持多個Action路由表。但一張路由表也有它的好處——惟一性。好比你設置了某個Action的路由,結果另外一個同事在另外一張路由表中也設置了同名的Action路由,一開始獨立開發時可能沒有問題,一旦整合在一塊兒,問題就出現了。因此,只有一張路由表是有好處的,大點不要緊。

6 Rebus中的組件

我們都知道ReactJs的一大特徵就是支持JSX語法,這使得JS代碼中能夠直接寫「類標籤代碼」,並且一個組件可以被嵌套在另外一個組件中,並接受從上級組件傳遞進來的參數。

這種一層一層嵌套的寫法雖然很直觀,但也很蛋疼。就拿上面Redux實現的Header組件添加新Todo這個操做,執行的是傳遞進來的回調函數addTodo(…)。

clipboard.png

這麼作有幾個問題:

1)寫代碼時,究竟是先約定Header組件要執行的回調函數叫addTodo,寫上級組件時按約定傳遞叫addTodo的參數?仍是先寫好上級組件,根據上級組件傳遞的參數名來執行回調函數?究竟是先有蛋仍是先有雞?

2)果上級組件傳參時傳錯了,或者子組件寫回調函數時名稱寫錯了,如何跟蹤代碼,只知道光從代碼上看,我TM怎麼知道這個回調函數是從哪一個組件傳進來的?雖然如今有些工具可以直接在瀏覽器上查看組件之間的嵌套關係,但那也是在應用可以正常跑起來的狀況才能Debug。

3)組件與組件之間的關係是經過硬編碼實現,若是如今有個子組件須要替換,但是這個子組件被嵌入在多個組件中,試問這得怎麼找?

組件嵌套是ReactJs的一大亮點,但也是不少人認爲ReactJs不適合作大型項目的緣由。但我以爲這並非ReactJs的問題,咱們徹底能夠其餘途徑解決上面這些問題。好比我們的Rebus,組件與組件之間不會直接嵌套,而是跟調用後臺Service同樣,經過Rebus.execute()方法,發起一個Action。好比TodoApp這個上層組件,它嵌套了TodoHead/TodoBody/TodoFoot這三個子組件,但你會發現TodoApp組件是經過execute了三個分別叫GET_TODOHEAD、GET_TODOBODY、GET_TODOFOOT的Action來引入三個子組件,具體引入是怎麼的組件,它並不關心。

clipboard.png

Rebus總線根據Action路由表(rebus.route.js),分別找到這三個Action對應實現者(在這裏我們經過一個「組件工廠」CompFactory來響應這些Action)。當咱們須要替換組件時,只須要在Action路由表中作出修改即是。

clipboard.png

換句話說,在Rebus總線面前,每一個組件都是平等的。組件只會跟Rebus總線溝通,不會直接嵌入其它組件,也不會被嵌到其它組件中。「組件樹」這個概念在Rebus是經過Action消息來實現的,是一種「動態嵌套」關係。

7 Rebus中的State

在Flux/Redux中,應用的各類狀態以一棵「狀態樹」的形式都是從根組件上灌進去,全部子組件的狀態一概從這個根組件上繼承下來(無論組件樹的結構有多深)。這樣作的好處就是一旦某個狀態發生變化,React組件自動從上到下進行更新。

可是,這麼作真的好嗎?並非說一個應用就一棵狀態樹這個想法很差,我也贊同這種設計,由於狀態是Web應用中最重要但又很是容易混亂的信息,「惟一性」對狀態來講,很是重要。

但是若是全部子組件的狀態都是從根組件一層一層傳遞進來的話,至少會有兩個問題:

  1. 組件之間的耦合性高,難以並行開發:子組件的狀態是由父組件決定。那到底先寫父組件仍是先寫子組件?

  2. 狀態變化後,難以跟蹤變化的組件:假設你的某個操做修改了某個狀態,但這個狀態的變化會致使哪些組件更新了?光從Store中是看不出的,也沒法跟蹤,只能從根組件一層一層往下查,看看這個State被傳遞到哪一個組件中。

在Rebus中,我們一樣維繫着一棵「狀態樹」,並在應用初始化的時就加載進來的。

clipboard.png

但不一樣的是,組件的狀態不是從上級組件中傳遞進來,是經過Rebus得到的,並且組件有權決定本身關心哪一個State的變化。

clipboard.png

這樣作有幾個好處:

  1. 方便並行開發:由於組件之間沒有太過的耦合性。狀態都是經過Rebus得到的,大部分狀況下都是直接返回狀態樹中的某個狀態,這樣的「淺處理」很是適用於複雜系統開發中。

  2. 方便單元測試:因爲組件直接與狀態綁定(監聽),要對一個組件進行單元測試,直接修改這個組件綁定的狀態即是,便是沒有上級組件的存在,也不影響測試。

  3. 方便維護代碼: 從上面的代碼中能夠清晰地看出某個組件監聽哪些狀態,但反過來,某個狀態被哪些組件監聽了?從組件的代碼中是無法直觀看出來的。這個問題也不該該經過查閱代碼的形式來解決,而應該經過我們的Rebus來解決。我們能夠給Rebus增長一個方法,打印每個State的監聽者。以下圖:

clipboard.png

clipboard.png

如今我們既能夠清晰地看出一個組件監聽了哪些狀態,也能看出一個狀態被哪些組件監聽。這爲代碼的調試與維護提供極大的方便。

另外,咱們能夠輕鬆地打印出某個時刻的狀態樹或具體某個狀態的值。

clipboard.png

clipboard.png

8 總結

先給有耐心看到這裏的人鼓個掌……而後也給我本身鼓個掌……由於對於一個拖延症極度病患者來講,用業餘時間寫這麼一篇技術貼真心不容易。當我寫這句話的時候,距離這個帖子的第一句話,整整隔了一個月!——大哥,你是一禪指敲鍵盤的嗎?

言歸正傳,總結下我們這個Rebus框架的特色:

  1. 實現了單向數據流模式,邏輯層次結構淺,思路清晰。

  2. React組件職責單一,只負責渲染與響應交互。

  3. 以路由表的形式控制Action數據的流向,直觀、易維護。

  4. React組件之間經過消息的形式實現動態嵌套。

相關文章
相關標籤/搜索