dva
是螞蟻金服推出的一個單頁應用框架,對redux
,react-router
,redux-saga
進行了上層封裝,沒有引入新的概念,可是極大的程度上提高了開發效率;下面內容爲本人理解,若有錯誤,還請指出,不勝感激。react
redux
的優勢不少,痛點也有,好比異步控制,redux-saga
的出現使得異步操做變得優雅,可是基於redux-saga
不得不認可的一點就是開發過程實在是太麻煩了,倘若增長一個操做,不得不操做actions
,reducers
,sagas
,對於sagas
能夠還須要進行watch
,然後還要進行fork
;(PS: 原本就夠麻煩了,再加上一個sagas
);在添加一個操做時,不得不操做這麼多的文件,實在是麻煩,而dva
的出如今必定程度上解決了這個問題。redux
未使用dva
下的目錄常常是這樣的:api
actions --/ user.js --/ team.js reducers --/ user.js --/ team.js sagas/ --/ user.js --/ team.js
dva
將其合併:數組
models --/ user.js --/ team.js
dva
中有着幾個概念:react-router
namespace => combineReducers中對應的key值 state => 對應初始的state,也就是initialState effects => saga的處理函數 reducers => 對應reducers,不一樣的是,寫法上將switch...case轉化爲對象
除了這些之外,dva
中還有subscriptions
,這一律念來源於elm
,app
const app = dva({ history: browserHistory });
上面的過程發生了什麼?dva
本質上調用了下面函數:框架
function dva(hooks = {}) { const history = hooks.history || defaultHistory; const initialState = hooks.initialState || {}; delete hooks.history; delete hooks.initialState; const plugin = new Plugin(); plugin.use(hooks); const app = { // properties _models: [], _router: null, _store: null, _history: null, _plugin: plugin, _getProvider: null, // methods use, model, router, start, }; return app; }
hooks
爲傳入的一些配置,例如能夠經過傳入history
來改變路由的實現,dva
默認採用的是hashHistory
;從這裏能夠看出dva
暴露出來的方法:dom
app.router()
:指定路由,須要傳入一個函數,通常相似於({ history }) => (<Router>...</Router>)
異步
app.use()
:添加插件,這個稍後來看~ide
app.model()
:添加model
,也就是對應的添加一個store
下的數據,該方法作的就是對傳入的model
進行檢查,對reducers
添加命名空間,然後將其push
到_models
中。
namespace
必須且惟一,由於內置了react-redux-router
,因此namespace
也不能爲routing
subscriptions
與effects
均爲可選參數,傳入的話必須爲對象
reducers
爲可選,支持對象和數組兩種傳入方式(傳入數組的方式,每每伴隨着高階reducer
的應用,具體稍後再看~)
app.start()
:初始化應用,接受參數爲選擇器或者DOM
節點
須要注意的是:
reducers
和effects
的key
不須要用namespace/action
的形式了,由於dva
會自動將其加上,dispatch
的時候,saga
須要加上namespace
,而saga
中的put
不須要加入namespace
,緣由是dva
對put進行了重載
dva
同時支持rn應用,引入dva/mobile
便可,這時react-router
不在須要,利用rn中的Navigator
便可,不會引用react-router
與react-redux-router
,namespace
能夠命名爲routing
;正是因爲這點差別,做者將路由相關的內容做爲參數傳入了進去,具體能夠參見這個文件。
將一些配置項初始化好後,就能夠app.start
就是來建立一個應用,下面就一點點的看看start
的過程(如下基於默認狀況,也就是使用了react-router
):
參數校驗,是否爲DOM
元素或者檢查是否能夠根據傳入的選擇器字符串找到對應的DOM
,這個DOM
對應的就是ReactDOM.render
的第二個參數。
錯誤處理,使得發生錯誤時,不至於應用奔潰,固然須要傳入自定義hooks.onError
來處理:
// 傳入hooks.onError則調用,反之調用默認函數處理,拋出異常便可 const onError = plugin.apply('onError', (err) => { throw new Error(err.stack || err); }); // 目的是出現錯誤時,也能夠進行dispatch操做 const onErrorWrapper = (err) => { if (err) { if (typeof err === 'string') err = new Error(err); onError(err, app._store.dispatch); } };
遍歷_models
,初始化reducers,sagas
const sagas = []; // initalReducer爲{ routing: routerReducer } const reducers = { ...initialReducer }; // 爲rootReducer for (const m of this._models) { // 獲得默認的state reducers[m.namespace] = getReducer(m.reducers, m.state); if (m.effects) sagas.push(getSaga(m.effects, m, onErrorWrapper)); }
對於redux
的reducers
最多見的是基於switch..case
的,而dva
作出了一些改變,將每個case
分支變做了一個函數:
(PS: 本人認爲,這個能夠塊能夠更改,利用some
操做來儘量少的調用無心義的reducer
,因而我提了一個pr)
每個reducer
的實現以下:
// actionType對應的是dva的reducers中的key值 (state, action) => { const { type } = action; if (type && actionType !== type) { return state; } return reducer(state, action); };
看完了對於reducers
的處理,下面來看一下對於sagas
的處理:
function getSaga(effects, model, onError) { return function *() { for (const key in effects) { if (Object.prototype.hasOwnProperty.call(effects, key)) { const watcher = getWatcher(key, effects[key], model, onError); const task = yield sagaEffects.fork(watcher); // 爲了移除時能夠將saga任務註銷 yield sagaEffects.fork(function *() { yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`); yield sagaEffects.cancel(task); }); } } }; }
getWatcher
返回一個saga
監聽函數,也就是一般寫的watchXXX
,model.effects[key]
能夠是一個任務函數;也能夠是個數組,第一個參數爲任務函數,第二爲配置對象,能夠傳入type
,type
有4個可選值,takeEvery
(默認),takeLatest
,throttle
,watcher
四種,dva
對effects
作了一個錯誤處理:
effect => function *(...args) { try { yield effect(...args.concat(createEffects(model))); } catch (e) { onError(e); // 爲以前的onErrorWrapper } }
注意:
watcher
是指傳入的任務函數就是一個watcher
直接fork
就好
throttle
還要傳入一個ms
配置,這個ms
表明着在多少毫秒內只觸發一次同一類型saga
任務,而takeEvery
是不會限制同一類型執行次數,takeLatest
只能執行一個同一類型任務,有執行中的再次執行就會取消
由getSaga
能夠看出,${namespace}/@@CANCEL_EFFECTS
能夠取消對應的任務監聽
能夠經過配置hooks.onEffect
來增長saga
的watcher
redux
redux
中間件,由sagaMiddware
,routerMiddware
(啓用react-router
時),hooks.onAction
傳入的其它中間件,如redux-logger
等
其它加強,如redux-devtools
,內置了redux-devtools
,另需的話在hooks.extraReducers
傳入
const enhancers = [ applyMiddleware(...middlewares), devtools(), ...extraEnhancers, ]; const store = this._store = createStore( // eslint-disable-line createReducer(), initialState, compose(...enhancers), );
經過配置hooks.onStateChange
能夠指定redux
的state
改變後所觸發的回調函數:
const listeners = plugin.get('onStateChange'); for (const listener of listeners) { store.subscribe(() => { listener(store.getState()); }); } }
subscriptions
是一個新概念,會在dom ready
以後執行,在這裏面能夠作一些基礎數據的獲取:
通常會將初始數據的獲取放在react
的生命週期中,好比componentWillMount
,可是假設咱們作了代碼分割,實現了按需加載,那麼咱們開始獲取數據的時間爲:獲取相應的js
+解析js
+執行react
生命週期,可是redux
的數據加載和ui
組件沒有太大關係,能夠將數據獲取的時間點提早,subscriptions
提供瞭解決方法,其意義爲訂閱,對於上面的場景,咱們能夠訂閱路由,到了執行的路由執行相應的dispatch()
,如:
setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/users') { dispatch({ type: 'fetch', payload: query }); } }); }
(PS: 對於這個新概念,我也不是很清楚,後面的文章會有專門的描述,你們先有一個概念就好)
上述過程均爲初始化的過程,就是獲取到須要的reducers
,sagas
以及對於一些中間件和插件的配置,下面要進行的就是掛載了,也就熟悉的render(<Provider>, container)
。
dva.model
與dva.unmodel
,封裝了在運行時的store
進行一類增長和刪除的操做,例如能夠再切換到某一路由時動態的加入一個model
(我的猜想,熱更新頗有可能也利用了這個兩個api
與hooks.onHmr
)。
關於redux
還有一個利器,那就是高階reduce
,固然在dva
中也有體現,這篇文章已經很長了,這些內容留在下一篇中介紹。以上是本人對於dva
的粗略的理解,內容若有錯誤,還請你們指出。dva
的確簡化了開發的流程,並且在螞蟻金服的不少業務線也有着應用,是一個很值得你們一試!