有请主角登场:react
Dva:我目前 Github 上 12432 个星,你呢?
React-coat:我目前 74 个。
Dva:那你还敢吐槽我?
React-coat:我星少我怕谁?
Dva:...
复制代码
Dva:我来自阿里,系出名门,你呢?
React-coat:我的项目。
Dva:那你还敢挑剔我?
React-coat:我野蛮生长我怕谁?
Dva:...
复制代码
DvaJS 和 React-coat 都是 React+Redux+Redux-router 生态的框架,都是把传统MVC的调用风格引入MVVM,二者诸多地方颇为类似。webpack
DvaJS已经广为人知,上线已经好几年了,从文档、稳定性、测试充分度、辅助工具等方面都天然比 react-coat 强。React-coat 只不过是个人我的项目,以前一直在公司内部使用,今年 1 月升级到 4.0 后感受较稳定了才开始向外界发布。git
本文撇开其它因素,仅从设计思路和用户使用 API 对二者进行深度对比。互联网是一个神奇的世界,人人都有机会发表自已的观点,正所谓初生蚂蚁不畏象,但愿 Dva 不要介意,毕竟二者不是一个量级,没有吐槽哪有进步嘛。另外若是存在对 DvaJS 理解错误的地方,请网友们批评指正。github
>-<,好吧,我认可有点标题党了。客官别急,请上坐,饮杯茶歇息一下。。。web
虽然 Dva 号称支持 Typescript,但是看了一下官方给出的:使用 TypeScript 的例子,彻底感受不到诚意,action、model、view 之间的数据类型都是孤立的,没有相互约束?路由配置里的文件路径也是没法反射,全是字符串,看得我一头雾水...typescript
举个 Model 例子,在 Dva 中定义一个 Model:npm
export default {
effects: {
// Call、Put、State类型都需自已手动引入
*fetch(action: {payload: {page: number}}, {call: Call, put: Put}) {
//使用 yield 后,data 将反射不到 usersService.fetch
const data = yield call(usersService.fetch, {page: action.payload.page});
// 这里将触发下面 save reducer,但是它们之间没有创建强关联
// 如何让这里的 playload 类型与下面 save reducer中的 playload 类型自动约束?
// 若是下面 save reducer 更名为 save2,如何让这里的 type 自动感应报错?
yield put({type: "save", payload: data});
},
},
reducers: {
save(state: State, action: {payload: {list: []}}) {
return {...state, ...action.payload};
},
},
};
复制代码
反过来看看在 React-coat 中定义一个一样的 Model:编程
class ModuleHandlers extends BaseModuleHandlers {
// this.state、this.actions、this.dispatch都集成在Model中,直接调用便可
@effect("loading") // 注入loading状态
public async fetch(payload: {page: number}) {
// 使用 await 更直观,并且 data 能自动反射类型
const data = await usersService.fetch({page: action.payload.page});
// 使用方法调用,更直观,并且参数类型和方法名都有自动约束
this.dispatch(this.actions.save(data));
}
@reducer
public save(payload: {list: []}): State {
return {...this.state, ...payload};
}
}
复制代码
另外,在 react-coat 的 demo 中用了大量的 TS 泛型运算来保证 module、model、action、view、router 之间相互检查与约束,具体可看一下react-coat-helloworldapi
结论:bash
二者集成框架都差很少,都属于 Redux 生态圈,最大差异:
Redux-Saga 有不少优势,好比方便测试、方便 Fork 多任务、 多个 Effects 之间 race 等。但缺点也很明显:
结论:
你喜不喜欢 Saga,这是我的选择的问题了,没有绝对的标准。
umi 和 dva 都喜欢用 Page 为主线来组织站点结构,并和 Router 绑定,官方文档中这样说:
在组件设计方法中,咱们提到过 Container Components,在 dva 中咱们一般将其约束为 Route Components,由于在 dva 中咱们一般以页面维度来设计 Container Components。
因此,dva 的工程多为这种目录结构:
src
├── components
├── layouts
├── models
│ └── globalModel.js
├── pages
│ ├── photos
│ │ ├── page.js
│ │ └── model.js
│ ├── videos
│ │ ├── page.js
│ │ └── model.js
复制代码
几个质疑:
来看看 React-coat
在 React-coat 中没有 Page 的概念,只有 View,由于一个 View 有可能被路由加载成为一个所谓的 Page,也可能被一个 modal 弹出成为一个弹窗,也可能被其它 View 直接嵌套。
假若有一个 PhotosView:
// 以路由方式加载,所谓的 Page
render() {
return (
<Switch>
<Route exact={true} path="/photos/:id" component={DetailsView} />
<Route component={ListView} />
</Switch>
);
}
复制代码
// 也能够直接用 props 参数来控制加载
render() {
const {showDetails} = this.props;
return showDetails ? <DetailsView /> : <ListView />; } 复制代码
在 React-coat 中的组织结构的主线是 Module,它以业务功能的**高内聚,低耦合**的原则划分:一个 Module = 一个model(维护数据)
和一组view(展示交互)
。典型的目录结构以下:
src
├── components
├── modules
│ ├── app
│ │ ├── views
│ │ │ ├── View1.tsx
│ │ │ ├── View2.tsx
│ │ │ └── index.ts
│ │ ├── model.ts
│ │ └── index.ts
│ ├── photos
│ │ ├── views
│ │ │ ├── View1.tsx
│ │ │ ├── View2.tsx
│ │ │ └── index.ts
│ │ ├── model.ts
│ │ └── index.ts
复制代码
结论:
在 Dva 中的路由是集中配置
式的,须要用 app.router()方法来注册。比较复杂,涉及到 Page、Layout、ContainerComponents、RealouteComponents、loadComponent、loadMode 等概念。复杂一点的应用会有动态路由、权限判断等,因此 Router.js 写起来又臭又长,可读性不好。并且使用一些相对路径和字符串名称,没办法用引发 TS 的检查。
后面在 umi+dva 中,路由以 Pages 目录结构自动生成,对于简单应用尚可,对于复杂一点的又引起出新问题。好比某个 Page 可能被多个 Page 嵌套,某个 model 被多个 page 共用等。因此,umi 又想出来一些潜规则:
model 分两类,一是全局 model,二是页面 model。全局 model 存于 /src/models/ 目录,全部页面均可引用;页面 model 不能被其余页面所引用。
规则以下:
src/models/**/*.js 为 global model
src/pages/**/models/**/*.js 为 page model
global model 全量载入,page model 在 production 时按需载入,在 development 时全量载入
page model 为 page js 所在路径下 models/**/*.js 的文件
page model 会向上查找,好比 page js 为 pages/a/b.js,他的 page model 为 pages/a/b/models/**/*.js + pages/a/models/**/*.js,依次类推
约定 model.js 为单文件 model,解决只有一个 model 时不须要建 models 目录的问题,有 model.js 则不去找 models/**/*.js
复制代码
看看在 React-coat 中:
不使用路由集中配置,路由逻辑分散在各个组件中,没那么多强制的概念和潜规则。
一句话:一切皆 Component
结论:
React-coat 的路由无限制,更简单明了。
在 Dva 中,由于 Page 是和路由绑定的,因此按需加载只能使用在路由中,须要配置路由:
{
path: '/user',
models: () => [import(/* webpackChunkName: 'userModel' */'./pages/users/model.js')],
component: () => import(/* webpackChunkName: 'userPage' */'./pages/users/page.js'),
}
复制代码
几个问题:
在 React-coat 中,View 能够用路由加载,也能够直接加载:
// 定义代码分割
export const moduleGetter = {
app: () => {
return import(/* webpackChunkName: "app" */ "modules/app");
},
photos: () => {
return import(/* webpackChunkName: "photos" */ "modules/photos");
},
}
复制代码
// 使用路由加载:
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main");
...
<Route exact={false} path="/photos" component={PhotosView} />
复制代码
// 直接加载:
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main");
...
render() {
const {showDetails} = this.props;
return showDetails ? <DetailsView /> : <ListView />; } 复制代码
React-coat 这样作的好处:
结论:
在使用 Dva 时发现一个严重的问题,让我一度怀疑是自已哪里弄错了:
1.首先进入一个页面:localhost:8000/pages,此时查看 Redux-DevTools 以下:
2.而后点击一个 link 进入 localhost:8000/photos,此时查看 Redux-DevTools 以下:
眼尖的伙伴们看出什么毛病来没有?
加载 photos model 时,第一个 action @@INIT 时的 State 快照居然变了,把 photos 强行塞进去了。Redux 奉行的不是不可变数据么???
结论:
Dva 动态加载 model 时,破坏了 Redux 的基本原则,而 React-coat 不会。
model 分两类,一是全局 model,二是页面 model。全局 model 存于 /src/models/ 目录,全部页面均可引用;页面 model 不能被其余页面所引用。
global model 全量载入,page model 在 production 时按需载入,在 development 时全量载入。
复制代码
一个字:绕
React-coat 中 model 跟着业务功能走,一个 module 只能有一个 model:
在 Module 内部,咱们可进一步划分为`一个model(维护数据)`和`一组view(展示交互)`
集中在一个名为model.js的文件中编写 Model,并将此文件放在本模块根目录下
model状态能够被全部Module读取,但只能被自已Module修改,(切合combineReducers理念)
复制代码
结论:
Dva 中定义 model 使用一个 Object 对象,有五个约定的 key,例如:
{
namespace: 'count',
state: 0,
reducers: {
aaa(payload) {...},
bbb(payload) {...},
},
effects: {
*ccc(action, { call, put }) {...},
*ddd(action, { call, put }) {...},
},
subscriptions: {
setup({ dispatch, history }) {...},
},
}
复制代码
这样有几个问题:
如何保证 reducers 和 effects 之间命名不重复?简单的一目了然还好,若是是复杂的长业务流程,可能涉及到重用和提取,用到 Mixin 和 Extend,这时候怎么保证?
如何重用和扩展?官方文档中这样写道:
从这个角度看,咱们要新增或者覆盖一些东西,都会是比较容易的,好比说,使用 Object.assign 来进行对象属性复制,就能够把新的内容添加或者覆盖到原有对象上。注意这里有两级,model 结构中的 state,reducers,effects,subscriptions 都是对象结构,须要分别在这一级去作 assign。能够借助 dva 社区的 dva-model-extend 库来作这件事。换个角度,也能够经过工厂函数来生成 model。
仍是一个字:绕
如今反过来看看 React-coat 怎么解决这两个问题:
class ModuleHandlers extends BaseModuleHandlers<State, RootState, ModuleNames> {
@reducer
public aaa(payload): State {...}
@reducer
protected bbb(payload): State {...}
@effect("loading")
protected async ccc(payload) {...}
}
复制代码
结论:
react-coat 的 model 利用 Class 和装饰器来实现,更简单,更适合 TS 类型检查,也更利于重用与提取。
在 Dva 中,派发 action 里要手动写 type 和 payload,缺乏类型验证和静态检查
dispatch({ type: 'moduleA/query', payload:{username:"jimmy"}} })
复制代码
在 React-coat 中直接利用 TS 的类型反射:
dispatch(moduleA.actions.query({username:"jimmy"}))
复制代码
结论:
react-coat 的 Action 派发方式更优雅
咱们能够简单的认为:在 Redux 中 store.dispatch(action),能够触发一个注册过的 reducer,看起来彷佛是一种观察者模式。推广到以上的 effect 概念,effect 一样是一个观察者。一个 action 被 dispatch,可能触发多个观察者被执行,它们多是 reducer,也多是 effect。因此 reducer 和 effect 统称为:ActionHandler
ActionHandler 机制对于复杂业务流程、跨 model 之间的协做有着强大的做用,举例说明:
在 React-coat 中,有一些框架级的特别 Action 在适当的时机被触发,好比:
**module/INIT**:模块初次载入时触发
**@@router/LOCATION_CHANGE**: 路由变化时触发
**@@framework/ERROR**:发生错误时触发
**module/LOADING**:loading状态变化时触发
**@@framework/VIEW_INVALID**:UI界面失效时触发
复制代码
有了 ActionHandler 机制,它们所有变成了可注入的 hooks,你能够监听它们,例如:
// 兼听自已的INIT Action
@effect()
protected async [ModuleNames.app + "/INIT"]() {
const [projectConfig, curUser] = await Promise.all([settingsService.api.getSettings(), sessionService.api.getCurUser()]);
this.updateState({
projectConfig,
curUser,
startupStep: StartupStep.configLoaded,
});
}
复制代码
在 Dva 中,要同步处理 effect 必须使用 put.resolve,有点抽象,在 React-coat 中直接 await 更直观和容易理解。
// 在 Dva 中处理同步 effect
effects: {
* query (){
yield put.resolve({type: 'otherModule/query',payload:1});
yield put({type: 'updateState', payload: 2});
}
}
// 在React-coat中,可以使用 awiat
class ModuleHandlers {
async query (){
await this.dispatch(otherModule.actions.query(1));
this.dispatch(thisModule.actions.updateState(2));
}
}
复制代码
// 在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});
}
}
}
// 在React-coat中,可以使用ActionHandler观察者模式:
class ModuleB {
//在ModuleB中兼听"ModuleA/update" action
async ["ModuleA/update"] (){
....
}
}
class ModuleC {
//在ModuleC中兼听"ModuleA/update" action
async ["ModuleA/update"] (){
....
}
}
复制代码
结论
React-coat 中由于引入了 ActionHandler 机制,对于复杂流程和跨 model 协做比 Dva 简单清晰得多。
好了,先对比这些点,其它想起来再补充吧!百闻不如一试,只有切身用过这两个框架才能感觉它们之间的差异。因此仍是请君一试吧:
git clone https://github.com/wooline/react-coat-helloworld.git
npm install
npm start
复制代码
固然,Dva 也有不少优秀的地方,由于它已经广为人知,因此就不在此复述了。重申一下,以上观点仅表明我的,若是文中对 Dva 理解有误,欢迎批评指正。