dva的入门和使用

dva的入门和使用

创建项目

  1. 安装cli npm i dva-cli -g
  2. 创建新项目 dva new [project name]
  3. 启动项目 npm run start

目录介绍

    |- mock                     // 存放用于 mock 数据的文件
    |- node_modules
    |- package.json
    |- public                   // 一般用于存放静态文件,打包时会被直接复制到输出目录(./dist)
    |- src                      // 文件夹用于存放项目源代码
        |- asserts                  // 用于存放静态资源,打包时会经过 webpack 处理
        |- components               // 用于存放 React 组件,一般是该项目公用的无状态组件
        |- models                   // 用于存放模型文件
        |- routes                   // 用于存放需要 connect model 的路由组件
        |- services                 // 用于存放服务文件,一般是网络请求等
        |- utils                    // 工具类库
        |- router.js                // 路由文件
        |- index.js                 // 项目的入口文件
        |- index.css                // 一般是共用的样式
    |- .editorconfig            // 编辑器配置文件
    |- .eslintrc                // ESLint配置文件
    |- .gitignore               // Git忽略文件
    |- .roadhogrc.mock.js       // Mock配置文件
    |- .webpackrc               // 自定义的webpack配置文件,JSON格式

使用antd

  1. 安装 npm i antd babel-plugin-import -S
  2. 配置 .roadhogrc 文件 / 或者 .webpackrc 文件
        "extraBabelPlugins": [
              "transform-runtime",
              ["import", {"libraryName":"antd", "style":"css"}]
          ]
    

配置代理

    // 在 .roadhogrc 文件, 加上 “proxy” 配置 / 或者 `.webpackrc` 文件
    "proxy": {
        "/api": {
            "target": "http://localhost:9093/",
            "changeOrigin": true,
            "pathRewrite": {"^/api": ""}
        }
    }

HMR热更新替换

    // 在 .webpackrc 中添加如下配置

    "env": {
        "development": {
            "extraBabelPlugins": [
                "dva-hmr"
            ]
        }
    }
    // 如果无效,请尝试更新一下 babel-plugin-dva-hmr

开发简单的项目示例

  1. 创建项目, 完成上述配置

  2. 创建后台接口, 也可mock数据

    • 我自己是用nodejs创建的接口
        // 创建与src同级的文件夹 server
        // 在server下创建server.js  model.js   api.js
        
        // server.js ===============================================================
    
        const express = require('express')
        const bodyParser = require('body-parser')   // 解析传输的body
        const apiRoute = require('./api')    // 引入路由
    
        // 创建app
        const app = express()
    
        // 使用中间件
        app.use(bodyParser.json())
        app.use(bodyParser.urlencoded({ extended: false }))
    
        // 使用路由
        app.use('/api', apiRoute)
    
        // 开启服务
        app.listen(9093, () => {
            console.log('Node app start at port 9093')
        })
    
    
        // model.js ===============================================================
    
         // 引入mongoose库, 该库是nodejs来操作mongodb的
         const mongoose = require('mongoose')
    
         // 连接mongo, 并且使用dva-todo这个集合
         const DB_URL = 'mongodb://localhost:27017/dva-todo'
         mongoose.connect(DB_URL)
    
        // 定义表结构
        const models = {
            todo: {
                'text': {type: String, require: true},
                'checked': {type: Boolean, require: true},
            }
        }
    
        // 快速生成对应结构的表
        for (let m in models){
            mongoose.model(m, new mongoose.Schema(models[m]))
        }
    
        // 输出获取数据库的方法
        module.exports = {
            getModel(name) {
                return mongoose.model(name)
            }
        }
    
    
        // api.js ================================================================
        
        const express = require('express')  // 加载express框架
        const Router = express.Router()     // 使用express的路由
        const apiModel = require('./model')    // 引入数据库操作
        const todo = apiModel.getModel('todo') // 使用数据库的todo表
        const multipart = require('connect-multiparty') // 使得post请求能支持formdata
        const multipartMiddleware = multipart()
    
        // 查询数据
        Router.get('/get', (req, res) => {
            todo.find({}, (err, doc) => {
                if(err) return res.json({code: 1, msg: '获取数据失败!'})
                return res.json({code: 0, data: doc})
            })
        })
    
        // 添加数据
        Router.post('/add', multipartMiddleware, (req, res) => {
            const {text} = req.body
            const newTodo = new todo({text: text, checked: false})
            newTodo.save((err, doc) => {
                if(err) return res.json({code: 1, msg: '添加数据失败!'})
                return res.json({code: 0, data: doc})
            })
        })
    
        // 选中
        Router.post('/check', (req, res) => {
            const {id, checked} = req.body
            todo.findByIdAndUpdate(id, {checked: checked}, (err, doc) => {
                if(err) return res.json({code: 1, msg: '删除数据失败!'})
                return res.json({code: 0, data: doc})
            })
        })
    
        // 全选或反选
        Router.post('/checkAll', (req, res) => {
            const {checked} = req.body
            todo.update({}, {'$set': {checked: checked}}, { multi: true }, (err, doc) => {
                if(err) return res.json({code: 1, msg: '删除数据失败!'})
                return res.json({code: 0})
            })
        })
    
    
        // 删除
        Router.post('/delete', (req, res) => {
            const {id} = req.body
            todo.remove({_id:id}, (err, doc) => {
                if(err) return res.json({code: 1, msg: '删除数据失败!'})
                return res.json({code: 0})
            })
        })
    
        // 清空
        Router.get('/clear', (req, res) => {
            todo.remove({}, (err, doc) => {
                if(err) return res.json({code: 1, msg: '清空数据失败!'})
                return res.json({code: 0})
            })
        })
    
        // 输出路由
        module.exports = Router
    
  3. src/index.js => dva的入口

import dva from 'dva'
import './index.css'
import createLoading from 'dva-loading'  // 需要npm安装
import { createBrowserHistory } from 'history'

// 1. Initialize
const app = dva({
    history: createBrowserHistory()
})

// 2. Plugins
// app.use({});
app.use(createLoading())

// 3. Model
app.model(require('./models/main').default)

// 4. Router
app.router(require('./router').default)

// 5. Start
app.start('#root')
  1. src/index.css => 公共的css
html, body, :global(#root) {
  height: 100%;
}
  1. src/router.js => 路由配置
import React from 'react'
import { Router, Route, Switch } from 'dva/router'
import All from './routes/all/all'
import Active from './routes/active/active'
import Completed from './routes/completed/completed'

function RouterConfig ({ history }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/all" exact component={All} />
        <Route path="/active" exact component={Active} />
        <Route path="/completed" exact component={Completed} />
        <Route component={All} />
      </Switch>
    </Router>
  )
}

export default RouterConfig
  1. src/utils/request.js => fetch请求数据的配置
import fetch from 'dva/fetch'

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

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response
  }

  const error = new Error(response.statusText)
  error.response = response
  throw error
}

/**
 * Requests a URL, returning a promise.
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 * @return {object}           An object containing either "data" or "err"
 */
export default function request(url, options) {
  const defaultOptions = {
    credentials: 'include',
  }
  const newOptions = { ...defaultOptions, ...options }

  if (newOptions.method === 'POST' || newOptions.method === 'PUT' || newOptions.method === 'DELETE') {
    if (!(newOptions.body instanceof FormData)) {
      newOptions.headers = {
        Accept: 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        ...newOptions.headers,
      }
      newOptions.body = JSON.stringify(newOptions.body)
    } else {
      // newOptions.body is FormData
      newOptions.headers = {
        Accept: 'application/json',
        ...newOptions.headers,
      }
    }
  }


  return fetch(url, newOptions)
    .then(checkStatus)
    .then(parseJSON)
    .then(data => ({ data }))
    .catch(err => ({ err }))
}
  1. src/services/main.js => 配置各种接口
import request from '../utils/request'
import {stringify} from 'qs'

export function get() {
  return request('/api/get')
}

export function add(obj) {
  return request('/api/add', {method: 'POST', body: obj})
}

export function remove(obj) {
  return request('/api/delete', {method: 'POST', body: obj})
}

export function check(obj) {
  return request('/api/check', {method: 'POST', body: obj})
}

export function clear() {
  return request('/api/clear')
}

export function checkAll(obj) {
  return request('/api/checkAll', {method: 'POST', body: obj})
}
  1. src/routes => 路由页面
// src/routes/all/all.js ====================================================
import React from 'react'
import { connect } from 'dva'
import { Row, Col } from 'antd'
import Header from '../../components/header'
import Footer from '../../components/footer'
import MainList from '../../components/main'

class All extends React.Component{
    render (){
      return (
        <div style={{height: '100%'}}>
          <Row type="flex" justify="center" style={{height: '10%', alignItems: 'center', background: '#001529'}}>
            <Col>
              <Header></Header>
            </Col>
          </Row>

          <Row type="flex" justify="center" style={{height: '80%', background: '#f0f2f5'}}>
            <MainList dataType='all'/>
          </Row>

          <Row type="flex" justify="center" style={{height: '10%', alignItems: 'center', background: '#f0f2f5'}}>
            <Col>
              <Footer></Footer>
            </Col>
          </Row>
        </div>
      )
    }
}

All.propTypes = {}

export default connect()(All)



// src/routes/active/active.js========================================================================
import React from 'react'
import { connect } from 'dva'
import { Row, Col } from 'antd'
import Header from '../../components/header'
import Footer from '../../components/footer'
import MainList from '../../components/main'

class Active extends React.Component{
    render (){
      return (
        <div style={{height: '100%'}}>
          <Row type="flex" justify="center" style={{height: '10%', alignItems: 'center', background: '#001529'}}>
            <Col>
              <Header></Header>
            </Col>
          </Row>

          <Row type="flex" justify="center" style={{height: '80%', background: '#f0f2f5'}}>
            <MainList dataType='active'/>
          </Row>

          <Row type="flex" justify="center" style={{height: '10%', alignItems: 'center', background: '#f0f2f5'}}>
            <Col>
              <Footer></Footer>
            </Col>
          </Row>
        </div>
      )
    }
}

Active.propTypes = {}

export default connect()(Active)



// src/routes/completed/completed.js=====================================================================
import React from 'react'
import { connect } from 'dva'
import { Row, Col } from 'antd'
import Header from '../../components/header'
import Footer from '../../components/footer'
import MainList from '../../components/main'

class Completed extends React.Component{
    render (){
      return (
        <div style={{height: '100%'}}>
          <Row type="flex" justify="center" style={{height: '10%', alignItems: 'center', background: '#001529'}}>
            <Col>
              <Header></Header>
            </Col>
          </Row>

          <Row type="flex" justify="center" style={{height: '80%', background: '#f0f2f5'}}>
            <MainList dataType='completed'/>
          </Row>

          <Row type="flex" justify="center" style={{height: '10%', alignItems: 'center', background: '#f0f2f5'}}>
            <Col>
              <Footer></Footer>
            </Col>
          </Row>
        </div>
      )
    }
}

Completed.propTypes = {}

export default connect()(Completed)
  1. src/models/main.js => dva最重要的部分, 将redux全都集中在一个文件进行操作
import {add, get, remove, check, clear, checkAll} from '../services/main'

export default {

  // 命名空间, 以命名空间来区分不同的model
  namespace: 'todo',   

  // 初始state
  state: [],    

  // 监听任务 => 一般用来监听路由变化
  subscriptions: {    
    setup({ dispatch, history }) {  // eslint-disable-line
      history.listen(location => {
        // console.log(1, location)
      })
    },
  },

  // 进行各种异步操作, 组件内可以dispatch来触发, 但是此处不能修改state, 只有reducers可以
  effects: {
    *get({ payload }, { call, put }) {  // eslint-disable-line
      const response = yield call(get, payload)
      const data = response.data.code === 0 ? response.data.data : []
      yield put({ type: 'getData' , payload: data})
    },
    *clear({ payload }, { call, put }) {
      const response = yield call(clear, payload)
      if(response.data.code === 0){
        yield put({ type: 'clearData'})
      }else{
        console.error(response.data.msg)
      }
    },
    *add({ payload }, { call, put }) {
      const response = yield call(add, payload)
      if(response.data.code === 0){
        yield put({ type: 'addData', payload: response.data.data})
      }else{
        console.error(response.data.msg)
      }
    },
    *remove({ payload }, { call, put }) {
      const response = yield call(remove, payload)
      if(response.data.code === 0){
        yield put({ type: 'removeData', payload: payload})
      }else{
        console.error(response.data.msg)
      }
    },
    *check({ payload }, { call, put }) {
      const response = yield call(check, payload)
      if(response.data.code === 0){
        yield put({ type: 'checkData', payload: response.data.data})
      }else{
        console.error(response.data.msg)
      }
    },
    *checkAll({ payload }, { call, put }) {
      const response = yield call(checkAll, payload)
      if(response.data.code === 0){
        yield put({ type: 'checkAllData', payload: payload})
      }else{
        console.error(response.data.msg)
      }
    }
  },

  // 修改state的各种操作, 可以通过effects进行调用, 也可以通过组件dispatch进行调用
  reducers: {
    getData(state, action) {
      return [ ...state, ...action.payload ]
    },
    clearData(state) {
      state = []
      return state
    },
    addData(state, action) {
      return [...state, {_id: action.payload._id, text: action.payload.text, checked: false}]
    },
    removeData(state, action) {
      const id = action.payload.id
      return state.filter(v => v._id !== id)
    },
    checkData(state, action) {
      const {_id, checked} = action.payload
      state.forEach(v => {
        if(v._id === _id){
          v.checked = !checked
        }
      })
      return state
    },
    checkAllData(state, action) {
      state.forEach(v => {
        v.checked = action.payload.checked
      })
      return state
    }
  },
}
  1. src/components => 各个组件
 // src/components/header.js=============================================================
import React from 'react'

const Header = () => {
  return (
    <div>
      <h1 style={{color: '#fff'}}>todos</h1>
    </div>
  )
}

Header.propTypes = {
}

export default Header


// src/components/footer.js=============================================================
import React from 'react'

const Footer = () => {
  return (
    <div>
      <a style={{color: '#999'}}>todo list</a>
    </div>
  )
}

Footer.propTypes = {}

export default Footer


// src/components/main.js================================================================
import React from 'react'
import { connect } from 'dva'
import { Link } from 'dva/router'
import PropTypes from 'prop-types'
import { Row, Col, Form, Input, Checkbox, Button, List, Icon } from 'antd'

// 此处的connect是装饰器的写法, 需要配置装饰器
@connect(state => ({
  data: state
}))

export default class MainList extends React.Component{
  static propTypes = {
    dataType: PropTypes.string.isRequired
  }

  constructor(props){
    super(props)
    this.state = {
      showClose: true
    }
    this.handleAdd = this.handleAdd.bind(this)
    this.deleteItem = this.deleteItem.bind(this)
    this.clearData = this.clearData.bind(this)
    this.checkAll = this.checkAll.bind(this)
  }

  componentDidMount(){
    const { dispatch } = this.props
    this.props.data.todo = []
    dispatch({
      type: 'todo/get',
      payload: {}
    })
  }

  clearData(){
    this.props.dispatch({type: 'todo/clear'})
    this.props.dispatch({type: 'todo/get',payload: {}})
  }

  handleAdd(e){
    if(e.keyCode !== 13) return
    const text = e.target.value
    e.target.value = ''
    const { dispatch } = this.props
    dispatch({
      type: 'todo/add',
      payload: {text}
    })
  }

  deleteItem(id){
    this.props.dispatch({
      type: 'todo/remove',
      payload: {id}
    })
  }

  handleCheck(id, checked) {
    this.props.dispatch({
      type: 'todo/check',
      payload: {id, checked: !checked}
    })
  }

  checkAll() {
    const tempData = Object.values(this.props.data.todo)
    const flag = tempData.every(v => v.checked === true)
    this.props.dispatch({
      type: 'todo/checkAll',
      payload: {checked: !flag}
    })
  }

  render() {
    const FormItem = Form.Item
    const ListItem = List.Item
    let allData = []
    allData = Object.values(this.props.data.todo)
    
    const leftNum = allData.filter(v => v.checked === false).length

    if(this.props.dataType === 'active'){
      allData = allData.filter(v => v.checked === false)
    }else if(this.props.dataType === 'completed'){
      allData = allData.filter(v => v.checked === true)
    }

    return (
      <div style={{width: '100%'}}>
        <Row style={{minHeight: 50}}></Row>
        <Row>
          <Col span={12} offset={6}>
            <Form>
              <FormItem>
                <Button icon="down" onClick={this.checkAll}></Button>
                <Input style={{width: 'calc(100% - 32px)'}} onKeyDown={this.handleAdd}></Input>
              </FormItem>

              <FormItem>
                <List bordered
                  pagination={{
                    onChange: (page) => {
                      console.log(page)
                    },
                    pageSize: 5,
                  }}
                  dataSource={allData}
                  renderItem={v => (
                    <ListItem style={{position: 'relative', paddingLeft: 22}}  key={v._id} onMouseEnter={() => (this.setState({showClose: v._id}))} onMouseLeave={() => (this.setState({showClose: -1}))}>
                      <Checkbox style={{position: 'absolute', top: 13, left: 0}} checked={v.checked} onChange={() => this.handleCheck(v._id, v.checked)}></Checkbox>
                      <ListItem.Meta description={v.text} title=''/>
                      {this.state.showClose === v._id ? <Icon type='close' style={{cursor: 'pointer', color: 'red'}} onClick={() => this.deleteItem(v._id)}/> : <Icon type='check' style={{color: 'rgb(240, 242, 245)'}}/>}
                    </ListItem>
                  )}
                >
                </List>
              </FormItem>

              <FormItem>
                <span style={{float: 'left'}}>{leftNum} items left</span>
                <span style={{float: 'left', marginLeft: '22%'}}>
                  <Link style={{margin: '0 25px', color: '#666'}} to='/all'>all</Link>
                  <Link style={{margin: '0 25px', color: '#666'}} to='/active'>active</Link>
                  <Link style={{margin: '0 25px', color: '#666'}} to='/completed'>completed</Link>
                </span>
                <span style={{float: 'right',cursor: 'pointer'}} onClick={this.clearData}>clear all</span>
              </FormItem>
            </Form>
          </Col>
        </Row>
        <Row style={{minHeight: 50}}></Row>
      </div>
    )
  }
}
  1. 附package.json
{
  "private": true,
  "scripts": {
    "start": "roadhog server",
    "build": "roadhog build",
    "lint": "eslint --ext .js src test",
    "precommit": "npm run lint"
  },
  "dependencies": {
    "antd": "^3.6.6",
    "body-parser": "^1.18.3",
    "cookie-parser": "^1.4.3",
    "dva": "^2.3.1",
    "express": "^4.16.3",
    "mongoose": "^5.2.3",
    "react": "^16.2.0",
    "react-dom": "^16.2.0"
  },
  "devDependencies": {
    "babel-plugin-dva-hmr": "^0.3.2",
    "babel-plugin-import": "^1.8.0",
    "connect-multiparty": "^2.1.1",
    "dva-loading": "^2.0.3",
    "eslint": "^4.14.0",
    "eslint-config-umi": "^0.1.1",
    "eslint-plugin-flowtype": "^2.34.1",
    "eslint-plugin-import": "^2.6.0",
    "eslint-plugin-jsx-a11y": "^5.1.1",
    "eslint-plugin-react": "^7.1.0",
    "history": "^4.7.2",
    "husky": "^0.12.0",
    "qs": "^6.5.2",
    "redbox-react": "^1.4.3",
    "roadhog": "^2.0.0"
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容