欢迎您开始 @medux 之旅,建议您依次阅读以下 4 篇文章,这将耗费您大约 30 分钟。
第 2 篇:@medux 基础概念速览
8 个新概念与名词
假设你了解过 Redux
或者别的Flux
框架,那么应当知道 Store、State、Reducer、Action、Dispatch 是什么意思。没错,在 @medux 中它们依然受用,只是 Action 的概念发生了一点微妙的变化,它具有更多 Event 事件的特性。
Effect
我们知道在 Redux 中,改变 State 必须通过 dispatch action 以触发 reducer。而 effect 是相对于 reducer 而言的,它也必须通过 dispatch action 来触发,不同的是:
- 它是一个非纯函数,可以包含副作用,可以无返回,也可以是异步的
- 它不能直接改变 State,必须再次 dispatch action 来触发 reducer
ActionHandler
我们可以简单的认为:store.dispatch(action),可以触发 reducer 和 effect 执行,看起来 action 似乎可以当作一种事件。reducer 和 effect 可以当作是该事件的监听者,所以 reducer 和 effect 统称为:ActionHandler。
Module
我们通常以高内聚、低偶合的原则进行模块划分,一个 Module 是相对独立的业务功能的集合,它通常包含一个 Model (用来处理业务逻辑) 和一组 View (用来展示数据与交互),需要注意的是:不要以 UI 视觉作为划分原则。
Model
上面说过 Module 中包括一个model(维护数据)
和一组view(展现交互)
,而 model 主要包含两大职能:
- ModuleState 的定义
- ActionHandler 的定义
数据流是从 Model 单向流入 View,所以 Model 是独立不依赖于 View 的。理论上即使没有 View,整个程序依然是可以通过数据来驱动。
ModuleState、RootState
系统被划分为多个相对独立的 Module,不仅体现在文件夹目录,更体现在 State 数据结构中。每个 Module 负责维护和管理 State 下的一个子节点,我们称之为 ModuleState,而整个 State 我们习惯称之为RootState
- 每个 ModuleState 都是 Store 的一级子节点,以 Module 名为 Key
- 每个 Module 只能修改自已的 ModuleState,但是可以读取其它 ModuleState
- 每个 Module 修改自已的 ModuleState,必须通过 dispatch action 来触发
- 每个 Module 可以监听其它 Module 发出的 action,来配合修改自已的 ModuleState
View、Component
View 本质上还是一个 Component,它们有逻辑上的区别:
- View 用来展示业务,Component 用来展示交互
- View 一定属于某个 Module,Component 可以属于某个 Module 专用,也可以属于全部 Module
- View 通常订阅了 Store,并从 Store 中之间获得数据,Component 则只能通过 props 来进行传递
典型工程结构
src
├── assets // 存放公共静态资源
├── entity // 存放业务实体类型定义
├── common // 存放公共代码
├── components // 存放UI公共组件
├── modules
│ ├── app //一个名为app的module
│ │ ├── assets //存放该module私有的静态资源
│ │ ├── components //存放该module私有的UI组件
│ │ ├── views
│ │ │ ├── TopNav
│ │ │ ├── BottomNav
│ │ │ └── ...
│ │ ├── model.ts //定义本模块model
│ │ └── index.ts //导出本模块
│ ├── photos //另一个名为photos的module
│ │ └── ...
│ └── index.ts //模块配置与管理
└──index.ts 启动入口
一个 model 的定义
// 定义本模块的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;
};
}
// 定义本模块ModuleState的初始值
export const initState: State = {
listSearch: {username: null, page: 1, pageSize: 20},
listItems: null,
listSummary: null,
loading: {
searchLoading: LoadingState.Stop,
},
};
// 定义本模块所有的ActionHandler
class ModuleHandlers extends BaseModuleHandlers {
@reducer
public putSearchList({listItems, listSummary}): State {
return {...this.state, listItems, listSummary};
}
@effect('searchLoading')
public async searchList(options: {username?: string; page?: number; pageSize?: number} = {}) {
const listSearch = {...this.state.listSearch, ...options};
const {listItems, listSummary} = await api.searchList(listSearch);
this.dispatch(this.action.putSearchList({listItems, listSummary}));
}
// 可以监听其它Module发出的Action,然后改变自已的ModuleState
@effect(null)
protected async ['medux.RouteChange']() {
if (this.rootState.route.location.pathname === '/list') {
await this.dispatch(this.action.searchList());
}
}
}
能静能动的模块加载机制
模块的加载策略通常集中在 modules/index.ts 中配置:
import * as app from 'modules/app';
// 定义模块的加载方案,同步或者异步均可
export const moduleGetter = {
app: () => {
// 使用import同步加载
return app;
},
photos: () => {
// 使用import()异步加载
return import(/* webpackChunkName: "photos" */ 'modules/photos');
},
};
几个特殊的内置 Action
- medux.RouteChange:路由发生变化时将触发此 action
- medux.Error:捕获到 error 时将自动派发此 action
- moduleName.Init:模块初次载入时会触发此 action
- moduleName.RouteParams:模块路由参数发生变化时会触发此 action
- moduleName.Loading:跟踪加载进度时会触发此 action
关于错误处理
执行发生错误时,框架会自动 dispatch 一个 type 为 medux.Error 的 errorAction,你可以监听此 action 来处理错误,例如:
@effect(null)
protected async [ActionTypes.Error](error: CustomError) {
if (error.code === '401') {
this.dispatch(this.actions.putShowLoginPop(true));
} else if (error.code === '404') {
this.dispatch(this.actions.putShowNotFoundPop(true));
} else {
error.message && Toast.fail(error.message);
//继续向上throw错误将导致运行中断
throw error;
}
}
路由状态化
- State = Combine(Route)
- UI = Render(State)
medux 将路由视为类似于 Redux 的另一种 Store,它跟 Redux 一样记录着应用的即时状态。只不过 Redux 是记录在内存中由程序自动维护,而 Route 是记录在浏览器地址栏中,由用户手工输入维护,例如:
评论区块的展示与关闭,你可以使用 2 种方式来触发:
- '/article/10' => '/article/10/showComments',路由变化可以引起评论区块的展示与关闭
- {showComments: false} => {showComments: true},state 变化也可以达到同样的效果
到底是使用路由来控制还是 state 控制?我们希望 component 中不要做刻意的区分,这样后期修改方案时无需动到 component 本身。
你把路由当成另一个 Store 就对了,只不过这个 RouteStore 可以任由用户在地址栏中直接修改,这和用户鼠标点击交互修改本质上是一样的。所以做好准备把 ReduxStore 中的一部分数据抽离出来放入 RouteStore 中,然后让用户通过 URL 任意修改吧...
路由的终极目的就是为了变更 UI,所以不管什么路由方案,总能解析出以下通用信息:
- 当前路由会展示哪些 view
- 以及展示这些 view 需要的参数
medux 将这些通用信息抽象成状态。至此,你可以忘掉路由了,一切都是 state,一切都遵循 UI=Render(State)。于是乎原来包含副作用的路由组件变成了普通组件:
//原来需要路由组件
//现在直接变成普通组件
{routeViews.adminHome?.Main && }
{routeViews.adminRole?.List && }
{routeViews.adminMember?.List && }
具体如何提取通用信息,又如何将其转换成为状态呢?方案有很多种,我实现了一种:
你也可以实现更多 plan-b、plan-c...
CoreAPI
Demo
- medux-react-admin:基于
@medux/react-web-router
和最新的ANTD 4.x
开发的通用后台管理系统,除了演示 medux 怎么使用,它还创造了不少独特的理念 - medux-react-ssr:Fork自 medux-react-admin,将其改造为服务器同构渲染,你可以看到如何将一个 SinglePage(
单页应用
) 快速转换为支持 SEO 的多页应用。