前言:上一篇梳理了上手Ant Design Pro需要了解的一些基础知识,这一篇记录一些在开发【后台管理系统】登录注册、数据获取、列表展示等功能时需要注意的地方。
一、与服务器交互的一般步骤
- 代理到后端,配置跨域
- 修改 config/config.js
- 配置 proxy 属性。只要 proxy 和 mock url 不同,是可以共存的。
proxy: { '/login': { target: 'http://192.168.1.106:9099', changeOrigin: true, pathRewrite: { '^/login': '' }, }, }
- 添加要请求的接口
- 修改 services/api.js (可以是service目录下其他自定义文件)
import request from '@/utils/request'; //ant design pro封装的reques请求文件 export async function fakeAccountLogin(params) { return request('/login', { method: 'POST', body: params }); }
-
在modal里面写effect调取接口方法
- 修改 model/login.js (可以是model目录下其它自定义文件)
import { fakeAccountLogin, getFakeCaptcha } from '@/services/api'; //请求的接口方法 namespace: 'login', //要唯一 state: { list: [] //后台返回的数据存储在该list中,名字想怎么起怎么起 }, effects: { *login({ payload }, { call, put }) { //login是界面要调取的接口名称 const response = yield call(fakeAccountLogin, payload); //yield call()真正调用接口,传递数据,返回响应的方法 yield put({ //一个yield put只能触发一个action type: 'queryList', //通过调用这个reducer把返回数据传给list payload: response, }); reducers: { queryList(state, action) { return { ...state, list: action.payload, //这就拿到数据啦 }; }, } } }
-
组件中创建连接
-
在 pages/User/Login.js 组件中通过 dva提供的connect高阶组件连接组件和dva,传入namespace(唯一)获得其中的state和effects(dispatch方法)
import { connect } from 'dva' @connect(({ login, loading }) => ({ //login是namespace loading是对应使用的方法 login, //login是namespace submitting: loading.effects['login/login'], //login 命名空间的login请求接口(入口) }))
-
一般在componentDidMount生命钩子中发送请求获取数据
componentDidMount() { //注意务必先使用dva中的connect建立连接,否则是无法调用props中的dispatch法的 this.props.dispatch({ //调用model中的方法发起请求,(model的命名空间+方法名) type: 'mbit/firstRequest', //设置参数 payload:{ args1:"参数1", args2:"参数2", }, }); }
-
登录提交等操作方法中发送请求获取数据
handleSubmit = (err, values) => { const { type } = this.state; if (!err) { const { dispatch } = this.props; dispatch({ type: 'login/login', payload: { login_type: "usernameAndPassword", credentials: { username: values.userName, password: values.password }, ...values }, }); } };
-
获取数据后的其它操作
-
显示后台返回的数据
const {Login: { list },loading} = this.props; //这个就在对应namespace下面list数组,之前存放后台返回数据的list数组 <Table columns={columns} dataSource={list?list.content:[]} rowKey="id"/> //dataSource里面是通过list获取到的数据
-
跳转路由页面
import { routerRedux } from 'dva/router'; yield put(routerRedux.replace('/'));
-
将获取到的数据存入localStorage
localStorage.setItem('login_token', response.data.token);
-
每次请求中带着token 获取localStorage中的token封装进请求头中(修改 request.js 请求文件)
let login_token; newOptions.headers = { Accept: 'application/json', 'Content-Type': 'application/json; charset=utf-8', ...newOptions.headers, }; if (localStorage.getItem('login_token') != null){ login_token = localStorage.getItem('login_token'); newOptions.headers['AuthorizationToken'] = localStorage.getItem('login_token'); }
二、关于@connect装饰器
- 组件写法中调用了
dva
所封装的react-redux
的@connect
装饰器
- 用来接收绑定的
list
这个 model 对应的 redux store。 - 这里的装饰器实际除了
app.state.list
以外还实际接收app.state.loading
作为参数,这个loading
的来源是src/index.js
中调用的dva-loading
这个插件。/* * src/index.js */ import createLoading from 'dva-loading'; app.use(createLoading());
它返回的信息包含了 global、model 和 effect 的异步加载完成情况。数据map一
{ "global": true, "models": { "list": false, "user": true, "rule": false }, "effects": { "list/fetch": false, "user/fetchCurrent": true, "rule/fetch": false } }
在这里带上
{count: 5}
这个 payload (参数)向 store 进行了一个类型为list/fetch
的 dispatch,在src/models/list.js
中就可以找到具体的对应操作。import { queryFakeList } from '../services/api'; export default { namespace: 'list', state: { list: [], }, effects: { *fetch({ payload }, { call, put }) { const response = yield call(queryFakeList, payload); yield put({ type: 'queryList', payload: Array.isArray(response) ? response : [], }); }, /* ... */ }, reducers: { queryList(state, action) { return { ...state, list: action.payload, }; }, /* ... */ }, };
-
View中使用@connect
@connect(({ list, loading }) => ({ list,//① loading: loading.models.list,//② }))
-
connect 有两个参数,mapStateToProps以及mapDispatchToProps,一个将state状态绑定到组件的props,一个将dispatch方法绑定到组件的props
-
代码①:将实体list中的state数据绑定到props,注意绑定的是实体list整体,使用时需要list.[state中的具体变量]
-
代码②:通过loading将上文“数据map一”中的models的list的key对应的value读取出来。赋值给loading,以方便使用,如表格是否有加载图标(也可以通过key value编写:loading.effects["list/fetch"],如下↓可取多个)
//pages/Dashboard/WorkPlace.js @connect(({ user, project, activities, chart, loading }) => ({ currentUser: user.currentUser, project, activities, chart, currentUserLoading: loading.effects['user/fetchCurrent'], projectLoading: loading.effects['project/fetchNotice'], activitiesLoading: loading.effects['activities/fetchList'], }))
-
变量获取
-
因,在
src/models/list.js
export default { namespace: 'list', state: { list: [], },
-
故,在view中
render() { const { list: { list }, loading } = this.props;
定义使用时:list: { list } ,含义:实体list下的state类型的list变量
三、项目实践注意点
- 登录注册
- 登录关键点:登录成功后,请求中始终带着存着登录信息的token,在其他功能中,如需要获取用户信息,则直接从token中获取
- 注册关键点:注册必须输入正确的手机验证码,在校验手机号格式正确后就可通过阿里云发送验证码,但是同一号码多次发送,可能会被封号
- 数据获取
- 表格Table组件中的单选/多选,获取当前选中项的列表数据:record
{ title: '资料审核', dataIndex: 'detailInfo', render: (text, record) => <a onClick={() => this.previewItem(record.id)}>资料详情>></a> },
- 在所有选中项的列表数据中删选/计算符合条件的数据:selectRows
//components/StandardTable/index.js handleRowSelectChange = (selectedRowKeys, selectedRows) => { let { noPayList, payedList, needTotalList } = this.state; noPayList = initNoPayList(selectedRows); payedList = initPayedList(selectedRows); needTotalList = needTotalList.map(item => ({ ...item, total: selectedRows.reduce((sum, val) => sum + parseFloat(val[item.dataIndex], 10), 0), })); const { onSelectRow } = this.props; if (onSelectRow) { onSelectRow(selectedRows); } this.setState({ selectedRowKeys, selectedRows, noPayList, payedList, needTotalList }); };
- 表单Form组件中获取用户提交的数据:fields / fieldsValue
const okHandle = (record) => { form.validateFields((err, fieldsValue) => { if (err) return; form.resetFields(); handleRemark(record, fieldsValue); }); };
const CreateForm = Form.create()(props => { const { modalVisible, record, form, handleRemark, handleModalVisible } = props; const rowObject = { minRows: 2, maxRows: 6 } const okHandle = (record) => { form.validateFields((err, fieldsValue) => { if (err) return; form.resetFields(); handleRemark(record, fieldsValue); }); }; return ( <Modal destroyOnClose title="添加备注" visible={modalVisible} okText="确定" cancelText="取消" onOk={() => okHandle(record)} onCancel={() => handleModalVisible()} > <FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }}> {form.getFieldDecorator('remark', { rules: [{ required: true, message: '请输入至少五个字符的审批备注!', min: 5 }], })( <TextArea autosize={rowObject} /> )} </FormItem> </Modal> ); });
- 列表展示
- componentDidMount() 组件成功挂载后通过this.props中的dispatch方法传递参数、发送请求、获取数据,完成state数据初始化
- 定义 columns 列表项数组,传给Table组件的 column属性,其中 title为列表项标题、dataIndex为与服务端返回数据对应字段、render方法对数据处理后展示
- 从this.props中拿到的store中存储的 list (或其它)列表数据,传给Table组件的dataSource,列表才可以将column数组中的字段与数据一一对应
- 通常,列表上方有【查询】、【编辑】等操作,在输入查询内容时,要对字符串做 去首尾空格,确保执行查询时为完整无空格字符串
trimStr = str => { return str.replace(/(^s*)|(s*$)/g,""); } //查询 handleSearch = e => { e.preventDefault(); const { dispatch, form } = this.props; form.validateFields((err, fieldsValue) => { if (err) return; let values = { ...fieldsValue, updatedAt: fieldsValue.updatedAt && fieldsValue.updatedAt.valueOf(), }; Object.keys(values).forEach(key => { if(values[key]){ values[key] = this.trimStr(values[key]) } }); this.setState({ formValues: values, }); dispatch({ type: 'agent/fetch', payload: { currentPage: 1, pd: {}, showCount: 10 }, }); }); };
- 其它
- 向对象中添加新的属性与属性值:Object['属性'] = 值;
- 遍历对象修改每一个对象属性:Object.keys(values).forEach(key => { ……})
- forEach直接操作原数组,不会返回新值,map会返回新值:在React中根据数据动态循环添加元素,使用map
<Row gutter={24}> { infoPics.map((infoPic, index) => (<Col key={index} xl={6} lg={12} md={24} sm={24} xs={24}> <div className={styles.infoTitle}>{`代理资料${index+1}`}</div> <div className={styles.infoPic} style={{ backgroundImage: `url(${infoPic})` }} > { infoPic == '' ? <div className={styles.empty}><Empty /></div> : '' } </div> </Col>) ) } </Row>
-
资源文件需要加统一前缀时,在配置文件中定义方法,应用时直接在数据前调用方法即可
//util/util.js export function setFileHost(){ return 'http://baidu.com/'; } //RightContent.js import { setFileHost } from '@/utils/utils' <Avatar size="small" className={styles.avatar} src={`${setFileHost()+currentUser.headIcon}`} alt="avatar" />
- 后台标题:标题修改是在 src/layouts/BasicLayout.js 中找到 getPageTitle 进行修改
- 后台Logo:Logo位于 src/components/SideMenu/SideMenu.js 中,原先的logo是props传过来的,所以我在引用logo文件的时候加了import yhzLogo from '../../assets/logo.png';避免参数名重复,另外logo图片文件最好放在src/assets 里面
参考资料
注:转载请注明出处