dva的入门和使用
创建项目
- 安装cli
npm i dva-cli -g
- 创建新项目
dva new [project name]
- 启动项目
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格式,如果需要 JS 格式,可修改为 webpackrc.js
使用antd
- 安装 npm i antd babel-plugin-import -S
- 配置 .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
开发简单的项目示例
-
创建项目, 完成上述配置
-
创建后台接口, 也可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
-
src/index.js => dva的入口
import dva from 'dva'
import './index.css'
import createLoading from 'dva-loading' // 需要npm安装
import createHistory from 'history/createBrowserHistory'
// 1. Initialize
const app = dva({
history: createHistory()
})
// 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')
- src/index.css => 公共的css
html, body, :global(#root) {
height: 100%;
}
- 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
- 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 }))
}
- 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})
}
- 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)
- 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
}
},
}
- 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 href='https://github.com/OlivierHu/dva-redux/tree/master/experience_dva' style={{color: '#999'}}>版权所有 @OlivierHu</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>
)
}
}
- 附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"
}
}
网友评论