Dva 概念

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

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过dispatch发起一个action,如果是 同步行为会直接通过reducers 改变state,如果是异步行为(副作用)会先触发effects,然后流向reducers最终改变state,所以在dva中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
PPrerEAKbIoDZYr.png

Models 介绍

State

State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当做不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。

在 dva 中你可以通过 dva 的实例属性 _store 看到顶部的 state 数据。

  1. const app = dva();
  2. console.log(app._store); // 顶部的 state 数据

Action

Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从UI事件、网络回调,还是 WebSocket 等数据源所获取的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。
action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch

  1. import {connect} from 'dva';
  2. function App(props){
  3. const action = {
  4. type: 'loginModel/userInfo', //action动作
  5. payload: {}, //额外参数
  6. }
  7. return (<div onClick={() => props.dispatch(action}></div>)
  8. }
  9. export default connect(mapStateToProps)(App)
  10. // 在 dva 中,通过 connect(mapStateToProps, dispatchActionToProps, ...)(App) 连接组件和Model
  11. // 如果dispatchActionToProps不传值时,connect Model 的组件通过 props 可以访问到 dispatch,
  12. // 可以调用 Model 中的 Reducer 或者 Effects

dispatch 函数

type dispatch = (a: Action) => Action
它是一个用于触发 action 的函数,action是改变 Satate 的唯一途径,但是它只描述一个行为,而 dispatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。

Reducer

type Reducer<S, A> = (state: S, action: A) => S
Reducer 函数接受2个参数:旧state对象、派发的action,返回新的state对象。
Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且每一次的计算都应该使用 immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。

Effect

Effect 被称为副作用,常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变的不纯,同样的输入不一定获得同样的输出。

dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于 纯函数,如果你想了解更多可以阅读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南

Subscription

Subscription 是一种从 获取数据的方法,它来自于 elm。
Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

  1. import key from 'keymaster';
  2. ...
  3. app.model({
  4. namespace: 'count',
  5. subscriptions: {
  6. // 键盘事件监听
  7. keyEvent({dispatch}) {
  8. key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
  9. },
  10. // 路由变化监听
  11. setup({ dispatch, history }){
  12. history.listen(location => {
  13. });
  14. }
  15. }
  16. });

Router

这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。
dva 实例提供了 router 方法来控制路由,使用的是react-router

  1. import { Router, Route } from 'dva/router';
  2. app.router(({history}) =>
  3. <Router history={history}>
  4. <Route path="/" component={HomePage} />
  5. </Router>
  6. );

Route Components

组件设计方法中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。
所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件(Presentational Components)。

dva-cli(dva脚手架)

https://github.com/dvajs/dva-cli

  1. npm install -g dva-cli # 全局安装dva-cli
  2. dva -v # 查看dva版本号
  3. # 创建名字为demo的项目,包含项目初始化目录和文件,
  4. # 并提供开发服务器、构建脚本、数据 mock 服务、代理服务器等功能。
  5. # 创建好后,默认打开 http://localhost:8000
  6. dva new demo

初始化项目(以“基本计数器”为例介绍dva)

  • history的最新版为5.0,而connect-react-router使用的history版本为4.7,不兼容
  • 不显式安装history或者指定老版本history进行安装。否则自己安装的history是使用history5
    1. npx create-react-app 8.dva
    2. cd 8.dva
    3. cnpm i -S dva redux react-redux redux-saga react-router-dom connected-react-router

reducers

rwe.gif

src/index.js

  1. import React from 'react';
  2. import dva from 'dva';
  3. import Counter1 from './components/Counter1.js';
  4. import Counter2 from './components/Counter2.js';
  5. // 创建dva实例
  6. const app = dva();
  7. // 定义全局model(可定义多个)
  8. app.model(require('./models/counter1').default);
  9. app.model(require('./models/counter2').default);
  10. // 定义路由规则
  11. // app.router 接收一个函数组件,回传参数是 {app, history}
  12. app.router(({app, history}) => {
  13. console.log(app, history);
  14. return (
  15. <div>
  16. <Counter1 />
  17. <Counter2 />
  18. </div>
  19. )
  20. });
  21. // 启动
  22. app.start('#root');

src/models

src/models/counter1.js
  1. // dva 通过 model 的概念把一个领域的模型管理起来,类似于redux的仓库。
  2. // 包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。
  3. export default {
  4. namespace: 'counter1', //命名空间,表示在全局 state 上的 key
  5. state: { //初始值
  6. number: 5,
  7. },
  8. // reducers 等同于 redux 里的 reducer,接收 action,同步更新 state
  9. reducers: {
  10. add(state, action){
  11. return {...state, number: state.number+1};
  12. }
  13. }
  14. }

src/models/counter2.js
  1. export default {
  2. namespace: 'counter2',
  3. state: {
  4. number: 10,
  5. },
  6. reducers: {
  7. add: (state, action) => ({...state, number: state.number+1}),
  8. }
  9. }

src/components

src/components/Counter1.js
  1. // connect就是react-redux的connect,把model和component串联起来。
  2. import {connect} from 'dva';
  3. function Counter1(ps){
  4. return (
  5. <div>
  6. <div>{ps.number}</div>
  7. <div>
  8. <button onClick={() => ps.dispatch({type: 'counter1/add'})}>同步+</button>
  9. </div>
  10. </div>
  11. )
  12. }
  13. let mapStateToProps = state => state.counter1;
  14. export default connect(mapStateToProps)(Counter1);

src/components/Counter1.js
  1. import {connect} from 'dva';
  2. function Counter2(ps){
  3. return (
  4. <div>
  5. <div>{ps.number}</div>
  6. <div>
  7. <button onClick={() => ps.dispatch({type: 'counter2/add'})}>同步+</button>
  8. </div>
  9. </div>
  10. )
  11. }
  12. export default connect(({counter2}) => counter2)(Counter2);

effects

rwe.gif

src/models

src/models/delay.js
  1. // 模拟延迟
  2. export const delay = (ms=1000, val=true) => new Promise((resolve, reject) => {
  3. setTimeout(resolve, ms, val);
  4. });
  5. // 模拟异步返回数据
  6. export const delayMock = (data, ms) => delay(ms, {
  7. result: 1,
  8. data,
  9. })

src/models/counter1.js
  1. import {delayMock} from './delay';
  2. // dva 通过 model 的概念把一个领域的模型管理起来,类似于redux的仓库。
  3. // 包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。
  4. const counter1 = {
  5. namespace: 'counter1', //命名空间,表示在全局 state 上的 key
  6. state: { //初始值
  7. number: 5,
  8. },
  9. // 异步(副作用)
  10. effects: {
  11. *asyncAdd(action, {call, put}){
  12. const data = yield call(delayMock, {id: 1, name: 'jack'});
  13. console.log('data=>', data);
  14. // 如果在effects里派发动作,如果是派发给自己的model的话,不需要加namespace前缀
  15. yield put({type: 'add'});
  16. // 如果派发给其它model,需要加前缀
  17. yield put({type: 'counter2/add'});
  18. }
  19. },
  20. // reducers 等同于 redux 里的 reducer,接收 action,同步更新 state
  21. reducers: {
  22. add(state, action){
  23. return {...state, number: state.number+1};
  24. }
  25. }
  26. }
  27. export default counter1;

src/models/counter2.js
  1. import {delayMock} from './delay';
  2. const counter2 = {
  3. namespace: 'counter2',
  4. state: {
  5. number: 10,
  6. },
  7. effects: {
  8. *asyncAdd(action, {call, put}){
  9. yield call(delayMock);
  10. yield put({type: 'add'});
  11. }
  12. },
  13. reducers: {
  14. add: (state, action) => ({...state, number: state.number+1}),
  15. }
  16. }
  17. export default counter2;

src/components

src/components/Counter1.js
  1. import {connect} from 'dva';
  2. function Counter1({dispatch, ...ps}){
  3. return (
  4. <div>
  5. <div>{ps.number}</div>
  6. <div>
  7. <button onClick={() => dispatch({type: 'counter1/add'})}>同步+</button>
  8. + <button onClick={() => dispatch({type: 'counter1/asyncAdd'})}>异步+</button>
  9. </div>
  10. </div>
  11. )
  12. }
  13. let mapStateToProps = state => state.counter1;
  14. export default connect(mapStateToProps)(Counter1);

src/components/Counter1.js
  1. import {connect} from 'dva';
  2. function Counter2({dispatch, ...ps}){
  3. return (
  4. <div>
  5. <div>{ps.number}</div>
  6. <div>
  7. <button onClick={() => dispatch({type: 'counter2/add'})}>同步+</button>
  8. + <button onClick={() => dispatch({type: 'counter2/asyncAdd'})}>异步+</button>
  9. </div>
  10. </div>
  11. )
  12. }
  13. export default connect(({counter2}) => counter2)(Counter2);

路由使用 dva/route

默认输出 react-router 接口, react-router-redux 的接口通过属性 routerRedux 输出。

src/index.js

  1. import React from 'react';
  2. import dva from 'dva';
  3. import history from './routes/history';
  4. import RouterConfig from './routes';
  5. // 创建dva实例
  6. const app = dva({
  7. history,
  8. });
  9. // 定义全局model(可定义多个)
  10. app.model(require('./models/counter1').default);
  11. app.model(require('./models/counter2').default);
  12. // 定义路由规则
  13. // app.router 接收一个函数组件,回传参数是 {app, history}
  14. // app.router(require('./routes').default);
  15. app.router(RouterConfig);
  16. // 启动
  17. app.start('#root');

src/routes/history.js

  1. // import { createHashHistory } from 'history';
  2. import { createBrowserHistory } from 'history';
  3. const history = createBrowserHistory({
  4. basename: '/web', // 基链接
  5. forceRefresh: false, //是否强制刷新整个页面
  6. // keyLength: 6, //location.key的长度
  7. // getUserConfirmation: (message,callback) => callback(window.confirm(message)) // 跳转拦截函数
  8. });
  9. export default history;

src/routes/index.js

  1. import React from "react";
  2. import {Router, Route, Switch, Link} from "dva/router";
  3. import Counter1 from '../components/Counter1.js';
  4. import Counter2 from '../components/Counter2.js';
  5. function RouterConfig({history, app}){
  6. console.log(history, app);
  7. return (
  8. <div>
  9. <Router history={history}>
  10. <>
  11. <ul>
  12. <li><Link to="counter1">page counter1</Link></li>
  13. <li><Link to="counter2">page counter2</Link></li>
  14. </ul>
  15. <Switch>
  16. <Route path="/counter1" component={props => <Counter1 {...props} />} />
  17. <Route path="/counter2" component={props => <Counter2 {...props} />} />
  18. </Switch>
  19. </>
  20. </Router>
  21. </div>
  22. )
  23. }
  24. export default RouterConfig;

路径跳转

src/routes/index.js

  1. import React from "react";
  2. + import {Router, Route, Switch, Link, routerRedux} from "dva/router";
  3. import Counter1 from '../components/Counter1';
  4. import Counter2 from '../components/Counter2';
  5. + const {ConnectedRouter} = routerRedux;
  6. function RouterConfig({history, app}){
  7. console.log(history, app);
  8. return (
  9. <div>
  10. + <ConnectedRouter history={history}>
  11. <>
  12. <ul>
  13. <li><Link to="counter1">page counter1</Link></li>
  14. <li><Link to="counter2">page counter2</Link></li>
  15. </ul>
  16. <Switch>
  17. <Route path="/counter1" component={props => <Counter1 {...props} />} />
  18. <Route path="/counter2" component={props => <Counter2 {...props} />} />
  19. </Switch>
  20. </>
  21. + </ConnectedRouter>
  22. </div>
  23. )
  24. }
  25. export default RouterConfig;

src/models/counter1.js

  1. import {delayMock} from './delay';
  2. + import {routerRedux} from 'dva/router';
  3. const counter1 = {
  4. namespace: 'counter1',
  5. state: {
  6. number: 5,
  7. },
  8. effects: {
  9. *asyncAdd(action, {call, put}){
  10. yield call(delayMock);
  11. yield put({type: 'add'});
  12. },
  13. + *goto({payload}, {put}){
  14. + yield put(routerRedux.push(payload));
  15. + }
  16. },
  17. reducers: {
  18. add(state, action){
  19. return {...state, number: state.number+1};
  20. }
  21. }
  22. }
  23. export default counter1;

src/components/Counter1.js

  1. import {connect} from 'dva';
  2. import { routerRedux } from 'dva/router';
  3. function Counter1({dispatch, ...ps}){
  4. console.log('Counter1', ps);
  5. return (
  6. <div>
  7. <div>{ps.number}</div>
  8. <div>
  9. <button onClick={() => dispatch({type: 'counter1/add'})}>同步+</button>
  10. <button onClick={() => dispatch({type: 'counter1/asyncAdd'})}>异步+</button>
  11. <button onClick={() => {
  12. dispatch({type: 'counter1/goto', payload: '/counter2'})
  13. }}>跳到/counter2</button>
  14. <button onClick={() => {
  15. dispatch(routerRedux.push('/counter2'))
  16. }}>跳到/counter2</button>
  17. </div>
  18. </div>
  19. )
  20. }
  21. let mapStateToProps = ({routing, counter1}) => ({
  22. number: counter1.number,
  23. })
  24. export default connect(mapStateToProps)(Counter1);

dva/dynamic 按需加载model

  1. import dynamic from 'dva/dynamic';
  2. const UserPageComponent = dynamic({
  3. app, //dva 实例,加载 models 时需要
  4. //返回 Promise 数组的函数,Promise 返回 dva model
  5. models: () => [
  6. import('./models/users'),
  7. ],
  8. //返回 Promise 的函数,Promise 返回 React Component
  9. component: () => import('./routes/UserPage'),
  10. });

基础使用

src/index.js

  1. import dva from 'dva';
  2. import history from './routes/history';
  3. import RouterConfig from './routes';
  4. const app = dva({
  5. history,
  6. });
  7. app.router(RouterConfig);
  8. app.start('#root');

src/routes/core.js

  1. import dynamic from 'dva/dynamic';
  2. /** 创建动态组件
  3. *
  4. * @param {*} app dva 实例,加载 models 时需要
  5. * @param {*} models 该路由组件加载的model数组
  6. * @param {*} component 动态路由组件 () => import('@/pages/Home')
  7. * @returns
  8. */
  9. export const dynamicWrapper = (app, models, component) => dynamic({
  10. app,
  11. models: () => models,
  12. component,
  13. });

src/routes/index.js

  1. import React from "react";
  2. import {Route, Switch, Link, routerRedux} from "dva/router";
  3. import {dynamicWrapper} from './core';
  4. const {ConnectedRouter} = routerRedux;
  5. // 创建一级路由
  6. const routesData = (app) => {
  7. const Counter1 = dynamicWrapper(
  8. app,
  9. [import('../models/counter1'), import('../models/counter2')],
  10. () => import('../components/Counter1'),
  11. );
  12. const Counter2 = dynamicWrapper(
  13. app,
  14. [import('../models/counter2')],
  15. () => import('../components/Counter2')
  16. );
  17. return (
  18. <Switch>
  19. <Route path="/counter1" component={Counter1} />
  20. <Route path="/counter2" render={props => (
  21. <Counter2 {...props} app={app} /> {/*传递app过去,用于创建二级路由时使用*/}
  22. )}/>
  23. </Switch>
  24. )
  25. }
  26. function RouterConfig({history, app}){
  27. return (
  28. <div>
  29. <ConnectedRouter history={history}>
  30. <>
  31. <ul>
  32. <li><Link to="/counter1">counter1</Link></li>
  33. <li><Link to="/counter2">counter2</Link></li>
  34. <li><Link to="/counter2/movie">counter2/movie</Link></li>
  35. <li><Link to="/counter2/music">counter2/music</Link></li>
  36. </ul>
  37. {routesData(app)}
  38. </>
  39. </ConnectedRouter>
  40. </div>
  41. )
  42. }
  43. export default RouterConfig;

src/models/movie.js

  1. const movie = {
  2. namespace: 'movie',
  3. state: {
  4. movieName: '海上钢琴师',
  5. },
  6. }
  7. export default movie;

src/components/Counter2/index.js

  1. import {connect} from 'dva';
  2. import {Route, Switch} from "dva/router";
  3. import {dynamicWrapper} from '../routes/core';
  4. // 创建二级路由
  5. const routesData = (app) => {
  6. const Movie = dynamicWrapper(
  7. app,
  8. [import('../models/movie')], //路由/counter2/movie 按需加载了 counter2 movie 2个model
  9. () => import('./Movie'),
  10. );
  11. const Music = dynamicWrapper(
  12. app,
  13. [], //路由/counter2/music 按需加载了 counter2 1个model
  14. () => import('./Music')
  15. );
  16. return (
  17. <Switch>
  18. <Route path="/counter2/movie" component={Movie} />
  19. <Route path="/counter2/music" component={Music}/>
  20. </Switch>
  21. )
  22. }
  23. function Counter2(ps){
  24. return (
  25. <div>
  26. <div>{ps.number}</div>
  27. <div>
  28. <button onClick={() => ps.dispatch({type: 'counter2/add'})}>同步+</button>
  29. <button onClick={() => ps.dispatch({type: 'counter2/asyncAdd'})}>异步+</button>
  30. </div>
  31. <div>
  32. <h2>二级路由</h2>
  33. <div>{routesData(ps.app)}</div>
  34. </div>
  35. </div>
  36. )
  37. }
  38. export default connect(({counter2}) => counter2)(Counter2);

src/components/Counter2/Movie.js

  1. import {connect} from "dva";
  2. function App(ps){
  3. console.log('movie=>', ps);
  4. return <div>counter2/movie</div>
  5. }
  6. export default connect(({counter2, movie}) => ({counter2, movie}))(App);

src/componnets/Counter2/Music.js

  1. function Music(){
  2. return <div>counter2/music</div>
  3. }
  4. export default Music;

路由配置简单封装

本例是不向下面组件传递 app,初始化的时候是一次性把所有路由创建好。
当然,你也可以把 app 向下传,在用到子路由的地方再创建子路由组。

src/routes/core.js

  1. import {Route, Switch, Redirect} from "dva/router";
  2. import dynamic from 'dva/dynamic';
  3. // 设置页面标题
  4. export function PageTitle({title, children}){
  5. document.title = title;
  6. return children;
  7. }
  8. /** 创建动态组件
  9. *
  10. * @param {*} app dva 实例,加载 models 时需要
  11. * @param {*} models 该路由组件加载的model数组
  12. * @param {*} component 动态路由组件 () => import('@/pages/Home')
  13. * @returns
  14. */
  15. export const dynamicWrapper = (app, models, component) => dynamic({
  16. app,
  17. models: () => models,
  18. component
  19. });
  20. // 路由映射表
  21. window.dva_router_pathMap = {};
  22. /** 创建 组路由
  23. *
  24. * @param {*} app
  25. * @param {*} routesFn 返回一组路由配置的函数
  26. * @param {*} isAddSwitch 是否添加 Switch 包裹
  27. * @param {*} opt.disabledNotFound 是否禁止添加 404 重定向
  28. */
  29. export const createRoutes = (app, routesFn, isAddSwitch, opt={}) => {
  30. const RoutesComArr = routesFn(app).map(routeConfig => createRoute(app, (app) => routeConfig));
  31. // 是否禁止添加 404 重定向
  32. if (!opt.disabledNotFound){
  33. RoutesComArr.push(<Redirect key={`/not-found_redirect`} to="/not-found" />);
  34. }
  35. if (!isAddSwitch) return <Switch>{RoutesComArr}</Switch>;
  36. return RoutesComArr;
  37. };
  38. /** 创建单个路由
  39. *
  40. * @param {*} app
  41. * @param {*} routeFn 返回一个路由配置的函数
  42. */
  43. export const createRoute = (app, routeFn) => {
  44. const routeConfig = routeFn(app);
  45. const {component: Com, path, exact, indexRoute, title, ...extraProps} = routeConfig;
  46. if (path && path !== '/'){
  47. window.dva_router_pathMap[path] = { path, title, ...extraProps };
  48. }
  49. let baseTitle = '标题 - {title}';
  50. let outputTitle = title ? baseTitle.replace(/{.*}/gi, title) : baseTitle.slice(0, -10);
  51. let routeProps = Object.assign(
  52. {
  53. key: path || Math.random(4),
  54. render: props => (
  55. <PageTitle title={outputTitle}>
  56. <Com extraProps={extraProps} {...props} />
  57. </PageTitle>
  58. ),
  59. },
  60. path && {path},
  61. exact && {exact},
  62. )
  63. if (indexRoute) {
  64. return [
  65. <Redirect key={path + "_redirect"} exact from={path} to={indexRoute} />,
  66. <Route {...routeProps} />
  67. ];
  68. }
  69. return <Route {...routeProps} />;
  70. }

src/routes/index.js

  1. import React from "react";
  2. import {Link, Switch, routerRedux} from "dva/router";
  3. import {createRoutes} from './core';
  4. import * as routers from './export';
  5. // 创建函数-返回路由配置数组
  6. const routesFn = (app) => {
  7. return Object.keys(routers).reduce((arr, item) => arr.concat(routers[item](app)), []);
  8. }
  9. const {ConnectedRouter} = routerRedux;
  10. function RouterConfig({history, app}){
  11. return (
  12. <div>
  13. <ConnectedRouter history={history}>
  14. <>
  15. <ul>
  16. <li><Link to="/counter1">counter1</Link></li>
  17. <li><Link to="/counter2">counter2</Link></li>
  18. <li><Link to="/counter2/movie">counter2/movie</Link></li>
  19. <li><Link to="/counter2/music">counter2/music</Link></li>
  20. </ul>
  21. {createRoutes(app, routesFn, true)}
  22. </>
  23. </ConnectedRouter>
  24. </div>
  25. )
  26. }
  27. export default RouterConfig;

src/routes/export.js

  1. export {default as counter} from './counter';

src/routes/counter.js(一级路由)

  1. import {dynamicWrapper, createRoutes} from './core';
  2. import counter2Routes from '../components/Counter2/routes';
  3. const routesCounter = app => {
  4. let result = [
  5. // 页面:/counter1
  6. {
  7. path: '/counter1',
  8. title: 'page counter1',
  9. //如果项目里全都是懒加载路由,可以只传component、model, 生成动态组件可以在createRoute里做。
  10. component: dynamicWrapper(
  11. app,
  12. [import('../models/counter1'), import('../models/counter2')],
  13. () => import('../components/Counter1'),
  14. ),
  15. },
  16. // 页面:/counter2
  17. {
  18. path: '/counter2',
  19. indexRoute: '/counter2/movie',
  20. title: 'page counter2',
  21. component: dynamicWrapper(
  22. app,
  23. [import('../models/counter2')],
  24. () => import('../components/Counter2'),
  25. ),
  26. // 二级路由引用:方式1(单独一条一条生成路由)
  27. // childRoutes: [
  28. // counter2Routes.movie(app),
  29. // counter2Routes.music(app),
  30. // ],
  31. // 二级路由引用:方式2(直接生成路由组)
  32. childRoutes: createRoutes(app, counter2Routes, true),
  33. },
  34. ]
  35. return result;
  36. }
  37. export default routesCounter;

src/components/Counter2/routes.js (二级路由)

  1. import { dynamicWrapper, createRoute } from "../../routes/core";
  2. // /counter2/movie
  3. const movie = (app) => ({
  4. path: '/counter2/movie',
  5. title: "page counter2-movie",
  6. component: dynamicWrapper(
  7. app,
  8. [import("../../models/movie")],
  9. () => import("./Movie")
  10. ),
  11. });
  12. // /counter2/music
  13. const music = (app) => ({
  14. path: '/counter2/music',
  15. title: "page counter2-music",
  16. component: dynamicWrapper(
  17. app,
  18. [],
  19. () => import("./Music")
  20. ),
  21. });
  22. // 二级路由引用:方式1(单独一条一条生成路由)
  23. // const routesCounter2 = {
  24. // movie: (app) => createRoute(app, movie),
  25. // music: (app) => createRoute(app, music),
  26. // }
  27. // 二级路由引用:方式2(直接生成路由组)
  28. const routesCounter2 = app => ([
  29. movie(app),
  30. music(app),
  31. ])
  32. export default routesCounter2;

src/components/Counter2/index.js

  1. import {connect} from 'dva';
  2. import {Switch} from "dva/router";
  3. function Counter2(ps){
  4. console.log('Counter2=>', ps);
  5. return (
  6. <div>
  7. <div>{ps.number}</div>
  8. <div>
  9. <button onClick={() => ps.dispatch({type: 'counter2/add'})}>同步+</button>
  10. <button onClick={() => ps.dispatch({type: 'counter2/asyncAdd'})}>异步+</button>
  11. </div>
  12. <div>
  13. <h2>二级路由</h2>
  14. <div>
  15. {/* 二级路由引用:方式1(单独一条一条生成路由) */}
  16. {/* <Switch>{ps.extraProps.childRoutes}</Switch> */}
  17. {/* 二级路由引用:方式2(直接生成路由组) */}
  18. {ps.extraProps.childRoutes}
  19. </div>
  20. </div>
  21. </div>
  22. )
  23. }
  24. export default connect(({counter2}) => counter2)(Counter2);

dva-loading

路由未加载完成时,显示Loading组件

src/index.js

  1. import dva from 'dva';
  2. import history from './routes/history';
  3. import dynamic from "dva/dynamic";
  4. import createLoading from "dva-loading";
  5. import RouterConfig from './routes';
  6. // 创建dva实例
  7. const app = dva({
  8. history,
  9. // onError: error => message.error(error.message),
  10. });
  11. // 引用插件
  12. app.use(createLoading());
  13. function Loading(){
  14. return <div style={{position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1000}}></div>
  15. }
  16. // -> loading 使用自定义loading组件
  17. dynamic.setDefaultLoadingComponent(() => <Loading />);
  18. // 定义根model(根model可应用于所有路由)(model可定义多个)
  19. // app.model(require('./models/counter1').default);
  20. // app.model(require('./models/counter2').default);
  21. // 定义路由规则
  22. // app.router 接收一个函数组件,回传参数是 {app, history}
  23. // app.router(require('./routes').default);
  24. app.router(RouterConfig);
  25. // 启动
  26. app.start('#root');

dva/fetch

异步请求库,输出 isomorphic-fetch 的接口。不和 dva 强绑定,可以选择任意的请求库。

  1. import fetch from 'dva/fetch';
  2. function checkStatus(response) {
  3. if (response.status >= 200 && response.status < 300) {
  4. return response;
  5. }
  6. const error = new Error(response.statusText);
  7. error.response = response;
  8. throw error;
  9. }
  10. // 格式化数据
  11. const parseJSON = response => response.json();
  12. export async function get(url, params={}, onSuc, onErr, config={}) {
  13. params.platform = 'pc';
  14. let str = qsStringify(params, {prefix: '?'});
  15. let allUrl = url + str;
  16. return await fetch(allUrl, {
  17. credentials: 'include', //fetch 默认不带 cookie 如果你想在fetch请求里附带cookies之类的凭证信息,可以将 credentials参数设置成 “include” 值。
  18. headers: getHeaders(config),
  19. })
  20. .then(checkStatus)
  21. .then(parseJSON)
  22. .then((res) => successNetwork(allUrl, res, onSuc, onErr, config))
  23. .catch((res) => errorNetwork(allUrl, res, onErr));
  24. }
  25. export async function post(url, params={}, onSuc, onErr, config={}) {
  26. params.platform = 'pc';
  27. let formData = new FormData();
  28. if (params){
  29. for(let k in params){
  30. formData.append(k, params[k]);
  31. }
  32. }
  33. return await fetch(url, {
  34. method : 'POST',
  35. credentials: 'include',
  36. headers: getHeaders(config),
  37. body: config.isBodyJSONStringify ? JSON.stringify(params) : formData,
  38. })
  39. .then(checkStatus)
  40. .then(parseJSON)
  41. .then((res) => successNetwork(url, res, onSuc, onErr, config))
  42. .catch((res) => errorNetwork(url, res, onErr));
  43. }

dva/saga

输出 redux-saga 的接口,主要用于用例的编写。(用例中需要用到 effects)