dva理论到实践——帮你扫清dva的知识盲点

dva数据流图

笔者升级了 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

dva_api.png

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中的hashHistorybrowserHistory的区别大家可以看: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):这个是你数据逻辑处理,数据流动的地方。

dva_modal.png


modaldva里面与我们真正进行项目开发,逻辑处理,数据流动的地方。这里面涉及到的namespaceModaleffectsreducer等概念都很重要,我们会在第四部分详细讲解。

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

modeldva 中最重要的概念,ModelMVC 中的 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' });
    },
    
effect_api图

在项目中最主要的会用到的是 putcall

  • 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);


五,整体架构

dva数据流图

我简单的分析一下这个图:

首先我们根据 url 访问相关的 Route-Component,在组件中我们通过 dispatch 发送 actionmodel 里面的 effect 或者直接 Reducer

当我们将action发送给Effect,基本上是取服务器上面请求数据的,服务器返回数据之后,effect 会发送相应的 actionreducer,由唯一能改变 statereducer 改变 state ,然后通过connect重新渲染组件。

当我们将action发送给reducer,那直接由 reducer 改变 state,然后通过 connect 重新渲染组件。

这样我们就能走完一个流程了。


六,项目案例

这一节我们会根据dva的快速搭建一个计数器。官方的例子是都把所有的逻辑写在了入口文件HomePage.js里,我会在下面的demo中,把例子中的各个模块抽出来,放在相应的文件夹中。让大家能更加清楚每一个模块的作用。
dva_demo.gif


1,首先全局安装dva-cli,我的操作在桌面进行的,大家可以自行选择项目目录。

$ npm install -g dva-cli


2,接着使用dva-cli创建我们的项目文件夹

$ dva new myapp


3,进入myapp目录,安装依赖,执行如下操作。

$ cd myapp
$ npm start


浏览器会自动打开一个窗口,如下图。

dva_cli.png

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;
  }
}


此时你的页面应该是如下图所示

dva_2.png

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 名字改为 countstate 对象加上 recordcurrent 属性。如下:

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);


打开网页:你应该能看到下图:

dva_4.png


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);


效果如下图:

dva_add1.gif

8,接下来我们来使用 effect 模拟一个数据接口请求,返回之后,通过 yield put() 改变相应的 state

首先我们替换相应的 models/example.jseffect

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图里面的效果,大家看下图:

effect.png

要做到gif里面的效果,我们应该在effect中发送一个关于添加的action,但是我们在effect中不能直接这么写:

effects: {
    *add(action, { call, put }) {
      yield put({ type: 'add' });
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },


因为如果这样的话,effectreducers中的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图中的效果:

dva-test.gif


至此我们的简单的demo就结束了,通过这个例子大家可以基本上了解dva的基本概念。

如果还想深入了解dva的各个文件夹中文件的特性,大家可以看快速上手dva的一个简单demo,这里面会很详细的讲到我们该怎么写 model、怎么使用effect请求接口数据等等。


这段时间我也利用业余时间,使用dva+thinkphp构建一个类似boss直聘的手机端web应用,项目还没全部做完,大家如果感兴趣的话,可以下载下来看看,一起探讨相关思路哦。


七,参考网址

  1. 初识 Dva
  2. Dva-React 应用框架在蚂蚁金服的实践
  3. Dva-api文档
  4. 10分钟 让你dva从入门到精通
  5. roadhog介绍
  6. 基于dva-cli&antd的react项目实战
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容