不少同窗用過了Flux,也用過了Redux,但仍是以爲不趁心?要不要本身造一個?一百行來代碼就基本搞定,So easy, so good!git
其實,本身造的框架實不實用,並不重要,重要的是思想。有了設計框架的思想後,再去看人家的框架,就會更多地關注人家爲什麼要這麼設計?好處在哪?弊端在哪?是否有改進的地方?明白了框架設計者的想法,才能更好地使用框架。github
如今,我們就一塊兒來設計一個React框架,這個框架具有如下幾個的特色:瀏覽器
單向數據流:業務數據從UI層觸發,經處理到Module層便結束,再也不須要人爲地將數據反映到UI層。mvc
消息機制:組件與服務之間經過消息總線完成,包括組件與組件之間的嵌套關係。框架
我們給這個框架起個響亮的名字——Rebus(React-Bus)。這裏的Bus不是公交車的Bus,是計算機基礎原理中「Bus」(總線)。很顯然,我們要用「消息總線」這樣的思想,實現ReactJs的單向數據流開發模式。一句話歸納我們的框架:Rebus是一個基於消息總線的,單向數據流的,ReactJs開發框架。dom
這裏是用Rebus寫的一個TodoMVC實例:https://github.com/odebo/todomvc-rebus(看在個人代碼寫得如此粗糙的份上,大蝦們賞顆星星鼓勵鼓勵下唄)。異步
什麼是「單向數據流模式」?這個概念對不少人來講可能有點陌生。下面是Facebook的Flux官網(http://facebook.github.io/flux/)提供的說明圖:函數
好像有點抽象?那我們先補補腦,看看什麼是雙向數據流模式。工具
什麼是雙向數據模式?簡單地說就是UI層的一個操做通過UI層(View)、控制層(Control)、模式層(Model),作完增、刪、改、查等處理後,還得反過來,手動地將增刪改查後的數據反映到UI層上。這就雙向數據流模式。單元測試
而Flux中所謂的單向數據流模式是指:UI層監聽應用的「狀態」,當一個操做(Action)通過Dispatch(分發器)、Store(狀態容器),最後更新了「狀態」,UI層自動根據「狀態」的變化而更新界面。
這裏的「狀態」是指一個應用某個時刻的某個狀態:好比左側菜單欄展開與否——狀態;導航中高亮項是誰——狀態;用戶是否登陸,用戶是誰——狀態;Table中多少個Item,分別是什麼內容——狀態。
簡單地說,單向數據流就是單向綁定,UI層與狀態綁定,當狀態發生變化,UI層自動更新。
可能有的同窗會問,既然有AngularJs這樣的雙向綁定的MVVM模式,還搞什麼單向綁定模式,聽起來弱爆了。雙向綁定確定比單向綁定高大上得多。
這個問題不太好下結論,雙向綁定當然有雙向綁定的好處,但也有它的弊端。而相比單向數據流的邏輯處理思路更加單純清晰。
理解了單向數據流後,我們給出Rebus框架的數據流模式(以下圖)。歸納起來就三個步驟:
UI層觸發的一個Action。
Rebus總線根據Action路由表選擇對應的Service進行處理。
Service處理後,更新狀態(State),結束。
這裏的Services層指的是業務服務層,提供業務處理接口,包括對狀態的修改,對後臺數據的異步處理等等。若是以爲這一層太厚,能夠分離出專門的Modle接口層。但無論怎樣,一個業務操做從UI層到最後修改狀態便結束,數據流方向只有一個。
但光這麼說仍是太抽象了,我們直接上代碼,看看在TodoMVC這個例子中,添加一個新的Todo這個操做是怎麼被處理的。
是否是挺簡單,簡潔的三層結構,清晰的數據流:
ReactJs組件只負責渲染和觸發Action,具體誰來響應Action,它無論。
Rebus總線根據Action路由表,調用對應的Service進行處理。
Service層進行完邏輯處理後,經過Rebus.setState()方法更新狀態。
但你必定會問:React組件是怎麼監聽狀態的變化的?其實很簡單,直接看代碼:好比我們但願添加新的Todo後,TodoBody組件會自動更新。因此TodoBody組件應該監聽狀態「todos」的變化。
用過Flux的同窗都知道Flux中有個叫Dispatch的模塊,用來dispatch各類Action。而我們的Rebus.execute()的做用與Dispatch.dispatch()差很少(以下圖)。
不同的是Rebus.execute(actionHead, arg1,arg2,…)的第一個參數是action頭,其它參數直接跟在action頭後面。Action頭中包含兩個信息:要作什麼?從哪裏來?
「從哪裏來」這個參數很重要,由於它給我們開發、調試提供了極大的便利。試想下,在Action路由表中,我們可以很清晰地看出一個Action將會到哪一個Service處理,但無法直觀地看出一個Action是從哪裏觸發的,並且一樣的Action可能由不一樣的組件觸發,這是無法從Action路由表中直觀看出來的。
因此,我們給Rebus增長了一個調試功能,只要打開這個功能,即可以打印Action信息。
另外,若是一個Action被觸發,卻沒在路由表中找到這個Action的路由,Rebus會經過打印錯誤信息的方式提醒開發者。
自從Action有了源信息,領導不再用擔憂我找不到代碼的出處了,歐耶!
Action路由表這個概念在Flux與Redux中沒有,但也很好理解,就是一個很直觀的路由配置信息表。它是在Web應用開始初始化時,加載進來的。
在這張Action路由表中,你能夠直觀地添加、修改、跟蹤一個Action會被哪一個Service處理。當你但願某個Action被另外一個Service處理時,直接在這個Action路由表中進行修改即是。
另外,在這個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路由,一開始獨立開發時可能沒有問題,一旦整合在一塊兒,問題就出現了。因此,只有一張路由表是有好處的,大點不要緊。
我們都知道ReactJs的一大特徵就是支持JSX語法,這使得JS代碼中能夠直接寫「類標籤代碼」,並且一個組件可以被嵌套在另外一個組件中,並接受從上級組件傳遞進來的參數。
這種一層一層嵌套的寫法雖然很直觀,但也很蛋疼。就拿上面Redux實現的Header組件添加新Todo這個操做,執行的是傳遞進來的回調函數addTodo(…)。
這麼作有幾個問題:
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來引入三個子組件,具體引入是怎麼的組件,它並不關心。
Rebus總線根據Action路由表(rebus.route.js),分別找到這三個Action對應實現者(在這裏我們經過一個「組件工廠」CompFactory來響應這些Action)。當咱們須要替換組件時,只須要在Action路由表中作出修改即是。
換句話說,在Rebus總線面前,每一個組件都是平等的。組件只會跟Rebus總線溝通,不會直接嵌入其它組件,也不會被嵌到其它組件中。「組件樹」這個概念在Rebus是經過Action消息來實現的,是一種「動態嵌套」關係。
在Flux/Redux中,應用的各類狀態以一棵「狀態樹」的形式都是從根組件上灌進去,全部子組件的狀態一概從這個根組件上繼承下來(無論組件樹的結構有多深)。這樣作的好處就是一旦某個狀態發生變化,React組件自動從上到下進行更新。
可是,這麼作真的好嗎?並非說一個應用就一棵狀態樹這個想法很差,我也贊同這種設計,由於狀態是Web應用中最重要但又很是容易混亂的信息,「惟一性」對狀態來講,很是重要。
但是若是全部子組件的狀態都是從根組件一層一層傳遞進來的話,至少會有兩個問題:
組件之間的耦合性高,難以並行開發:子組件的狀態是由父組件決定。那到底先寫父組件仍是先寫子組件?
狀態變化後,難以跟蹤變化的組件:假設你的某個操做修改了某個狀態,但這個狀態的變化會致使哪些組件更新了?光從Store中是看不出的,也沒法跟蹤,只能從根組件一層一層往下查,看看這個State被傳遞到哪一個組件中。
在Rebus中,我們一樣維繫着一棵「狀態樹」,並在應用初始化的時就加載進來的。
但不一樣的是,組件的狀態不是從上級組件中傳遞進來,是經過Rebus得到的,並且組件有權決定本身關心哪一個State的變化。
這樣作有幾個好處:
方便並行開發:由於組件之間沒有太過的耦合性。狀態都是經過Rebus得到的,大部分狀況下都是直接返回狀態樹中的某個狀態,這樣的「淺處理」很是適用於複雜系統開發中。
方便單元測試:因爲組件直接與狀態綁定(監聽),要對一個組件進行單元測試,直接修改這個組件綁定的狀態即是,便是沒有上級組件的存在,也不影響測試。
方便維護代碼: 從上面的代碼中能夠清晰地看出某個組件監聽哪些狀態,但反過來,某個狀態被哪些組件監聽了?從組件的代碼中是無法直觀看出來的。這個問題也不該該經過查閱代碼的形式來解決,而應該經過我們的Rebus來解決。我們能夠給Rebus增長一個方法,打印每個State的監聽者。以下圖:
如今我們既能夠清晰地看出一個組件監聽了哪些狀態,也能看出一個狀態被哪些組件監聽。這爲代碼的調試與維護提供極大的方便。
另外,咱們能夠輕鬆地打印出某個時刻的狀態樹或具體某個狀態的值。
先給有耐心看到這裏的人鼓個掌……而後也給我本身鼓個掌……由於對於一個拖延症極度病患者來講,用業餘時間寫這麼一篇技術貼真心不容易。當我寫這句話的時候,距離這個帖子的第一句話,整整隔了一個月!——大哥,你是一禪指敲鍵盤的嗎?
言歸正傳,總結下我們這個Rebus框架的特色:
實現了單向數據流模式,邏輯層次結構淺,思路清晰。
React組件職責單一,只負責渲染與響應交互。
以路由表的形式控制Action數據的流向,直觀、易維護。
React組件之間經過消息的形式實現動態嵌套。