《React后台管理系统实战:十》Redux项目实战(一):搭建redux环境、用redux管理状态控制头部标题

49 篇文章 6 订阅

一、搭建环境

恢复原项目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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值