Dva介绍
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
安装
$: npm install dva-cli -g
$: dva -v
项目搭建
- 使用脚手架工具创建项目:
$: dva new news-ui
- 启动测试:
$: cd news-ui
$: npm start
集成Antd
通过 npm 安装
antd
和babel-plugin-import
。babel-plugin-import
是用来按需加载 antd 的脚本和样式的,详见 repo 。
- 安装依赖:
$: npm install antd babel-plugin-import --save
- 编辑
.webpackrc
,使babel-plugin-import
插件生效:
{
+ "extraBabelPlugins": [
+ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+ ]
}
创建页面
- 在
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>
);
}
}
- 配置路由
在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
即可访问页面。
关联数据
- 将
src/models
下的example.js
复制,命名为news.js
,修改namespace
为news
,并添加默认数据:
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 };
},
},
};
- 入口文件使用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');
- 页面中使用数据:
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新闻案例结束。
案例源码
- 后台接口地址:https://github.com/AdoredU/news-app;
- 前端dva地址:https://github.com/AdoredU/news-ui;