mobx
是一款基於觀察者模式的響應式數據管理框架,相對於redux
來講是後起之秀。前端
有一種觀點認爲mobx
不適合構建大型項目,這源於mobx
過於靈活的特色。靈活即意味着隨意,這在開發日益複雜的大型項目是致命的弱點。redux
則否則,它的惟一數據源、reducer
純函數、只能經過dispatch
修改狀態等幾個特性保證了代碼書寫格式的高度統一。node
本文不會討論mobx
的使用細節,只會在充分利用mobx
優點的基礎上,對開發格式進行統一,保證開發大型項目的可維護性。react
mobx
的優點極其優秀,面向對象編程、響應式編程、mutable
的數據處理方式、精準更新組件的能力,這裏不過多討論。webpack
state
的做用;也能夠定義在單獨的store
內store
聲明的方法內。store
的數據處理,store
間可能造成交叉引用的網狀結構。store
每每按頁面和模塊劃分,散落在各處,很差統一管理。store
實例化的時機和方式不可控。store
由於業務變動須要支持多實例時,改造難度極大node
端在讀取數據填充頁面時,還須要把數據存儲到頁面,供前端加載時從數據恢復到store
(redxu
的createStore
自然支持從initialState
恢復數據的能力)面對以上的種種問題,大部分人都會持有mobx
不適合大型項目的觀點。web
在筆者用mobx
+react
作了諸多中大型的前端項目以後,對這些劣勢深惡痛絕,也逐漸摸索出了一些方案來解決上述的問題。編程
爲了解決數據定義,數據共享以及邏輯代碼如何防止等問題,首先對項目結構進行分層。redux
stores
、actions
、views
分爲三層stores
定義頁面內各個數據模型及數據的操做方法,各個store之間互相獨立views
層做爲視圖層,接收stores
注入的數據負責渲染actions
層處理交互邏輯,引用各個store
方法調用更新數據,又mobx
自動觸發視圖刷新以上是一個典型的mvc
分層結構,這種方式很大程度上解決了問題點0、一、2。api
經過第一步的改造,項目的可維護性可謂上升一個臺階。promise
可是頁面的store
和action
須要手動實例化並手動注入到每一個頁面組件,着實是一個負擔。而且store
實例化自由,管理起來較爲混亂。並未解決三、四、5的問題。
因此須要開發一個狀態管理庫,主要實現以下功能
store
和action
的自動查找加載。store
和action
分頁面放置,經過某種機制進行查找store
和action
自動實例化,並造成全局惟一數據源store
提供配置單例或多實例的配置項,減小因需求變動致使的代碼改造工做量store
。好比訪問頁面A
,只需實例化A
頁面依賴的store
store
和action
的查找方式簡單介紹兩種,一種是經過webpack
提供的require.context
動態的引入特定目錄下的store
和action
模塊,第二種是經過裝飾器模式進行加載。 僞代碼以下
//webpack
require.context('./',true,/^(.+\/)*stores\/(.+)\.(t|j)sx?$/i)
//裝飾器
@store({
path:'pageA.storeA', //在全局store中的訪問路徑
type:'singleton'|'multi' // 聲明單例仍是多實例
})
class StoreA{
}
// store裝飾器的實現
let store = (config) => target => {
target['__storeType'] = config.type //保存
App['__stores'] = App['__stores'] || [] //App爲狀態管理類
App['__stores'].push({ target, path: config.path})
return target;
}
複製代碼
拿到全部store
的信息以後,就能夠在管理類裏對stores
和actions
進行處理,組裝全局惟一的rootStore
了,action
處理也是同樣。
若是爲了追求性能,能夠考慮實現這麼一個特性。實現方式能夠用訪問器屬性,在訪問到store
屬性時,再進行動態的實例化。僞代碼以下
Object.defineProperty(rootAction, 'storeA', {
configurable: true,
enumerable: true,
get() {
StoreA['__instance'] = StoreA['__instance'] || new StoreA()
return StoreA['__instance']
},
set() {
throw Error("can not set store")
}
})
複製代碼
經過這麼一個狀態管理庫,咱們解決了三、四、5,對於問題6 服務端渲染,也能夠經過簡單的處理對rootStore
進行恢復。
上面的裝飾器@store
須要手動指定store
在rootStore
中所處的節點,能不能經過store
文件所在的目錄名、文件名、store
類名等信息直接映射到對應的結構呢?
答案是能夠的,只須要編寫一個babel
轉換插件,在編譯時對文件的抽象語法樹進行分析替換,自動填充@store
的path
屬性就行了。(筆者項目用的是ts
,提供了一個ts transformer
完成一樣的功能)
store
文件、action
文件,或是jsx
、css
文件,都有或多或少的樣板代碼。爲了開發流程的自動化,能夠開發腳手架工具,自動生成頁面骨架。一是爲了提高開發效率,二能夠規範開發流程。ts
的話,這種全局自動加載造成的store
會丟失類型信息。因此須要自動的生成一份類型聲明文件(.d.ts
)幫助有更好的開發體驗。最後一個話題,如何更嚴格的規範代碼的書寫方式。
即便咱們限定了業務邏輯只能在action
內處理,但終歸是口頭約定。老成員總有圖便利把邏輯寫到view
層的時候,新成員剛加入時的代碼更可能如此。
因此咱們須要提供一種機制來保證只能在action
內調用store
的方法進行邏輯處理,而在action
外的store
調用都無效,並在開發環境給以警告。
這個問題若是你認爲很簡單,多是由於你還沒理解到這個的關鍵點在哪。下面經過例子來討論解決方案。
//聲明一個store
class StoreA{
age = null;
setAge(age){
this.age = age;
}
}
//聲明一個action
class ActionA{
//調用store方法
setAge(age){
this.storeA.setAge(age); //有效
}
}
//組件內
storeA.setAge(age) //無效
複製代碼
對於上述場景,處理方法比較簡單。只須要
flag
store
和action
時對實例的方法分別進行包裝action
的方法調用前設置flag
爲true
,執行action
的方法,而後設置flag
爲false
。store
的方法若是在action
內調用時訪問到的flag
爲true
,在其餘地方訪問到的flag
爲false
。store
方法的包裝比較簡單,判斷flag
,爲true
執行數據操做,爲false
進行友好提示通過上述幾步,就完成了同步場景的限制處理。
但實際的項目中大量的存在異步操做,若是action
以下所示,會如何呢?
class ActionA{
//調用store方法
async setAge(age){
await saveAge(url); //接口調用
this.storeA.setAge(age); //有效
}
}
複製代碼
這時storeA.setAge
雖然處於action
內,但訪問到的flag
倒是false
,方案失效了。
對同步操做的處理如此簡單,異步操做倒是一個巨大的難題。如今的課題能夠抽象爲以下描述
如何實如今同一個方法內的調用(包括同步操做, setTimeout、promise、rAF、各類事件等異步操做的回調內...)都能訪問到同一個上下文(true),而在這個方法外訪問到的是另外一個(false)
複製代碼
心裏隱隱約約有一個答案,若是在action
調用時保存這個上下文,並在各類異步的回調裏再取出這個上下文便可實現功能。但這是一個可怕的事情,意味着須要咱們去代理全部的異步調用,換句話說咱們須要覆蓋原生的方法來作這麼一件事情!
這彷佛是很難去實現的,直到我發現了zone.js
。
簡單介紹一下,zone.js
是angular
框架的核心組件,angular
利用zone.js
監聽全部(可能致使數據變化)的異步事件。
這跨度有點大,怎麼又扯到了angular
。
不要緊,從新介紹一下。zone.js
描述了JavaScript
執行過程的上下文,能夠在異步任務之間進行持久性傳遞。
重點就是這句話,我翻譯一下,zonejs
能保持同一個方法內的調用(不管同步仍是異步的)都能訪問到同一個上下文對象。這不正好解決了咱們的問題嗎?
如今利用zonejs
來解決咱們以前的問題。代碼以下
//這裏並無闡述zone.js如何使用,若是看過zonejs文檔應該很容易理解下面的代碼所作的事情
const zone = Zone.root.fork({
name: '__mobx__zone'
});
//包裝action的setAge方法,使得action內的方法調用訪問到Zone.current都爲zone
let oldFn = ActionA.setAge
ActionA.setAge = (...args) => {
return zone.run(oldFn, context, args)
}
//包裝store的方法,判斷Zone.current是否爲zone,若是在action以外調用則爲Zone.root
let oldFn = StoreA.setAge
StoreA.setAge = (...args) => {
if(Zone.current === zone){
return oldFn.apply(context,args)
}else{
//在action外調用store方法觸發警告
console.error('invalid call')
}
}
//以上的包裝方法均在內部處理,不暴露在業務代碼中
複製代碼
利用zone.js
能夠很容易的實現咱們想要的功能,經過粗略的源碼瀏覽發現zone.js
正是暴力的代理了原生的api
。
經過上述幾步處理,咱們就能夠愉快的拿mobx
進行大型項目的構建和持續迭代了。
本文並未涉及過多的代碼細節,對於mobx
如何使用也並未闡述。本文着重去解決在使用mobx
過程當中可能引起的問題,而且在規範成員的代碼風格方面作了嘗試,使得在用mobx
進行項目的開發時能最大限度的保證代碼格式的統一,下降項目的維護成本。 關於如何開發和維護一個大型項目是一個很大的話題,應該在約定或者強制某些規範的基礎上,再根據所處的業務場景進行特定的設計纔可能作好。