[MobX State Tree数据组件化开发][0]:开篇

4,165 阅读5分钟

👉系列文章目录👈

组件化的时代

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的时候总结的一些技巧和经验。

本系列文章更新周期不确定,笔者会尽可能的抽出时间来编写后续文章。

喜欢本文的欢迎关注+收藏,转载请注明出处,谢谢支持。