[MobX State Tree資料元件化開發][0]:開篇

awaw00發表於2019-01-30

?系列文章目錄?

元件化的時代

React、Vue、Angular等庫(框架)出現後,前端進入了UI元件化開發的時代。通過合理地劃分應用的功能,封裝成一個個從底層到高層的元件,最後構造為一顆元件樹,完成我們的應用:

  • App
    • Page1
      • Component1
      • Component2
      • ...
    • Page2
    • ...

看起來真棒,不是嗎?

但是在實際開發中,還有一道繞不過去的坎:狀態管理。怎麼組織和劃分應用的狀態,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),NotificationLoadable以及RemoteResource。其中Notification提供訊息通知的功能,Loadable提供了loading狀態以及切換loading狀態的方法,而RemoteResource在前兩者的基礎上,提供了載入遠端資源的能力。

三個BaseModel的實現非常簡單,並且與業務邏輯零耦合。最後,通過組合BaseModel並擴充套件出對應的功能,實現了ProductListFavorites兩個Model。

在構造應用的時候,把應用的功能拆分成這樣一個個簡單的BaseModel,這樣應用的程式碼看起來就會賞心悅目並且更易於維護。

關於本文

本篇文章是“MobX State Tree資料元件化開發”系列文章的開篇,本系列文章將會為大家介紹MST的使用以及筆者在使用MST的時候總結的一些技巧和經驗。

本系列文章更新週期不確定,筆者會盡可能的抽出時間來編寫後續文章。

喜歡本文的歡迎關注+收藏,轉載請註明出處,謝謝支援。

相關文章