Dva练习:新闻发布案例


Dva介绍

官网:https://dvajs.com

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

安装

$: npm install dva-cli -g
$: dva -v

项目搭建

  1. 使用脚手架工具创建项目:
$: dva new news-ui
  1. 启动测试:
$: cd news-ui
$: npm start

集成Antd

通过 npm 安装 antdbabel-plugin-importbabel-plugin-import 是用来按需加载 antd 的脚本和样式的,详见 repo

  1. 安装依赖:
$: npm install antd babel-plugin-import --save
  1. 编辑 .webpackrc,使 babel-plugin-import 插件生效:
{
+  "extraBabelPlugins": [
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+  ]
}

创建页面

  1. src/routes目录下创建news目录,创建News.js页面和news.less样式文件

News.js:

import React, {PureComponent} from 'react';

export default class News extends PureComponent {

  render() {
    return (
      <div>
      	<h1>新闻管理</h1>
      </div>
    );
  }
}
  1. 配置路由

src/router.js里添加路由:

import React from 'react';
import { Router, Route, Switch } from 'dva/router';
import IndexPage from './routes/IndexPage';
import News from './routes/news/News';

function RouterConfig({ history }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={IndexPage} />
        <Route path="/news" exact component={News} />
      </Switch>
    </Router>
  );
}

export default RouterConfig;

此时访问http://localhost:8000/#/news即可访问页面。

关联数据

  1. src/models下的example.js复制,命名为news.js,修改namespacenews,并添加默认数据:
export default {

  namespace: 'news',

  state: {
    list: [
      {
        "id": 1,
        "title": "NBA交易",
        "author": "腾讯新闻",
        "source": "nba官网",
        "createTime": "2020-04-27 10:44:51",
        "content": "麦迪交易至马刺"
       }
    ]
  },

  subscriptions: {
    setup({ dispatch, history }) {  // eslint-disable-line
    },
  },

  effects: {
    *fetch({ payload }, { call, put }) {  // eslint-disable-line
      yield put({ type: 'save' });
    },
  },

  reducers: {
    save(state, action) {
      return { ...state, ...action.payload };
    },
  },

};
  1. 入口文件使用modes:
import dva from 'dva';
import './index.css';

// 1. Initialize
const app = dva();

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

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

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

// 5. Start
app.start('#root');
  1. 页面中使用数据:
import React, {PureComponent} from 'react';
import {connect} from 'dva';

class News extends PureComponent {

  render() {
    return (
      <div>
      	<h1>新闻管理</h1>
      </div>
    );
  }
}

// 这里可以使用{news}解构值代替state,这样下面使用时不需要在state.news
const mapStateToProps = ({news}) => ({
  list: news.list,
});

export default connect(mapStateToProps)(News);

此时,News组件中this.props就有了list属性,且有默认数据。

使用Antd布局页面

使用antd中Grid、Layout、Table、Button等组件简单布局页面,并自定义样式文件。

news.less:

.news {
  height: 100%;
  .header {
    background: #f1f1f1;
    text-align: center;
  }
  .main {
    background: #fff;
    padding-top: 30px;
    .btn {
      margin-bottom: 10px;
    }
  }
  .footer {
    background: #f1f1f1;
    text-align: center;
  }
}

News.js:

import React, {PureComponent, Fragment} from 'react';
import {connect} from 'dva';
import {Layout, Row, Col, Table, Button, Divider, Popconfirm} from 'antd';
import styles from './News.less';

const {Header, Footer, Content} = Layout;

class News extends PureComponent {

  columns = [
    {
      title: '标题',
      dataIndex: 'title',
      key: 'title',
    },
    {
      title: '作者',
      dataIndex: 'author',
      key: 'author',
    },
    {
      title: '来源',
      dataIndex: 'source',
      key: 'source',
    },
    {
      title: '时间',
      key: 'createTime',
      dataIndex: 'createTime',
    },
    {
      title: '操作',
      key: 'action',
      render: (text, record) => (
        <span>
        <Button type="primary">编辑</Button>
        <Divider type="vertical"/>
        <Popconfirm
          title="确认删除吗?"
          okText="确定"
          cancelText="取消"
        >
          <Button type="danger">删除</Button>
        </Popconfirm>
      </span>
      )
    },
  ];

  render() {
    const {list} = this.props;
    return (
      <Fragment>
        <Layout className={styles.news}>
          <Header className={styles.header}><h1>新闻发布系统</h1></Header>
          <Content className={styles.main}>
            <Row>
              <Col span={4}></Col>
              <Col span={16}>
                <Button className={styles.btn} type="primary">发布新闻</Button>
                <Table size="small" rowKey={record=>record.id} columns={this.columns} dataSource={list} />
              </Col>
              <Col span={4}></Col>
            </Row>
          </Content>
          <Footer className={styles.footer}>@ AdoredU</Footer>
        </Layout>
      </Fragment>
    );
  }
}

const mapStateToProps = ({news}) => ({
  list: news.list,
});

export default connect(mapStateToProps)(News);

封装请求

封装request层

这里使用axios封装请求。安装axios和qs(qs用来处理post请求参数,防止后端接收不到):

$: cnpm install --save axios qs

重写src/utils/request.js

/*
 * 封装通用的工具函数发送ajax请求
 */
import axios from 'axios';
import qs from 'qs';

// 设置请求的服务器根路径
axios.defaults.baseURL = "http://localhost:8080";

// 封装get和post请求
export default {
  get(url, params = {}) {
    return new Promise((resolve, reject) => {
      axios.get(url,{params})
        .then(response=>{
          resolve(response.data);  // 处理请求成功的结果
        })
        .catch(err=>{
          reject(err);  // 处理请求失败的错误信息
        })
    });
  },
  post(url, params = {}) {
    return new Promise((resolve, reject)=>{
      axios.post(url, qs.stringify(params))  // 使用qs避免后端接收不到参数
        .then(response => {
          resolve(response.data);
        })
        .catch(err=>{
          reject(err);
        });
    })
  }
}

封装service层

src/services新建news.js:

import req from '../utils/request';

/*发布新闻*/
export const publishNews = params => req.post('/news/publishNews', params);

/*获取新闻列表*/
export const getNewsList = () => req.get('/news/getNews');

/*删除新闻*/
export const delNews = params => req.get('/news/deleteNews', params);

/*更新新闻*/
export const editNews = params => req.get('/news/updateNews', params);

models层使用

利用dva的subscriptions实现在页面初始化时加载列表,src/models/news.js

import {publishNews, getNewsList, editNews, delNews} from '../services/news';

export default {

  namespace: 'news',

  state: {
    list: []
  },

  subscriptions: {
    setup({ dispatch, history }) {
      if (history.location.pathname === '/news') {  // 当访问路径为news时执行:
        dispatch({
          type: 'getNews',
        });
      }
    },
  },

  effects: {
    *getNews({ payload }, { call, put }) {
      const {status, data} = yield call(getNewsList);
      if (status === 0) {
        yield put({ type: 'getNewsList', payload: {list: data} });
      }
    }
  },

  reducers: {
    getNewsList(state, action) {
      return { ...state, ...action.payload };
    },
  },

};

此时,新闻列表显示正常。

删除功能

修改组件,添加删除函数:

import React, {PureComponent, Fragment} from 'react';
import {connect} from 'dva';
import {Layout, Row, Col, Table, Button, Divider, Popconfirm} from 'antd';
import styles from './News.less';

const {Header, Footer, Content} = Layout;

class News extends PureComponent {

  handleDelete = (id) => {
    this.props.dispatch({
      type: 'news/handleDelNews',
      payload: {id}
    });
  };

  columns = [
    {
      title: '标题',
      dataIndex: 'title',
      key: 'title',
    },
    {
      title: '作者',
      dataIndex: 'author',
      key: 'author',
    },
    {
      title: '来源',
      dataIndex: 'source',
      key: 'source',
    },
    {
      title: '时间',
      key: 'createTime',
      dataIndex: 'createTime',
    },
    {
      title: '操作',
      key: 'action',
      render: (text, record) => (
        <span>
        <Button type="primary">编辑</Button>
        <Divider type="vertical"/>
        <Popconfirm
          title="确认删除吗?"
          onConfirm={() => this.handleDelete(record.id)}
          okText="确定"
          cancelText="取消"
        >
          <Button type="danger">删除</Button>
        </Popconfirm>
      </span>
      )
    },
  ];

  render() {
    const {list} = this.props;
    return (
      <Fragment>
        <Layout className={styles.news}>
          <Header className={styles.header}><h1>新闻发布系统</h1></Header>
          <Content className={styles.main}>
            <Row>
              <Col span={4}></Col>
              <Col span={16}>
                <Button className={styles.btn} type="primary">发布新闻</Button>
                <Table size="small" rowKey={record=>record.id} columns={this.columns} dataSource={list} />
              </Col>
              <Col span={4}></Col>
            </Row>
          </Content>
          <Footer className={styles.footer}>@ AdoredU</Footer>
        </Layout>
        <NewsModel ref={newsModel=>this.newsModel=newsModel}/>
      </Fragment>
    );
  }
}

const mapStateToProps = ({news}) => ({
  list: news.list,
});

export default connect(mapStateToProps)(News);

删除逻辑:

import {publishNews, getNewsList, editNews, delNews} from '../services/news';

export default {

  namespace: 'news',

  state: {
    list: []
  },

  subscriptions: {
    setup({ dispatch, history }) {
      if (history.location.pathname === '/news') {
        dispatch({
          type: 'getNews',
        });
      }
    },
  },

  effects: {
    *getNews({ payload }, { call, put }) {
      const {status, data} = yield call(getNewsList);
      if (status === 0) {
        yield put({ type: 'getNewsList', payload: {list: data} });
      }
    },
    *handleDelNews({payload}, {call, put}) {
      const {status, obj} = yield call(delNews, payload);
      if (status === 0) {
        yield put({type: 'getNews'});
      }
    }
  },

  reducers: {
    getNewsList(state, action) {
      return { ...state, ...action.payload };
    },
  },

};

添加/编辑组件封装

src/components下创建newsModel文件夹,下创建index.js:

import React, {PureComponent} from 'react';
import { Modal, Form, Input } from 'antd';

const {TextArea} = Input;

class NewsModel extends PureComponent {
  state = {
    visible: false,
    title: ''
  };

  showModal = (title, record) => {
    this.setState({
      visible: true,
      title
    });
  };

  handleOk = e => {
    this.form.submit();
  };

  handleCancel = e => {
    this.setState({
      visible: false,
    });
  };

  onFinish = values => {
    console.log('Success:', values);
  };

  onFinishFailed = errorInfo => {
    console.log('Failed:', errorInfo);
  };

  render() {
    return (
      <div>
        <Modal
          title={this.state.title}
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
          cancelText="取消"
          okText="确定"
        >
          {/*表单*/}
          <Form ref={form=>this.form=form} labelCol= wrapperCol=
		            onFinish={this.onFinish}
                onFinishFailed={this.onFinishFailed}
          >
            <Form.Item
              label="标题"
              name="title"
              rules={[
                {
                  required: true,
                  message: '标题不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="作者"
              name="author"
              rules={[
                {
                  required: true,
                  message: '作者不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="来源"
              name="source"
              rules={[
                {
                  required: true,
                  message: '来源不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="内容"
              name="content"
              rules={[
                {
                  required: true,
                  message: '内容不能为空!',
                },
              ]}
            >
              <TextArea/>
            </Form.Item>
          </Form>
        </Modal>
      </div>
    );
  }
}

export default NewsModel;

在组件中引入:

import React, {PureComponent, Fragment} from 'react';
import {connect} from 'dva';
import {Layout, Row, Col, Table, Button, Divider, Popconfirm} from 'antd';
import NewsModel from '../../components/newsModel';
import styles from './News.less';

const {Header, Footer, Content} = Layout;

class News extends PureComponent {

  handleDelete = (id) => {
    this.props.dispatch({
      type: 'news/handleDelNews',
      payload: {id}
    });
  };

  columns = [
    {
      title: '标题',
      dataIndex: 'title',
      key: 'title',
    },
    {
      title: '作者',
      dataIndex: 'author',
      key: 'author',
    },
    {
      title: '来源',
      dataIndex: 'source',
      key: 'source',
    },
    {
      title: '时间',
      key: 'createTime',
      dataIndex: 'createTime',
    },
    {
      title: '操作',
      key: 'action',
      render: (text, record) => (
        <span>
        <Button onClick={()=>this.editNews(record)} type="primary">编辑</Button>
        <Divider type="vertical"/>
        <Popconfirm
          title="确认删除吗?"
          onConfirm={() => this.handleDelete(record.id)}
          okText="确定"
          cancelText="取消"
        >
          <Button type="danger">删除</Button>
        </Popconfirm>
      </span>
      )
    },
  ];

  handlePublishNews = () => {
    this.newsModel.showModal("发布新闻");
  };

  editNews = (record) => {
    this.newsModel.showModal("编辑新闻", record);
  };

  render() {
    const {list} = this.props;
    return (
      <Fragment>
        <Layout className={styles.news}>
          <Header className={styles.header}><h1>新闻发布系统</h1></Header>
          <Content className={styles.main}>
            <Row>
              <Col span={4}></Col>
              <Col span={16}>
                <Button onClick={this.handlePublishNews} className={styles.btn} type="primary">发布新闻</Button>
                <Table size="small" rowKey={record=>record.id} columns={this.columns} dataSource={list} />
              </Col>
              <Col span={4}></Col>
            </Row>
          </Content>
          <Footer className={styles.footer}>@ AdoredU</Footer>
        </Layout>
        <NewsModel ref={newsModel=>this.newsModel=newsModel}/>
      </Fragment>
    );
  }
}

const mapStateToProps = ({news}) => ({
  list: news.list,
});

export default connect(mapStateToProps)(News);

此时点击发布新闻和编辑时都可正常弹出新闻模态框。

添加功能

修改模态组件,完成添加功能:

import React, {PureComponent} from 'react';
import { Modal, Form, Input } from 'antd';
import {connect} from 'dva';

const {TextArea} = Input;

class NewsModel extends PureComponent {
  state = {
    visible: false,
    title: ''
  };

  showModal = (title, record) => {
    this.setState({
      visible: true,
      title
    });
  };

  handleOk = () => {
    this.form.submit();
  };

  handleCancel = e => {
    this.setState({
      visible: false,
    });
  };

  onFinish = values => {
    this.props.dispatch ({
      type: 'news/handlePublishNews',
      payload: values
    });
    this.setState({
      visible: false
    });
    console.log('Success:', values);
  };

  onFinishFailed = errorInfo => {
    console.log('Failed:', errorInfo);
  };

  render() {
    return (
      <div>
        <Modal
          title={this.state.title}
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
          cancelText="取消"
          okText="确定"
          destroyOnClose={true}
        >
          {/*表单*/}
          <Form ref={form=>this.form=form} labelCol= wrapperCol=
                onFinish={this.onFinish}
                onFinishFailed={this.onFinishFailed}
          >
            <Form.Item
              label="标题"
              name="title"
              rules={[
                {
                  required: true,
                  message: '标题不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="作者"
              name="author"
              rules={[
                {
                  required: true,
                  message: '作者不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="来源"
              name="source"
              rules={[
                {
                  required: true,
                  message: '来源不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="内容"
              name="content"
              rules={[
                {
                  required: true,
                  message: '内容不能为空!',
                },
              ]}
            >
              <TextArea/>
            </Form.Item>
          </Form>
        </Modal>
      </div>
    );
  }
}

export default NewsModel;

models中添加功能补充:

import {publishNews, getNewsList, editNews, delNews} from '../services/news';

export default {

  namespace: 'news',

  state: {
    list: []
  },

  subscriptions: {
    setup({ dispatch, history }) {
      if (history.location.pathname === '/news') {
        dispatch({
          type: 'getNews',
        });
      }
    },
  },

  effects: {
    *getNews({ payload }, { call, put }) {
      const {status, data} = yield call(getNewsList);
      if (status === 0) {
        yield put({ type: 'getNewsList', payload: {list: data} });
      }
    },
    *handleDelNews({payload}, {call, put}) {
      const {status} = yield call(delNews, payload);
      if (status === 0) {
        yield put({type: 'getNews'});
      }
    },
    *handlePublishNews({payload}, {call, put}) {
      const {status} = yield call(publishNews, payload);
      if (status === 0) {
        yield put({type: 'getNews'});
      }
    }
  },

  reducers: {
    getNewsList(state, action) {
      return { ...state, ...action.payload };
    },
  },

};

修改功能

添加id作为是添加还是修改的判断依据:

import React, {PureComponent} from 'react';
import { Modal, Form, Input } from 'antd';

const {TextArea} = Input;

class NewsModel extends PureComponent {
  state = {
    visible: false,
    title: '',
    id: null  // 使用id判断是新增还是修改
  };

  showModal = (title, record, id) => {
    this.setState({
      visible: true,
      title,
      id
    }, ()=>{
      this.form.setFieldsValue(record);
    });

  };

  handleOk = () => {
    this.form.submit();
  };

  handleCancel = e => {
    this.setState({
      visible: false,
    });
  };

  onFinish = values => {
    console.log(values);
    if (this.state.id) {
      this.props.dispatch({
        type: 'news/handleEditNews',
        payload: {...values, id: this.state.id}
      });
    } else {
      this.props.dispatch({
        type: 'news/handlePublishNews',
        payload: values
      });
    }
    this.setState({
      visible: false
    });
    console.log('Success:', values);
  };

  onFinishFailed = errorInfo => {
    console.log('Failed:', errorInfo);
  };

  render() {
    return (
      <div>
        <Modal
          title={this.state.title}
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
          cancelText="取消"
          okText="确定"
          destroyOnClose={true}
          forceRender={true}
        >
          {/*表单*/}
          <Form ref={form=>this.form=form} labelCol= wrapperCol=
                onFinish={this.onFinish}
                onFinishFailed={this.onFinishFailed}
          >
            <Form.Item
              label="标题"
              name="title"
              rules={[
                {
                  required: true,
                  message: '标题不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="作者"
              name="author"
              rules={[
                {
                  required: true,
                  message: '作者不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="来源"
              name="source"
              rules={[
                {
                  required: true,
                  message: '来源不能为空!',
                },
              ]}
            >
              <Input/>
            </Form.Item>
            <Form.Item
              label="内容"
              name="content"
              rules={[
                {
                  required: true,
                  message: '内容不能为空!',
                },
              ]}
            >
              <TextArea/>
            </Form.Item>
          </Form>
        </Modal>
      </div>
    );
  }
}

export default NewsModel;

编辑打开模态框时传入id:

import React, {PureComponent, Fragment} from 'react';
import {connect} from 'dva';
import {Layout, Row, Col, Table, Button, Divider, Popconfirm} from 'antd';
import NewsModel from '../../components/newsModel';
import styles from './News.less';

const {Header, Footer, Content} = Layout;

class News extends PureComponent {

  handleDelete = (id) => {
    this.props.dispatch({
      type: 'news/handleDelNews',
      payload: {id}
    });
  };

  columns = [
    {
      title: '标题',
      dataIndex: 'title',
      key: 'title',
    },
    {
      title: '作者',
      dataIndex: 'author',
      key: 'author',
    },
    {
      title: '来源',
      dataIndex: 'source',
      key: 'source',
    },
    {
      title: '时间',
      key: 'createTime',
      dataIndex: 'createTime',
    },
    {
      title: '操作',
      key: 'action',
      render: (text, record) => (
        <span>
        <Button onClick={()=>this.editNews(record)} type="primary">编辑</Button>
        <Divider type="vertical"/>
        <Popconfirm
          title="确认删除吗?"
          onConfirm={() => this.handleDelete(record.id)}
          okText="确定"
          cancelText="取消"
        >
          <Button type="danger">删除</Button>
        </Popconfirm>
      </span>
      )
    },
  ];

  handlePublishNews = () => {
    this.newsModel.showModal("发布新闻");
  };

  editNews = (record) => {
    this.newsModel.showModal("编辑新闻", record, record.id);
  };

  render() {
    const {list, dispatch} = this.props;
    return (
      <Fragment>
        <Layout className={styles.news}>
          <Header className={styles.header}><h1>新闻发布系统</h1></Header>
          <Content className={styles.main}>
            <Row>
              <Col span={4}></Col>
              <Col span={16}>
                <Button onClick={this.handlePublishNews} className={styles.btn} type="primary">发布新闻</Button>
                <Table size="small" rowKey={record=>record.id} columns={this.columns} dataSource={list} />
              </Col>
              <Col span={4}></Col>
            </Row>
          </Content>
          <Footer className={styles.footer}>@ AdoredU</Footer>
        </Layout>
        <NewsModel ref={newsModel=>this.newsModel=newsModel} dispatch={dispatch}/>
      </Fragment>
    );
  }
}

// 这里可以使用{news}解构值代替state,这样下面使用时不需要在state.news
const mapStateToProps = ({news}) => ({
  list: news.list,
});

export default connect(mapStateToProps)(News);

models中补充修改逻辑:

import {publishNews, getNewsList, editNews, delNews} from '../services/news';

export default {

  namespace: 'news',

  state: {
    list: []
  },

  subscriptions: {
    setup({ dispatch, history }) {
      if (history.location.pathname === '/news') {
        dispatch({
          type: 'getNews',
        });
      }
    },
  },

  effects: {
    *getNews({ payload }, { call, put }) {
      const {status, data} = yield call(getNewsList);
      if (status === 0) {
        yield put({ type: 'getNewsList', payload: {list: data} });
      }
    },
    *handleDelNews({payload}, {call, put}) {
      const {status} = yield call(delNews, payload);
      if (status === 0) {
        yield put({type: 'getNews'});
      }
    },
    *handlePublishNews({payload}, {call, put}) {
      const {status} = yield call(publishNews, payload);
      if (status === 0) {
        yield put({type: 'getNews'});
      }
    },
    *handleEditNews({payload}, {call, put}) {
      const {status} = yield call(editNews, payload);
      console.log(status);
      if (status === 0) {
        yield put({type: 'getNews'});
      }
    },
  },

  reducers: {
    getNewsList(state, action) {
      return { ...state, ...action.payload };
    },
  },

};

至此,完整的dva新闻案例结束。

案例源码