项目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:
因此本框架对于模块和视图的加载灵活简单,无需复杂配置与修改: