本文会介绍一下
dva的相应知识点,使用dva的流程,以及使用dva中的坑。并会通过
dva-cli和create-umi写一个 倒计时计数器 的例子。希望通过这篇文章能让你大致了解
dva的使用流程。
Dva 简介
-
借鉴 elm 的概念,Reducer,Effect 和 Subscription
-
框架,而非类库
-
基于 redux,react-router,redux-saga 的轻量级封装
Dva 的特性
-
仅有 5 个 API,其用法我们会在之后详细介绍。
-
支持 HMR,支持模块的热更新。
-
支持 SSR (ServerSideRender),支持服务器端渲染。
-
支持 Mobile/ReactNative,支持移动手机端的代码编写。
-
支持 TypeScript,支持 TypeScript,毋庸置疑这个会是 Javascript 的一个趋势。
-
支持路由和 Model 的动态加载。
-
…...
Dva 的 5 个API
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。
app.use(Hooks)
配置 hooks 或者注册插件。
这里最常见的就是 dva-loading 插件的配置,
import createLoading from 'dva-loading';
// ...
app.use(createLoading(opts));
但是一般对于全局的 loading 我们会根据业务的不同来显示相应不同的 loading 图标,我们可以根据自己的需要来选择注册相应的插件。
app.model(ModelObject)
这个是你数据逻辑处理,数据流动的地方。
modal 是 dva 里面与我们真正进行项目开发,逻辑处理,数据流动的地方。
这里面涉及到的 namespace、Modal、effects 和 reducer 等概念都很重要,我们会在接下来的部分详细讲解。
app.router(Function)
注册路由表,我们做路由跳转的地方。
一般都是通过下面的方式编写:
import { Router, Route } from 'dva/router';
app.router(({ history }) => {
return (
<Router history={history}>
<Route path="/" component={App} />
<Router>
);
});
但是如果你的项目特别的庞大,我们就要考虑到相应的性能的问题,但是入门可以先看一下这个。
解决性能问题,其实就是对路由进行按需加载,实现起来也比较简单,就是使用了 require.ensure 这个方法来帮我们实现,同时我们也可以使用 ES6 module 的动态 import 来实现按需加载。不过动态 import 这个方法还在提案中,我们需要接着 babel 插件来帮我们实现,具体笔者就不展开了。
app.start([HTMLElement], opts)
启动应用,将我们的应用跑起来。
Dva 九个概念
State(状态)
初始值,我们在 dva() 初始化的时候和在 modal 里面的 state 对其两处进行定义,其中 modal 中的优先级低于传给 dva() 的 opts.initialState。
如下:
// dva()初始化
const app = dva({
initialState: { count: 1 },
});
// modal()定义事件
app.model({
namespace: 'count',
state: 0,
});
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())
action 更多内容可以查看文档:redux——action
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 路由变化、键盘事件等等。
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 中,把例子中的各个模块抽出来,放在相应的文件夹中。让大家能更加清楚每一个模块的作用。
安装与初始化
首先全局安装 dva-cli,我的操作在桌面进行的,大家可以自行选择项目目录。
$ npm install -g dva-cli
安装完成后,我们可以通过以下命令查看当前版本号:
$ dva -v // dva-cli version 0.10.1
接着使用 dva-cli 创建我们的项目文件夹
$ dva new myapp
这个时候 dva-cli 会提示我们 dva-cli 已经过期了,让我们使用 create-umi 代替:
从这里也可以看出前端技术的更新迭代是很快的,我们必须不断的学习才能跟上社区的步伐。
这个是题外话,我们还是先使用
dva-cli来创建项目,在接下来我们也会讲一下umi。
进入 myapp 目录,安装依赖,执行如下操作。
$ cd myapp
$ npm start
浏览器会自动打开一个窗口,如下图。
目录结构
.
├── mock // mock数据文件夹
├── node_modules // 第三方的依赖
├── public // 存放公共public文件的文件夹
│ ├── index.html // 项目模版文件
├── src // 最重要的文件夹,编写代码都在这个文件夹下
│ ├── assets // 可以放图片等公共资源
│ ├── components // 就是react中的木偶组件
│ ├── models // dva最重要的文件夹,所有的数据交互及逻辑都写在这里
│ ├── routes // 就是react中的智能组件,不要被文件夹名字误导。
│ ├── services // 放请求借口方法的文件夹
│ ├── utils // 自己的工具方法可以放在这边
│ ├── index.css // 入口文件样式
│ ├── index.js // 入口文件
│ └── router.js // 项目的路由文件
├── .eslintrc // bower安装目录的配置
├── .editorconfig // 保证代码在不同编辑器可视化的工具
├── .gitignore // git上传时忽略的文件
├── .webpackrc // 项目的配置文件,配置接口转发,css_module等都在这边。
├── .roadhogrc.mock.js // 项目的配置文件
└── package.json // 当前整一个项目的依赖
编写代码
前端页面
在 dva-cli 初始化的项目中 react 的版本是 16.2.0,但现在众所周知,react 的版本已经到了 16.13.0 了,而且 react hooks 的写法得到了大范围的推广,官方也说这是未来编写 react 的趋势。
但是我们在例子中还是暂时使用 class 形式来创建组件,对 react hooks 有兴趣的大家可以自行查阅相关文档。
我们先修改前端页面: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;
}
此时你的页面应该是如下图所示
更改 state
在 model 处理 state ,在页面里面输出 model 中的 state。
首先我们在 index.js 中将 models/example.js,即将 model 下一行的的注释打开,使用 es6 的形式导入:
import dva from 'dva';
//使用import导入。
import countModel from './models/example';
import router from './router';
import './index.css';
// 1. Initialize
const app = dva();
// 2. Plugins
// app.use({});
// 3. Model
app.model(countModel);
// 4. Router
app.router(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}>
{/*将count的record输出*/}
Highest 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.count };
} // 获取state
export default connect(mapStateToProps)(IndexPage);
打开网页:你应该能看到下图:
使用 action 与 reducer
通过 + 发送 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
// ...
class IndexPage extends React.Component {
render() {
// ...
<button
+ onClick={
+ () => {
+ dispatch({ type: 'count/add1' });
+ }
+ }
>
+
</button>
// ...
}
}
// ...
效果如下图:
使用 effect
接下来我们来使用 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 ,一般请求接口的函数都会写在 serveices 文件夹中。
export function delay(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}
接着我们在 models/example.js 导入这个 utils.js
import { delay } from '../utils/utils';
使用 subscriptions
订阅订阅键盘事件,使用 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 -D
现在你可以按住 command + up 就可以使 current 加 1 了。
实现 gif 的最终效果
例子中我们看到当我们不断点击 + 按钮之后,我们会看到current会不断加一,但是 1s 过后,他会自动减到零。
官方的 demo 的代码没有实现 gif 图里面的效果,因为官方代码中没有 dispatch 一个 minus 的 action,大家看下图:
要做到 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,如下:
// ...
effects: {
*add(action, { call, put }) {
yield put({ type: 'add1' });
yield call( delay, 1000 );
yield put({ type: 'minus' });
},
},
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
};
},
},
// ...
这样我们就实现了 gif 图中的效果:
至此我们的简单的 demo 就结束了,通过这个例子大家可以基本上了解 dva 的基本概念。
如果还想深入了解 dva 的各个文件夹中文件的特性,大家可以看 快速上手dva的一个简单demo,这里面会很详细的讲到我们该怎么写 model、怎么使用 effect 请求接口数据等等。
使用 umi
umi,中文可发音为乌米,是一个可插拔的企业级 react 应用框架,安装了 umi 就相当于安装了 roadhog + 路由 + HTML 生成 + 完善的插件机制,能在提升开发者效率方面发挥出更大的价值。
业界流行的 Ant Design Pro 就是基于 umi 搭建起来的。
与 dva 和 doadhog 的关系
以下比较引自官网:
roadhog是基于webpack的封装工具,目的是简化webpack的配置umi可以简单地理解为roadhog+ 路由,思路类似next.js/nuxt.js,辅以一套插件机制,目的是通过框架的方式简化React开发dva目前是纯粹的数据流,和umi以及roadhog之间并没有相互的依赖关系,可以分开使用也可以一起使用,更多可以参考:使用 umi 改进 dva 项目开发
安装 umi
我们可以通过一下 create-umi 命令,来快速新建一个初始化项目:
$ mkdir umi-demo && cd umi-demo
$ yarn create umi
$ npm start // 启动项目
启动项目之后我们可以看到下图:
目录
.
├── dist/ // 默认的 build 输出目录
├── mock/ // mock 文件所在目录,基于 express
└── src/ // 源码目录,可选
├── layouts/index.js // 全局布局
├── pages/ // 页面目录,里面的文件即路由\
├── .umi/ // dev 临时目录,需添加到 .gitignore
├── document.ejs // HTML 模板
├── 404.js // 404 页面
├── page1.js // 页面 1,任意命名,导出 react 组件
├── page1.test.js // 用例文件,umi test 会匹配所有 .test.js 和 .e2e.js 结尾的文件
└── page2.js // 页面 2,任意命名
├── global.css // 约定的全局样式文件,自动引入,也可以用 global.less
├── global.js // 可以在这里加入 polyfill
├── app.js // 运行时配置文件
├── .umirc.js // umi 配置,同 config/config.js,二选一
├── .env // 环境变量
└── package.json
umi 路由
我们使用 umi + dva 来快速写一个上面的计数器例子,我们假设要将这个计数器页面显示在 http://localhost:8000/home 这个路由下面,我们可以通过两种方式来做:
- 配置式路由
直接在 .umirc.js 下面新增一个 home 配置即可
// ...
export default {
// ...
routes: [
{
path: '/',
component: '../layouts/index',
routes: [
{ path: '/', component: '../pages/index' }
]
}
],
// ...
}
- 约定式路由
第二种就是 umi 采用约定式路由,会根据 pages 目录自动生成路由配置。就我们只有在 page 目录下新增一个 home 文件夹就行了。
+ pages/
+ home/
- index.js
- index.css
- index.js
这种情况下,umi 会自动生成路由配置如下:
[
{ path: '/', component: './pages/index.js' },
{ path: '/home/', component: './pages/home/index.js' },
]
当采用了配置式路由路由之后,约定式路由就会失效。看使用者更偏向于哪一个。不过 umi 是推荐我们使用约定式路由的。
更多的笔者在这里就不细讲了,大家可以参考:约定式路由。
使用 umi 写一个计数器
页面
我们在 pages 目录下新增一个 home 目录,同时新建 index.js 和 index.css 文件,将我们之前将 dva 中写的 indexPage.js 和 indexPage.css 文件拷贝过来。
model
同时我们在 model 目录下新建 count.js,这个是我们之前在 dva 中提到的 model/example.js,直接拷贝过来
utils
接着我们还是以 delay 这个方法来模拟接口请求,所以我们在在 src 下新建一个 utils 目录,并创建 utils.js。
最后我们访问 http://localhost:8000/home,页面也成功运行了:
总结
文章中我们主要讲了一下 dva 的概念以及用法,同时还讲了通过 dva-cli 和 create-umi 快速创建一个简单项目,最后通过一个小例子 倒计时计数器 来大致过了一下开发的流程。
这篇文章其实是想起一个 抛砖引玉 的作用,希望能帮大家了解这一块的技术,激发起对这方面的兴趣,那么文章的效果也就达到了,剩下的更多更深层次的技术还是需要大家自己不断去探索的。
参考链接
- umi 官网
- roadhog介绍
- dva 官网
- webpack4.0 学习文档
- 初识 Dva
- Dva-React 应用框架在蚂蚁金服的实践
- Dva-api文档
- 10分钟 让你dva从入门到精通
- 基于dva-cli&antd的react项目实战