缘起
module-reaction是我在上家公司时写的react业务框架,对redux/react-redux进行了封装,用来规范react项目中的业务数据管理流程,同时提供一种模式来简化开发套路,减少一定的代码量。根据该框架在几个项目中的实际使用来看,同事反响还不错。
近期有点空闲时间,于是乎,针对框架之前暴露出的问题,进行了优化和重构,现在开源出来,给大家安利一波。
特性
- 模块化数据集
- 数据修改安全
- 事务原子化
- 原生异步事务处理设计
- 更少的代码量
- 简易的api
众所周知,随着项目复杂度的增加,我们通常会把软件划分成多个业务模块,各业务模块的数据相对独立,模块下的功能也通常只会使用和修改本模块的数据,只有在少量场景下才需要使用外模块的数据。
基于以上原则,module-reaction在设计上要求每个业务模块有自己独立的数据集;且,隶属于本模块的动作/事务,只能修改本模块的数据集!同时,事务必须原子化。
在module-reaction中,模块数据集=moduleStore, 动作/事务=moduleAction,下面慢慢展开讲:
使用
安装
通过npm: npm install module-reaction
通过yarn: yarn add module-reaction
代码
首先,类似于使用react-redux, 你需要引入Provider,并将其作为APP节点的最外层:(以下代码为typescript)
import { Provider } from 'module-reaction';
ReactDOM.render(
,
document.getElementById('root')
);
(注:事实上,这里的Provider就是对react-redux的Provider加了一层封装)
然后,你就可以把关注点投入到你的业务模块了。
假设你思考了项目的功能需求, 划分出了: 模块A,模块B,模块C ..., 并且对于各个业务模块,我们习惯于将其内部再分为model层和view层(即通常所说的MVx设计模式)
so, 在model层,让我们先声明一下模块A的数据集:
export const MODULE_A = 'module_a';
export const mStoreA: ModuleStore = {
module: MODULE_A,
size: '2*2',
count: 10,
price: 9.9,
infos: {
madeIn: 'China',
saleTo: 'anywhere'
}
}
声明之后,可以手动调用一下regStore来将它注册进框架(不是必须的,因为后面有种语法糖可以帮你自动注册)。
然后来到view层,在react项目中,view层就是一些React.Component组件。
我们使用mapProp来为组件注入props.
mapProp是装饰器函数,在ES6和typescript中,装饰器为开发提供了多种便利,以下示例代码为PageA注入了mStoreA数据集里的['size','price','count','infos']的数据:
@mapProp(mStoreA, 'size', 'price', 'count', 'infos')
export class PageA extends React.Component {
render() {
return (
{this.props.size},
{this.props.price * this.props.count},
{this.props.infos.madeIn}
)
}
语法糖 :当你想要把一个moduleStore里的所有数据都注入时,可以省略mapProp的第2-n个参数,像这样:
@mapProp(mStoreA)
export class PageA extends React.Component {
...
}
注意:
mapProp的第一个参数为想要注入的moduleStore,可以是字符串或者moduleStore对象,当你传字符串时,该字符串代表模块数据集的名字,此时需要你在别的地方手动调用过regStore注册过该数据集才行,不然会报错; 如果你传的是moduleStore对象,那么mapProp内部会检查你之前有没有注册过该moduleStore,没有的话自动clone一份进行注册。
所以,如果你之前手动调用过:
regStore(mStoreA);
那么,这里可以传给mapProp一个模块名:
@mapProp(MODULE_A, 'size', 'price', 'count', 'infos')
export class PageA extends React.Component {
...
}
如果想给一个Component注入多个模块的数据呢?
你猜对了,就是这样:
@mapProp(MODULE_A)
@mapProp(MODULE_B, 'propxxx', 'p', 'sth')
@mapProp(mStoreC, 'sss', 'sd', 'sth:sth2')
export class PageA extends React.Component {
...
}
注意:你可能已经关注到上面的代码里有个sth:sth2 这是注入时的重命名语法。我们的实际开发中经常遇到,mStoreB和mStoreC可能是两个同事写的,他们碰巧声明了一个同名的属性,比如,都声明了个叫sth的属性, 当需要将两个moduleStore里的同名属性注入到同一Component时,可以使用冒号语法进行重命名,上面的例子中,PageA的props里,props.sth = mStoreB.sth; props.sth2 = mStoreC.sth;
view层现在通过mapProp拿到了数据集,那么如果需要修改数据呢,有请doAction出场!
doAction接收到参数如下:
function doAction(
moduleAction: ModuleAction | string,
payload?: P,
loadingTag: string | 'none' = 'none'
)
第一个参数为moduleAction对象或模块名string
第二个参数为附带的数据,该数据会作为moduleAction.process函数的入参
第三个参数为标记 执行此moduleAction时是否显示loading
我们先来看moduleAction.
moduleAction代表一个对指定module数据集进行修改的原子化操作。
在后端开发中,事务原子化是一个常见的理念,简单举例,比如应对客户端的请求,会把所需的数据一次性组装给客户端,而通常不会把把请求拆成多个api,让客户端请求多次,每次只给一种数据。 然而,随着GraphQL的流行,以及'无服务器'方案的出现,包括客户端实际开发时的一些复杂场景,客户端经常需要在一次交互操作中做很多事情,从多个地方获取数据,加工后再用于view层的呈现。因此,事务原子化在客户端也变成一个良好的开发理念。
回到 module-reaction里,继续示例代码,我们先定义一个moduleAction,用来修改mStoreA里的count值:
export const increaseCountAction: MoudleAction = {
module: MODULE_A,
process: async (payload: KV, moduleState: ModuleStore) => {
let count = moduleState.count;
count++;
return {count};
}
}
...
...
...
private increaseCnt = e => {
doAction(increaseCountAction);
}
可以看到,ModuldeAction通常需要提供以下属性:
1.module 该属性的值是所属模块名的字符串,表示此ModuleAction只能修改其所指定模块的数据,上例中,increaseCountAction只能修改mStoreA里的数据。
2.process 该属性是一个异步函数,接受两个参数,
第一个 payload 即是业务里调用doAction时传入的那个payload;
第二个 moduleState 是此process函数执行时,所属的moduleStore的快照(本例中即mStoreA的深拷贝);
process函数需要返回一个json对象,代表要更新到moduleStore的值,本例中,只修改了count的值,所以返回了 {count} (ES6语法);
语法糖 对于上例中这种很简单的修改moduleStore值的场景,其实可以不需单独定义一个moduleAction, 你可以直接这样写:
doAction(MODULE_A, {count: this.props.count+1});
规则: doAction的第一个参数是string,或者第一个参数传入的moduleAction没有process属性时,就会把第二个参数payload直接作为要修改的数据,合入到第一个参数所指定的moduleStore中。
事实上,moduleAction的process函数就是为了处理复杂的原子化任务而存在的,如果不需要复杂操作,那就用上面的语法糖写法吧。
下面贴一个复杂点的例子:
export const freshUserMsgAction: ModuleAction = {
module: MODULE_B,
process: async (payload: KV, moduleState: IModuleB) => {
// 从服务器请求数据
const msg = await fetchNewMsg();
// 对拿到的数据做一些耗时的复杂处理
await doSomethDealWith(msg);
// 从其他moduleStore里取点数据过来
const username = getModuleProp(MODULE_A,'username');
msg.username = username;
const lists = moduleState.lists;
lists.push(msg);
// moduleState是当前moduleStore的快照!!
// 所以直接改lists,不会对redux里的真实moduleStore起作用
// 你想改变lists,只能返回一个包含lists的对象
return { lists, upateTime: Date.now() }
}
}
注:moduleAction还有两个可选属性:
1.name 该moduleAction的名字标识,当启用reduxDevtools时方便你查看具体执行了那个moduleAction
2.maxProcessSeconds 允许的最长执行秒数,默认值是8,
超过这个时间后,框架认为该moduleAction出了问题,process的执行结果将被丢弃;
然后跳过它去执行下个moduleAction。
所以,如果你预料到你的moduleAction耗时很久,记得给它的maxProcessSeconds设置一个较大的值!!!
还有个plusAction,不太常用,放到后面 api里讲...
基本用法就是这些了,更多内容,可以看源码里的实例!!
https://github.com/swellee/reaction 记得给加个star啊 亲!
api
-
regStore
- 用于手动注册一个moduleStore, 手动注册后,可以在view层调用mapProps时第一参数使用string.
- 区别于mapProp接受到moduleStore参数时的自动注册:mapProp的自动注册时会检测该模块有没有注册过,没有时才自动注册;而手动调用regStore是不做检测,如果之前注册过,会强制覆盖;
- mapProp内部也是调用的regStore
- regStore执行时,注册进redux的是moduleStore的深拷贝!!
所以,举例,当你某个时候想要将redux[MODULE_A]的数据重置会初始状态时,just: doAction(MODULE_A,mStoreA)就可以了。 - 推荐大家:非必要情况,尽量不用自己手动调用regStore了
mapProp
mapProp是一个ES6/typescript装饰器,如果不想用装饰器语法,可以作为普通的函数,像react-redux的connect函数那样使用,示例代码:
class PageA extends React.Component{
...
}
export mapProp(mStoreA, 'xx','xx2')(PageA);
其他说明参见使用
doAction
当你需要修改某个模块数据时候,调用这个函数吧,如果只是简单的数据修改,别忘了语法糖哦。
plusAction
-
doFunction
这里有一个重要补充说明:
所有的moduleAction都是按队列执行的!!
也就是说,执行完一个,才会执行下一个。
plusAction 是应对这样的场景:在一个moduleAction.process执行的时候,发现需要临时新增启动另外一个moduleAction,或者在一个process里面需要根据已经得到的数据,按条件判断下一个该启动那个moduleAction, 此时调用plusAction(otherAction,payload,...)函数,框架会在当前action结束后,紧接着执行otherAction, 等otherAction完事后再继续原来的action队列;
doFunction 其实是一个语法糖,方便在action队列里插入一条函数执行体。
啰嗦百句,不如一例:
假如已定义了actionA、actionB、actionC、functionD、actionE、actionF。
且,actionB里调用了plusAction:actionB: ModuleAction = { module: MODULE_B, process: async () => { ... plusAction(actionE) plusAction(actionF) ... // 记住,每个process必须要有个json返回对象 return {someThing: 'someValue'} } }
那么:
doAction(actionA); doAction(actionB); doAction(actionC); doFunction(functionD);
其执行顺序为:actionA->actionB->actionE->actionF->actionC->functinD
-
Provider
ReactDOM.render要用到的根节点包装器
reaction
一个常量对象,包含了一些全局配置项
export const reaction: ReactionDb = {
store: Object.create({}),
showLoading: testLoadingFn,
hideLoading: testLoadingFn,
defaultMaxProcessSeconds: 8 // by default, one action's process function is allow to execute 8s
}
getGlobalState
获取全局的redux store(默认返回的是快照)
getModuleState
获取指定模块的数据集(默认返回的是快照)
getModuleProp
获取指定模块的数据集的某个属性值(默认返回的是快照)
-
enableDevtools
启用Redux DevTools chrome 插件调试
interface
下面列出框架里的一些关键interface定义:
KV :sth key-value (alias for Object)
interface KV {
[k: string]: any
}
ModuleStore :the modulized store:
interface ModuleStore extends KV {
module: string;
}
ModuleAction :a moduleAction is a processor to deal with some datas and make the changes to the specific module.
interface ModuleAction {
module: string;
name?: string;
maxProcessSeconds?: number;
process?: (payload: PAYLOAD_TYPE, moduleStore: MODULE_STORE) => Promise;
}
update:
更新到了1.0.6,修复了plusAction报错的问题
next plan
准备设计插件体系,并提供用于action的撤销、重做的插件
恭喜你!认真的看完了全部文章,应该已经了解了module-reaction的特点和使用方式了!再次提醒,别忘了给个star哦!https://github.com/swellee/reaction
btw, 如果你玩flutter,这里还有一个flutter的实现:
https://github.com/swellee/flutter_reaction
enjoy!!