dva框架使用详解及Demo教程

dva框架的使用详解及Demo教程

在前段时间,我们也学习讲解过Redux框架的基本使用,但是有很多同学在交流群里给我的反馈信息说,redux框架理解上有难度,看了之后还是一脸懵逼不知道如何下手,很多同学就转向选择使用dva框架。其实dva框架就是一个redux框架与redux-saga等框架的一个集大成者,把几个常用的数据处理框架进行了再次封装,在使用方式上给使用者带来了便利,下面我们就来简单的介绍下dva框架的基本API和基本使用

Demo运行效果图

这里和讲解Redux框架一样,作者任然是提供了两个经典的Demo示例,CounterApp 和 TodoList 来帮助初学者更好的理解和使用

http://ovyjkveav.bkt.clouddn.com/17-12-22/25369015.jpg
http://ovyjkveav.bkt.clouddn.com/17-12-22/35513550.jpg

Demo地址

  • CounterApp

https://github.com/guangqiang-liu/react-dva-counter

  • TodoList

https://github.com/guangqiang-liu/react-dva-todoList

dva的由来

D.Va拥有一部强大的机甲,它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。—— 来自 守望先锋 。

dva 官方地址

https://github.com/dvajs/dva/blob/master/README_zh-CN.md

dva核心API

  • app = dva(opts)

创建应用,返回 dva 实例(注:dva 支持多实例)

opts 包含如下配置:

  1. history:指定给路由用的 history,默认是 hashHistory
  2. initialState:指定初始数据,优先级高于 model 中的 state,默认是 {}

如果配置history 为 browserHistory,则创建dva对象可以写成如下写法

import createHistory from 'history/createBrowserHistory';
const app = dva({
  history: createHistory(),
})

另外,出于易用性的考虑,opts 里也可以配所有的 hooks ,下面包含全部的可配属性:

const app = dva({
  history,
  initialState,
  onError,
  onAction,
  onStateChange,
  onReducer,
  onEffect,
  onHmr,
  extraReducers,
  extraEnhancers,
})
  • app.use(hooks)

配置 hooks 或者注册插件。(插件最终返回的是 hooks )

比如注册 dva-loading 插件的例子:

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

hooks包含如下配置项:

1、 onError((err, dispatch) => {})

effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态

注意:subscription 并没有加 try...catch,所以有错误时需通过第二个参数 done 主动抛错

例子:

app.model({
  subscriptions: {
    setup({ dispatch }, done) {
      done(e)
    },
  },
})

如果我们使用antd组件,那么最简单的全局错误处理通常会这么做:

import { message } from 'antd'
const app = dva({
  onError(e) {
    message.error(e.message, 3)
  },
})

2、 onAction(fn | fn[])

在action被dispatch时触发,用于注册 redux 中间件。支持函数或函数数组格式

例如我们要通过 redux-logger 打印日志:

import createLogger from 'redux-logger';
const app = dva({
  onAction: createLogger(opts),
})

3、 onStateChange(fn)

state 改变时触发,可用于同步 state 到 localStorage,服务器端等

4、 onReducer(fn)

封装 reducer 执行,比如借助 redux-undo 实现 redo/undo :

import undoable from 'redux-undo';
const app = dva({
  onReducer: reducer => {
    return (state, action) => {
      const undoOpts = {};
      const newState = undoable(reducer, undoOpts)(state, action);
      // 由于 dva 同步了 routing 数据,所以需要把这部分还原
      return { ...newState, routing: newState.present.routing };
    },
  },
})

5、 onEffect(fn)

封装 effect 执行。比如 dva-loading 基于此实现了自动处理 loading 状态

6、 onHmr(fn)

热替换相关,目前用于 babel-plugin-dva-hmr

7、 extraReducers

指定额外的 reducer,比如 redux-form 需要指定额外的 form reducer:

import { reducer as formReducer } from 'redux-form'
const app = dva({
  extraReducers: {
    form: formReducer,
  },
})
  • app.model(model)

注册model,这个操作时dva中核心操作,下面单独做详解

  • app.unmodel(namespace)

取消 model 注册,清理 reducers, effects 和 subscriptions。subscription 如果没有返回 unlisten 函数,使用 app.unmodel 会给予警告⚠️

  • app.router(({ history, app }) => RouterConfig)

注册路由表,这一操作步骤在dva中也很重要

// 注册路由
app.router(require('./router'))
// 路由文件
import { Router, Route } from 'dva/router';
import IndexPage from './routes/IndexPage'
import TodoList from './routes/TodoList'

function RouterConfig({ history }) {
  return (
    <Router history={history}>
        <Route path="/" component={IndexPage} />
        <Route path='/todoList' components={TodoList}/>
    </Router>
  )
}
export default RouterConfig

当然,如果我们想解决组件动态加载问题,我们的路由文件也可以按照下面的写法来写

import { Router, Switch, Route } from 'dva/router'
import dynamic from 'dva/dynamic'

function RouterConfig({ history, app }) {
  const IndexPage = dynamic({
    app,
    component: () => import('./routes/IndexPage'),
  })

  const Users = dynamic({
    app,
    models: () => [import('./models/users')],
    component: () => import('./routes/Users'),
  })

  return (
    <Router history={history}>
      <Switch>
        <Route exact path="/" component={IndexPage} />
        <Route exact path="/users" component={Users} />
      </Switch>
    </Router>
  )
}

export default RouterConfig

其中dynamic(opts) 中opt包含三个配置项:

  • opts

    • app: dva 实例,加载 models 时需要
    • models: 返回 Promise 数组的函数,Promise 返回 dva model
    • component:返回 Promise 的函数,Promise 返回 React Component
  • app.start(selector?)

启动应用,selector 可选,如果没有 selector 参数,会返回一个返回 JSX 元素的函数

app.start('#root')

那么什么时候不加 selector?常见场景有测试、node端、react-native 和 i18n 国际化支持

比如通过 react-intl 支持国际化的例子:

import { IntlProvider } from 'react-intl'
...
const App = app.start()
ReactDOM.render(<IntlProvider><App /></IntlProvider>, htmlElement)

dva框架中的核心层:Model

下面是简单常规的 model 文件的写法

/** Created by guangqiang on 2017/12/17. */

import queryString from 'query-string'
import * as todoService from '../services/todo'

export default {
  namespace: 'todo',
  state: {
    list: []
  },
  reducers: {
    save(state, { payload: { list } }) {
      return { ...state, list }
    }
  },
  effects: {
    *addTodo({ payload: value }, { call, put, select }) {
      // 模拟网络请求
      const data = yield call(todoService.query, value)
      console.log(data)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      const tempObj = {}
      tempObj.title = value
      tempObj.id = list.length
      tempObj.finished = false
      list.push(tempObj)
      yield put({ type: 'save', payload: { list }})
    },
    *toggle({ payload: index }, { call, put, select }) {
      // 模拟网络请求
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.finished = !obj.finished
      yield put({ type: 'save', payload: { list } })
    },
    *delete({ payload: index }, { call, put, select }) {
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      list.splice(index, 1)
      yield put({ type: 'save', payload: { list } })
    },
    *modify({ payload: { value, index } }, { call, put, select }) {
      const data = yield call(todoService.query, value)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.title = value
      yield put({ type: 'save', payload: { list } })
    }
  },
  subscriptions: {
    setup({ dispatch, history }) {
      // 监听路由的变化,请求页面数据
      return history.listen(({ pathname, search }) => {
        const query = queryString.parse(search)
        let list = []
        if (pathname === 'todoList') {
          dispatch({ type: 'save', payload: {list} })
        }
      })
    }
  }
}

model对象中包含5个重要的属性:

  • namespace

model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过.的方式创建多层命名空间

  • state

reducer的初始值,优先级低于传给dva()的 opts.initialState

例如:

const app = dva({
  initialState: { count: 1 },
});
app.model({
  namespace: 'count',
  state: 0,
})

此时,在 app.start() 后 state.count 为 1

  • reducers

以 key/value 格式定义reducer,用于处理同步操作,唯一可以修改 state 的地方,由 action 触发

格式为 (state, action) => newState[(state, action) => newState, enhancer]

namespace: 'todo',
  state: {
    list: []
  },
  // reducers 写法
  reducers: {
    save(state, { payload: { list } }) {
      return { ...state, list }
    }
  }
  • effects

以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state。由action 触发,可以触发action,可以和服务器交互,可以获取全局 state 的数据等等

注意: dva框架中的effects 模块的设计思想来源于 redux-saga 框架,如果同学们对 redux-saga 框架不熟悉,可以查看作者对 redux-saga的讲解:https://www.jianshu.com/p/7cac18e8d870

格式为 *(action, effects) => void[*(action, effects) => void, { type }]

type 类型有有如下四种:

1、takeEvery

2、takeLatest

3、throttle

4、watcher

// effects 写法
effects: {
    *addTodo({ payload: value }, { call, put, select }) {
      // 模拟网络请求
      const data = yield call(todoService.query, value)
      console.log(data)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      const tempObj = {}
      tempObj.title = value
      tempObj.id = list.length
      tempObj.finished = false
      list.push(tempObj)
      yield put({ type: 'save', payload: { list }})
    },
    *toggle({ payload: index }, { call, put, select }) {
      // 模拟网络请求
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.finished = !obj.finished
      yield put({ type: 'save', payload: { list } })
    },
    *delete({ payload: index }, { call, put, select }) {
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      list.splice(index, 1)
      yield put({ type: 'save', payload: { list } })
    },
    *modify({ payload: { value, index } }, { call, put, select }) {
      const data = yield call(todoService.query, value)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.title = value
      yield put({ type: 'save', payload: { list } })
    }
  }
  • subscriptions

以 key/value 格式定义 subscription,subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action

在 app.start() 时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等

格式为 ({ dispatch, history }, done) => unlistenFunction

注意:如果要使用 app.unmodel(),subscription 必须返回 unlisten 方法,用于取消数据订阅

// subscriptions 写法
subscriptions: {
    setup({ dispatch, history }) {
      // 监听路由的变化,请求页面数据
      return history.listen(({ pathname, search }) => {
        const query = queryString.parse(search)
        let list = []
        if (pathname === 'todoList') {
          dispatch({ type: 'save', payload: {list} })
        }
      })
    }
  }

使用dva框架和直接使用redux写法的区别

  • 使用 redux

actions.js 文件

export const REQUEST_TODO = 'REQUEST_TODO';
export const RESPONSE_TODO = 'RESPONSE_TODO';

const request = count => ({type: REQUEST_TODO, payload: {loading: true, count}});

const response = count => ({type: RESPONSE_TODO, payload: {loading: false, count}});

export const fetch = count => {
  return (dispatch) => {
    dispatch(request(count));

    return new Promise(resolve => {
      setTimeout(() => {
        resolve(count + 1);
      }, 1000)
    }).then(data => {
      dispatch(response(data))
    })
  }
}

reducer.js 文件

import { REQUEST_TODO, RESPONSE_TODO } from './actions';

export default (state = {
  loading: false,
  count: 0
}, action) => {
  switch (action.type) {
    case REQUEST_TODO:
      return {...state, ...action.payload};
    case RESPONSE_TODO:
      return {...state, ...action.payload};
    default:
      return state;
  }
}

app.js 文件

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import * as actions from './actions';

const App = ({fetch, count, loading}) => {
  return (
    <div>
      {loading ? <div>loading...</div> : <div>{count}</div>}
      <button onClick={() => fetch(count)}>add</button>
    </div>
  )
}

function mapStateToProps(state) {
  return state;
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(actions, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

index.js 文件

import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux'
import thunkMiddleware from 'redux-thunk';

import reducer from './app/reducer';
import App from './app/app';

const store = createStore(reducer, applyMiddleware(thunkMiddleware));

render(
  <Provider store={store}>
    <App/>
  </Provider>
  ,
  document.getElementById('app')
)
  • 使用dva

model.js 文件

export default {
  namespace: 'demo',
  state: {
    loading: false,
    count: 0
  },
  reducers: {
    request(state, payload) {
      return {...state, ...payload};
    },
    response(state, payload) {
      return {...state, ...payload};
    }
  },
  effects: {
    *'fetch'(action, {put, call}) {
      yield put({type: 'request', loading: true});

      let count = yield call((count) => {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(count + 1);
          }, 1000);
        });
      }, action.count);

      yield put({
        type: 'response',
        loading: false,
        count
      });
    }
  }
}

app.js 文件

import React from 'react'
import { connect } from 'dva';

const App = ({fetch, count, loading}) => {
  return (
    <div>
      {loading ? <div>loading...</div> : <div>{count}</div>}
      <button onClick={() => fetch(count)}>add</button>
    </div>
  )
}

function mapStateToProps(state) {
  return state.demo;
}

function mapDispatchToProps(dispatch) {
  return {
    fetch(count){
      dispatch({type: 'demo/fetch', count});
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

index.js 文件

import dva from 'dva';
import model from './model';
import App from './app';

const app = dva();

app.use({});

app.model(model);

app.router(() => <App />);

app.start();

我们通过上面两种不同方式来实现一个异步的计数器的代码结构发现:

  1. 使用 redux 需要拆分出action模块和reducer模块

  2. dva将actionreducer封装到model中,异步流程采用Generator处理

总结

本篇文章主要讲解了dva框架中开发常用API和一些使用技巧,如果想查看更多更全面的API,请参照dva官方文档:https://github.com/dvajs/dva

如果同学们看完教程还是不知道如何使用dva框架,建议运行作者提供的Demo示例结合学习

作者建议:同学们可以将作者之前讲解的redux框架和dva框架对比来学习理解,这样更清楚他们之间的区别和联系。

更多文章

  • 作者React Native开源项目OneM【500+ star】地址(按照企业开发标准搭建框架完成开发的):https://github.com/guangqiang-liu/OneM:欢迎小伙伴们 star
  • 作者简书主页:包含60多篇RN开发相关的技术文章http://www.jianshu.com/u/023338566ca5 欢迎小伙伴们:多多关注多多点赞
  • 作者React Native QQ技术交流群:620792950 欢迎小伙伴进群交流学习
  • 友情提示:在开发中有遇到RN相关的技术问题,欢迎小伙伴加入交流群(620792950),在群里提问、互相交流学习。交流群也定期更新最新的RN学习资料给大家,谢谢大家支持!

小伙伴们扫下方二维码加入RN技术交流QQ群

QQ群二维码,500+ RN工程师在等你加入哦
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容