React学习-角色管理\redux
1 React后台谷粒系统角色管理
1.1 分析架构
首先需要分析架构:
可以看到总体是一个Card, Card的title是一个Button, Card的内部是一个 Table, 依据此可以写出基本的静态页面.
1.2 静态页面搭建(查)
Table需要一个dataSource
和columns
, columns
可以由上图得知有用户名 \ 邮箱 \ 电话 \ 注册时间 \ 所属角色 和 操作, 在constructor中使用一个函数来初始化, 代码如下:
constructor(props) {
super(props)
this.initColumns()
}
initColumns = () => {
this.columns = [
{
title: "用户名",
dataIndex: "username",
},
{
title: "邮箱",
dataIndex: "email",
},
{
title: "电话",
dataIndex: "phone",
},
{
title: "注册时间",
dataIndex: "create_time",
render: formateDate
},
{
title: "所属角色",
dataIndex: "role_id",
},
{
title: "操作",
render: (user) => (
<span>
<LinkButton>修改</LinkButton>
<LinkButton>删除</LinkButton>
</span>
)
}
]
}
其中用户名所对应的是username, 邮箱所对应的是email, 电话所对应的是phone, 注册时间所对应的是craete_time, 可以使用之前所写的工具模块里的formateDate
来格式化时间, 所属角色所对应的是role_id, ,不过我们想对应的是role的name, 暂时先用role_id代替, 操作则是两个自己写的LinkButton, 其没有button边框, 没有下划线, 字体有颜色, 对应着修改和删除用户的功能.
然后我们应该想办法获取到数据来加入到dataSource中, 根据API文档写出接口函数:
// 获取用户列表
export const reqGetUesrsList = () => ajax(BASE + "/manage/user/list")
返回的结果包括所有的用户列表和角色列表, 我们在state中加入users 和 roles并默认设置为空数组, 并在组件被挂载上时调用生命周期钩子来初始化users 和 roles:代码如下:
state = {
users: [], // 所有用户列表
roles: [], // 所有角色的列表
loading: false // 显示是否正在加载
}
componentDidMount() {
this.getUsers()
}
getUsers = async () => {
this.setState({ loading: true })
const result = await reqGetUesrsList()
if (result.status === 0) {
const { users, roles } = result.data
this.initRoleNames(roles)
this.setState({
users,
roles,
loading: false
})
}
}
// 根据role的数组, 生成包含所有角色名的对象
initRoleNames = (roles) => {
// 保存
this.roleNames = roles.reduce((pre, role) => {
pre[role._id] = role.name
return pre
}, {})
}
值得注意的是, 我们为什么要执行initRoleNames
函数呢? 因为上面我们提过, 我们在Table所属角色那列想展示的是角色的名字而不是角色的id, 其中的render确实可以从roles数组中遍历并查找, 但是需要一个一个查找, 效率太慢, 合理的解决方法是, 生成一个对象, 其中的key为角色的id, 值为角色的名字, 通过键值对的方式来提高性能和效率, 此时initColumns
的所属角色那列就可以加上render了:
{
title: "所属角色",
dataIndex: "role_id",
render: (role_id) => this.roleNames[role_id] || "无所属角色"
},
这样我们就可以把基本的静态页面搭建起来了:
import React, { Component } from 'react'
import {
Card,
Button,
Table
} from "antd"
import { formateDate } from '../../utils/dataUtils'
import LinkButton from '../../components/link-button'
import { PAGE_SIZE } from '../../utils/constants'
import { reqGetUesrsList } from "../../api/index"
/*
用户路由
*/
export default class User extends Component {
constructor(props) {
super(props)
this.initColumns()
}
state = {
users: [], // 所有用户列表
roles: [], // 所有角色的列表
loading: false // 显示是否加载
}
initColumns = () => {
this.columns = [
{
title: "用户名",
dataIndex: "username",
},
{
title: "邮箱",
dataIndex: "email",
},
{
title: "电话",
dataIndex: "phone",
},
{
title: "注册时间",
dataIndex: "create_time",
render: formateDate
},
{
title: "所属角色",
dataIndex: "role_id",
render: (role_id) => this.roleNames[role_id] || "无所属角色"
},
{
title: "操作",
render: (user) => (
<span>
<LinkButton}>修改</LinkButton>
<LinkButton}>删除</LinkButton>
</span>
)
}
]
}
// 根据role的数组, 生成包含所有角色名的对象
initRoleNames = (roles) => {
// 保存
this.roleNames = roles.reduce((pre, role) => {
pre[role._id] = role.name
return pre
}, {})
}
getUsers = async () => {
this.setState({ loading: true })
const result = await reqGetUesrsList()
if (result.status === 0) {
const { users, roles } = result.data
this.initRoleNames(roles)
this.setState({
users,
roles,
loading: false
})
}
}
componentDidMount() {
this.getUsers()
}
render() {
const { users, loading, roles } = this.state;
const title = (
<Button type="primary">创建用户</Button>
)
return (
<Card title={title}>
<Table
bordered
rowKey="_id"
dataSource={users}
columns={this.columns}
pagination={{ defaultPageSize: PAGE_SIZE }}
loading={loading}
/>
</Card>
)
}
}
1.3 删除用户(删)
然后我们就要开始写增删改查功能了, 首先我们先写删除功能, 因为这个比较简单, 先写对应的API, 然后在删除按钮处加上点击回调就可以了
// 删除用户
export const reqDeleteUser = (userId) => ajax(BASE + "/manage/user/delete", {userId}, "POST")
// 删除按钮处加的回调, 注意有参数, 是Table该行传入的user信息, 应该使用箭头函数包裹一下
<LinkButton onClick={() => this.deleteUser(user)}>删除</LinkButton>
// 注意此处使用了Modal的confirm方法, 可以简单的做一个确认框
// icon使用的是Antd的Icon库组件
// 删除指定用户
deleteUser = (user) => {
Modal.confirm({
title: `确认删除${user.username}吗?`,
icon: <QuestionCircleOutlined />,
okText: '确认',
cancelText: '取消',
onOk: async () => {
const userId = user._id
const result = await reqDeleteUser(userId)
if (result.status === 0) {
message.success('删除用户成功!')
this.getUsers()
}
}
})
}
此时最终代码如下:
import React, { Component } from 'react'
import {
Card,
Button,
Table,
Modal,
message
} from "antd"
import { formateDate } from '../../utils/dataUtils'
import LinkButton from '../../components/link-button'
import { PAGE_SIZE } from '../../utils/constants'
import { reqDeleteUser, reqGetUesrsList } from "../../api/index"
import { QuestionCircleOutlined } from "@ant-design/icons"
/*
用户路由
*/
export default class User extends Component {
constructor(props) {
super(props)
this.initColumns()
}
state = {
users: [], // 所有用户列表
roles: [], // 所有角色的列表
loading: false // 显示是否加载
}
initColumns = () => {
this.columns = [
{
title: "用户名",
dataIndex: "username",
},
{
title: "邮箱",
dataIndex: "email",
},
{
title: "电话",
dataIndex: "phone",
},
{
title: "注册时间",
dataIndex: "create_time",
render: formateDate
},
{
title: "所属角色",
dataIndex: "role_id",
render: (role_id) => this.roleNames[role_id] || "无所属角色"
},
{
title: "操作",
render: (user) => (
<span>
<LinkButton>修改</LinkButton>
<LinkButton onClick={() => this.deleteUser(user)}>删除</LinkButton>
</span>
)
}
]
}
// 根据role的数组, 生成包含所有角色名的对象
initRoleNames = (roles) => {
// 保存
this.roleNames = roles.reduce((pre, role) => {
pre[role._id] = role.name
return pre
}, {})
}
// 删除指定用户
deleteUser = (user) => {
Modal.confirm({
title: `确认删除${user.username}吗?`,
icon: <QuestionCircleOutlined />,
okText: '确认',
cancelText: '取消',
onOk: async () => {
const userId = user._id
const result = await reqDeleteUser(userId)
if (result.status === 0) {
message.success('删除用户成功!')
this.getUsers()
}
}
})
}
getUsers = async () => {
this.setState({ loading: true })
const result = await reqGetUesrsList()
if (result.status === 0) {
const { users, roles } = result.data
this.initRoleNames(roles)
this.setState({
users,
roles,
loading: false
})
}
}
componentDidMount() {
this.getUsers()
}
render() {
const { users, loading, roles } = this.state;
const title = (
<Button type="primary"}>创建用户</Button>
)
return (
<Card title={title}>
<Table
bordered
rowKey="_id"
dataSource={users}
columns={this.columns}
pagination={{ defaultPageSize: PAGE_SIZE }}
loading={loading}
/>
</Card>
)
}
}
1.4 更新用户和创建用户 (增\ 改)
我们想把创建用户和更新用户放到同一个Modal框下面, 但是我们需要通过一个介质来区分到底是更新页面还是创建用户,先看一下具体的场景吧:
分析一下就能得知更新用户时合理的场景应该是创建用户时是没有用户信息的, 而修改用户时有用户信息, 我们可以存储一下user信息为对象, 并根据对象中是否有键值对来决定是添加用户还是修改用户, 具体的流程还是先写静态页面 -> 写接口API -> 写函数回调, 值得注意的是这次我们写了一个额外的UserForm.jsx来进行组件式编程, 具体的代码如下:
import React, { PureComponent } from 'react';
import PropTypes from "prop-types";
import {
Form,
Input,
Select
} from "antd";
const Item = Form.Item
const { Option } = Select;
// 添加或更新用户的form组件
export default class UserForm extends PureComponent {
// ref 用来创建ref
userRef = React.createRef();
// 组件挂载时将该组件的引用传递给父组件
componentDidMount() {
this.props.setUserForm(this.userRef);
}
static propTypes = {
setUserForm: PropTypes.func.isRequired, // 用来传递form对象的函数
roles: PropTypes.array.isRequired, // 用来接收数组
user:PropTypes.object // 用户数组
}
render() {
// 从props结构出 user 和 roles, 用来初始化
const { roles, user } = this.props
// 栅格式布局
const formItemLayout = {
labelCol: {span: 4},
wrapperCol: {span: 15},
}
return (
<Form ref={ this.userRef } {...formItemLayout}>
<Item
label="用户名称:"
name="username"
initialValue={user.username}
>
<Input placeholder="请输入用户名称" />
</Item>
{
// 如果没有user._id, 则证明user为空, 是添加用户
user._id ? null: (
<Item
label="密码:"
name="password"
initialValue={user.password}
>
<Input placeholder="请输入密码" type="password" />
</Item>
)
}
<Item
label="手机号:"
name="phone"
initialValue={user.phone}
>
<Input placeholder="请输入手机号" />
</Item>
<Item
label="邮箱:"
name="email"
initialValue={user.email}
>
<Input placeholder="请输入邮箱号"/>
</Item>
<Item
label="角色:"
name="role_id"
initialValue={user.role_id}
>
<Select style={{width: 120}} placeholder="请选择角色">
{
roles.map(e => {
return (<Option value={e._id} key={e._id}>{e.name}</Option>)
})
}
</Select>
</Item>
</Form>
)
}
}
Api的新增代码如下:
// 添加更新用户
export const reqAddOrUpdateUser = (user) => ajax(BASE + "/manage/user/" + (user._id ? "update" : "add"),user,"POST");
User组件的代码如下:
import React, { Component } from 'react'
import {
Card,
Button,
Table,
Modal,
message
} from "antd"
import { formateDate } from '../../utils/dataUtils'
import LinkButton from '../../components/link-button'
import { PAGE_SIZE } from '../../utils/constants'
import { reqDeleteUser, reqGetUesrsList, reqAddOrUpdateUser } from "../../api/index"
import { QuestionCircleOutlined } from "@ant-design/icons"
import UserForm from "./UserForm";
/*
用户路由
*/
export default class User extends Component {
constructor(props) {
super(props)
this.initColumns()
}
state = {
users: [], // 所有用户列表
roles: [], // 所有角色的列表
isShow: false, // 是否显示添加或删除的确认框
loading: false // 显示是否加载
}
initColumns = () => {
this.columns = [
{
title: "用户名",
dataIndex: "username",
},
{
title: "邮箱",
dataIndex: "email",
},
{
title: "电话",
dataIndex: "phone",
},
{
title: "注册时间",
dataIndex: "create_time",
render: formateDate
},
{
title: "所属角色",
dataIndex: "role_id",
render: (role_id) => this.roleNames[role_id] || "无所属角色"
},
{
title: "操作",
render: (user) => (
<span>
<LinkButton onClick={() => this.showUpdate(user)}>修改</LinkButton>
<LinkButton onClick={() => this.deleteUser(user)}>删除</LinkButton>
</span>
)
}
]
}
// 根据role的数组, 生成包含所有角色名的对象
initRoleNames = (roles) => {
// 保存
this.roleNames = roles.reduce((pre, role) => {
pre[role._id] = role.name
return pre
}, {})
}
// 删除指定用户
deleteUser = (user) => {
Modal.confirm({
title: `确认删除${user.username}吗?`,
icon: <QuestionCircleOutlined />,
okText: '确认',
cancelText: '取消',
onOk: async () => {
const userId = user._id
const result = await reqDeleteUser(userId)
if (result.status === 0) {
message.success('删除用户成功!')
this.getUsers()
}
}
})
}
// 显示修改页面
showUpdate = (user) => {
this.user = user // 保存user信息
this.setState({ isShow: true })
}
// 显示添加页面
showAdd = () => {
this.user = null // 将user信息清空
this.setState({ isShow: true })
}
// 添加或更新user
addOrUpdateUser = async () => {
// 隐藏Modal框
this.setState({ isShow: false })
// 1. 收集输入数据
const user = this.userRef.current.getFieldValue()
this.userRef.current.resetFields()
// 如果是更新,则添加_id属性
if (this.user && this.user._id) {
user._id = this.user._id
}
// 2.提交添加的请求
const result = await reqAddOrUpdateUser(user);
// 3.更新列表显示
if (result.status === 0) {
message.success(`${this.user ? "修改" : "添加"}用户成功`)
this.getUsers()
}
}
getUsers = async () => {
this.setState({ loading: true })
const result = await reqGetUesrsList()
if (result.status === 0) {
const { users, roles } = result.data
this.initRoleNames(roles)
this.setState({
users,
roles,
loading: false
})
}
}
componentDidMount() {
this.getUsers()
}
render() {
const { users, isShow, loading, roles } = this.state;
const user = this.user || {}
const title = (
<Button type="primary" onClick={this.showAdd}>创建用户</Button>
)
return (
<Card title={title}>
<Table
bordered
rowKey="_id"
dataSource={users}
columns={this.columns}
pagination={{ defaultPageSize: PAGE_SIZE }}
loading={loading}
/>
<Modal
title={user._id ? "修改用户" : "添加用户"}
visible={isShow}
onOk={this.addOrUpdateUser}
onCancel={() => { this.setState({ isShow: false }) }}
destroyOnClose={true}
>
<UserForm
setUserForm={e => this.userRef = e}
roles={roles}
user={user}
/>
</Modal>
</Card>
)
}
}
到这里增删改查就结束了, 但是我们做完用户管理之后, 就应该对每个用户只显示它所属角色所拥有权限的列表, 所以我们要修改左侧导航条显示的逻辑, 根据当前已登录用户的信息来实现.
2 左侧导航条的更改
首先我们要导入utils模块里的menuConfig
和 memoryUtils
, 其中存储着登录的用户信息, 然后改写获取菜单节点的函数, 如下:
/*
根据menu的数据数组生成对应的标签数组
使用map+递归调用
*/
getMenuNodes = (menuConfig) => {
return menuConfig.map(item => {
// 得到当前请求的路由路径
const path = this.props.location.pathname;
// 如果当前用户有item对应的权限,才显示对应的菜单项
if (this.hasAuth(item)) {
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 => path.indexOf(cItem.key) === 0)
// 如果存在,说明当前item的子列表需要打开
if (cItem) {
this.openKey = item.key;
}
return (
<SubMenu key={item.key} icon={item.icon} title={item.title}>
{this.getMenuNodes(item.children)}
</SubMenu>
)
}
} else {
return null
}
})
}
// 判断当前登录用户对item是否有权限
hasAuth = (item) => {
const { key, isPublic } = item
const username = memoryUtils.user.username
const menus = memoryUtils.user.role.menus
// 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)
}
return false
}
这样的话就能实现只显示用户所拥有的菜单节点了:
最后我们需要在更新当前用户所属角色的权限时,自动退出该用户并提示重新登录信息, 只需要做响应的判断, 清空
menuConfig
和 memoryUtils
并跳转即可, 代码如下:
import memoryUtils from '../../utils/memoryUtils';
import storageUtils from '../../utils/storageUtils';
// 设置角色权限
setRoleAuthority = async() => {
this.setState({isShowSet: false})
const role = this.state.role;
// 得到最新的menus
const menus = this.myAuth.current.getMenus()
role.menus = menus
// 请求更新
const result = await reqUpdateRole(role)
if(result.status === 0) {
// 如果当前更新的是自己角色的权限, 强制退出
if(role._id === memoryUtils.user.role_id) {
memoryUtils.user = {} // 将用户信息清空
storageUtils.removeUser() // 删除本地信息
this.props.history.replace("/login")
message.info("当前用户角色权限已更新,请重新登录!")
} else {
message.success("设置权限成功")
this.setState({
roles: [...this.state.roles]
})
}
} else {
message.error("设置权限失败")
}
}
3 Redux学习
3.1 redux理解
3.1.1 redux是什么?
redux
是一个独立专门用于做状态管理的JS库(不是React插件库);- 他是可以用在React, Angular, Vue等项目中, 但基本与React 配合使用;
- 作用: 集中式管理React 应用中多个组件共享的状态.
3.1.2 redux工作流程
3.1.3 什么时候需要使用redux
- 某个组件的状态需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另外一个组件的状态
3.2 redux核心API
3.2.1 createStore
-
作用: 创建包含指定reducer的store对象
-
编码:
import { createStore } from 'redux' import counter from './reducers/counter' const store = createStore(counter)
3.2.2 store对象
-
作用: redux库最核心的管理对象
-
他内部维护着: state reducer
-
核心方法:
getState() dispatch(action) subscribe(listener)
-
编码:
store.getState() store.dispatch({type:'INCREMENT', number}) store.subscribe(render)
3.2.3 applyMiddleware
-
作用:应用上基于redux的中间件(插件库)
-
编码:
import {createStore, applyModdleware} from 'redux' import thunk from 'redux-thunk' const store = createStore( counter, applyMiddleware(thunk) // 应用上异步中间件 )
3.2.4 combineReducers()
- 作用 合并多个reducer函数
- 编码:
export default combineReducers({
user,
chatUser.
})
3.3 redux的三个核心概念
3.3.1action
-
标识要执行行为的对象
-
包含2个方面的属性
- type: 标识属性, 值为字符串, 唯一, 必要属性
- xxx: 数据属性, 值类型任意, 可选属性
-
例子:
const action= { type:'INCREMENT', data:2 }
-
Action Creator(创建Action 的工厂函数)
const increment = (number) => {{type:'INCREMENT', data}, number}
3.3.2 reducer
-
根据老的state和action, 产生新的state的纯函数。
-
样例:
export default function counter(state=0, action) { switch(action.type) { case: 'INCREAMENT': return state + action.data case: 'DECREMENT': return state - action.data default: return state } }
-
注意
a. 返回一个新的状态
b. 不要修改原来的状态
3.3.3 store
-
将state,action与reducer联系在一起的对象
-
如何得到此对象
import { createStore } from 'redux' import counter from './reducers/counter' const store = createStore(counter)
-
此对象的功能?
getState(): 得到state. dispatch(action):分发action, 触发reducer调用, 产生新的state. subscribe(listener): 注册监听, 当产生了新的state时, 自动调用.
3.4 不用redux版本
首先在git上重新建立一个redux分支并且切换到该分支上:
git checkout -b redux
因为我们需要先写一个test案例, 所以我们先把原来的src改名为src-app, 然后重新新建一个src文件夹, 然后新建最核心的两个文件index.js 和 App.jsx, 核心代码很基础, 如下所示:
index.js
/*
入口JS
*/
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// 将App组件标签渲染岛index页面的div上
ReactDOM.render(<App/>, document.getElementById("root"));
App.jsx
import React, { Component } from 'react'
export default class App extends Component {
constructor(props){
super(props)
this.numberRef = React.createRef()
}
state = {
count: 0
}
increment = () => {
const number = this.numberRef.current.value * 1
this.setState(state => ({count: state.count + number}))
}
decrement = () => {
const number = this.numberRef.current.value * 1
this.setState(state => ({count: state.count - number}))
}
incrementIfOdd = () => {
if(this.state.count % 2 !== 0) {
const number = this.numberRef.current.value * 1
this.setState(state => ({count: state.count + number}))
}
}
incrementAsync = () => {
const number = this.numberRef.current.value * 1
setTimeout (() => {
this.setState(state => ({count: state.count + number}))
},1000)
}
render() {
const { count } = this.state
return (
<div>
<p>click {count} times</p>
<br/>
<br/>
<div>
<select ref={this.numberRef}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>add</button>
<button onClick={this.decrement}>dec</button>
<button onClick={this.incrementIfOdd}>add if odd</button>
<button onClick={this.incrementAsync}>add async</button>
</div>
</div>
)
}
}
目前已经可以实现我们想要的功能了:
3.5 reudx初版
要使用redux, 我们首先要创建store和reducer, 具体的步骤是
-
在src下新建一个redux文件夹, 用来存储redux中核心组件, 然后新建store.js和reducer.js.
-
想要新建store对象, 首先要使用redux库中的
createStore
函数和一个reducer. -
那么我们需要先写reducer, 它是根据当前state和指定的action返回一个新的state的函数模块:
注意函数的返回值是一个新的state值, 而不是对原state值进行修改.
/* reducer函数模块:根据当前state和指定的action返回一个新的state */ import {INCREMENT,DECREMENT} from "./action-types" // 管理count状态数据的reducer // 函数名count一般与要管理的state名字相同 // 函数参数state和action是固定的 // state是管理的数据本身,action是动作 export default function count(state=1, action) { console.log("count()", state, action) switch(action.type ) { case INCREMENT: return state + action.data case DECREMENT: return state - action.data default: return state } }
-
注意我们又新建了一个action-types.js文件, 目的是存储一些action type常量名称, 其目前结构如下:
/* 包含n个action type常量名称的模块 */ export const INCREMENT = "increment" export const DECREMENT = "de crement"
-
至此reducer.js文件就暂且写好了, 后续会有修改, 我们现在就可以写出store.js:
值得注意的一点是创建store时内部会第一次调用reducer()来进行初始化, 这儿初始值为1.
/* redux最核心的管理对象:store */ import { createStore } from "redux" import reducer from "./reducer" // 创建store时内部第一次调用reducer()得到初始状态值 export default createStore(reducer)
-
有了store之后我们可以将其作为App组件的prop,在入口文件index.js引入store并传入, 然后App组件也要使用PropTypes做出相应的检测优化,相关代码如下:
// index.js import store from "./redux/store" ReactDOM.render(<App store={store}/>, document.getElementById("root")); // App.js import PropTypes from "prop-types" // ... static propTypes = { store: PropTypes.object.isRequired }
-
这时如果想得到store里的state值,就可以使用
this.props.store.getState()
函数了, 因为我们的state初始值为1, 所以得到的值可以直接使用, 此时打开页面渲染就可以看到count值为1. -
那么下一步是根据action来通过store调用reducer来更新state. 我们需要在redux里新建一个action creators, 通常命名为actions.js, 包含n个用来创建action的工厂函数, 而aciotn实际为包含type和data的对象, type为处理方式, data为值, 据此我们可以写出action.js:
值得注意的是我们再一次用到了常量文件action-type,其可以有效避免因为拼写出错而导致的问题且更浅显易懂:
/* 包含n个用来创建action的工厂函数(action creator) */ import {INCREMENT, DECREMENT} from "./action-types" // 增加的action export const increment = (number) => ({type:INCREMENT,data:number}) // 减少的action export const decrement = (number) => ({type:DECREMENT,data:number})
-
此时我们可以在App组件中将action creators引入, 使用store的dispatch函数来进行数据的更新, 至此, App组件的代码可以暂且写成如下代码:
import React, { Component } from 'react' import PropTypes from "prop-types" import {increment,decrement} from "./redux/actions" export default class App extends Component { static propTypes = { store: PropTypes.object.isRequired } constructor(props){ super(props) this.numberRef = React.createRef() } increment = () => { const number = this.numberRef.current.value * 1 this.props.store.dispatch(increment(number)) } decrement = () => { const number = this.numberRef.current.value * 1 this.props.store.dispatch(decrement(number)) } incrementIfOdd = () => { if(this.props.store.getState() % 2 !== 0) { const number = this.numberRef.current.value * 1 this.props.store.dispatch(increment(number)) } } incrementAsync = () => { const number = this.numberRef.current.value * 1 setTimeout (() => { this.props.store.dispatch(increment(number)) },1000) } render() { const count = this.props.store.getState() return ( <div> <p>click {count} times</p> <br /> <br /> <div> <select ref={this.numberRef}> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button onClick={this.increment}>add</button> <button onClick={this.decrement}>dec</button> <button onClick={this.incrementIfOdd}>add if odd</button> <button onClick={this.incrementAsync}>add async</button> </div> </div> ) } }
-
此时重新渲染, 会发现虽然store里管理的state的值虽然改变了, 但是页面并没有刷新:
-
此时需要我们使用store的另外一个函数subscribe(), 其可以在store管理的state状态数据发生改变时产生一个回调, 我们只需要重新渲染App组件标签就可以了,入口index.js代码如下:
/* 入口JS */ import React from "react"; import ReactDOM from "react-dom"; import store from "./redux/store" import App from "./App"; // 将App组件标签渲染岛index页面的div上 ReactDOM.render(<App store={store}/>, document.getElementById("root")); // 给store绑定状态更新的监听 store.subscribe(() => { // store内部的状态数据发生改变时回调 // 重新渲染App组件标签 ReactDOM.render(<App store={store}/>, document.getElementById("root")); })
至此, 我们就实现了第一版redux, 虽然代码量增加了一倍, 但是他在工程量越大的时候, 会越凸显出优势.
目前redux代码存在的问题
- redux 与 react 组件的代码耦合度太高;
- 编码不够简洁.
3.6 react-redux
3.6.1 理解
- 一个react插件库
- 专门用来简化react应用中使用redux
3.6.2 React-Redux将所有组件分分两大类
-
UI组件
a. 只负责UI的呈现,不带有任何业务逻辑.
b. 通过props接收数据(一般数据和函数).
c. 不适用任何Redux 的API.
d. 一般保存在components 文件夹下.
-
容器组件
a. 负责管理数据和业务逻辑, 不负责UI的呈现.
b. 使用Redux的API.
c. 一般保存在containers 文件夹下.
3.6.3 相关API
-
Provider
// 让所有组件都可以得到state组件 <Provider store={store}> <App /> </Provider>
-
connect()
// 用于包装UI组件生成容器组件 connect( mapStateToprops, mapDispatchToProps )(Counter)
-
mapStateToProps()
// 函数: 将state数据转换为UI组件的标签属性 function mapStateToProps(state) { return { count: state, } }
-
mapDispatchToProps()
// 函数:用来将包含dispatch代码的函数映射成UI组件的函数属性的函数 function mapDispatchToProps(dispatch) { return { increment: (number) => dispatch(increment(number)), decrement: (number) => dispatch(decrement(number)) } }
3.6.4 使用react-redux
-
首先将App中的props写为state中的一般属性和dispatch的函数属性
static propTypes = { count: PropTypes.number.isRequired, increment:PropTypes.func.isRequired, decrement:PropTypes.func.isRequired }
-
然后在src下建立components文件夹和containers文件夹, 在components文件夹下新建一个Counter.jsx组件, 将目前App.jsx的代码复制过去, 然后将App.jsx文件移动到containers文件夹下, 删除所有代码, 替换为如下代码:
import React, { Component } from 'react' import {connect} from "react-redux" import Counter from "../components/Counter" import {increment, decrement} from "../redux/actions" /* 容器组件:通过connect包装UI组件产生的组件 connect():高阶函数, 返回一个函数 connect()返回的函数是一个高阶组件:接收一个UI组件,生成一个容器组件 容器组件的责任是向UI组件传入特定的属性 */ /* 用来将redux管理的state数据映射成UI组件的一般属性的函数 */ function mapStateToProps(state) { return { count: state, } } /* 用来将包含dispatch代码的函数映射成UI组件的函数属性的函数 */ function mapDispatchToProps(dispatch) { return { increment: (number) => dispatch(increment(number)), decrement: (number) => dispatch(decrement(number)) } } export default connect( mapStateToProps, // 指定一般属性 mapDispatchToProps // 指定函数属性 )(Counter)
-
有了此基础代码之后, 便可以进行优化, 形成精简版:
import {connect} from "react-redux" import Counter from "../components/Counter" import {increment, decrement} from "../redux/actions" export default connect( state => ({count:state}), { increment, decrement } )(Counter)
-
此时入口文件也不需要我们订阅了, 只要使用react-redux里的Provider就可以了:
/* 入口JS */ import React from "react"; import ReactDOM from "react-dom"; import {Provider} from "react-redux" import store from "./redux/store" import App from "./containers/App"; // 将App组件标签渲染岛index页面的div上 ReactDOM.render(( <Provider store={store}> <App/> </Provider> ), document.getElementById("root"));
3.7 redux 异步编程
3.7.1 下载redux插件(异步中间件)
npm install redux-thunk
3.7.2 redux/stiore.js
// redux最核心的管理对象store
import { createStore,applyMiddleware } from "redux"
import thunk from "redux-thunk"
import reducer from "./reducer"
export default createStore(reducer, applyMiddleware(thunk))
3.7.3 在action creators里面添加异步action代码
// 异步增加的action: 返回的是函数
export const incrementAsync = number => {
return dispatch => {
// 1.执行异步代码, ajax请求, promise
setTimeout(() => {
// 2. 当前异步任务执行完成时, 分发一个同步action
dispatch(increment(number))
}, 1000)
}
}
3.7.4 在containers的App.jsx里将该异步action添加到mapDispatchToProps里
import {connect} from "react-redux"
import Counter from "../components/Counter"
import {increment, decrement,incrementAsync} from "../redux/actions"
export default connect(
state => ({count:state}),
{
increment,
decrement,
incrementAsync
}
)(Counter)
3.7.5 最后在Counter里检查并使用
static propTypes = {
count: PropTypes.number.isRequired,
increment:PropTypes.func.isRequired,
decrement:PropTypes.func.isRequired,
incrementAsync:PropTypes.func.isRequired,
}
incrementAsync = () => {
const number = this.numberRef.current.value * 1
this.props.incrementAsync(number)
}
3.8 使用redux调试工具
3.8.1 安装浏览器插件
3.8.2 下载工具依赖包
npm install redux-devtools-extension
3.8.3 编码
/*
redux最核心的管理对象:store
*/
import { createStore,applyMiddleware } from "redux"
import thunk from "redux-thunk"
import {composeWithDevTools} from "redux-devtools-extension"
import reducer from "./reducer"
// 创建store时内部第一次调用reducer()得到初始状态值
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
3.9 使用combineReducers整合多个reducer
在实际开发环境中我们可能不止管理一个state, 可能会管理多个state, 而使用combineReducers 可以将他们整合起来统一暴露, 就像是一个菜馆里有的人负责做汤, 有的人负责做菜等, 但是她们又同属于一个菜馆并为客人提供服务.
3.9.1 在reducer.js中写多个管理状态的reducer,然后组合起来
/*
reducer函数模块:根据当前state和指定的action返回一个新的state
*/
import {INCREMENT,DECREMENT} from "./action-types"
import {combineReducers} from "redux"
// 管理count状态数据的reducer
function count(state=1, action) {
console.log("count()", state, action)
switch(action.type ) {
case INCREMENT:
return state + action.data
case DECREMENT:
return state - action.data
default:
return state
}
}
const initUser = {}
// 用来管理User状态数据的reducer
function user(state= initUser, action) {
switch(action.type) {
default:
return state
}
}
/*
combineReducers函数: 接收包含所有reducer函数的对象, 返回一个新的reducer(总reducer)
总的reducer函数管理的state的结构:
{
count: 1,
user: {}
}
*/
export default combineReducers({
count,
user
})
3.9.2 在container的App容器组件中将正确的值引用
import {connect} from "react-redux"
import Counter from "../components/Counter"
import {increment, decrement,incrementAsync} from "../redux/actions"
export default connect(
state => ({
count:state.count,
user:state.user
}),
{
increment,
decrement,
incrementAsync
}
)(Counter)
4 项目中使用redux
4.1 项目中部署redux
-
首先确保安装了redux所用的的所用包, 包括redux, react-redux, redux-thunk 和 redux-devtools-extension.
-
在src文件夹下创建redux文件夹,然后把store, reducer, action 和 action-type建立出来并初始化.
-
store.js
/* redux最核心的管理对象:store */ import {createStore, applyMiddleware} from "redux" import thunk from "redux-thunk" import {composeWithDevTools} from "redux-devtools-extension" import reducer from "./reducer" export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
-
reducer.js, 注意我们想要管理的共有两个state, 分别为用户的信息和头部的标题
/* 用来根据老的state和指定的action来生成返回新的state */ import { combineReducers } from "redux" import storageUtils from "../utils/storageUtils" // 用来管理头部标题的reducer函数 const initHeadTitle = "首页" function headTitle(state=initHeadTitle, action) { switch(action.type) { default: return state } } // 用来管理当前登录用户的reducer函数 const initUser = storageUtils.getUser() function user(state=initUser, action) { switch(action.type) { default: return state } } /* 向外默认暴露的是合并产生的总的reducer函数 管理的总的state对象结构: { headTitle:"首页", user:storageUtils.getUser() } */ export default combineReducers({ headTitle, user })
-
action.js 和 action-type暂时为空, 等实际用到了再进行编码.
-
在入口文件中使用Provider 将 App组件包裹,
/* 入口JS */ import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import memoryUtils from "./utils/memoryUtils"; import storageUtils from "./utils/storageUtils"; import { Provider } from "react-redux"; import store from "./redux/store"; // 读取local中保存的user, 保存在内存中 const user = storageUtils.getUser(); memoryUtils.user = user; // 将App组件标签渲染岛index页面的div上 ReactDOM.render(( <Provider store={store}> <App /> </Provider> ), document.getElementById("root"));
-
运行, 可以在开发者工具redux中看到初始值,说明部署完成:
4.2 使用redux管理头部标题
-
我们需要写一个修改headTitle的action,所以我们在action 和action-type中添加并在reducer中添加对应的case,代码如下所示:
action-type.js:
/* 包含 n个action 的type 常量标识名称的模块 */ // 设置头部标题 export const SET_HEAD_TITLE = "set_head_title"
action.js:
/* 包含n个action creator 函数的模块 同步action 对象{type:"xxx",data: 数据值} 异步action 函数 dispatch => { // 异步代码, 最终返回同步action } */ import {SET_HEAD_TITLE} from "./action-type" // 设置头部标题的同步action export const setHeadTitle = (headTitle) => ({type:SET_HEAD_TITLE,data:headTitle})
reducer.js中相关代码:
import {SET_HEAD_TITLE} from "./action-type" // 用来管理头部标题的reducer函数 const initHeadTitle = "首页" function headTitle(state=initHeadTitle, action) { switch(action.type) { case SET_HEAD_TITLE: return action.data default: return state } }
-
据分析可知, 我们共有两个组件(Header 和 LeftNav)需要读写headTitle, 所以需要这两个UI组件包装成容器组件, 我们这次直接在这两个index.jsx中包装.
-
我们首先分析Header组件, 这个组件只需要读取headtitle, 而不会 修改header, 相关代码如下:
import { connect} from "react-redux" // ... class Header extends Component { // ... render(){ const title = this.props.headTitle } } export default connect( state => ({ headTitle:state.headTitle }), {} )(withRouter(Header));
-
然后是LeftNav组件, 这个组件不需要读取headTitle, 但是需要在点击不同导航时修改headTitle的值,所以我们不仅需要导入connect, 还需要导入必要的action creator.
import { connect } from "react-redux" import {setHeadTitle} from "../../redux/actions" // ... class LeftNav extends Component { // ... <Link to={item.key} onClick={() => this.props.setHeadTitle(item.title)}> {item.title} </Link> // ... } export default connect( state => ({}), { setHeadTitle } )(withRouter(LeftNav));
-
此时去测试, 发现还有小问题, 就是当我们在某一路由上刷新页面时, 发现会出现路由不准确的情况(还是主页), 所以我们需要在加载前更新headTitle状态:
if(!item.key === path || path.indexOf(item.key) === 0) { // 更新headTitle状态 this.props.setHeadTitle(item.title) }
-
然后我们发现再退出登录, 然后再登录的时候headTitle还是上次保存的状态, 所以我们要找到Login组件中登录成功后的代码逻辑, 发现直接跳转到路由
("/")
, 而合理的场景应该是跳转后到主页("/home")
, 所以修改为如下代码:// 跳转到后台管理界面 不需要再回退回来 this.props.history.replace("/home")
-
至此, 代码优化完毕, 测试一下功能, 暂时没发现问题.
4.3 使用redux管理用户数据
之前用的方法是存在memoryUtils, 比较low, 我们将其写在redux中更好一些.
-
经过分析, 我们需要有登录的异步action, 成功登录的用户数据同步action 、显示错误信息的同步action和退出登录的action.
action-type.js相关代码
// 接收用户消息 export const RECEIVE_USER = "receive_user" // 显示错误信息 export const SHOW_ERROR_MSG = "show_error_msg" // 重置用户信息 export const RESET_USER = "reset_user"
action.js相关代码
import {SET_HEAD_TITLE, RECEIVE_USER, SHOW_ERROR_MSG, RESET_USER} from "./action-type" import { reqLogin } from "../api/index" import storageUtils from "../utils/storageUtils" // 请求成功的用户数据同步action export const receiveUser = (user) => ({type:RECEIVE_USER, user}) // 请求失败的显示错误信息同步action export const showErrorMsg = (errorMsg) => ({type:SHOW_ERROR_MSG, errorMsg}) // 退出登录的同步action export const logout = () => { // 清除loacl中的user storageUtils.removeUser() // 返回action对象 return {type: RESET_USER} } // 登录的异步action export const login = (username, password) => { return async dispatch => { // 1. 执行异步ajax请求 const result = await reqLogin(username,password); // {status: 0 data: user} {status:1, msg} // 2.1 如果成功, 分发成功的同步action if(result.status === 0) { const user = result.data // 保存到loacl中 storageUtils.saveUser(user) // 分发接收用户的同步action dispatch(receiveUser(user)) } // 2.2 如果失败, 分发失败的同步action else { const msg = result.msg dispatch(showErrorMsg(msg)) } } }
reducer.js相关代码
import {SET_HEAD_TITLE, RECEIVE_USER, SHOW_ERROR_MSG, RESET_USER} from "./action-type" // 用来管理当前登录用户的reducer函数 const initUser = storageUtils.getUser() function user(state=initUser, action) { switch(action.type) { case RECEIVE_USER: return action.user case SHOW_ERROR_MSG: const errorMsg = action.errorMsg return {...state, errorMsg} //return {...state, errorMsg} case RESET_USER: return {} default: return state } }
-
经过查找, 发现Admin\ Role \ Header 和 Login组件使用到了memoryUtils,分别修改即可
Admin.jsx
import React, { Component } from 'react' import { Redirect, Route, Switch } from 'react-router'; import { Layout } from 'antd'; import LeftNav from "../../components/left-nav" import Header from "../../components/header" import Home from "../Home"; import Category from "../Category"; import Product from "../Product"; import Role from "../Role"; import User from "../User"; import Bar from "../Charts/Bar"; import Line from "../Charts/Line"; import Pie from "../Charts/Pie"; import { connect } from 'react-redux'; const { Footer, Sider, Content } = Layout; /* 后台管理的路由组件 */ class Admin extends Component { render() { const user = this.props.user; if(!user || !user._id) { // 自动跳转到登录(在render里) return <Redirect to="/login"/>; } return ( <Layout style ={{minHeight:"100%"}}> <Sider><LeftNav /></Sider> <Layout> <Header></Header> <Content style={{margin:20 ,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="/charts/bar" component={Bar}/> <Route path="/charts/line" component={Line}/> <Route path="/charts/pie" component={Pie}/> <Redirect to="/home" /> </Switch> </Content> <Footer style={{textAlign:"center", color:"#cccccc"}}>推荐使用谷歌浏览器,可以获得更佳页面操作体验</Footer> </Layout> </Layout> ) } } export default connect( state => ({ user:state.user }), {} )(Admin)
Login.jsx:
/* 登录的路由组件 */ import React, { Component } from 'react' import { Form, Input, Button } from 'antd'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; import "./login.less" import logo from "../../assets/images/logo.png" import { Redirect } from 'react-router'; import { connect } from "react-redux" import {login} from "../../redux/actions" class Login extends Component { // onFinish仅校验通过后才能执行 onFinish = (values) => { const {username,password} = values; // console.log("提交登录的ajax请求:","username = ",username,"password = ", password); // 调用分发异步action的函数 发登录的异步请求, 有结果后更新状态 this.props.login(username, password) } render() { // 如果用户已经登录,自动跳转到管理界面 const user = this.props.user; if(user && user._id) { return <Redirect to="/home"/> } return ( <div className="login"> <header className="login-header"> <img src={logo} alt="logo" /> <h1>React项目: 后台管理系统</h1> </header> <section className="login-content"> <div className={user.errorMsg ? "error-msg show": "error-msg"}> {user.errorMsg} </div> <h2>用户登录</h2> <Form name="normal_login" className="login-form" initialValues={{ remember: true, }} onFinish={this.onFinish} ref={this.formRef} > <Form.Item name="username" // 声明式验证,直接使用别人定义好的验证规则进行验证 rules={[ { required: true, message: '请输入用户名', }, { min: 4, message: "用户名至少4位" }, { max: 12, message: "用户名最多12位" }, { pattern:"^[A-z0-9_]+$", message:"用户名必须由英文、数字、和下划线构成" } /* { validator: this.validateUsN } */ ]} > <Input prefix={<UserOutlined className="site-form-item-icon" style={{color: "rgba(0, 0, 0, .25)"}} />} placeholder="用户名" /> </Form.Item> <Form.Item name="password" // 声明式验证,直接使用别人定义好的验证规则进行验证 rules={[ { required: true, message: '请输入密码', }, { min: 4, message: "密码至少4位" }, { max: 12, message: "密码最多12位" }, { pattern:"^[A-z0-9_]+$", message:"密码必须由英文、数字、和下划线构成" } ]} > <Input prefix={<LockOutlined className="site-form-item-icon" style={{color: "rgba(0, 0, 0, .25)"}} />} type="password" placeholder="密码" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" className="login-form-button"> 登录 </Button> </Form.Item> </Form> </section> </div> ) } } export default connect( state => ({ user: state.user }), { login } )(Login)
Role.jsx相关代码
import { connect } from 'react-redux'; import { logout } from '../../redux/actions'; // ... // 请求更新 const result = await reqUpdateRole(role) if(result.status === 0) { // 如果当前更新的是自己角色的权限, 强制退出 if(role._id === this.props.user.role_id) { this.props.logout() message.info("当前用户角色权限已更新,请重新登录!") } else { message.success("设置权限成功") this.setState({ roles: [...this.state.roles] }) } } else { message.error("设置权限失败") } export default connect( state => ({user:state.user}), { logout } )(Role)
Header.jsx相关代码:
import { connect} from "react-redux" import {logout} from "../../redux/actions" // ... /* 退出登录 */ loginOut = () => { // 显示确认框 confirm({ title: '确认退出吗?', icon: <QuestionCircleOutlined />, okText: "退出", cancelText: "取消", onOk: () => {; // 清除保存的 user 数据 this.props.logout() } }); } // ... export default connect( state => ({ headTitle:state.headTitle, user:state.user }), {logout} )(withRouter(Header));