dva( 轻量级的应用框架 )

dva核心知识与实战运用

dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架!

介绍 | DvaJS

  • 易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API

  • elm 概念,通过 reducers, effects 和 subscriptions 组织 model

  • 插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading

  • 支持 HMR,基于 babel-plugin-dva-hmr 实现 components、routes 和 models 的 HMR

1. 如何使用dva?

1.1 在create-react-app的基础上使用dva

在create-react-app脚手架的基础上,额外安装的内容:

  • 无需手动进行antd按需导入

  • 无需安装:redux及redux-saga、react-redux、react-router-dom等,dva把这些东西都集成好了,安装一个dva就相当于安装了这些全部东西!!

    • react-router-dom使用的是v4版本「4.3.1」

    • redux使用的是 v3.7.2「我们之前使用的都是v4.0」

    • 集成的配套插件版本有点低

    • 在React18的脚手架中使用dva会有警告错误!!

  • history 是控制路由模式的

  • 其余的按照之前讲的配置方案去配置webpack,包括:less、跨域代理、兼容、响应式布局等

 注意安装的版本

{
    "dependencies": {
        "antd": "^5.0.0",
        "antd-icons": "^0.1.0-alpha.1",
        "dva": "^2.4.1",
        "http-proxy-middleware": "^2.0.6",
        "less": "^4.1.3",
        "less-loader": "^8.1.1",
        "prop-types": "^15.8.1",
        "styled-components": "^5.3.6",
        "history": "4.10.1",
        ......
    }
}

项目的结构目录,可以依然沿用之前的命名风格:

  • api 接口管理和请求封装

  • assets 静态资源文件

  • router 路由统一配置

  • store redux公共状态管理

  • views 普通业务组件

  • components 公共业务组件

  • index.jsx 入口

  • setupProxy.js 跨域代理

但是有很多文件的编写方式和之前是不一样的!!


index.js入口

import dva from 'dva';
import createHistory from 'history/createHashHistory';
import RouterConfig from './router';
import voteModel from './store/voteModel';

// 初始化配置
const app = dva({
  // 设置路由模式{默认HASH路由}
  history: createHistory()
});
// 使用插件
app.use({});
// redux公共状态管理
app.model(voteModel);
// 路由配置
app.router(RouterConfig);
// 启动dva
app.start('#root');

router/index.js 配置页面入口和路由

import React from 'react';
import { Router, Route, Switch, Redirect } from 'dva/router';
import Vote from '../views/Vote';
import Demo from '../views/Demo';
/* ANTD */
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import '../assets/reset.min.css';

function RouterConfig({ history }) {
    return (
        <ConfigProvider locale={zhCN}>
            <Router history={history}>
                <Switch>
                    <Route path="/" exact component={Vote} />
                    <Route path="/demo" component={Demo} />
                    <Redirect to="/" />
                </Switch>
            </Router>
        </ConfigProvider>
    );
}
export default RouterConfig;

store/voteModel.js 配置每个模块的Model,包含:状态、reducer、异步派发的方法等

import _ from '../assets/utils';
const delay = (interval = 1000) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, interval);
    });
};
export default {
    namespace: 'vote',
    state: {
        supNum: 10,
        oppNum: 5
    },
    reducers: {
        support(state, action) {
            state = _.clone(true, state);
            let { payload = 1 } = action;
            state.supNum += payload;
            return state;
        },
        oppose(state, action) {
            state = _.clone(true, state);
            let { payload = 1 } = action;
            state.oppNum += payload;
            return state;
        }
    },
    effects: {
        supportAsync: [
            function* ({ payload }, { call, put }) {
                yield call(delay, 2000);
                yield put({
                    type: 'support',
                    payload
                });
            },
            { type: 'takeLatest' }
        ],
        *opposeAsync({ payload }, { call, put }) {
            yield call(delay, 2000);
            yield put({
                type: 'oppose',
                payload
            });
        }
    }
};

在组件中如何使用呢?

import React from "react";
import styled from "styled-components";
import { Button } from 'antd';
import { connect } from 'dva';

// 样式处理
const VoteBox = styled.div`
    ...
`;

const Vote = function Vote(props) {
    let { supNum, oppNum, dispatch } = props;
    return <VoteBox>
        <div className="header">
            <h2 className="title">React是很棒的前端框架</h2>
            <span className="num">{supNum + oppNum}</span>
        </div>
        <div className="main">
            <p>支持人数:{supNum}人</p>
            <p>反对人数:{oppNum}人</p>
        </div>
        <div className="footer">
            <Button type="primary"
                onClick={() => {
                    dispatch({
                        type: 'vote/supportAsync',
                        payload: 10
                    });
                }}>
                支持
            </Button>
            <Button type="primary" danger
                onClick={() => {
                    dispatch({
                        type: 'vote/opposeAsync'
                    });
                }}>
                反对
            </Button>
        </div>
    </VoteBox>;
};
export default connect(state => state.vote)(Vote);

1.2 但是更多的时候,我们会直接使用 dva 自带的脚手架创建项目 

dva脚手架创建的项目是基于 roadhog /rəʊd hog/ 进行webpack的配置!!
roadhog是一个cli工具,提供server、 build和test三个命令,分别用于本地调试和构建,并且提供了特别易用的mock功能。命令行体验和create-react-app一致,配置略有不同,比如默认开启 css modules,然后还提供了JSON格式的配置方式!
$ npm install dva-cli -g
$ dva -v
$ dva new my-project

 

 package.json

{
  "private": true,
  "scripts": {
    "start": "cross-env PORT=3000 HOST=127.0.0.1 roadhog server", //开发环境启动
    "build": "roadhog build", //生产环境打包
    "lint": "eslint --ext .js src test", //单元测试
    "precommit": "npm run lint"
  },
  "dependencies": {
    "@babel/polyfill": "^7.12.1",
    "antd": "4.24.7", //注意版本用v4「不是最新的v5」
    "antd-icons": "^0.1.0-alpha.1",
    "babel-plugin-import": "^1.13.5", //antd按需导入
    "dva": "^2.4.1",
    "history": "4.10.1", //管理路由模式的「用v4不是最新的v5版本」
    "lib-flexible": "^0.3.2",
    "postcss-pxtorem": "5.1.1",
    "prop-types": "^15.8.1",
    "qs": "^6.11.0",
    "react": "^16.2.0", //react使用的是v16版本
    "react-dom": "^16.2.0",
    "styled-components": "^5.3.6"
  },
  "devDependencies": {
    "babel-plugin-dva-hmr": "^0.3.2", //热更新
    "cross-env": "^7.0.3",
    "less": "4.1.3",
    "less-loader": "8.1.1",
    ...
  }
}

 修改webpack配置项

修改启动的域名和端口号:设置环境变量即可

  • PORT

  • HOST

  • HTTPS 是否开启https,默认关闭

  • BROWSER 设为none时不自动打开浏览器

  • CLEAR_CONSOLE 设为none时清屏

“start”: “cross-env PORT=3000 HOST=127.0.0.1 roadhog server”,

.webpackrc改为.webpackrc.js,这样就可以按照JS方式去编写配置项了!!

  • 修改入口、出口、打包配置等

  • Antd按需导入

  • 配置跨域代理

  • 配置响应式布局方案

  • 配置less

  • 不同环境下的配置

  • 浏览器兼容

  • ……

PC配置:

import px2rem from 'postcss-pxtorem';
export default {
    /* 基础配置 */
    "entry": "src/index.js", //配置多入口:src/enter/*.js
    "outputPath": "./dist",
    "publicPath": "/",
    "hash": true,
    "html": {
        "template": "./public/index.ejs"
    },
    /* 配置LESS */
    "disableCSSModules": true,
    /* 配置PX转REM */
    "extraPostCSSPlugins": [
        px2rem({
            "rootValue": 75,
            "propList": ['*']
        })
    ],
    /* 配置BABEL的插件 */
    "extraBabelPlugins": [
        // antd按需导入
        [
            "import",
            {
                "libraryName": "antd",
                "libraryDirectory": "es",
                "style": "css"
            }
        ],
        // 配置PX转REM
        [
            "styled-components-px2rem",
            {
                "rootValue": 75
            }
        ]
    ],
    /* 配置跨域代理 */
    "proxy": {
        "/api": {
            "target": "https://news-at.zhihu.com/api/4",
            "changeOrigin": true,
            "ws": true,
            "pathRewrite": {
                "/api": ""
            }
        }
    },
    /* 不同环境下的不同配置 */
    "env": {
        "development": {
            "extraBabelPlugins": [
                "dva-hmr"
            ]
        }
    }
};

 浏览器兼容:默认情况下,ES6语法和CSS3的兼容已经处理,如果想处理ES6内置API的兼容,则导入@babel/polyfill即可「入口导入」!!

移动配置:

import px2rem from 'postcss-pxtorem';
export default {
    // 对于css的处理 
    disableCSSModules: true,
    disableCSSSourceMap: true,
    /* 基础配置 */
    "entry": "src/index.js", //配置多入口:src/enter/*.js
    "outputPath": "./dist",
    "publicPath": "/",
    "hash": true,
    // "html": {
    //     "template": "./public/index.ejs"
    // },
    /* 配置LESS */
    "disableCSSModules": true,
    /* 配置PX转REM */
    "extraPostCSSPlugins": [
        px2rem({
            "rootValue": 75,
            "propList": ['*']
        })
    ],
    /* 配置BABEL的插件 */
    "extraBabelPlugins": [
        // antd按需导入
        [
            "import",
            {
                "libraryName": "antd",
                "libraryDirectory": "es",
                "style": "css"
            }
        ],
        // 配置PX转REM
        [
            "styled-components-px2rem",
            {
                "rootValue": 75
            }
        ]
    ],
    /* 配置跨域代理 */
    "proxy": {
        "/api": {
            "target": "https://localhost:8888",
            "changeOrigin": true,
            "ws": true,
            "pathRewrite": {
                "/api": ""
            }
        }
    },
    /* 不同环境下的不同配置 */
    "env": {
        "development": {
            "extraBabelPlugins": [
                "dva-hmr"
            ]
        }
    }
};

2. dva中的路由配置

index.js

import dva from 'dva';
/*
 安装history模块「安装v4.10.1版本,不建议安装最新版本」
 $ yarn add history@4.10.1
 默认开启的就是HASH路由,如果想使用History路由,则导入createBrowserHistory!!
*/
import createHistory from 'history/createHashHistory';
    const app = dva({
        // 指定路由模式
        history: createHistory()
    });
...
app.router(require('./router').default);
app.start('#root');

router.js

/* 
dva/router中包含了react-router-dom v5版本中所有API,以及react-router-redux中的的API 
*/
import React from 'react';
import { Router, Route, Switch, Redirect } from 'dva/router';
import Vote from './routes/Vote';
import Demo from './routes/Demo';
import Personal from './routes/Personal';

/* ANTD */
...

const RouterConfig = function RouterConfig({ history }) {
  return <ConfigProvider locale={zhCN}>
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={Vote} />
        <Route path="/demo" component={Demo} />
        <Route path="/personal" component={Personal} />
        <Redirect to="/" />
      </Switch>
    </Router>
  </ConfigProvider>;
}
export default RouterConfig;

路由懒加载

路由懒加载主要使用 dva下的dynamic

API | DvaJS

import React from 'react';
import { Router, Route, Switch, Redirect } from 'dva/router';
import Vote from './routes/Vote';
import dynamic from 'dva/dynamic'; //实现动态组件的API

/* ANTD */
...

const RouterConfig = function RouterConfig({ history, app }) {
  /* 异步组件 */
  const DemoAsync = dynamic({
    app,
    models: () => [
      import(/* webpackChunkName:"demo" */ './models/demoModel')
    ],
    component: () => import(/* webpackChunkName:"demo" */ './routes/Demo')
  });
  const PersonalAsync = dynamic({
    app,
    models: () => [
      import(/* webpackChunkName:"personal" */ './models/personalModel')
    ],
    component: () => import(/* webpackChunkName:"personal" */ './routes/Personal')
  });

  return <ConfigProvider locale={zhCN}>
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={Vote} />
        <Route path="/demo" component={DemoAsync} />
        <Route path="/personal" component={PersonalAsync} />
        <Redirect to="/" />
      </Switch>
    </Router>
  </ConfigProvider>;
}
export default RouterConfig;

配置路由表和二级路由

routerRoutes.js 路由表

import Vote from './routes/Vote';
import dynamic from 'dva/dynamic';
/* 配置路由懒加载 */
const lazy = function lazy(models, component) {
    return dynamic({
        app: window.app, //在入口处挂载到window上
        models,
        component
    });
};

const routes = [{
    path: '/',
    exact: true,
    component: Vote,
    meta: { title: '首页' }
}, {
    path: '/demo',
    component: lazy(
        () => [import(/* webpackChunkName:"demo" */ './models/demoModel')],
        () => import(/* webpackChunkName:"demo" */ './routes/Demo')
    ),
    meta: { title: '测试页' }
}, {
    path: '/personal',
    component: lazy(
        () => [import(/* webpackChunkName:"personal" */ './models/personalModel')],
        () => import(/* webpackChunkName:"personal" */ './routes/Personal')
    ),
    meta: { title: '个人中心' },
    /* 二级路由 */
    children: [{
        redirect: true,
        exact: true,
        from: '/personal',
        to: '/personal/order'
    }, {
        path: '/personal/order',
        component: lazy(
            () => [],
            () => import(/* webpackChunkName:"personal" */ './routes/personal/MyOrder')
        ),
        meta: { title: '个人中心-我的订单' }
    }, {
        path: '/personal/profile',
        component: lazy(
            () => [],
            () => import(/* webpackChunkName:"personal" */ './routes/personal/MyProfile')
        ),
        meta: { title: '个人中心-我的信息' }
    }]
}, {
    redirect: true,
    to: '/'
}];
export default routes;

router.js

import React from 'react';
import { Router, Route, Switch, Redirect } from 'dva/router';
import routes from './routerRoutes';
/* ANTD */
...
/* 动态创建路由 */
const createRoute = function createRoute(routes) {
  return <Switch>
    {routes.map((item, index) => {
      let { redirect, from, to, exact, path, meta, component: Component } = item,
        config = {};
      // 重定向
      if (redirect) {
        config = { to };
        if (from) config.from = from;
        if (exact) config.exact = exact;
        return <Redirect {...config} key={index} />;
      }
      // 正常路由
      config = { path };
      if (exact) config.exact = exact;
      return <Route {...config} key={index}
        render={(props) => {
          // 修改标题
          let { title = '' } = meta;
          document.title = `${title}-珠峰培训React`;
          return <Component {...props} />;
        }} />;
    })}
  </Switch>;
};
/* 一级路由 */
const RouterConfig = function RouterConfig({ history }) {
  return <ConfigProvider locale={zhCN}>
    <Router history={history}>
      {createRoute(routes)}
    </Router>
  </ConfigProvider>;
};
/* 二级路由 */
export const childrenRouter = function childrenRouter(path) {
  let item = routes.find(item => item.path === path),
    children;
  if (item) children = item.children;
  if (!children) return null;
  return createRoute(children);
};
export default RouterConfig;

index.js

import dva from 'dva';
import createHistory from 'history/createHashHistory';
import voteModel from './models/voteModel';
// 1. Initialize
const app = dva({
    history: createHistory()
});
window.app = app;
// 2. Plugins
// app.use({});
// 3. Model
app.model(voteModel);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');

Personal.jsx

import React from "react";
import { NavLink } from 'dva/router';
import styled from "styled-components";
import { childrenRouter } from '../router';
/* 样式处理 */
const PersonalBox = styled.div`
    ...
`;
const Personal = function Personal() {
    return <PersonalBox>
        <div className="menu">
            <NavLink to="/personal/order">我的订单</NavLink>
            <NavLink to="/personal/profile">我的信息</NavLink>
        </div>
        <div className="content">
            {childrenRouter('/personal')}
        </div>
    </PersonalBox>;
};
export default Personal;

路由跳转及传参

     history对象中提供了路由跳转的方法

       + go

       + goBack -> go(-1)

       + goFoward -> go(1)

       + push

       + replace

路径参数:把传递的信息当做路由地址的一部分,但是需要路由地址基于”:?“设置匹配的规则
路由地址:'/personal/profile/:lx?/:name?',
history.push(`/personal/profile/0/zhufeng`); 
 
问号传参:传递的信息会存在于地址栏中,即便用户刷新页面,依然可以获取相关传递的信息
history.push({
    pathname: '/personal/profile',
    search: 'lx=0&name=zhufeng'
}); 
 
隐式传参:基于state把信息传递给目标组件,但是传递的信息没有在地址中存在「不丑+安全」,这样在目标组件页面刷新,传递的信息就消失了!!
history.push({
    pathname: '/personal/profile',
    state: {
        lx: 0,
        name: 'zhufeng'
    }
});

方案一:Link 和 NavLink
NavLink可以和路由地址进行匹配,设置选中样式!!

<div className="menu">
    <NavLink to="/personal/order">我的订单</NavLink>
    <NavLink to="/personal/profile">我的信息</NavLink>
</div>

方案二:编程式导航

 routerRedux 是 react-router-redux 中提供的对象,此对象中包含了路由跳转的方法
   + go/goBack/goFoward
   + push/replace
 相比较于props.history对象来讲,routerRedux不仅可以在组件中实现路由跳转,而且可以在redux操作中实现路由的跳转!!它本身就是redux和router的结合操作!!

   在redux内部
     yield put(routerRedux.push(...))
   在redux外部「或者组件中」
     dispatch(
        routerRedux.push(...)
     )
     一定要基于dispatch进行派发才会跳转;因为执行routerRedux.xxx方法,只会返回一个action对象;
     action->{
        type:"@@router/CALL_HISTORY_METHOD",
        payload:{
            method:'push', 
            args:[...] 
        }
     }

import React from "react";
import { routerRedux } from 'dva/router';
import { connect } from 'dva';

const MyOrder = function MyOrder(props) {
     
    基于路由匹配的组件,其属性中包含:history、location、match!
      其中history就是实现路由跳转的
        + push
        + replace
        + go
        + goBack
        + goForward
      如果组件不是基于路由匹配的,可以基于 withRouter 高阶函数处理即可!!
    
    let { history, dispatch } = props;
    return <div className="myOrderBox">
        我的订单
        <button onClick={() => {
            // history.push('/personal/profile');

             
             routerRedux 也可以实现路由跳转,语法和history类似
             好处:可以在Effects中基于 yield 实现路由跳转
                // Inside Effects
                yield put(routerRedux.push('/logout'));

                // Outside Effects
                dispatch(routerRedux.push('/logout'));
             
            dispatch(routerRedux.push('/personal/profile'));
        }}>跳转</button>
    </div>;
};
export default connect()(MyOrder);

3. dva中Model处理 


model处理流程

1. 如果有引入多个model ,app.model可以多次执行  这样会降低首屏的加载速度

import voteModel from './models/vote';
app.model(voteModel);  
import voteModel from './models/vote2';
app.model(voteModel2);  

2. 页面需要的时候懒加载,配合路由使用懒加载 dynamic

3. Model的组成

  1. namespace 命名空间【模块名,后期获取状态和派发的标识】
  2. state 数据 【模块管理的公共状态】

  3. reducers 同步处理的方法 【已一个一个方法的模式,完成reducer中的派发行为标识的判断以及状态的更改+同步修改+外部修改+外部派发dispatch('/demo/xxx')】

  4. effects   redux-saga中异步处理方法【实现异步操作,异步派发】

  5. subscriptions  订阅【在这里订阅的方法,会在页面一加载的时候就会被通知执行,所以:我们把页面一加载就要做的事情 (和 redux 相关的)在这里处理,在这里我们可以基于 history.listen做监听,保证进入哪个组件再处理也可以】

4. 在组件中,可以基于 dva中提供的 connect高阶函数,使用公共状态及dispatch方法

入口

import voteModel from './models/voteModel';
...
app.model(voteModel);
...

基本结构

export default {
    // 命名空间「模块名:后期获取状态和派发都需要这个名字」
    namespace: 'vote',
    // 此模块管理的公共状态
    state: {},
    // 此模块需要判断的reducer「同步派发直达reducers」
    reducers: {},
    // 此模块需要异步派发的任务「基于redux-saga语法处理」
    effects: {},
    // 订阅方法,一开始就自动执行「获取数据,实现派发等」
    subscriptions: {}
};

实现计数器累计

Demo.jsx

import React from "react";
import styled from "styled-components";
import { connect } from 'dva'
import { Button } from 'antd';
...
const Demo = function Demo(props) {
    let { num, dispatch } = props;
    return <DemoBox>
        <span className="num">{num}</span>
        <Button type="primary"
            onClick={() => {
                dispatch({
                    type: "demo/increment",
                    payload: 5
                });
            }}>
            按钮
        </Button>
        <Button type="primary" danger
            onClick={() => {
                dispatch({
                    type: 'demo/incrementAsync',
                    payload: 10
                });
            }}>
            异步按钮
        </Button>
    </DemoBox>;
};
export default connect(state => state.demo)(Demo);

demoModel.js

import _ from '../utils/utils';
const delay = (interval = 1000) => {
    ...
};
export default {
    namespace: 'demo',
    state: {
        num: 0
    },
    reducers: {
        increment(state, action) {
            state = _.clone(true, state);
            let { payload = 1 } = action;
            state.num += payload;
            return state;
        }
    },
    effects: {
        *incrementAsync({ payload }, { call, put }) {
            yield call(delay, 2000);
            yield put({
                type: 'increment',
                payload
            });
        }
    }
};

effects中的特殊处理

effects: {
    incrementAsync: [
        function* ({ payload }, { call, put, select }) {
            try {
                // 获取状态
                let { num } = yield select(state => state.demo);
                // 发送请求
                yield call(delay, 2000);
                // 派发任务
                yield put({
                    type: 'increment',
                    payload
                });
            } catch (err) {
                // 异常捕获
                console.log(err);
            }
        },
        // 指定监听的类型,默认是takeEvery「还有:takeLatest、throttle等」
        { type: "takeLatest" },
        // { type: "throttle", ms: 1000 }
    ]
}

subscriptions

app.model({
    subscriptions: {
        setup({ dispatch, history }) {
            history.listen(location => {
                if (location.pathname === '/demo') {
                    dispatch({
                        type: 'demo/increment',
                        payload: 100
                    });
                }
            });
        }
    }
})

懒加载的model

const delay = (interval = 1000) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, interval);
    });
};
export default {
    namespace: 'demo',
    state: {
        num: 10
    },
    reducers: {
         
         把原有reducer函数中的每一种switch/case情况都写成一个单独的方法「纯函数」
           state:获取“本模块”的公共状态
           action:派发时候传递的action对象「包含type和传递的其他值(一般基于payload字段传递)」
           我们需要把获取的state克隆一份,然后函数最后返回的值,会替换当前模块的state!!
         
        increment(state, { payload = 1 }) {
            /* state = { ...state };
            state.num += payload;
            return state; */
            return {
                ...state,
                num: state.num + payload
            };
        }
    },
    effects: {
         
         redux-saga中我们基于take/takLatest/takeEvery等方式创建的监听器,此时写成一个个的“Generator函数”即可!!-> 默认是基于takeEvery的方式创建的监听器
           + 方法名是我们创建的监听器名字
           + 方法就是派发的任务被监听后,执行的working方法
           + 此处的函数名,不要和reducers中的函数名一致,因为:每一次派发,reducers和effects中的方法都会去匹配执行!如果函数名一样,则状态修改两次!!我们一般在effects写的名字,都加Async!!
         
         方法中的参数
           + action:在组件中进行派发时,传递的action对象
           + 第二个参数就是redux-saga中提供的EffectsAPI,但是没有delay/debounce...
             + 基于 yield select() 可以获取所有模块的公共状态
               yield select(state=>state.demo) 这样就是获取指定的状态信息
         
        *incrementAsync({ payload }, { call, put }) {
            yield call(delay, 2000);
            yield put({
                type: 'increment',
                payload
            });
        }
         如果想设置不同类型的监听器,则这样写
        /* incrementAsync: [
            // 数组第一项是working函数
            function* ({ payload }, { call, put }) {
                yield call(delay, 2000);
                yield put({
                    type: 'increment',
                    payload
                });
            },
            // 数组第二项中指定监听器的类型
            { type: 'takeLatest' }
            // { type: 'throttle', ms: 500 }
        ] */
    },
  
     demoModel是被懒加载的,只有访问了/demo这个地址(组件),demoModel才会被注册!!
       这里订阅的方法
       + 只有进入到这个组件,Model懒加载完毕,也被注册了,subscriptions中订阅的方法才会被执行
       + 而且只会执行一次,后期路由来回切换的时候,也不再执行了
   
    subscriptions: {
        setup() { },
      

    }
};

加载页面就注册

     这个板块的Model是加载页面时就被立即注册的 
       + subscriptions中写的方法,在页面一加载的时候,就会把所有设定的方法执行
       + 方法就是普通函数「不能是Generator函数」
         + 传递的实参对象中具备 history/dispatch 两个属性
         + history:包含路由跳转和监听的history对象
         + dispatch:进行派发的方法
       + 如果想页面一加载「或者是指定的某个条件下」,我们就想从服务器异步获取数据,修改此模块的状态值,则可以写在subscriptions中!!
     
    subscriptions: {
        // 方法只有页面一加载的时候,订阅执行一次,在后期路由切换中,不再执行
        /* async setup({ history, dispatch }) {
            console.log('VOTE-SETUP');
            await delay(2000);
            dispatch({
                type: 'support'
            });
        } */

         需求改变了一下:我们想的是,在页面第一次/重新加载的时候,只有进入Vote这个组件,我们在voteModel中写的setup,以及其内部的操作,才让其生效!!
        setup({ history, dispatch }) {
            // 在Model没有懒加载的情况下,我们可以让setup函数在页面第一次加载的过程中,就订阅到事件池里,并且通知执行!!我们在setup中基于history.listen创建路由跳转监听器:第一次会执行,以后每一次路由切换也会执行!!
            let unlisten = history.listen(async (location) => {
                let { pathname } = location;
                if (pathname === '/') {
                    await delay(2000);
                    dispatch({
                        type: 'support'
                    });
                    // 返回的函数就是移除此监听器的操作
                    unlisten();
                }
            });
        }
    }

4. dva-loading插件的应用


dva-loading 会监听指定的异步请求方法,方法开始时loading状态值为 true ,异步结束后该值自动置为 false , 可用于骨架屏或某些需要 loading 状态的场景!
$ yarn add dva-loading


使用方式:

  1. npm or yarn 安装dva-loading ,并在入口Index.js中引入, 示例:import createLoading from 'dva-loading';
  2. 在入口Index.js中 app.use( createLoading ) ,示例:app.use(createLoading());
  3. 在组件context高阶函数中 state可以拿到loading ,示例:state => {
            return {
                ...state.demo,
                loading: state.loading
            };
        }
  4. 组件内指定loading对应的 effects  示例:【loading = loading.effects['demo/testAsync'];】 

打印loading:

 index.js

import createLoading from 'dva-loading';
...
app.use(createLoading());
...

models/demoModel.js

const delay = (interval = 1000) => {
    ...
};
export default {
    namespace: 'demo',
    state: {
        num: 0
    },
    reducers: {
        test(state) {
            state = { ...state };
            state.num++;
            return state;
        }
    },
    effects: {
        *testAsync(action, { call, put }) {
            yield call(delay, 2000);
            yield put({
                type: 'test'
            });
        }
    }
};

组件中使用

import { connect } from "dva";
...
const Demo = function Demo({ num, loading, dispatch }) {
    loading = loading.effects['demo/testAsync'];
    return <DemoBox>
        <span className="num">{num}</span>
        <Button type="primary" danger
            loading={loading}
            onClick={() => {
                dispatch({ type: 'demo/testAsync' });
            }}>
            异步按钮
        </Button>
    </DemoBox>;
};
export default connect(
    state => {
        return {
            ...state.demo,
            loading: state.loading
        };
    }
)(Demo);

中间件:

npm view xxx version 查看历史版本

这里使用 2.10.2 版本

Redux Middleware

5. 基于dva重写投票案例


voteModel.js

import _ from '../utils/utils';
const delay = (interval = 1000) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, interval);
    });
};

export default {
    namespace: 'vote',
    state: {
        supNum: 10,
        oppNum: 5
    },
    reducers: {
        support(state, action) {
            state = _.clone(true, state);
            let { payload = 1 } = action;
            state.supNum += payload;
            return state;
        },
        oppose(state, action) {
            state = _.clone(true, state);
            let { payload = 1 } = action;
            state.oppNum += payload;
            return state;
        }
    },
    effects: {
        supportAsync: [
            function* ({ payload }, { call, put }) {
                yield call(delay, 2000);
                yield put({
                    type: 'support',
                    payload
                });
            },
            { type: 'takeLatest' }
        ],
        opposeAsync: [
            function* opposeAsync({ payload }, { call, put }) {
                yield call(delay, 2000);
                yield put({
                    type: 'oppose',
                    payload
                });
            },
            { type: 'takeLatest' }
        ]
    }
};

Vote.jsx

import React from "react";
import styled from "styled-components";
import { Button } from 'antd';
import { connect } from 'dva';
...
const Vote = function Vote(props) {
    let { supNum, oppNum, dispatch } = props;
    return <VoteBox>
        <div className="header">
            <h2 className="title">React是很棒的前端框架</h2>
            <span className="num">{supNum + oppNum}</span>
        </div>
        <div className="main">
            <p>支持人数:{supNum}人</p>
            <p>反对人数:{oppNum}人</p>
        </div>
        <div className="footer">
            <Button type="primary"
                onClick={() => {
                    dispatch({
                        type: 'vote/supportAsync',
                        payload: 10
                    });
                }}>
                支持
            </Button>
            <Button type="primary" danger
                onClick={() => {
                    dispatch({
                        type: 'vote/opposeAsync'
                    });
                }}>
                反对
            </Button>
        </div>
    </VoteBox>;
};
export default connect(state => state.vote)(Vote);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值