小而美的react 业务框架 module-reaction 开源了!

缘起

module-reaction是我在上家公司时写的react业务框架,对redux/react-redux进行了封装,用来规范react项目中的业务数据管理流程,同时提供一种模式来简化开发套路,减少一定的代码量。根据该框架在几个项目中的实际使用来看,同事反响还不错。
近期有点空闲时间,于是乎,针对框架之前暴露出的问题,进行了优化和重构,现在开源出来,给大家安利一波。

特性

  1. 模块化数据集
  2. 数据修改安全
  3. 事务原子化
  4. 原生异步事务处理设计
  5. 更少的代码量
  6. 简易的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

获取指定模块的数据集的某个属性值(默认返回的是快照)


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!!

你可能感兴趣的:(react.js,redux,typescript,javascript,module模式)