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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章