一、搭建环境
恢复原项目src后操作Le119
1.用cmd创建文件、文件夹
在src内建立一个a.bat把以下命令放进去即可
#建立2个文件夹
mkdir redux #containers(暂不用)
#建立5个空文件
#type nul> containers\App.js
type nul> redux\store.js
type nul> redux\reducer.js
type nul> redux\actions.js
type nul> redux\action-type.js
#移动app.js到components并重命名为container.js(暂时不要)
#move App.js components\container.js
#或建立文件并向其中写入内容
#echo 包含n个action模块的函数> redux\actions.js
2.安装redux及相关环境
cnpm install --save redux react-redux redux-thunk
#开发者工具-dev表示开发环境才有
cnpm install --save-dev redux-devtools-extension
或
yarn add redux react-redux redux-thunk redux-devtools-extension
二、基础代码部分
1.redux/store.js
//store:redux核心管理对象
import {createStore,applyMiddleware} from 'redux' //创建store及中间件工具
import thunk from 'redux-thunk' //异步工具
import {composeWithDevTools} from 'redux-devtools-extension' //开发者工具
import reducer from './reducer.js' //根据action处理state函数
//创建一个store
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
2.reducer.js
/*根据老的state和指定的action生成并返回新的state的函数*/
import {combineReducers} from 'redux' //用于合并多个reducer为一个,没有多个reducer则直接导出对应函数即可
import storageUtils from '../utils/storageUtils.js'
//用来控制头部显示标题的状态
const initHeadTitle='首页2'
function headTitle(state=initHeadTitle,action){
switch(action.type){
default:
return state
}
}
//用来管理登录用户的reducer函数
const initUser=storageUtils.getUser() //从从localSorage读取user
function user(state=initUser,action){
switch(action.type){
default:
return state
}
}
/*导出多个reducer函数
向外默认暴露的是合并产生的总的reducer函数管理的总的state的结构:
{ headTitle: '首页',
user: {}
}*/
export default combineReducers({
headTitle,
user
})
3.index.js
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'//[1]
import store from './redux/store.js'//[2]
import App from './App'
import memoryUtils from './utils/memoryUtils'
import storageUtils from './utils/storageUtils'
// 读取local中保存user, 保存到内存中
const user = storageUtils.getUser()
memoryUtils.user = user
//[3]加provider和store
ReactDOM.render((
<Provider store={store}>
<App/>
</Provider>
),document.getElementById('root'))
效果:npm start 打开网址 http://localhost:3000/home,显示redux工具及,首页2及可
三、头部标题用redux实现
首先打开:pages\left\index.js(左导航组件)、pages\header\index.js(头部组件)
1.actions.js
import {SET_HEAD_TITLE} from './action-type.js'
export const set_head_title=(headTitle)=>({type:SET_HEAD_TITLE,data:headTitle})
2.action-type.js
export const SET_HEAD_TITLE='set_head_title' //设置头部标题
3.reducer.js
[1]-[3]
/*根据老的state和指定的action生成并返回新的state的函数*/
import {combineReducers} from 'redux' //用于合并多个reducer为一个,没有多个reducer则直接导出对应函数即可
import storageUtils from '../utils/storageUtils.js'
import {SET_HEAD_TITLE} from './action-type.js'
//用来控制头部显示标题的状态
const initHeadTitle='首页'
function headTitle(state=initHeadTitle,action){
switch(action.type){
//[1]添加据action返回不同数据
case SET_HEAD_TITLE:
return action.data
default:
return state
}
}
//用来管理登录用户的reducer函数
const initUser=storageUtils.getUser() //从从localSorage读取user
function user(state=initUser,action){
switch(action.type){
default:
return state
}
}
/*导出多个reducer函数
向外默认暴露的是合并产生的总的reducer函数
管理的总的state的结构:
{
headTitle: '首页',
user: {}
}
*/
export default combineReducers({
headTitle,
user
})
4.pages\header\index.js读取redux的state
import React,{Component} from 'react'
import {connect} from 'react-redux' //[1]引入连接react和redux函数
import './header.less'
import {formateDate} from '../../../utils/dateUtils.js' //时间格式化工具
import memoryUtils from '../../../utils/memoryUtils' //内存中存取用户信息工具 默认导出,不用加花括号
import storageUtils from '../../../utils/storageUtils' //删除localstorage中的用户登录数据
import {reqWeather} from '../../../api/index' //引入接口函数,非默认导出,加花括号
import {withRouter} from 'react-router-dom' //用于包装当前组件,使其具体路由的3属性history
import menuList from '../../../config/menuConfig.js' //导入导航配置菜单
import {Modal} from 'antd'
import LinkButton from '../../../components/link-button/index'
class Header extends Component{
state={
curentTime:formateDate(Date.now()), //当前时间格式化后的字符串
dayPictureUrl:'', //天气小图标地址
weather:'', //天气文字
}
// 获取路径
// getPath=()=>{
// }
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)
// 如果有值才说明有匹配的
if(cItem) {
// 取出它的title
title = cItem.title
}
}
})
return title
}
//异步获取天气
getWeather = async () => {
//解构天气小图标,天气
const {dayPictureUrl, weather} = await reqWeather('徐州')
//更新状态
this.setState({dayPictureUrl, weather})
}
// 每过一秒获取一次系统时间
getTime=()=>{
//定时器函数setInterval()
this.intervalId = setInterval(()=>{
let curentTime=formateDate(Date.now()) //获取当前时间并格式化为字符串
this.setState({curentTime})
},1000)
}
//退出登录
loginOut=()=>{
Modal.confirm({
title: '确定要退出登录吗?',
content: '是请点确定,否则点取消',
onOk:()=> {//改成前头函数,因为下面要用到this.props.history.replace()
console.log('OK');
//删除localstorage中登录信息。及内存中登录信息
storageUtils.removeUser()
memoryUtils.user={}
//跳转到登录页面,用替换因为无需退回
this.props.history.replace('/login')
}//,取消时什么也不做,所以可省略不写
// onCancel() {
// console.log('Cancel');
// },
})
}
//在第一次render()之后执行一次
//一般在此执行异步操作: 发ajax请求启动定时器
componentDidMount(){
this.getTime();
this.getWeather();
}
/*
当前组件卸载之前调用清除定时器,避免其造成警告信息
*/
componentWillUnmount () {
// 清除定时器
clearInterval(this.intervalId)
}
render(){
//解构state内的数据
const {curentTime,dayPictureUrl,weather} = this.state
//获取用户名
const username = memoryUtils.user.username
// 得到当前需要显示的title
//const title = this.getTitle() 去除原来代码
//[3]新读headtitle方式
const title = this.props.headTitle
return(
<div className='header'>
<div className='header-top'>
<span>欢迎,{username}</span>
{/* href='javascript:' */}
<LinkButton onClick={this.loginOut}>退出</LinkButton>
</div>
<div className='header-bottom'>
<div className='header-bottom-left'>
<span>{title}</span>
</div>
<div className='header-bottom-right'>
<span>{curentTime}</span>
<img src={dayPictureUrl} alt='天气'/>
<span>{weather}</span>
</div>
</div>
</div>
)
}
}
//[2]把headTitle传给header组件
export default connect(
state =>({headTitle:state.headTitle}),
{}
)(withRouter(Header))
效果:显示首页,但点其它地方还不能自动改变
5.pages\left\index.js通过action-reducer写入state
【1】-【4】
import React,{Component} from 'react'
import {connect} from 'react-redux' //【1】引入连接函数
import {Link,withRouter} from 'react-router-dom' //withRouter:高阶函数,用于把非路由组件包装成路由组件
import './left.less'
import logo from '../../../assets/images/logo.png'
import { Menu, Icon } from 'antd';
import menuList from '../../../config/menuConfig.js'
import memoryUtils from '../../../utils/memoryUtils'
import {setHeadTitle} from '../../../redux/actions.js'//【2】引入action
const { SubMenu } = Menu;
class LeftNav extends Component{
state = {
collapsed: false,
};
// 控制左侧导航收缩
toggleCollapsed = () => {
this.setState({
collapsed: !this.state.collapsed,
});
};
// 根据配置文件自动写入左侧导航到页面
getMenuItem_map=(menuList)=>{
// 得到当前请求的路由路径
const path = this.props.location.pathname
return menuList.map(item=>{
if(!item.children){
return(
<Menu.Item key={item.key}>
<Link to={item.key}>
<Icon type={item.icon}/>
<span>{item.title}</span>
</Link>
</Menu.Item>
)
}else{
// 查找一个与当前请求路径匹配的子Item
const cItem = item.children.find(cItem => path.indexOf(cItem.key)===0)
// 如果存在, 说明当前item的子列表需要打开
if (cItem) {
this.openKey = item.key
}
return(
<SubMenu
key={item.key}
title={
<span>
<Icon type={item.icon}/>
<span>{item.title}</span>
</span>
}
>
{this.getMenuItem(item.children)}
</SubMenu>
)
}
})
}
//判断当前登陆用户对item是否有权限
hasAuth = (item) => {
const {key, isPublic} = item //取出key,菜单是否是公共的(无需权限也可见)
const menus = memoryUtils.user.role.menus //得到对应角色拥有的菜单
const username = memoryUtils.user.username //得到当前登录用户名
/*
1. 如果当前用户是admin
2. 如果当前item是公开的
3. 当前用户有此item的权限: key有没有存在于menus中
*/
if(username==='admin' || isPublic || menus.indexOf(key)!==-1) {
return true
} else if(item.children){ // 4. 如果当前用户有此item的某个子item的权限
return !!item.children.find(child => menus.indexOf(child.key)!==-1) //!!:强制转换成bool类型值
}
return false
}
//getMenuItem用reduce函数重写方便对每一条进行控制
getMenuItem=(menuList)=>{
const path=this.props.location.pathname //得到当前请求路径
return menuList.reduce((pre,item)=>{
// 如果当前用户有item对应的权限, 才需要显示对应的菜单项
if (this.hasAuth(item)) {
if(!item.children){//1.没有子菜单添加:
pre.push((
<Menu.Item key={item.key}>
{/**【4】点击时回调action去reducer更新state */}
<Link to={item.key} onClick={()=>this.props.setHeadTitle(item.title)}>
<Icon type={item.icon}/>
<span>{item.title}</span>
</Link>
</Menu.Item>
))
}else{//2.有子菜单
// 查找一个与当前请求路径,是否匹配的子Item
const cItem = item.children.find(cItem => path.indexOf(cItem.key)===0)
// 如果存在, 说明当前item的子列表需要展开
if (cItem) {
this.openKey = item.key
}
// 向pre添加<SubMenu>
pre.push((
<SubMenu
key={item.key}
title={
<span>
<Icon type={item.icon}/>
<span>{item.title}</span>
</span>
}
>
{this.getMenuItem(item.children)}
</SubMenu>
))
}
}
return pre
},[])
}
/*
在第一次render()之前执行一次
为第一个render()准备数据(必须同步的)
*/
componentWillMount () {
this.menuNodes = this.getMenuItem(menuList)
}
render(){
// 得到当前请求的路由路径
let path=this.props.location.pathname
// 得到需要打开菜单项的key
const openKey = this.openKey
return (
<div className='left'>
<Link to='/home' className='left-header'>
<img src={logo} alt='logo' />
<h1>深蓝管理后台</h1>
</Link>
<Menu
selectedKeys={[path]}
defaultOpenKeys={[openKey]}
mode="inline"
theme="dark"
>{/*inlineCollapsed={this.state.collapsed}*/}
{this.menuNodes}
</Menu>
</div>
)
}
}
/*用withRouter高阶组件:
包装非路由组件, 返回一个新的组件
新的组件向非路由组件传递3个属性: history/location/match
*/
//【3】容器组件connect,把action传给reducer用于改变state,把当前组件包装起来,实现和redux的连接
export default connect(
state=>({}),
{setHeadTitle}
)(withRouter(LeftNav))
效果:点左导航头自动显示对应标题
四、问题&修复
1.当刷新页面时,当前不论在哪都只显示“首页”
pages/admin/left/index.js
【1】处加个if语句判断
//getMenuItem用reduce函数重写方便对每一条进行控制
getMenuItem=(menuList)=>{
const path=this.props.location.pathname //得到当前请求路径
return menuList.reduce((pre,item)=>{
// 如果当前用户有item对应的权限, 才需要显示对应的菜单项
if (this.hasAuth(item)) {
//【1】判断item是否是当前对应的item
if (item.key===path || path.indexOf(item.key)===0) {
// 更新redux中的headerTitle状态
this.props.setHeadTitle(item.title)
}
if(!item.children){//1.没有子菜单添加:
pre.push((
<Menu.Item key={item.key}>
{/**点击时回调action去reducer更新state */}
<Link to={item.key} onClick={()=>this.props.setHeadTitle(item.title)}>
<Icon type={item.icon}/>
<span>{item.title}</span>
</Link>
</Menu.Item>
))
}else{//2.有子菜单
// 查找一个与当前请求路径,是否匹配的子Item
const cItem = item.children.find(cItem => path.indexOf(cItem.key)===0)
// 如果存在, 说明当前item的子列表需要展开
if (cItem) {
this.openKey = item.key
}
// 向pre添加<SubMenu>
pre.push((
<SubMenu
key={item.key}
title={
<span>
<Icon type={item.icon}/>
<span>{item.title}</span>
</span>
}
>
{this.getMenuItem(item.children)}
</SubMenu>
))
}
}
return pre
},[])
}
Last:附件
项目目录结构
│ App.js
│ index.js
│
├─api
│ ajax.js
│ index.js
│
├─assets
│ └─images
│ logo.png
│
├─components
│ │
│ └─link-button
│ index.jsx
│ index.less
│
├─config
│ menuConfig.js
│
│
│
├─pages
│ ├─admin
│ │ │ admin.jsx
│ │ │
│ │ ├─category
│ │ │ add-cate-form.jsx
│ │ │ index.jsx
│ │ │ index.less
│ │ │ update-cate-form.jsx
│ │ │
│ │ ├─charts
│ │ │ ├─bar
│ │ │ │ index.jsx
│ │ │ │ index.less
│ │ │ │
│ │ │ ├─line
│ │ │ │ index.jsx
│ │ │ │ index.less
│ │ │ │
│ │ │ └─pie
│ │ │ index.jsx
│ │ │ index.less
│ │ │
│ │ ├─header
│ │ │ header.less
│ │ │ index.jsx
│ │ │
│ │ ├─home
│ │ │ index.jsx
│ │ │ index.less
│ │ │
│ │ ├─left
│ │ │ index.jsx
│ │ │ left.less
│ │ │
│ │ ├─product
│ │ │ add-update.jsx
│ │ │ detail.jsx
│ │ │ home.jsx
│ │ │ index.jsx
│ │ │ pictures-wall.jsx
│ │ │ product.less
│ │ │ rich-text.jsx
│ │ │
│ │ ├─role
│ │ │ addForm.jsx
│ │ │ authForm.jsx
│ │ │ index.jsx
│ │ │ index.less
│ │ │
│ │ └─user
│ │ add-form.jsx
│ │ index.less
│ │ user.jsx
│ │
│ └─login
│ │ login.jsx
│ │ login.less
│ │
│ └─images
│ bg.jpg
│
├─redux
│ action-type.js
│ actions.js
│ reducer.js
│ store.js
│
└─utils
constans.js
dateUtils.js
memoryUtils.js
storageUtils.js