React、Vue、Angular等庫(框架)出現後,前端進入了UI組件化開發的時代。經過合理地劃分應用的功能,封裝成一個個從底層到高層的組件,最後構造爲一顆組件樹,完成咱們的應用:前端
看起來真棒,不是嗎?git
可是在實際開發中,還有一道繞不過去的坎:狀態管理。怎麼組織和劃分應用的狀態,UI組件如何得到自身須要的狀態數據,如何複用狀態部件等等,這些問題困擾了我好久。github
當前的狀態管理方案中,以Flux模式爲主流,表明性的有Redux、Vuex等。redux
舉個例子,假如要實現一個電商系統,這個系統中包含「商品列表」、「收藏夾」兩個功能,他們都包含一個元素結構類似的商品列表數據,可是數據的來源(接口)不一樣。在Redux中,須要這樣寫:api
// Reducers##
function productList (state = fromJS({loading: false, data: []}), {type, payload}) {
switch (type) {
case types.PRODUCT_GET_LIST_START:
return state.merge({loading: true});
case types.PRODUCT_GET_LIST_SUCCESS:
return state.merge({loading: false, data: payload});
case types.PRODUCT_GET_LIST_FAILURE:
return state.merge({loading: false});
default:
return state;
}
}
function favorites (state = fromJS({loading: false, data: []}), {type, payload}) {
switch (type) {
case types.FAVORITES_GET_START:
return state.merge({loading: true});
case types.FAVORITES_GET_SUCCESS:
return state.merge({loading: false, data: payload});
case types.FAVORITES_GET_FAILURE:
return state.merge({loading: false});
default:
return state;
}
}
// Actions
function getProducts (params) {
return (dispatch, getState) => {
dispatch({type: types.PRODUCT_GET_LIST_START});
return api.getProducts(params)
.then(res => {
dispatch({type: types.PRODUCT_GET_LIST_SUCCESS, payload: res});
})
.catch(err => {
dispatch({type: types.PRODUCT_GET_LIST_FAILURE, payload: err});
});
};
}
function getFavorites (params) {
return (dispatch, getState) => {
dispatch({type: types.FAVORITES_GET_START});
return api.getFavorites(params)
.then(res => {
dispatch({type: types.FAVORITES_GET_SUCCESS, payload: res});
})
.catch(err => {
dispatch({type: types.FAVORITES_GET_FAILURE, payload: err});
});
};
}
export const reducers = combineReducers({
productList,
favorites
});
export const actions = {
getProductList,
getFavorites
};
複製代碼
能夠看到,一樣是商品列表數據的加載,須要寫兩份幾乎相同的reducer和action。難受,很是難受!mvc
看到這,有的朋友可能會說,能夠封裝成一個工廠方法來生成呀,好比說:app
function creteProductListReducerAndAction (asyncTypes, service, initialState = fromJS({loading: false, data: []})) {
const reducer = (state = initialState, {type, action}) => {
switch (type) {
case asyncTypes.START:
return state.merge({loading: true});
...
}
};
const action = params => dispatch => {
dispatch({type: asyncTypes.START});
return service(params)
.then(res => {
dispatch({type: asyncTypes.SUCCESS, payload: res});
})
.catch(err => {
dispatch({type: asyncTypes.FAILURE, payload: err});
});
}
return {reducer, action};
}
複製代碼
乍一看也還能夠接受,可是若是有一天,我想要擴展一下favorites的reducer呢?當應用開始變得愈發豐滿,須要不斷地改造工廠方法才能知足業務的需求。框架
上面的例子比較簡單,固然還有更好的方案,社區也有諸如dva的框架產出,可是都不夠完美:複用和擴展狀態部件很是困難。dom
相似UI組件化,數據組件化很好地解決了Store模式難以複用和擴展的問題。像一個React組件,很容易在組件樹的各個位置重複使用。使用HOC等手段,也能方便地對組件自身的功能進行擴展。async
本系列文章的主角:MobX State Tree(後文中簡稱MST)正是實現數據組件化的利器。
React, but for data.
MST被稱爲數據管理的React,他創建在MobX的基礎之上,吸取了Redux等工具的優勢(state序列化、反序列化、時間旅行等,甚至可以直接替換Redux使用,見redux-todomvc example)。
對於MST的具體細節,在開篇中就不贅述了,先來看看如何用MST來編寫上文中的「商品列表」和「收藏夾」的數據容器:
import { types, applySnapshot } from 'mobx-state-tree';
// 消息通知 BaseModel
export const Notification = types
.model('Notification')
.views(self => ({
get notification () {
return {
success (msg) {
console.log(msg);
},
error (msg) {
console.error(msg);
}
};
}
}));
// 可加載 BaseModel
export const Loadable = types
.model('Loadable', {
loading: types.optional(types.boolean, false)
})
.actions(self => ({
setLoading (loading: boolean) {
self.loading = loading;
}
}));
// 遠端資源 BaseModel
export const RemoteResource = types.compose(Loadable, Notification)
.named('RemoteResource')
.action(self => ({
async fetch (...args) {
self.setLoading(true);
try {
// self.serviceCall爲獲取數據的接口方法
// 須要在擴展RemoteResource時定義在action
const res = await self.serviceCall(...args);
// self.data用於保存返回的數據
// 須要在擴展RemoteResource時定義在props中
applySnapshot(self.data, res);
} catch (err) {
self.notification.error(err);
}
self.setLoading(false);
}
}));
// 商品Model
export const ProductItem = types.model('ProductItem', {
prodName: types.string,
price: types.number,
...
});
// 商品列表數據Model
export const ProductItemList = RemoteResource
.named('ProductItemList')
.props({
data: types.array(ProductItem),
});
// 商品列表Model
export const ProductList = ProductItemList
.named('ProductList')
.actions(self => ({
serviceCall (params) {
return apis.getProductList(params);
}
}));
// 收藏夾Model
export const Favorites = ProductItemList
.named('Favorites')
.actions(self => ({
serviceCall (params) {
return apis.getFavorites(params);
}
}));
複製代碼
一不當心,代碼寫得比Redux版本還要多了[捂臉],可是仔細看看,上面的代碼中封裝了一些細粒度的組件,而後經過組合和擴展,幾行代碼就獲得了咱們想要的「商品列表」和「收藏夾」的數據容器。
在MST中,一個「數據組件」被稱爲「Model」,Model的定義採用了鏈式調用的方式,而且能重複定義props、views、actions等,MST會在內部將屢次的定義進行合併處理,成爲一個新的Model。
再來看上面的實現代碼,代碼中定義了三個BaseModel(提供基礎功能的Model),Notification
、Loadable
以及RemoteResource
。其中Notification
提供消息通知的功能,Loadable
提供了loading狀態以及切換loading狀態的方法,而RemoteResource
在前二者的基礎上,提供了加載遠程資源的能力。
三個BaseModel的實現很是簡單,而且與業務邏輯零耦合。最後,經過組合BaseModel並擴展出對應的功能,實現了ProductList
與Favorites
兩個Model。
在構造應用的時候,把應用的功能拆分紅這樣一個個簡單的BaseModel,這樣應用的代碼看起來就會賞心悅目而且更易於維護。
本篇文章是「MobX State Tree數據組件化開發」系列文章的開篇,本系列文章將會爲你們介紹MST的使用以及筆者在使用MST的時候總結的一些技巧和經驗。
本系列文章更新週期不肯定,筆者會盡量的抽出時間來編寫後續文章。
喜歡本文的歡迎關注+收藏,轉載請註明出處,謝謝支持。