項目github庫css
$ npm install react-coat
複製代碼
各主流瀏覽器、IE9 或 IE9 以上前端
本框架上手簡單react
8 個新概念:git
Effect、ActionHandler、Module、ModuleState、RootState、Model、View、Componentgithub
4 步建立:ajax
exportModel(), exportView(), exportModule(), createApp()typescript
3 個 Demo:npm
入手:Helloworldredux
進階:SPA(單頁應用)api
本框架與 Dvajs 理念略同,主要差別:
差別示例:使用強類型組織全部 reducer 和 effect
// Dva中常這樣寫
dispatch({ type: 'moduleA/query', payload:{username:"jimmy"}} })
//本框架中可直接利用ts類型反射和檢查:
this.dispatch(moduleA.actions.query({username:"jimmy"}))
複製代碼
差別示例:State 和 Actions 支持繼承
// Dva不支持繼承
// 本框架能夠直接繼承
class ModuleHandlers extends ArticleHandlers<State, PhotoResource> {
constructor() {
super({}, {api});
}
@effect()
protected async parseRouter() {
const result = await super.parseRouter();
this.dispatch(this.actions.putRouteData({showComment: true}));
return result;
}
@effect()
protected async [ModuleNames.photos + "/INIT"]() {
await super.onInit();
}
}
複製代碼
差別示例:在 Dva 中,由於使用 redux-saga,假設在一個 effect 中使用 yield put 派發一個 action,以此來調用另外一個 effect,雖然 yield 能夠等待 action 的派發,但並不能等待後續 effect 的處理:
// 在Dva中,updateState並不會等待otherModule/query的effect處理完畢了才執行
effects: {
* query (){
yield put({type: 'otherModule/query',payload:1});
yield put({type: 'updateState', payload: 2});
}
}
// 在本框架中,可以使用awiat關鍵字, updateState 會等待otherModule/query的effect處理完畢了才執行
class ModuleHandlers {
async query (){
await this.dispatch(otherModule.actions.query(1));
this.dispatch(thisModule.actions.updateState(2));
}
}
複製代碼
差別示例:若是 ModuleA 進行某項操做成功以後,ModuleB 或 ModuleC 都須要 update 自已的 State,因爲缺乏 action 的觀察者模式,因此只能將 ModuleB 或 ModuleC 的刷新動做寫死在 ModuleA 中:
// 在Dva中須要主動Put調用ModuleB或ModuleC的Action
effects: {
* update (){
...
if(callbackModuleName==="ModuleB"){
yield put({type: 'ModuleB/update',payload:1});
}else if(callbackModuleName==="ModuleC"){
yield put({type: 'ModuleC/update',payload:1});
}
}
}
// 在本框架中,可以使用ActionHandler觀察者模式:
class ModuleB {
//在ModuleB中兼聽"ModuleA/update"方法
async ["ModuleA/update"] (){
....
}
}
class ModuleC {
//在ModuleC中兼聽"ModuleA/update"方法
async ["ModuleA/update"] (){
....
}
}
複製代碼
前提:假設你已經熟悉了 React
和 Redux
,有過必定的開發經驗
以上概念與 Redux 基本一致,本框架無強侵入性,遵循 react 和 redux 的理念和原則:
咱們知道在 Redux 中,改變 State 必須經過 dispatch action 以觸發 reducer,在 reducer 中返回一個新的 state, reducer 是一個 pure function 純函數,無任何反作用,只要入參相同,其返回結果也是相同的,而且是同步執行的。而 effect 是相對於 reducer 而言的,與 reducer 同樣,它也必須經過 dispatch action 來觸發,不一樣的是:
咱們能夠簡單的認爲:在 Redux 中 store.dispatch(action),能夠觸發一個註冊過的 reducer,看起來彷佛是一種觀察者模式。推廣到以上的 effect 概念,effect 一樣是一個觀察者。一個 action 被 dispatch,可能觸發多個觀察者被執行,它們多是 reducer,也多是 effect。因此 reducer 和 effect 統稱爲:ActionHandler
若是有一組 actionHandler 在兼聽某一個 action,那它們的執行順序是什麼呢?
答:當一個 action 被 dispatch 時,最早執行的是全部的 reducer,它們被依次同步執行。全部的 reducer 執行完畢以後,纔開始全部 effect 執行。
我想等待這一組 actionHandler 所有執行完畢以後,再下一步操做,但是 effect 是異步執行的,我如何知道全部的 effect 都被處理完畢了? 答:本框架改良了 store.dispatch()方法,若是有 effect 兼聽此 action,它會返回一個 Promise,因此你可使用 await store.dispatch({type:"search"}); 來等待全部的 effect 處理完成。
當咱們接到一個複雜的前端項目時,首先要化繁爲簡,進行功能拆解。一般以高內聚、低偶合的原則對其進行模塊劃分,一個 Module 是相對獨立的業務功能的集合,它一般包含一個 Model(用來處理業務邏輯)和一組 View(用來展現數據與交互),須要注意的是:
Module 雖然是邏輯上的劃分,但咱們習慣於用文件夾目錄來組織與體現,例如:
src
├── modules
│ ├── user
│ │ ├── userOverview(Module)
│ │ ├── userTransaction(Module)
│ │ └── blacklist(Module)
│ ├── agent
│ │ ├── agentOverview(Module)
│ │ ├── agentBonus(Module)
│ │ └── agentSale(Module)
│ └── app(Module)
複製代碼
經過以上能夠看出,此工程包含 7 大模塊 app、userOverview、userTransaction、blacklist、agentOverview、agentBonus、agentSale,雖然 modules 目錄下面還有子目錄 user、angent,但它們僅屬於歸類,不屬於模塊。咱們約定:
系統被劃分爲多個相對獨立且平級的 Module,不只體如今文件夾目錄,更體如今 Store 上。每一個 Module 負責維護和管理 Store 下的一個節點,咱們稱之爲 ModuleState,而整個 Store 咱們習慣稱之爲RootState
例如:某個 Store 數據結構:
{
router:{...},// StoreReducer
app:{...}, // ModuleState
userOverview:{...}, // ModuleState
userTransaction:{...}, // ModuleState
blacklist:{...}, // ModuleState
agentOverview:{...}, // ModuleState
agentBonus:{...}, // ModuleState
agentSale:{...} // ModuleState
}
複製代碼
你可能注意到上面 Store 的子節點中,第一個名爲 router,它並非一個 ModuleState,而是一個由第三方 Reducer 生成的節點。咱們知道 Redux 中容許使用多個 Reducer 來共同維護 Stroe,並提供 combineReducers 方法來合併。因爲 ModuleState 的 key 名即爲 Module 名,因此:Module名天然也不能與其它第三方Reducer生成節點重名
。
在 Module 內部,咱們可進一步劃分爲一個model(維護數據)
和一組view(展示交互)
,此處的 Model 實際上指的是 view model,它主要包含兩大功能:
數據流是從 Model 單向流入 View,因此 Model 是獨立的,是不依賴於 View 的。因此理論上即便沒有 View,整個程序依然是能夠經過命令行來驅動的。
咱們約定:
例如,userOverview 模塊中的 Model:
src
├── modules
│ ├── user
│ │ ├── userOverview(Module)
│ │ │ ├──views
│ │ │ └──model.ts
│ │ │
複製代碼
src/modules/user/userOverview/model.ts
// 定義本模塊的ModuleState類型
export interface State extends BaseModuleState {
listSearch: {username:string; page:number; pageSize:number};
listItems: {uid:string; username:string; age:number}[];
listSummary: {page:number; pageSize:number; total:number};
loading: {
searchLoading: LoadingState;
};
}
// 定義本模塊全部的ActionHandler
class ModuleHandlers extends BaseModuleHandlers<State, RootState, ModuleNames> {
constructor() {
// 定義本模塊ModuleState的初始值
const initState: State = {
listSearch: {username:null, page:1, pageSize:20},
listItems: null,
listSummary: null,
loading: {
searchLoading: LoadingState.Stop,
},
};
super(initState);
}
// 一個reducer,用來update本模塊的ModuleState
@reducer
public putSearchList({listItems, listSummary}): State {
return {...this.state, listItems, listSummary};
}
// 一個effect,使用ajax查詢數據,而後dispatch action來觸發以上putSearchList
// this.dispatch是store.dispatch的引用
// searchLoading指明將這個effect的執行狀態注入到State.loading.searchLoading中
@effect("searchLoading")
public async searchList(options: {username?:string; page?:number; pageSize?:number} = {}) {
// this.state指向本模塊的ModuleState
const listSearch = {...this.state.listSearch, ...options};
const {listItems, listSummary} = await api.searchList(listSearch);
this.dispatch(this.action.putSearchList({listItems, listSummary}));
}
// 一個effect,監聽其它Module發出的Action,而後改變自已的ModuleState
// 由於是監聽其它Module發出的Action,因此它不須要主動觸發,使用非public權限對外隱藏
// @effect(null)表示不須要跟蹤此effect的執行狀態
@effect(null)
protected async ["@@router/LOCATION_CHANGE]() {
// this.rootState指向整個Store
if(this.rootState.router.location.pathname === "/list"){
// 使用await 來等待全部的actionHandler處理完成以後再返回
await this.dispatch(this.action.searchList());
}
}
}
複製代碼
須要特別說明的是以上代碼的最後一個 ActionHandler:
protected async ["@@router/LOCATION_CHANGE](){
// this.rootState指向整個Store
if(this.rootState.router.location.pathname === "/list"){
await this.dispatch(this.action.searchList());
}
}
複製代碼
前面有強調過兩點:
另外注意到語句:await this.dispatch(this.action.searchList()):
dispatch 派發一個名爲 searchList 的 action 能夠理解,但是爲何前面還能 awiat?難道 dispatch action 也是異步的?
答:dispatch 派發 action 自己是同步的,咱們前面講過 ActionHandler 的概念,一個 action 被 dispatch 時,可能有一組 reducer 或 effect 在兼聽它,reducer 是同步處理的,但是 effect 多是異步處理的,若是你想等全部的兼聽都執行完成以後,再作下一步操做,此處就可使用 await,不然,你能夠不使用 await。
在 Module 內部,咱們可進一步劃分爲一個model(維護數據)
和一組view(展示交互)
。因此一個 Module 中的 view 可能有多個,咱們習慣在 Module 根目錄下建立一個名爲 views 的文件夾:
例如,userOverview 模塊中的 views:
src
├── modules
│ ├── user
│ │ ├── userOverview(Module)
│ │ │ ├──views
│ │ │ │ ├──imgs
│ │ │ │ ├──List
│ │ │ │ │ ├──index.css
│ │ │ │ │ └──index.ts
│ │ │ │ ├──Main
│ │ │ │ │ ├──index.css
│ │ │ │ │ └──index.ts
│ │ │ │ └──index.ts
│ │ │ │
│ │ │ │
│ │ │ └──model.ts
│ │ │
複製代碼
exportView()
導出。例如,某個 LoginForm:
interface Props extends DispatchProp {
logining: boolean;
}
class Component extends React.PureComponent<Props> {
public onLogin = (evt: any) => {
evt.stopPropagation();
evt.preventDefault();
// 發出本模塊的action,將觸發本model中定義的名爲login的ActionHandler
this.props.dispatch(thisModule.actions.login({username: "", password: ""}));
};
public render() {
const {logining} = this.props;
return (
<form className="app-Login" onSubmit={this.onLogin}>
<h3>請登陸</h3>
<ul>
<li><input name="username" placeholder="Username" /></li>
<li><input name="password" type="password" placeholder="Password" /></li>
<li><input type="submit" value="Login" disabled={logining} /></li>
</ul>
</form>
);
}
}
const mapStateToProps = (state: RootState) => {
return {
logining: state.app.loading.login !== LoadingState.Stop,
};
};
export default connect(mapStateToProps)(Component);
複製代碼
從以上代碼可看出,View 就是一個 Component,那 View 和 Component 有區別嗎?編碼上沒有,邏輯上是有的:
react-coat 贊同 react-router 4 組件化路由
的理念,路由即組件,嵌套路由比如嵌套 component 同樣簡單,無需繁瑣的配置。如:
import {BottomNav} from "modules/navs/views"; // BottomNav 來自於 navs 模塊
import LoginForm from "./LoginForm"; // LoginForm 來自於本模塊
// PhotosView 和 VideosView 分別來自於 photos 模塊和 videos 模塊,使用異步按需加載
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main");
const VideosView = loadView(moduleGetter, ModuleNames.videos, "Main");
<div className="g-page">
<Switch>
<Route exact={false} path="/photos" component={PhotosView} />
<Route exact={false} path="/videos" component={VideosView} />
<Route exact={true} path="/login" component={LoginForm} />
</Switch>
<BottomNav />
</div>
複製代碼
以上某個 view 中以不一樣加載方式嵌套了多個其它 view:
因此本框架對於模塊和視圖的加載靈活簡單,無需複雜配置與修改: