?系列文章目錄?
元件化的時代
React、Vue、Angular等庫(框架)出現後,前端進入了UI元件化開發的時代。通過合理地劃分應用的功能,封裝成一個個從底層到高層的元件,最後構造為一顆元件樹,完成我們的應用:
- App
- Page1
- Component1
- Component2
- ...
- Page2
- ...
- Page1
看起來真棒,不是嗎?
但是在實際開發中,還有一道繞不過去的坎:狀態管理。怎麼組織和劃分應用的狀態,UI元件如何獲得自身需要的狀態資料,如何複用狀態部件等等,這些問題困擾了我很久。
Flux模式
當前的狀態管理方案中,以Flux模式為主流,代表性的有Redux、Vuex等。
舉個例子,假如要實現一個電商系統,這個系統中包含“商品列表”、“收藏夾”兩個功能,他們都包含一個元素結構相似的商品列表資料,但是資料的來源(介面)不同。在Redux中,需要這樣寫:
// 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。難受,非常難受!
看到這,有的朋友可能會說,可以封裝成一個工廠方法來生成呀,比如說:
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的框架產出,但是都不夠完美:複用和擴充套件狀態部件非常困難。
MobX State Tree:資料元件化
類似UI元件化,資料元件化很好地解決了Store模式難以複用和擴充套件的問題。像一個React元件,很容易在元件樹的各個位置重複使用。使用HOC等手段,也能方便地對元件自身的功能進行擴充套件。
本系列文章的主角: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的時候總結的一些技巧和經驗。
本系列文章更新週期不確定,筆者會盡可能的抽出時間來編寫後續文章。
喜歡本文的歡迎關注+收藏,轉載請註明出處,謝謝支援。