一步一步创建dva项目,完成用户管理的 CURD 应用 (react+dva+antd)(填坑优化版)

前言

dva 官方项目里面有些用法不是最新的,本文针对性的解决并以正确的用法告知刚开始接触dva 项目的码友们 ,帮助大家避免入坑。
12 步 30 分钟,完成用户管理的 CURD 应用 优化版
git地址:https://github.com/Sawyer-china/react-user-dashboard
现在我们就开始一步一步的构建 如遇见问题可 添加 qq群:218618405 进行提问

如果你觉得该文章对你有帮助加个喜欢,github 加个 start 谢谢

1. 安装全局 dva-cli

$ npm install dva-cli -g

2. 创建项目

选好目录然后创建项目

$ dva new user-dashboard

3. 根据提示进入目录并运行项目

$ cd user-dashboard
$ npm start

如果看到以下页面,那么恭喜你,我们往下进行了


image.png

4. 引入antd组件库 (andt: 官方地址 https://ant.design/index-cn)

$ npm install antd --save

安装完成后打开 src/routes/IndexPage.js 引入一个antd组件试试 在文件头部引入

import Button from 'antd/lib/button'
import 'antd/lib/button/style/css';  

在function IndexPage 函数中使用组件

<Button type="primary">Primary</Button>

如果页面出现一个Button 则代表成功了

5. 按需加载

在项目中往往我们需要进行组件的按需加载以免去不必要的组件被打包到实际的项目中,以减少js的体积大小
安装以下插件

$ cnpm install babel-plugin-import --save-dev

并找到根目录下面的.webpackrc文件,并在文件中添加插件配置

"extraBabelPlugins": [
    ["import", { "libraryName": "antd", "style": "css" }]
]

配置更多玩法参考:https://github.com/sorrycc/roadhog/blob/master/README_zh-cn.md
修改以下代码

// import Button from 'antd/lib/button'
// import 'antd/lib/button/style/css'; 
import { Button } from 'antd'

6. 做 webpack 反向代理

在配置文件中添加以下代码

"proxy": {
        "/api": {
            "target": "http://jsonplaceholder.typicode.com/",
            "changeOrigin": true,
            "pathRewrite": { "^/api": "" }
        }
    }

访问 http://localhost:8000/api/users 如果你看见一串json数据代表代理成功,就可以进行下一步开发了(该json数据是dva官方提供的测试数据,使用Mockjs开发)
完成以上准备工作我们就开始正式的demo开发了

7. 创建 Users.js Router

在routes目录下创建Users.js

import React, { Component } from 'react'
import { connect } from 'dva'


import styles from './Users.css'

class Users extends Component {
    render() {
        return (
            <div className={styles.normal}>
                Users.js
            </div>
        )
    }
}

Users.propsTypes = {}

export default connect()(Users)

8. 配置路由 打开根目录router.js

import React from 'react'
import { Router, Route, Switch, Redirect, routerRedux } from 'dva/router'
import IndexPage from './routes/IndexPage'

import dynamic from 'dva/dynamic' // 路由按需加载

const { ConnectedRouter } = routerRedux

function RouterConfig({ history, app }) {
    const IndexPage = dynamic({
        app,
        component: () => import('./routes/IndexPage')
    })
    const Users = dynamic({
        app,
        component: () => import('./routes/Users')
    })
    return (
        <ConnectedRouter history={history}>
                <Switch>
                    <Route path="/" exact component={IndexPage} />
                    <Route path="/users" exact component={Users} />
                </Switch>
        </ConnectedRouter>
    )
}

export default RouterConfig

浏览地址 输入 http://localhost:8000/#/users 将会看到 users 路由页面

9. 在components 文件夹下 新建 MainLayout/Header.js

import React, { Component } from 'react'
import { Menu, Icon } from 'antd'
import { connect } from 'dva'

import { Link, routerRedux } from 'dva/router'

class Header extends Component {
    render() {
        const { location } = this.props
        return (
            <Menu
                selectedKeys={[location.pathname]}
                mode="horizontal"
                theme="dark"
            >
                <Menu.Item key="/users">
                    <Link to="/users">
                        <Icon type="bars" />Users
                    </Link>
                </Menu.Item>
                <Menu.Item key="/">
                    <Link to="/">
                        <Icon type="home" />Home
                    </Link>
                </Menu.Item>
                <Menu.Item key="/404">
                    <Link to="/page-you-dont-know">
                        <Icon type="frown-circle" />404
                    </Link>
                </Menu.Item>
                <Menu.Item key="/antd">
                    <a href="https://github.com/dvajs/dva">dva</a>
                </Menu.Item>
            </Menu>
        )
    }
}

export default connect()(Header)

10. 在 components/MainLayout 新建MainLayout.js

import React, { Component } from 'react'
import styles from './MainLayout.css'
import Header from './Header'

class MainLayout extends Component {
    render() {
        const { children, location } = this.props
        return (
            <div className={styles.normal}>
                <Header />
                <div className={styles.content}>
                    <div className={styles.main}>
                        {children}
                    </div>
                </div>
            </div>
        )
    }
}

export default MainLayout

11. 在 routes 中添加 App.js

import React, { Component } from 'react'
import { connect } from 'dva'
import { withRouter } from 'dva/router'

import MainLayout from '../components/MainLayout/MainLayout'


class App extends Component {
    render() {
        let { children, location } = this.props
        return (
            <MainLayout location={location}>
                {children}
            </MainLayout>
        )
    }
}

App.propTypes = {}

export default withRouter(
    connect(({ app, loading }) => ({
        app,
        loading
    }))(App)
)

添加完成之后修改 router.js 页面
在头部引入 App.js

import App from './routes/App'

然后修改return 中的代码

return (
        <ConnectedRouter history={history}>
            <App>
                <Switch>
                    <Route path="/" exact component={IndexPage} />
                    <Route path="/users" exact component={Users} />
                    <Route path="*" render={() => <Redirect to="users" />} />
                </Switch>
            </App>
        </ConnectedRouter>
    )

现在就可以切换路由了如下图示:


222.gif

接下来着重users页面的开发

12. 创建 users model 和 service

新建 src/models/users.js:

// user api
import * as usersService from '../services/users'
// 引入 node 模块
// import url from 'url'
// import qs from 'qs'

export default {
   namespace: 'users',
   state: {
       list: [],
       total: 0,
       page: 0
   },
   reducers: {
       /**
        * test
        * @param {*} state 
        * @param {*} param1 
        */
       save(state, { payload: { data: list, total, page } }) {
           return { ...state, list, total, page }
       },
       search(state) {
           return { ...state }
       }
   },
   effects: {
       *fetch({ payload: { page } }, { call, put }) {
           const { data, headers } = yield call(usersService.fetch, { page })
           yield put({
               type: 'save',
               payload: {
                   data,
                   total: headers['x-total-count'],
                   page: parseInt(page, 10)
               }
           })
       },
       *create({ payload: values }, { call, put }) {
           yield call(usersService.create, values)
       },
       *patch({ payload: { id, values } }, { call, put }) {
           yield call(usersService.patch, { id, values })
           yield put({ type: 'reload' })
       },
       *remove({ payload: { id } }, { call, put }) {
           yield call(usersService.remove, { id })
           yield put({ type: 'reload' })
       },
       *reload(action, { put, select }) {
           const page = yield select(state => state.users.page)
           yield put({ type: 'fetch', payload: { page } })
       }
   },
   subscriptions: {
       // setup({ dispatch }, done) {
       //     done('错了错了')
       // throw new Error('Whoops!')
       // }
       setup({ dispatch, history }) {
           return history.listen(({ pathname, search }) => {
               // const { query } = url.parse(search)
               // const oPath = qs.parse(query)
               // if (pathname === '/users') {
               //     console.log('/users')
               //     console.log(oPath)
               //     dispatch({ type: 'fetch', payload: oPath })
               // }
           })
       }
   }
}

新建 src/services/users.js

import request from '../utils/request'

export function queryUsers() {
    return request('/api/users')
}

export function fetch({ page = 1 }) {
    return request(`/api/users?_page=${page}&_limit=5`)
}

export function create(values) {
    return request('/api/users', {
        methods: 'POST',
        data: JSON.stringify(values)
    })
}

export function patch({ id, values }) {
    return request(`/api/users/${id}`, {
        methods: 'PATCH',
        data: JSON.stringify(values)
    })
}

export function remove({ id }) {
    return request(`/api/users/${id}`, {
        methods: 'DELETE'
    })
}

由于我们需要从 response headers 中获取 total users 数量,所以需要改造下 src/utils/request.js:

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) {
//   return fetch(url, options)
//     .then(checkStatus)
//     .then(parseJSON)
//     .then(data => ({ data }))
//     .catch(err => ({ err }));
// }

async function request(url, options) {
    const response = await fetch(url, options)
    checkStatus(response)
    const data = await response.json()
    const ret = {
        data,
        headers: {}
    }   

    if (response.headers.get('x-total-count')) {
        ret.headers['x-total-count'] = response.headers.get('x-total-count')
    }
    return ret
}

export default request

剩余部分请参考
https://github.com/sorrycc/blog/issues/18 (该代码可能有部分内容会导致错误)
请以一下链接为准
https://github.com/Sawyer-china/react-user-dashboard
最终完成效果如下图所示:

222.gif

如遇问题欢迎加群讨论
QQ群: 218618405
github:https://github.com/Sawyer-china

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

推荐阅读更多精彩内容