frontend9 / fe9-library

九部知识库
1.94k stars 138 forks source link

怎样在 dva 中写轮询逻辑 #25

Open xc1427 opened 6 years ago

xc1427 commented 6 years ago

dva 封装了 redux-saga,以 effects 的概念呈现,以 generator 函数 组织代码,优雅地处理了 React 应用中数据层的异步逻辑。本文以 umi 作为开发框架,展示如何在 dva/redux-saga 中实现如下的 轮询(polling) 逻辑。本文中涉及的代码在此

dva_polling

通过 umi 快速搭建本地开发环境

保证你的开发环境中有安装 node (版本 >= 8),并根据 官方文档 快速安装 umi。

umi 是 基于约定 的前端开发框架。umi 深度整合了 dva,使得开发基于 dva 的应用更加便捷。本文中使用的 umi 版本是 2.0.0-beta.17,安装命令为 npm i -g umi@2.0.0-beta.17.

环境安装完毕后,创建目录并通过 umi g 命令行创建第一个页面,

mkdir demo-umi-polling
cd demo-umi-polling
npm init -y
mkdir src
cd src
umi g page index

得到一个最简单的工程目录结构,

.
├── package.json
└── src
    └── pages
        ├── index.css
        └── index.js

执行 npm i ,然后启动本地开发服务器,

umi dev

如果一切顺利,可以在浏览器中看到下面的页面,

umi_dev

使用 dva

umi 默认采用了「目录即路由」的约定,并且深度整合了 dva,你不需要做诸如 app = dva()app.model()app.router()app.start() 这些事情,框架会根据「约定」自动帮你做了。

umi 采取插件机制,启动对 dva 的支持只需要安装插件 umi-plugin-react

如果是使用 umi@1.x 的话,需安装插件 umi-plugin-dva,但强烈建议您使用 umi@2。

首先,安装 umi-plugin-react

npm i --save umi-plugin-react

然后,在 umi 的配置中打开对 dva 的支持,

// 在工程根目录建立文件 .umirc.js,然后写入内容

export default {
  plugins: [
    ['umi-plugin-react', {
      dva: true,
    }],
  ],
};

然后,书写 model 定义,

/**
 * umi 约定 src 下的 models 目录可以用来放置 model 定义,
 * 因此,我们在 src 下建立 models 目录,并在其中建立文件 foo.js,文件名不重要。
 */
const namespace = 'bar';

export default {
  namespace,
  state: {
    dogImgURL: 'https://images.dog.ceo/breeds/puggle/IMG_114654.jpg',
  },
}

强烈建议安装浏览器插件 Redux DevTools 来监视 dva model,

redux-devtool

最后,我们的目录变为下面的结构,

.
├── .umirc.js
├── package.json
└── src
    ├── models
    │   └── foo.js
    └── pages
        ├── index.css
        └── index.js

重新启动开发服务器,并打开 Redux DevTools,会看到 model 已经生效了。

dva_model

通过 webapi 获取图片

在实现轮询之前,我们先实现点击按钮获取图片的功能。要使用的 webapi 是:

# 随机获取狗狗的图片
https://dog.ceo/api/breeds/image/random

首先,我们把 dva model 中的图片 URL 注入页面展示。

import { PureComponent } from 'react';
import styles from './index.css';
import { connect } from 'dva';
import fooModel from '../models/foo';

const { namespace } = fooModel;

const mapStateToProps = (state) => {
  const { dogImgURL } = state[namespace];
  return { dogImgURL };
};

@connect(mapStateToProps)
export default class IndexPage extends PureComponent {
  render() {
    return (
      <div>
        <div className={styles.normal}>
          <h1>Show random dog picture</h1>
        </div>
        <div>
          <img src={this.props.dogImgURL} alt="dog image" height="300" />
        </div>
      </div>
    );
  }
}

不出意外应该看到下图,

dva_model_connect

然后,我们使用 webapi 动态地获取图片 URL,点击按钮触发 webapi 的调用。

做网络请求的库很多,我们这里使用 dva 提供的 isomorphic-fetch,并提供简单的封装。

fetch 函数是 W3C 标准,返回 Promise,想对 fetch 函数有更多了解,可以参考这篇 google 的文章 introduction-to-fetch

在 src 目录下建立目录和文件 utils/request.js,写入以下内容,

import fetch from 'dva/fetch';

function status(response) {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response);
  } else {
    return Promise.reject(new Error(response.statusText));
  }
}

function json(response) {
  return response.json();
}

function err(err) {
  console.error(err);
}

export default function request(url, option) {
  return fetch(url, option)
    .then(status)
    .then(json)
}

在 model 中加入 webapi 获取图片的逻辑。

import request from '../utils/request';

const namespace = 'bar';

const acTyp = {
  fetch_dogImg: 'fetch_dogImg',
  fetch_dogImg_success: 'fetch_dogImg_success',
};

Object.freeze(acTyp);

export default {
  namespace,
  acTyp,
  state: {
    dogImgURL: 'https://images.dog.ceo/breeds/puggle/IMG_114654.jpg',
  },
  effects: {
    *[acTyp.fetch_dogImg](_, sagaEffects) {
      const { call, put } = sagaEffects;
      const rsp = yield call(request, 'https://dog.ceo/api/breeds/image/random');
      yield put({ type: acTyp.fetch_dogImg_success, dogImgURL: rsp.message });
    },
  },
  reducers: {
    [acTyp.fetch_dogImg_success](state, { dogImgURL }) {
      return { ...state, dogImgURL };
    },
  },
}

给页面添加按钮,绑定点击事件。

import { PureComponent } from 'react';
import styles from './index.css';
import { connect } from 'dva';
import fooModel from '../models/foo';

const { namespace, acTyp } = fooModel;

const mapStateToProps = (state) => {
  const { dogImgURL } = state[namespace];
  return { dogImgURL };
};

const mpaDispatchToProps = (dispatch) => {
  return {
    onClickFetchImg() {
      return dispatch({ type: `${namespace}/${acTyp.fetch_dogImg}` });
    },
  };
};

@connect(mapStateToProps, mpaDispatchToProps)
export default class IndexPage extends PureComponent {
  render() {
    return (
      <div>
        <div className={styles.normal}>
          <h1>Show random dog picture</h1>
        </div>
        <div>
          <img src={this.props.dogImgURL} alt="dog image" height="300" />
        </div>
        <div style={{ marginTop: '16px' }}>
          <button onClick={this.props.onClickFetchImg}>点击获取图片</button>
        </div>
      </div>
    );
  }
}

最后,我们的目录结构变为,

.
├── .umirc.js
├── package.json
└── src
    ├── models
    │   └── foo.js
    ├── pages
    │   ├── index.css
    │   └── index.js
    └── utils
        └── request.js

效果如下图所示,而且看到 action 在点击后被派发,

dva_fetch

实现图片的轮询

在 redux-saga 中,实现轮询逻辑需要两个包含 while (true) 循环的 saga。一个用来监听轮询启动和暂停的指令,起着 saga watcher 的作用,一个用来间隔性地调用 webapi,起着 saga worker 的作用。暂停轮询时需要使用 race effect。二话不说,直接上代码。

首先,在 model 文件中加入轮询的逻辑,

 import request from '../utils/request';

+function delay(millseconds) {
+  return new Promise(function(resolve) {
+    setTimeout(resolve, millseconds);
+  });
+}
+
 const namespace = 'bar';

 const acTyp = {
   fetch_dogImg: 'fetch_dogImg',
   fetch_dogImg_success: 'fetch_dogImg_success',
+  start_polling_dogImg: 'start_polling_dogImg',
+  stop_polling_dogImg: 'stop_polling_dogImg',
 };

 Object.freeze(acTyp);

+const endPointURL = 'https://dog.ceo/api/breeds/image/random';
+
+function* pollingDogImgSagaWorker(sagaEffects) {
+  const { call, put } = sagaEffects;
+  while (true) {
+    const rsp = yield call(request, endPointURL);
+    yield call(delay, 1000);
+    yield put({ type: acTyp.fetch_dogImg_success, dogImgURL: rsp.message });
+  }
+}
+
 export default {
   namespace,
   acTyp,
   state: {
     dogImgURL: 'https://images.dog.ceo/breeds/puggle/IMG_114654.jpg',
   },
   effects: {
     *[acTyp.fetch_dogImg](_, sagaEffects) {
       const { call, put } = sagaEffects;
-      const rsp = yield call(request, 'https://dog.ceo/api/breeds/image/random');
+      const rsp = yield call(request, endPointURL);
       yield put({ type: acTyp.fetch_dogImg_success, dogImgURL: rsp.message });
     },
+    'poll dog image': [function*(sagaEffects) {
+      const { call, take, race } = sagaEffects;
+      while (true) {
+        yield take(acTyp.start_polling_dogImg);
+        yield race([
+          call(pollingDogImgSagaWorker, sagaEffects),
+          take(acTyp.stop_polling_dogImg),
+        ]);
+      }
+    }, { type: 'watcher' }],
   },
   reducers: {
     [acTyp.fetch_dogImg_success](state, { dogImgURL }) {
       return { ...state, dogImgURL };
     },
+    [acTyp.start_polling_dogImg](state) {
+      return { ...state };
+    },
+    [acTyp.stop_polling_dogImg](state) {
+      return { ...state };
+    },
   },
 }

然后,在页面中加入可以派发 action 的按钮,

 import { PureComponent } from 'react';
 import styles from './index.css';
 import { connect } from 'dva';
 import fooModel from '../models/foo';

 const { namespace, acTyp } = fooModel;

 const mapStateToProps = (state) => {
   const { dogImgURL } = state[namespace];
   return { dogImgURL };
 };

 const mpaDispatchToProps = (dispatch) => {
   return {
     onClickFetchImg() {
       return dispatch({ type: `${namespace}/${acTyp.fetch_dogImg}` });
     },
+    onStartPolling() {
+      return dispatch({ type: `${namespace}/${acTyp.start_polling_dogImg}` });
+    },
+    onStopPolling() {
+      return dispatch({ type: `${namespace}/${acTyp.stop_polling_dogImg}` });
+    },
   };
 };

 @connect(mapStateToProps, mpaDispatchToProps)
 export default class IndexPage extends PureComponent {
   render() {
     return (
       <div>
         <div className={styles.normal}>
           <h1>Show random dog picture</h1>
         </div>
         <div>
           <img src={this.props.dogImgURL} alt="dog image" height="300" />
         </div>
         <div style={{ marginTop: '16px' }}>
           <button onClick={this.props.onClickFetchImg}>点击获取图片</button>
+          <button onClick={this.props.onStartPolling}>启动图片轮询</button>
+          <button onClick={this.props.onStopPolling}>停止图片轮询</button>
         </div>
       </div>
     );
   }
 }

如果一切顺利,将会看到本文开头展示的画面,

dva_polling

结论

这里着重解释一下轮询的过程:

没有晦涩的程序跳转,没有递归的 setTimeout,更没有闭包之外的状态变量。笔者认为 redux-saga 提供了一种非常优雅且易于推理的描述异步过程的方式。

作者需要您的支持,如果您觉得本文对您有帮助,请留个言或点个赞 😄,谢谢。

jean26bzh commented 6 years ago

赞,先 mark 一记,前端轮子太多

stefyzx commented 6 years ago

Thanks for sharing !

wangpipi1991 commented 6 years ago

第一次听到了轮询逻辑的概念,涨知识了!

tolerance-go commented 6 years ago

了解这个特效可能会更好理解本文

race 的另一个有用的功能是,它会自动取消那些失败的 Effects。

jeffrey-fu commented 6 years ago

用了Dva,对react 的reducer action saga 只有一个基础的了解特别是saga,

xc1427 commented 6 years ago

用了Dva,对react 的reducer action saga 只有一个基础的了解特别是saga,

@jeffrey-fu 看看这篇 https://github.com/frontend9/fe9-library/issues/26

1921622004 commented 6 years ago

学习了

dengnan123 commented 5 years ago

前端这样轮询的,学习了

FrankMojito commented 5 years ago

pai风哥 牛批

xiaohuoni commented 5 years ago

学习了

fireairforce commented 5 years ago

mark!

1123305308 commented 5 years ago

前端这样轮询的,学习了

jszyy commented 4 years ago

向大佬学习~

waybi commented 4 years ago

为什么不用saga的poll poll写起来代码更简洁

miaozilong commented 4 years ago

为什么不用saga的poll poll写起来代码更简洁

saga不支持poll吧,dva新版才支持