该项目是属于一个前后台分离的后台管理的SPA,即单页Web应用(single page web application,SPA),包含了用户管理、商品分类管理、商品管理以及权限管理等功能模块,在技术方面采用的是React的相关知识、AntD组件以及Github第三方库进行模块化、工程化的模式开发。
总的来说,这个后台管理系统实现的是增删改查的基本操作,但是对于像我这种前端小白来说还是收获匪浅,起码是对于一个项目的开发流程有所了解,现在根据每个组件记录一下学习历程,知识点的总结以及踩过的坑。
提前声明一下,是在网上找到的别人搭建好的接口,利用谷歌浏览器的扩展工具 Postman进行接口测试,发送ajax请求实现前后台的数据交互,然后运用webpack进行项目的打包生成build文件。
组件简介: 是静态组件,自定义了一部分的样式布局,使用AntD组件里面的 Form Input Button Icon 等组件实现用户登录的表单界面
组件作用: 主要是用来进行表单数据(用户数据)的收集以及表单验证,其表单验证规则直接使用AntD组件文档里的属性
组件相关操作:
// 保存user
const user = result.data
memoryUtils.user = user //保存在内存中
storeageUtils.saveUser(user) //保存到local中去
localStorage.setItem(USER_KEY,JSON.stringify(user))
const storageUtils = {
// 保存user
saveUser(user) {
// localStorage.setItem(USER_KEY,JSON.stringify(user))
store.set(USER_KEY,user)
},
// 读取user
getUser(){
// return JSON.parse(localStorage.getItem(USER_KEY) || '{}' )
return store.get(USER_KEY) || {}
},
// 删除user
removeUser(){
// localStorage.removeItem(USER_KEY)
store.remove(USER_KEY)
}
}
export default storageUtils
注意点:在上述的代码中 getUser() 的返回值是一个有意思的设计,因为在读取用户时,考虑到用户可能是第一次登陆,为了防止后续获取用户信息出错,最好返回一个空对象
// 读取local中保存的user 保存到内存中
const user = storageUtils.getUser()
memoryUtils.user = user
// 跳转到管理界面(不需要再回退回到登录)
props.history.replace('/home')
if(!user || !user._id) {
// 自动跳转到登录页面
return <Redirect to='/login'/>
}
项目知识点
async 和 await
1、作用?
简化promise对象的使用:不再使用then() 来指定成功/失败的回调函数
以同步编码(没有回调函数) 方式实现异步流程
2、哪里写await
在返回promise的表达式左侧写await: 不想要promise 想要promise异步执行的成功的value数据
3、哪里写async
await所在函数(最近的)的左侧写async
高阶函数:
(1)一类特别的函数
接收函数类型的参数
返回值是函数
(2)常见的高阶函数
定时器:setTimeout() setInterval() Promise
数组相关的遍历方法:forEach() filter() map() reduce() find()
函数对象的bind()
(3)高阶函数更新动态 更加具有扩展性
高阶组件
本质上就是一个函数
接收一个组件(被包装组件)返回一个新的组件(包装组件)包装组件会向被包装组件传入特定属性
作用:扩展组件的功能
高阶组件 与 高阶函数的关系
高阶组件是特别的高阶函数
接收一个组件函数 返回是一个新的组件函数
该组件为后台管理系统的主界面,包括左侧菜单栏 LeftNav 组件,头部 Header 组件,中间用来展示内容的各个小组件,以及底部的 Footer组件。主要运用AntD组件完成布局,以及子路由的配置。
return (
<Layout style={{minHeight:'100%'}}>
<Sider>
<LeftNav/>
</Sider>
<Layout>
<Header>Header</Header>
<Content style={{margin:'20px', backgroundColor:'#fff'}}>
<Switch>
<Route path='/home' component={Home}/>
<Route path='/category' component={Category}/>
<Route path='/product' component={Product}/>
<Route path='/role' component={Role}/>
<Route path='/user' component={User}/>
<Route path='/chars/bar' component={Bar}/>
<Route path='/chars/line' component={Line}/>
<Route path='/chars/pie' component={Pie}/>
<Redirect to='/home'/>
</Switch>
</Content>
<Footer style={{textAlign:'center',color:'#ccc'}}>推荐使用谷歌浏览器,可以获得更佳页面操作体验!</Footer>
</Layout>
</Layout>
)
组件简介: 用来生成菜单的导航栏
组件知识点
将菜单项的数据单独抽离出来,作为一个独立的模块,暴露这个数组 menuList ,其中这个数组的每一项都对应了一个对象
为匹配请求路径所对应的界面,需要设置 AntD 组件中的 Menu 组件的属性:defaultSelectedKeys,但是这里有个小坑,一旦设置为路由路径作为初始选中菜单项之后,它只能改变一次,后续的操作不会生效!根据文档,需要将 defaultSelectedKeys 变为 selectedKeys(根据页面变化重新进行动态匹配菜单项)
针对于有子菜单项的情况,需要在页面打开之后,自动展开其子菜单项。根据文档需要设置 openKey 属性,令其值为当前菜单项的key值即可
运用数组的map以及递归调用的方法根据导入的 menuList 动态生成菜单导航
getMenuNodes = (menuList) => {
// 得到当前请求的路由路径
const path = this.props.location.pathname
return menuList.map(item => {
/*
这里的item的形式如下:
{
title: '首页', //菜单标题名称
key: '/home', //对应的path
icon: 'home', //图标名称
children: [] 可能有也可能没有
},
*/
if (!item.children) {
return (
<Menu.Item key={item.key} icon={item.icon}>
<Link to={item.key}>{item.title}</Link>
</Menu.Item>
)
} else {
// 查找一个与当前请求路径匹配的子item
const cItem = item.children.find(cItem => cItem.key === path)
// 如果存在 说明当前item的子列表需要打开
if (cItem) {this.openKey = item.key}
return (
<SubMenu key={item.key} icon={item.icon} title={item.title}>
{/* 递归调用 */}
{
this.getMenuNodes(item.children)
}
</SubMenu>
)
}
})
}
export default withRouter(LeftNav)
组件简介: 用来展示实时时间与天气情况,还有用户名,实现用户的退出功能以及显示菜单标题名称。
组件知识点
jsonp解决跨域的基本原理将在下面进行详细阐述
// 删除保存的数据
storageUtils.removeUser()
memoryUtils.user = {}
// 跳转到login页面
this.props.history.replace('/login')
getTitle = () => {
// 得到当前请求路径
const path = this.props.location.pathname
let title
menuList.forEach(item => {
if (item.key === path) { //如果当前item对象的key与path匹配 则item的title就是需要显示的title
title = item.title
} else if (item.children) {
// 在所有的子item中进行查找匹配 返回一个布尔值
const cItem = item.children.find(cItem => path.indexOf(cItem.key)===0)
// 如果有值则说明成功匹配 且取出他的title显示
if (cItem) {
title = cItem.title
}
}
})
return title
}
jsonp解决Ajax跨域的原理
来请求后台接口(src 就是接口的 url),定义好用于接收响应数据的函数 fn()
,并将函数名通过请求参数提交给后台(举个栗子:callbabck=fn
)该组件主要是用来显示商品的分类列表,设置自定义组件用来实现添加分类及更新分类的功能,用到 antD 的 Table Modal Select Card 等组件
组件知识点:
if (result.status === 0) {
// 说明成功取出分类数组 (可能是一级的也可能是二级)
const categorys = result.data
if (parentId === '0') {
// 说明是一级列表
// 更新状态
this.setState({categorys})
} else {
// 二级列表
this.setState({subCategorys:categorys})
}
} else {
message.error('获取数据失败')
}
showSubCategorys = (category) => {
// 更新状态----> 异步
this.setState({
parentId: category._id,
parentName: category.name,
},()=>{
// 回调函数会在状态更新并且重新render()后执行
// console.log('parentId',this.state.parentId); parentId,'xxxxx'
// 获取二级分类列表
this.getCategorys()
})
// setState()不能立即获取最新的状态:因为是异步更新状态的
// console.log('parentId',this.state.parentId); parentId,0
}
<UpdateForm categoryName={category.name}
setForm={(form) => {this.form = form}}/>
updateCategory = () => {
// console.log('update',this.form);
// 进行表单验证 只有通过才能验证
this.form.validateFields().then( async values => {
// 1、隐藏显示框
this.setState({
showStatus:0
})
// 准备数据
const categoryId = this.category._id
// console.log('fansili ',this.form);
// const categoryName = this.form.getFieldValue('categoryName')
const {categoryName} = values
// 清除输入文本框数据
this.form.resetFields()
// 2、发请求更新分类
const result = await reqUpdateCategorys({categoryId,categoryName})
if (result.status === 0) {
// 3、重新显示列表
this.getCategorys()
}
}
)
}
该组件主要的功能有: