笔者升级了
dva
的版本,同时新增了umi
的使用,具体可以参考这篇文章 dva理论到实践——帮你扫清dva的知识盲点
本文中我会介绍一下相应的dva的相应知识点和实战练习。
同时我也会介绍使用dva的流程,以及介绍使用dva中的坑。希望大家通过这篇文章,能大致了解dva的使用流程。
一,Dva简介
1,借鉴 elm 的概念,Reducer, Effect 和 Subscription
2,框架,而非类库
3,基于 redux, react-router, redux-saga 的轻量级封装
二,Dva的特性
1,仅有 5 个 API,仅有5个主要的api
,其用法我们会在第三节详细介绍。
2,支持 HMR,支持模块的热更新。
3,支持 SSR (ServerSideRender),支持服务器端渲染。
4,支持 Mobile/ReactNative,支持移动手机端的代码编写。
5,支持 TypeScript,支持TypeScript,个人感觉这个会是javascript
的一个趋势。
6,支持路由和 Model 的动态加载。
7,…...
三,Dva的5个API
1,app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)
在opts
可以配置所有的hooks
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});
这里比较常用的是,history的配置,一般默认的是hashHistory
,如果要配置 history 为 browserHistory
,可以这样:
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
- 关于react-router中的
hashHistory
和browserHistory
的区别大家可以看:react-router。 -
initialState
:指定初始数据,优先级高于 model 中的 state,默认是{}
,但是基本上都在modal里面设置相应的state。
2,app.use(Hooks):配置 hooks 或者注册插件。
这里最常见的就是dva-loading插件的配置,
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
但是一般对于全局的loading
我们会根据业务的不同来显示相应不同的loading
图标,我们可以根据自己的需要来选择注册相应的插件。
3,app.model(ModelObject):这个是你数据逻辑处理,数据流动的地方。
modal
是dva
里面与我们真正进行项目开发,逻辑处理,数据流动的地方。这里面涉及到的namespace
、Modal
、effects
和reducer
等概念都很重要,我们会在第四部分详细讲解。
4,app.router(Function):注册路由表,我们做路由跳转的地方。
一般都是这么写的
import { Router, Route } from 'dva/router';
app.router(({ history }) => {
return (
<Router history={history}>
<Route path="/" component={App} />
<Router>
);
});
但是如果你的项目特别的庞大,我们就要考虑到相应的性能的问题,但是入门可以先看一下这个。对于如何做到按需加载大家可以看10分钟 让你dva从入门到精通,里面有简单提到router
按需加载的写法。
5,app.start([HTMLElement], opts)
启动应用,即将我们的应用跑起来。
四,Dva九个概念
1,State(状态)
初始值,我们在 dva()
初始化的时候和在 modal
里面的 state
对其两处进行定义,其中 modal
中的优先级低于传给 dva()
的 opts.initialState
如下:
// dva()初始化
const app = dva({
initialState: { count: 1 },
});
// modal()定义事件
app.model({
namespace: 'count',
state: 0,
});
2,Action:表示操作事件,可以是同步,也可以是异步
action
的格式如下,它需要有一个 type
,表示这个 action
要触发什么操作;payload
则表示这个 action
将要传递的数据
{
type: String,
payload: data,
}
我们通过 dispatch
方法来发送一个 action
Action
Action 表示操作事件,可以是同步,也可以是异步
{
type: String,
payload: data
}
格式
dispatch(Action);
dispatch({ type: 'todos/add', payload: 'Learn Dva' });
其实我们可以构建一个Action
创建函数,如下
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
//我们直接dispatch(addTodo()),就发送了一个action。
dispatch(addTodo())
具体可以查看文档:redux——action
3,Model
model
是 dva
中最重要的概念,Model
非 MVC
中的 M
,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发
-
state
这里的
state
跟我们刚刚讲的state
的概念是一样的,只不过她的优先级比初始化的低,但是基本上项目中的state
都是在这里定义的。 -
namespace
model
的命名空间,同时也是他在全局state
上的属性,只能用字符串,我们发送在发送action
到相应的reducer
时,就会需要用到namespace
。 -
Reducer
以
key/value
格式定义reducer
,用于处理同步操作,唯一可以修改state
的地方。由action
触发。其实一个纯函数。 -
Effect
用于处理异步操作和业务逻辑,不直接修改
state
,简单的来说,就是获取从服务端获取数据,并且发起一个action
交给reducer
的地方。其中它用到了redux-saga,里面有几个常用的函数。
*add(action, { call, put }) { yield call(delay, 1000); yield put({ type: 'minus' }); },
在项目中最主要的会用到的是 put
与 call
。
-
Subscription
subscription
是订阅,用于订阅一个数据源,然后根据需要dispatch
相应的action
。在app.start()
时被执行,数据源可以是当前的时间、当前页面的url
、服务器的websocket
连接、history
路由变化等等。
4,Router
Router
表示路由配置信息,项目中的 router.js
。
export default function({ history }){
return(
<Router history={history}>
<Route path="/" component={App} />
</Router>
);
}
-
RouteComponent
RouteComponent
表示Router
里匹配路径的Component
,通常会绑定model
的数据。如下:
import { connect } from 'dva';
function App() {
return <div>App</div>;
}
function mapStateToProps(state) {
return { todos: state.todos };
}
export default connect(mapStateToProps)(App);
五,整体架构
我简单的分析一下这个图:
首先我们根据 url
访问相关的 Route-Component
,在组件中我们通过 dispatch
发送 action
到 model
里面的 effect
或者直接 Reducer
当我们将action
发送给Effect
,基本上是取服务器上面请求数据的,服务器返回数据之后,effect
会发送相应的 action
给 reducer
,由唯一能改变 state
的 reducer
改变 state
,然后通过connect
重新渲染组件。
当我们将action
发送给reducer
,那直接由 reducer
改变 state
,然后通过 connect
重新渲染组件。
这样我们就能走完一个流程了。
六,项目案例
这一节我们会根据dva的快速搭建一个计数器。官方的例子是都把所有的逻辑写在了入口文件HomePage.js
里,我会在下面的demo中,把例子中的各个模块抽出来,放在相应的文件夹中。让大家能更加清楚每一个模块的作用。
1,首先全局安装dva-cli
,我的操作在桌面进行的,大家可以自行选择项目目录。
$ npm install -g dva-cli
2,接着使用dva-cli
创建我们的项目文件夹
$ dva new myapp
3,进入myapp
目录,安装依赖,执行如下操作。
$ cd myapp
$ npm start
浏览器会自动打开一个窗口,如下图。
4,目录结构介绍
.
├── mock // mock数据文件夹
├── node_modules // 第三方的依赖
├── public // 存放公共public文件的文件夹
├── src // 最重要的文件夹,编写代码都在这个文件夹下
│ ├── assets // 可以放图片等公共资源
│ ├── components // 就是react中的木偶组件
│ ├── models // dva最重要的文件夹,所有的数据交互及逻辑都写在这里
│ ├── routes // 就是react中的智能组件,不要被文件夹名字误导。
│ ├── services // 放请求借口方法的文件夹
│ ├── utils // 自己的工具方法可以放在这边
│ ├── index.css // 入口文件样式
│ ├── index.ejs // ejs模板引擎
│ ├── index.js // 入口文件
│ └── router.js // 项目的路由文件
├── .eslintrc // bower安装目录的配置
├── .editorconfig // 保证代码在不同编辑器可视化的工具
├── .gitignore // git上传时忽略的文件
├── .roadhogrc.js // 项目的配置文件,配置接口转发,css_module等都在这边。
├── .roadhogrc.mock.js // 项目的配置文件
└── package.json // 当前整一个项目的依赖
5,首先是前端的页面,我们使用 class
形式来创建组件,原例子中是使用无状态来创建的。react
创建组件的各种方式,大家可以看React创建组件的三种方式及其区别
我们先修改route/IndexPage.js
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';
class IndexPage extends React.Component {
render() {
const { dispatch } = this.props;
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: 1</div>
<div className={styles.current}>2</div>
<div className={styles.button}>
<button onClick={() => {}}>+</button>
</div>
</div>
);
}
}
export default connect()(IndexPage);
同时修改样式routes/IndexPage.css
.normal {
width: 200px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
box-shadow: 0 0 20px #ccc;
}
.record {
border-bottom: 1px solid #ccc;
padding-bottom: 8px;
color: #ccc;
}
.current {
text-align: center;
font-size: 40px;
padding: 40px 0;
}
.button {
text-align: center;
button {
width: 100px;
height: 40px;
background: #aaa;
color: #fff;
}
}
此时你的页面应该是如下图所示
6,在 model
处理 state
,在页面里面输出 model
中的 state
首先我们在index.js
中将models/example.js
,即将model下一行的的注释打开。
import dva from 'dva';
import './index.css';
// 1. Initialize
const app = dva();
// 2. Plugins
// app.use({});
// 3. Model
app.model(require('./models/example')); // 打开注释
// 4. Router
app.router(require('./router'));
// 5. Start
app.start('#root');
接下来我们进入 models/example.js
,将namespace
名字改为 count
,state
对象加上 record
与 current
属性。如下:
export default {
namespace: 'count',
state: {
record: 0,
current: 0,
},
...
};
接着我们来到 routes/indexpage.js
页面,通过的 mapStateToProps
引入相关的 state
。
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';
class IndexPage extends React.Component {
render() {
const { dispatch, count } = this.props;
return (
<div className={styles.normal}>
<div className={styles.record}>
Highest Record: {count.record} // 将count的record输出
</div>
<div className={styles.current}>
{count.current}
</div>
<div className={styles.button}>
<button onClick={() => {} } >
+
</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return { count: state };
} // 获取state
export default connect(mapStateToProps)(IndexPage);
打开网页:你应该能看到下图:
7,通过 +
发送 action
,通过 reducer
改变相应的 state
首先我们在 models/example.js
,写相应的 reducer
。
export default {
...
reducers: {
add1(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1 };
},
},
};
在页面的模板 routes/IndexPage.js
中 +
号点击的时候,dispatch
一个 action
import React from 'react';
import { connect } from 'dva';
import styles from './IndexPage.css';
class IndexPage extends React.Component {
render() {
const { dispatch, count } = this.props;
return (
<div className={styles.normal}>
<div className={styles.record}>Highest Record: {count.record}</div>
<div className={styles.current}>{count.current}</div>
<div className={styles.button}>
<button
+ onClick={() => { dispatch({ type: 'count/add1' });}
}>+</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return { count: state.count };
}
export default connect(mapStateToProps)(IndexPage);
效果如下图:
8,接下来我们来使用 effect
模拟一个数据接口请求,返回之后,通过 yield put()
改变相应的 state
首先我们替换相应的 models/example.js
的 effect
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
这里的 delay
,是我这边写的一个延时的函数,我们在 utils
里面编写一个 utils.js
,一般请求接口的函数都会写在 servers
文件夹中。
export function delay(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}
接着我们在 models/example.js
导入这个 utils.js
import { delay } from '../utils/utils';
9,订阅订阅键盘事件,使用 subscriptions
,当用户按住 command+up
时候触发添加数字的 action
在 models/example.js
中作如下修改
+import key from 'keymaster';
...
app.model({
namespace: 'count',
+ subscriptions: {
+ keyboardWatcher({ dispatch }) {
+ key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
+ },
+ },
});
在这里你需要安装 keymaster
这个依赖
npm install keymaster --save
现在你可以按住 command+up
就可以使 current
加1了。
10,例子中我们看到当我们不断点击+
按钮之后,我们会看到current
会不断加一,但是1s过后,他会自动减到零。
官方的demo
的代买没有实现gif图里面的效果,大家看下图:
要做到gif里面的效果,我们应该在effect
中发送一个关于添加的action
,但是我们在effect
中不能直接这么写:
effects: {
*add(action, { call, put }) {
yield put({ type: 'add' });
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
因为如果这样的话,effect
与reducers
中的add
方法重合了,这里会陷入一个死循环,因为当组件发送一个dispatch
的时候,model
会首先去找effect
里面的方法,当又找到add
的时候,就又会去请求effect
里面的方法。
我们应该更改reducers
里面的方法,使它不与effect
的方法一样,将reducers
中的add
改为add1
,如下:
reducers: {
add1(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield put({ type: 'add1' });
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
这样我们就实现了gif图中的效果:
至此我们的简单的demo
就结束了,通过这个例子大家可以基本上了解dva
的基本概念。
如果还想深入了解dva的各个文件夹中文件的特性,大家可以看快速上手dva的一个简单demo,这里面会很详细的讲到我们该怎么写 model
、怎么使用effect
请求接口数据等等。
这段时间我也利用业余时间,使用dva
+thinkphp
构建一个类似boss直聘的手机端web应用,项目还没全部做完,大家如果感兴趣的话,可以下载下来看看,一起探讨相关思路哦。