緣起
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(
<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
獲取指定模塊的數據集的某個屬性值(默認返回的是快照)
-
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<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!!