react 业务框架 module-reaction 开源了!

2019-10-19 admin

缘起

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


  • Provider ReactDOM.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_reaction enjoy!!

[转载]原文链接:https://segmentfault.com/a/1190000020732014

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-77734.html

文章标题:react 业务框架 module-reaction 开源了!

相关文章
可以从CSS框架中借鉴到什么
现在很多人会使用 CSS 框架进行快速建站。   那 CSS 框架是什么呢,它通常是一些 CSS 文件的集合,这些文件包括基本布局、表单样式、网格、简单组件、以及样式重置。使用 CSS 框架大大降低工作成本进行快速建站。   当然对于一些大...
2016-03-11
React Native 用JavaScript编写原生ios应用
ReactNative 可以基于目前大热的开源JavaScript库React.js来开发iOS和Android原生App。而且React Native已经用于生产环境——Facebook Groups iOS 应用就是基于它开发的。 Re...
2015-11-12
好消息!好消息!饿了么ElementUI用户的福音——ElementUIVerify!
如果你受够了饿了么ElementUI原生的校验方式,那就来试试它吧! 前言 饿了么ElementUI虽好,但表单校验的体验不够理想 如果说产品开发要讲究用户体验,那插件开发也要讲究开发体验,而好的开发体验,要靠好的api设计来保障 本人专注...
2017-12-24
从 Node.js 分裂出 Io.js 事件看开源软件谁做主
Node.js 作为服务器编程语言的后起之秀,常用来构建和运行 Web 应用,近日却爆出其社区出现分裂。由于对官方运营商 Joyent 公司在 Node.js 管理上的长期不满,多位核心开发者另立门户,创建了分支 Io.js。从 GitHu...
2015-11-12
前端MV*框架的意义
经常有人质疑,在前端搞MV有什么意义?也有人提出这样的疑问:以AngularJS,Knockout,BackBone为代表的MV框架,它跟jQuery这样的框架有什么区别?我jQuery用得好好的,有什么必要再引入这种框架? 回答这些问题之...
2016-03-11
前端问答社区成立了
由雷锋网友提供的给大家相互交流的前端问答社区正式上线了,欢迎大家来此相互交流相互学习 ...
2016-03-30
[翻译]基于Webpack4使用懒加载分离打包React代码
原文地址:https://engineering.innovid.com/code-splitting-using-lazy-loading-with-react-redux-typescript-and-webpack-4-3ec601...
2018-03-11
VSCode配置react开发环境的步骤
vscode 默认配置对于 react 的 JSX 语法不友好,体现在使用自动格式化或者粘贴后默认缩进错误,尽管可以通过改变 language mode 缓解错误,但更改 language mode 后的格式化依然不够理想。 通过搭配使用 ...
2017-12-28
百度新功能【特效搜索】演示 惊呆了小伙伴
百度搜索最近又出新玩意新功能了,可能你还没有发现,名为【百度特效搜索】已经默默上线了,有什么效果呢? 在百度搜索中根据用户搜索的关键词来出发某些动作,例如笔者搜索“打雷”关键字,在搜索结果中你会听到有打雷声, 黑洞,闪烁、翻转、跳跃,打雷,...
2015-11-12
2014年,你是不是被这5个HTML5技术刷屏了?
如今,几乎每天都有HTML5页面的推广以及小游戏、小测试在微信上传播,用户也逐渐习惯被各种HTML5轰炸。那么在刚刚过去的一年究竟有哪些HTML5技术堪称火爆,让人们的微信频频被刷屏呢? 1、2048 2048 在4x4的棋盘上,用方向键选...
2015-11-12
回到顶部