👉系列文章目录👈
组件化的时代
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的时候总结的一些技巧和经验。
本系列文章更新周期不确定,笔者会尽可能的抽出时间来编写后续文章。
喜欢本文的欢迎关注+收藏,转载请注明出处,谢谢支持。