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(
        <Provider><App /></Provider>, 
        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<KV, {}> {
    render() {
      return (
        <div>
        {this.props.size},
        {this.props.price * this.props.count},
        {this.props.infos.madeIn}
        </div>
      )
    }

语法糖:当你想要把一个moduleStore里的所有数据都注入时,可以省略mapProp的第2-n个参数,像这样:

@mapProp(mStoreA)
  export class PageA extends React.Component<KV, {}> {
    ...
  }

注意: mapProp的第一个参数为想要注入的moduleStore,可以是字符串或者moduleStore对象,当你传字符串时,该字符串代表模块数据集的名字,此时需要你在别的地方手动调用过_regStore_注册过该数据集才行,不然会报错; 如果你传的是moduleStore对象,那么_mapProp_内部会检查你之前有没有注册过该moduleStore,没有的话自动clone一份进行注册。 所以,如果你之前手动调用过:

regStore(mStoreA);

那么,这里可以传给mapProp一个模块名:

@mapProp(MODULE_A, 'size', 'price', 'count', 'infos')
  export class PageA extends React.Component<KV, {}> {
    ...
  }

如果想给一个Component注入多个模块的数据呢? 你猜对了,就是这样:

@mapProp(MODULE_A)
  @mapProp(MODULE_B, 'propxxx', 'p', 'sth')
  @mapProp(mStoreC, 'sss', 'sd', 'sth:sth2')
  export class PageA extends React.Component<KV, {}> {
    ...
  }

注意:你可能已经关注到上面的代码里有个sth:sth2这是注入时的重命名语法。我们的实际开发中经常遇到,mStoreB和mStoreC可能是两个同事写的,他们碰巧声明了一个同名的属性,比如,都声明了个叫_sth_的属性, 当需要将两个moduleStore里的同名属性注入到同一Component时,可以使用冒号语法进行重命名,上面的例子中,PageA的props里,props.sth = mStoreB.sth; props.sth2 = mStoreC.sth;

view层现在通过mapProp拿到了数据集,那么如果需要修改数据呢,有请doAction_出场! **_doAction**接收到参数如下:

function doAction<P = KV>(
    moduleAction: ModuleAction<any, any, any> | 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};
       }
     }
...
   <button onClick={this.increaseCnt}></button>
   ...
   ...
   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<KV, IModuleB> = {
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是一个ES6/typescript装饰器,如果不想用装饰器语法,可以作为普通的函数,像react-redux的connect函数那样使用,示例代码:

class PageA extends React.Component{
    ...
   }
   export mapProp(mStoreA, 'xx','xx2')(PageA);

其他说明参见使用


当你需要修改某个模块数据时候,调用这个函数吧,如果只是简单的数据修改,别忘了语法糖哦。


  • 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


  • ProviderReactDOM.render要用到的根节点包装器

一个常量对象,包含了一些全局配置项

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
    }

获取全局的redux store(默认返回的是快照)

获取指定模块的数据集(默认返回的是快照)

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


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<PAYLOAD_TYPE = any, MODULE_STORE = ModuleStore, PROCEED_RESULT = KV> {
     module: string;
     name?: string;
     maxProcessSeconds?: number;
     process?: (payload: PAYLOAD_TYPE, moduleStore: MODULE_STORE) => Promise<PROCEED_RESULT>;
   }

恭喜你!认真的看完了全部文章,应该已经了解了module-reaction的特点和使用方式了!再次提醒,别忘了给个star哦!https://github.com/swellee/reaction

btw, 如果你玩flutter,这里还有一个flutter的实现: https://github.com/swellee/flutter_reactionenjoy!!

原文链接:segmentfault.com

上一篇:React高频困惑点解疑
下一篇:5.5种常见垂直居中方式

相关推荐

  • 高频数据交换下Flutter与ReactNative的对比

    (标题图片来自网络,侵删) 后端使用go写的socketio服务模拟期货行情数据,每10ms推送10条行情数据 ReactNative已经尽力优化了。 Flutter由于没fluttersock...

    2 年前
  • 高性能迷你React框架 anu1.3.0 发布

    anujs1.3.0是一款高性能Reactlike框架,是目前世界上对React16兼容最好的迷你库。 自React16起,相继推出createContext,createPortal, creat...

    2 年前
  • 高德地图 react-amap 实战

    clipboard.png(https://img.javascriptcn.com/5a33946ad8c0ea8ee7870f74f331d0c0 "clipboard.png") reacta...

    9 个月前
  • 高品质 React UI 组件

    (https://img.javascriptcn.com/cca319311c2ea59a2b5cdaa63b997828)(https://link.funteas.com?target=http...

    2 年前
  • 骚操作!在react中使用vuex

    原文地址(https://github.com/zyl1314/blog/issues/12) 前言 笔者最近在学习使用,提到react就绕不过去。redux是一个状态管理架构,被广泛用于rea...

    2 年前
  • 项目文档说明:react + Ant Design 的 blog-react-admin

    效果图(https://img.javascriptcn.com/734ce56fe3a37ab11e9744473f56bae1 "效果图") 前言 此 blogreactadmin 项目是基...

    2 年前
  • 面试题:Hooks 与 React 生命周期的关系

    React 生命周期很多人都了解,但通常我们所了解的都是 单个组件 的生命周期,但针对 Hooks 组件、多个关联组件(父子组件和兄弟组件) 的生命周期又是怎么样的喃?你有思考和了解过吗,接下来我们将...

    10 个月前
  • 面试官:请你在React中引入Vue3的@vue/reactivity,实现响应式。

    前言 React的状态管理是一个缤纷繁杂的大世界,光我知道的就不下数十种,其中有最出名immutable阵营的redux,有mutable阵营的mobx,reacteasystate,在hooks诞生...

    4 个月前
  • 面试官我想做个Reacter(React路由)

    路由的基础用法回顾,源码study,文章首发于docs,求个star 依赖 路由依赖于 reactrouter ,但是一般 pc 路由使用 reactrouterdom ,它依赖于 reactrout...

    3 个月前
  • 面试中React与Vue的比对

    1.virtual dom 用JS模拟DOM结构,DOM变化的对比,放在JS层做,以提高重绘性能 DOM操作昂贵,JS运行效率高,要减少DOM操作 使用:snabbdom的使用 ...

    2 年前

官方社区

扫码加入 JavaScript 社区