五、react-router
路由理解
SPA: 单页Web应用(Single Page Application),整个应用只有一个完整的页面。点击页面中的链接不会刷新页面,只会做页面的局部更新。数据都需要通过ajax请求获取,并在前端异步展现。
路由: 一个路由就是一个映射关系(key-value)。key是路径,value可能是function或者component
路由的分类
-
后端路由
- 理解:value是function,用来处理客户端提交的请求
- 注册路由:router.get(path, function(req, res))
- 工作过程:当node接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应数据
-
前端路由
- 浏览器端路由,value是component,用于展示页面内容
- 注册路由: <Router path="/text" component={Test}>(6版本component变为了element)
- 工作过程:当浏览器的path变为/test时,当前路由组件就会变为Test组件
react-router API
react的一个插件库,专门用来实现一个SPA应用。
react-router-dom 安装(我这里是6.x版本):npm i -S react-router-dom
相关API
- 内置组件
- <BrowserRouter>
- <HashRouter>
<Redirect>(6.x移除。使用<Navigate>)- <Link>
- <NavLink>
<Switch>(6.x移除,新增<Routes />)- <Router>
- 其他
- history对象
- match对象
- withRouter函数
路由基本使用
- 明确好界面中的导航区、展示区
- 导航区的<a>标签修改为<Link>标签
- <Link to="/xxx">XXX
- 展示区写<Route>标签进行路径的匹配
- <Route path="/home" element={} />
- <App>的最外侧包一个<BrowserRouter>或<HashRouter>
index.js:
ReactDOM.render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'))
App.js:
export default function App() {
return (
<div>
<div>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/about">About</NavLink>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home">Home</NavLink>
</div>
<div style={{ border: "1px dashed orange", width: "400px", height: "100px" }}>
{/* 注册路由 */}
<Routes>
<Route path="/about" element={<About />} />
<Route path="/home" element={<Home />} />
</Routes>
</div>
</div>
)
}
模糊匹配与精准匹配
在v6版本中,默认为精准匹配。如果想要变为模糊匹配,则需要加 /*
<Routes>
<Route path="/about" element={<About />} />
<Route path="/home/*" element={<Home />} />
</Routes>
Navigate重定向
在react-router-dom v6版本中,<Redirect>标签被删除了。使用v6版本中提供的<Navigate>标签一样可以实现重定向功能
<Routes>
<Route path="/about" element={<About />} />
<Route path="/home" element={<Home />} />
<Route path="/" element={<Navigate to="/about" />} />
</Routes>
export default function Home() {
const [sum, setSum] = useState(1);
return (
<div>
Home
{sum === 2 ? <Navigate to="/about" /> : <h5>当前Sum的值:{sum}</h5>}
<button onClick={() => {setSum(2);}}>点击将sum变为2</button>
</div>
);
}
路由表
import { Navigate } from "react-router-dom";
import About from '../pages/About';
import Home from '../pages/Home'
export default [
{ path: '/', element: <Navigate to="/about" /> },
{ path: '/about', element: <About /> },
{ path: '/home', element: <Home /> }
]
import React from 'react'
import { NavLink, useRoutes } from "react-router-dom";
import routes from './routes'
import './App.css'
export default function App() {
const main = useRoutes(routes) // 引入使用路由表
return (
<div>
<div>
<div>
<div>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/about">About</NavLink>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home">Home</NavLink>
</div>
</div>
<div style={{ border: "1px dashed orange", width: "400px", height: "200px" }}>
{/* 注册路由 */}
{main}
</div>
</div>
</div>
)
}
嵌套路由
5.x版本:
一级:
<Routes>
<Route path="/about" element={<About />} />
<Route path="/home/*" element={<Home />} />
</Routes>
二级:
export default class Home extends Component {
render() {
return (
<div>
<div>
<div>
<NavLink
className={({ isActive }) => (isActive ? "demostyle" : "")}
to="/home/news">News</NavLink>
<NavLink
className={({ isActive }) => (isActive ? "demostyle" : "")}
to="/home/message">Message</NavLink>
</div>
</div>
<div>
{/* 同样需要注册路由 */}
<Routes>
<Route path="/news" element={<News />} />
<Route path="/message" element={<Message />} />
</Routes>
</div>
</div>
);
}
}
6.x版本:
路由表
import { Navigate } from "react-router-dom";
import About from '../pages/About';
import Home from '../pages/Home'
import News from "../pages/News";
import Message from "../pages/Message";
export default [
{ path: '/', element: <Navigate to="/about" /> },
{ path: '/about', element: <About /> },
{
path: '/home', element: <Home />,
children: [
{ path: 'message', element: <Message /> }, // 注意这里这里不要加/
{ path: 'news', element: <News /> }, // 注意这里这里不要加/
]
}
]
组件使用:
import React from "react";
import { NavLink,Outlet } from "react-router-dom";
export default function Home() {
return (
<div>
Home
<div>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="news">
News
</NavLink>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="message">
Message
</NavLink>
</div>
<div style={{ border: "1px dashed orange", width: "200px", height: "100px" }}>
<Outlet />
</div>
</div>
);
}
如果不想让父级高亮只有子级高亮,可以添加 end属性
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home" end>
Home
</NavLink>
向路由传递参数
1)param传参
接收参数时使用useParams函数
父组件:
import React, { Component } from "react";
import { Link, Routes, Route } from "react-router-dom";
import Detail from "./Detail";
export default class Message extends Component {
state = {
messageArr: [
{ id: "1001", title: "我是消息1" },
{ id: "1002", title: "我是消息2" },
{ id: "1003", title: "我是消息3" },
],
};
render() {
const { messageArr } = this.state;
return (
<div>
<ul>
{messageArr.map((msgObj) => {
return (
<li key={msgObj.id}>
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
</li>
);
})}
</ul>
{/* 同样需要注册路由 */}
<Routes>
<Route path="/detail/:id/:title" element={<Detail />} />
</Routes>
</div>
);
}
}
子组件:
import React from "react";
import { useParams } from "react-router-dom";
export default function Detail() {
// 注意!!!! 这里不能使用类式组件,需要用函数式组件
const params = useParams();
return (
<div>
<ul>
<li>ID:{params.id}</li>
<li>Title:{params.title}</li>
<li>Content:我有故人抱剑去,斩尽春风未曾归</li>
</ul>
</div>
);
}
2)search传参
需要用useSearchParams()接收参数
父组件:
<li key={msgObj.id}>
<Link to={`/home/message/detail?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
</li>
....
<Routes>
<Route path="/detail" element={<Detail />} />
</Routes>
子组件:
import React from "react";
import { useSearchParams } from "react-router-dom";
export default function Detail() {
// 注意这里不能使用类式组件
const [search] = useSearchParams();
const id = search.get("id");
const title = search.get("title");
return (
<div>
<ul>
<li>ID:{id}</li>
<li>Title:{title}</li>
<li>Content:我有故人抱剑去,斩尽春风未曾归</li>
</ul>
</div>
);
}
3)state传参
这种方式不会把参数在地址栏中展示出来。需要用useLocation()接收参数
父组件:
<li key={msgObj.id}>
<Link to="/home/message/detail" state={{ id:msgObj.id, title:msgObj.title }}>{msgObj.title}</Link>
</li>
子组件:
import React from "react";
import { useLocation } from "react-router-dom";
export default function Detail() {
// 注意这里不能使用类式组件
const location = useLocation()
console.log("location", location);
const { state:{id, title} } = location; // 连续解构
return (
<div>
<ul>
<li>ID:{id}</li>
<li>Title:{title}</li>
<li>Content:我有故人抱剑去,斩尽春风未曾归</li>
</ul>
</div>
);
}
路由跳转方式
1)push
false代表使用push模式,可以回退(用的比较多)
<Link replace={false} to="/home/message/detail">{msgObj.title}</Link>
2)replace
true代表使用replace模式,可以替换
<Link replace={true} to="/home/message/detail">{msgObj.title}</Link>
BrowserRouter与HashRouter
两者之间的区别:
- 底层原理不一样
- BrowserRouter使用的是H5的history API,不兼容IE9及以下版本
- HashRouter使用的是URL的哈希值
- url表现形式不一样
- BrowserRouter的路径中没有# ,例如:localhost:3000/demo/test (使用较多)
- HashRouter的路径中包含# ,例如:localhost:3000/#/demo/test
- 刷新后对路由state参数的影响
- BrowserRouter没有任何影响,因为state保存在history对象中
- HashRouter刷新后会导致路由state参数的丢失
- 备注
- HashRouter可以用于解决一些路径错误相关的问题
- 一般还是使用BrowserRouter多一些
编程式路由导航
const navigate = useNavigate()
function showDetail(){
// navigate("/about");
navigate("detail", {
replace: false, // 默认就是false
state:{id:m.id,title:m.title}
});
}
useInRouterContext()
作用:如果组件在<Router>的上下文中呈现,则useInRouterContext钩子返回true,否则返回false
(这里App组件被<BrowserRouter>组件包裹了,那么App和他的子组件就都处在路由的上下文环境中了)
import { useInRouterContext } from "react-router-dom";
export default function App() {
console.log('!!!', useInRouterContext())
....
}
useNavigationType()
作用:返回当前的导航类型(即:用户是如何来到当前页面的)
返回值:POP、PUSH、REPLACE
注:POP是指在浏览器中直接打开了这个路由组件(刷新页面)
import { useNavigationType } from "react-router-dom";
export default function Home() {
console.log(useNavigationType());
// 点击进来是PUSH,直接刷新页面是POP,进来之前的链接有replace参数那么就是REPLACE
}
useOutlet()
作用:用来呈现当前组件中要渲染的嵌套路由
- 如果嵌套路由还没有挂载,则result为null
- 如果嵌套路由已经挂载,则展示嵌套的路由对象
const result = useOutlet()
useResolvedPath()
作用:给定一个URL值,解析其中的:path、search、hash
console.log("useResolvedPath", useResolvedPath('/user?id=1001&name=Tom'));
输出结果:
useResolvedPath {pathname: '/user', search: '?id=1001&name=Tom', hash: ''}
六、UI组件库
ant-design
安装:npm add antd 适用于后台管理系统的开发
element-ui
七、redux
redux理解
中文官网:cn.redux.js.org/
redux是一个专门用于做状态管理的JS库。它可以用在react/angular/vue等项目中,但基本与react配合使用。它的作用就是集中式管理react应用中多个组件共享的状态。 【注意:实际开发过程中,更多是使用dva的数据流,我们在另一篇文章中学习】
什么情况下需要使用:
- 某个组件的状态,需要让其他组件可以随时拿到(共享)
- 一个组件需要改变另一个组件的状态(通信)
redux原理图
React Components:组件
Action Creators:创建action(动作对象,包含两个属性:type data)
Store:调度
Reducers:初始化状态、加工状态(根据旧的state和action,产生新的state的纯函数)
redux精简版
⚠️⚠️注意:这里的代码实际开发过程并不推荐这么写,主要用于理解上面的redux原理
Component:
import React, { Component } from "react";
import store from "../../redux/store";
export default class Count extends Component {
// 组件挂载,监测redux中状态的变化
// componentDidMount() {
// // redux中任意状态发生变化,都会触发此回调函数
// store.subscribe(() => {
// this.setState({}); // 假更新
// });
// }
increment = () => {
const { value } = this.selectNumber;
store.dispatch({ type: "increment", data: value * 1 });
};
decrement = () => {
const { value } = this.selectNumber;
store.dispatch({ type: "decrement", data: value * 1 });
};
incrementIfOdd = () => {
const { value } = this.selectNumber;
if (store.getState() % 2 === 1) {
store.dispatch({ type: "increment", data: value * 1 });
}
};
incrementAsyc = () => {
const { value } = this.selectNumber;
setTimeout(() => {
store.dispatch({ type: "increment", data: value * 1 });
}, 2000);
};
render() {
return (
<div>
<h2>当前求和结果为:{store.getState()}</h2>
<select ref={(c) => (this.selectNumber = c)}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
<button onClick={this.incrementAsyc}>异步+</button>
</div>
);
}
}
Store:
// 引入createStore
import {createStore} from 'redux'
// 引入为Count组件服务的reducer
import countReducer from './count_reducer'
// 对外暴露store
export default createStore(countReducer)
Reducers
const initState = 0
// Reducer函数接收两个参数:之前的状态、动作对象
// preState=initState 形参默认值
export default function countReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case 'increment':
return preState + data
case 'decrement':
return preState - data
default:
// 初始化
return preState
}
}
但是这样的话,state变化并不会引起页面刷新,所以需要在index页面:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";
ReactDOM.render(<App />, document.getElementById('root'))
// redux中任意状态发生变化,都会触发此回调函数
store.subscribe(() => {
ReactDOM.render(<App />, document.getElementById('root'))
});
redux完整版
创建Action Creator,用于给Count组件创建action对象:
import { INCREMENT, DECREMENT } from "./constant";
// 为Count组件生成action对象
export const createIncrementAction = value => ({ type: INCREMENT, data: value })
export const createDecrementAction = value => ({ type: DECREMENT, data: value })
然后组件使用时:
import React, { Component } from "react";
import store from "../../redux/store";
// 引入Action Creator
import {
createIncrementAction,
createDecrementAction,
} from "../../redux/count_action";
export default class Count extends Component {
increment = () => {
const { value } = this.selectNumber;
store.dispatch(createIncrementAction(value * 1));
};
decrement = () => {
const { value } = this.selectNumber;
store.dispatch(createDecrementAction(value * 1));
};
incrementIfOdd = () => {
const { value } = this.selectNumber;
if (store.getState() % 2 === 1) {
store.dispatch(createIncrementAction(value * 1));
}
};
incrementAsyc = () => {
const { value } = this.selectNumber;
setTimeout(() => {
store.dispatch(createIncrementAction(value * 1));
}, 2000);
};
render() { return (<div>...</div>); }
}
异步Action
- Object[]类型的action叫同步action
- function类型的action叫异步action
首先需要在store里开启对异步action的支持:
// 引入createStore
import { createStore, applyMiddleware } from 'redux'
// 引入为Count组件服务的reducer
import countReducer from './count_reducer'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
// 对外暴露store
export default createStore(countReducer, applyMiddleware(thunk))
在action里增加一个异步action:
import { INCREMENT, DECREMENT } from "./constant";
// 为Count组件生成action对象
export const createIncrementAction = value => ({ type: INCREMENT, data: value })
export const createDecrementAction = value => ({ type: DECREMENT, data: value })
// 异步action。里面会调用同步action
export const createIncrementAsyncAction = (value, time) => {
// action的值为函数
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(value));
}, time);
}
}
在index里使用:
incrementAsyc = () => {
const { value } = this.selectNumber;
store.dispatch(createIncrementAsyncAction(value * 1, 2000));
};
react-redux基本使用
react-redux是官方出品的一个插件库
- 所有的UI组件都应该包裹一个容器组件,他们是父子关系
- 容器组件是真正和redux打交道的,里面可以随意使用redux的api
- UI组件不能使用任何redux的api
- 容器组件会传递给UI组件:redux中保存的状态+用于操作状态的方法
- 注意,容器组件给UI组件传递状态和操作状态的方法,均是通过props传递
基本使用代码流程如下:
- index.js是整个程序入口
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";
ReactDOM.render(<App />, document.getElementById('root'))
// 监测redux中状态的改变。若redux的状态发生了改变,则重新渲染App组件
store.subscribe(() => {
ReactDOM.render(<App />, document.getElementById('root'))
});
- App.js为所有组件的外壳组件。在这里会引入容器组件,并将store传递给容器组件
import React, { Component } from "react";
import Count from './containers/Count'
import store from './redux/store'
// 所有组件的外壳组件App
export default class App extends Component {
render() {
return (
<div>
{/* 渲染容器组件,并传递store用于在容器组件里连接UI组件和redux */}
<Count store={store}/>
</div>
);
}
}
-
container/Count。是Count的容器组件。在这里最主要的就是创建并暴露一个Count的容器组件,并通过connect连接 :
export default connect(mapStateToProps, mapDispatchToProps)(UI组件);其中mapStateToProps是容器组件给UI组件传递的状态,mapDispatchToProps是容器组件给UI组件传递的操作状态的方法。容器中的store是靠props传进去的,而不是在容器组件中直接引入的。
其中操作状态的方法,引入了创建好的Action。
// 这里是容器组件
// 引入action
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction,
} from "../../redux/count_action";
// 引入UI组件
import countUI from "../../components/Count";
// // 引入redux(其实是store)
// import store from '../../redux/store'
// 引入connect用于连接UI组件和redux
import { connect } from "react-redux";
function mapStateToProps(state) {
// 容器组件给UI组件传递状态
return { count: state };
}
function mapDispatchToProps(dispatch) {
// 容器组件给UI组件传递操作状态的方法
return {
increment: (value) => {
dispatch(createIncrementAction(value));
},
decrement: (value) => {
dispatch(createDecrementAction(value));
},
syncIncrement: (value, time) => {
dispatch(createIncrementAsyncAction(value, time));
},
};
}
// 创建并暴露一个Count的容器组件
export default connect(mapStateToProps, mapDispatchToProps)(countUI);
- count_action.js,Count组件的ActionCreator,用于给Count组件创建action对象:
import { INCREMENT, DECREMENT } from "./constant";
// 为Count组件生成action对象
export const createIncrementAction = value => ({ type: INCREMENT, data: value })
export const createDecrementAction = value => ({ type: DECREMENT, data: value })
// 异步action。里面会调用同步action
export const createIncrementAsyncAction = (value, time) => {
// action的值为函数
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(value));
}, time);
}
}
- Store,redux的调度中心
// 引入createStore
import { createStore, applyMiddleware } from 'redux'
// 引入为Count组件服务的reducer
import countReducer from './count_reducer'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
// 对外暴露store
export default createStore(countReducer, applyMiddleware(thunk))
- reducer里面做真正的操作:
import { INCREMENT, DECREMENT } from "./constant";
const initState = 0
// Reducer函数接收两个参数:之前的状态、动作对象
// preState=initState 形参默认值
export default function countReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
// 初始化
return preState
}
}
- 最后在真正的UI组件里,只需要使用props传递过来的状态和操作状态的方法即可:
// 这里是UI组件
import React, { Component } from "react";
export default class Count extends Component {
increment = () => {
const { value } = this.selectNumber;
this.props.increment(value * 1);
};
decrement = () => {
const { value } = this.selectNumber;
this.props.decrement(value * 1);
};
incrementIfOdd = () => {
const { value } = this.selectNumber;
if (this.props.count % 2 === 1) {
this.props.increment(value * 1);
}
};
incrementAsyc = () => {
const { value } = this.selectNumber;
this.props.syncIncrement(value * 1, 2000);
};
render() {
return (
<div>
<h2>当前求和结果为:{this.props.count}</h2>
<select ref={(c) => (this.selectNumber = c)}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
<button onClick={this.incrementAsyc}>异步+</button>
</div>
);
}
}
📎 手工绘图理解:
react-redux使用优化
1、mapDispatchToProps 精简写法:
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction,
} from "../../redux/count_action";
import countUI from "../../components/Count";
import { connect } from "react-redux";
// 创建并暴露一个Count的容器组件
export default connect(
(state) => ({ count: state }),
// // mapDispatchToProps的一般写法
// (dispatch) => ({
// increment: (value) => dispatch(createIncrementAction(value)),
// decrement: (value) => dispatch(createDecrementAction(value)),
// syncIncrement: (value, time) =>
// dispatch(createIncrementAsyncAction(value, time)),
// })
// mapDispatchToProps的精简写法:redux会帮忙自动分发
{
increment: createIncrementAction,
decrement: createDecrementAction,
syncIncrement: createIncrementAsyncAction,
}
)(countUI);
2、index中无需再实现监测:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
// import store from "./redux/store";
ReactDOM.render(<App />, document.getElementById('root'))
// 当使用了react-redux后,就不用监测了。因为容器组件已经有了监测的能力
// 监测redux中状态的改变。若redux的状态发生了改变,则重新渲染App组件
// store.subscribe(() => {
// ReactDOM.render(<App />, document.getElementById('root'))
// });
3、store不再直接传递给容器组件
import React, { Component } from "react";
import Count from './containers/Count'
// import store from './redux/store'
// 所有组件的外壳组件App
export default class App extends Component {
render() {
return (
<div>
<Count/>
{/* <Count store={store}/> */}
</div>
);
}
}
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
4、整合UI组件和容器组件为一个文件:
import React, { Component } from "react";
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction,
} from "../../redux/count_action";
import { connect } from "react-redux";
// UI组件
class Count extends Component {
increment = () => {
const { value } = this.selectNumber;
this.props.increment(value * 1);
};
decrement = () => {
const { value } = this.selectNumber;
this.props.decrement(value * 1);
};
incrementIfOdd = () => {
const { value } = this.selectNumber;
if (this.props.count % 2 === 1) {
this.props.increment(value * 1);
}
};
incrementAsyc = () => {
const { value } = this.selectNumber;
this.props.syncIncrement(value * 1, 2000);
};
render() {
return (
<div>
<h2>当前求和结果为:{this.props.count}</h2>
<select ref={(c) => (this.selectNumber = c)}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
<button onClick={this.incrementAsyc}>异步+</button>
</div>
);
}
}
// 创建并暴露一个Count的容器组件
export default connect(
(state) => ({ count: state }),
{
increment: createIncrementAction,
decrement: createDecrementAction,
syncIncrement: createIncrementAsyncAction,
}
)(Count);
总结,一个最简单的案例理解:
import React, { Component } from "react";
import { connect } from "react-redux";
import { createIncrementAction } from "../../redux/count_action";
class Count extends Component {
add = () => {
// 通知redux进行加1
this.props.jiafa(1);
};
render() {
return (
<div>
<h2>当前求和结果为:{this.props.sum}</h2>
<button onClick={this.add}>点我加一</button>
</div>
);
}
}
export default connect((state) => ({ sum: state }), {
jiafa: createIncrementAction,
})(Count);
数据共享
person的reducer:
import { ADD_PERSON } from "../constant";
const initState = [{ id: '1001', name: 'tom', age: 18 }]
// Reducer函数接收两个参数:之前的状态、动作对象
export default function personReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case ADD_PERSON:
return [data, ...preState] // ⚠️注意这里
default:
return preState
}
}
首先需要再store里面将所有的reducer合并到一个对象里:
// 引入createStore
import { createStore, applyMiddleware, combineReducers } from 'redux'
// 引入reducer
import countReducer from './reducers/count'
import personReducer from './reducers/person'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
// 合并reducer到一个对象里
const allReducers = combineReducers({
he: countReducer,
rens: personReducer
})
// 对外暴露store
export default createStore(allReducers, applyMiddleware(thunk))
组件里就可以使用所有状态对象了:
import React, { Component } from "react";
import { nanoid } from "nanoid";
import { connect } from "react-redux";
import { createAddPersonAction } from "../../redux/actions/person";
class Person extends Component {
addPerson = () => {
const name = this.nameNode.value;
const age = this.ageNode.value;
const personObj = { id: nanoid(), name, age };
this.props.addPerson(personObj);
this.nameNode.value = ""; // 添加后清空数据
this.ageNode.value = ""; // 添加后清空数据
};
render() {
return (
<div>
<h2>总人数为:{this.props.renshu.length}</h2>
<h3>上方组件求和结果为:{this.props.count}</h3>
<input
ref={(c) => (this.nameNode = c)}
type="text"
placeholder="请输入姓名"
/>
<input
ref={(c) => (this.ageNode = c)}
type="text"
placeholder="请输入年龄"
/>
<button onClick={this.addPerson}>添加</button>
<ul>
{this.props.renshu.map((item) => {
return (
<li key={item.id}>
姓名:{item.name}---年龄:{item.age}
</li>
);
})}
</ul>
</div>
);
}
}
// 创建并暴露一个Count的容器组件
export default connect((state) => ({ renshu: state.rens, count: state.he }), {
addPerson: createAddPersonAction,
})(Person);
redux开发者工具
1、下载插件:Redux DevTools
2、下载安装 npm install redux-devtools-extension
3、修改store:
// 引入createStore
import { createStore, applyMiddleware, combineReducers } from 'redux'
// 引入reducer
import countReducer from './reducers/count'
import personReducer from './reducers/person'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
// 引入redux-devtools-extension 用于支持开发者工具
import {composeWithDevTools} from 'redux-devtools-extension'
// 合并reducer到一个对象里
const allReducers = combineReducers({
he: countReducer,
rens: personReducer
})
// 对外暴露store
// export default createStore(allReducers, composeWithDevTools())
// export default createStore(allReducers, applyMiddleware(thunk)) //异步action
export default createStore(allReducers, composeWithDevTools(applyMiddleware(thunk)))
4、浏览器里使用:
代码总结
1)整体文件目录
2)入口文件 index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from "./redux/store";
import { Provider } from 'react-redux';
ReactDOM.render(
// 使用Redux的Provider包裹APP,是为了让APP所有的后代容器组件都能接收到store
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'))
3)外壳组件 App.js
import React, { Component } from "react";
// 引入容器组件
import Count from './containers/Count'
import Person from './containers/Person'
// 所有组件的外壳组件App
export default class App extends Component {
render() {
return (
<div>
<Count /><hr /><Person />
</div>
);
}
}
4)Count组件 Count.jsx
import React, { Component } from "react";
import {
increment,
decrement,
incrementAsync,
} from "../../redux/actions/count";
import { connect } from "react-redux";
// UI组件
class Count extends Component {
increment = () => {
const { value } = this.selectNumber;
this.props.increment(value * 1);
};
decrement = () => {
const { value } = this.selectNumber;
this.props.decrement(value * 1);
};
incrementIfOdd = () => {
const { value } = this.selectNumber;
if (this.props.count % 2 === 1) {
this.props.increment(value * 1);
}
};
incrementAsyc = () => {
const { value } = this.selectNumber;
this.props.syncIncrement(value * 1, 2000);
};
render() {
return (
<div>
<h2>当前求和结果为:{this.props.count}</h2>
<h3>下方组件的人数为:{this.props.personArr.length}</h3>
<select ref={(c) => (this.selectNumber = c)}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数,+</button>
<button onClick={this.incrementAsyc}>异步+</button>
</div>
);
}
}
// 创建并暴露一个Count的容器组件
export default connect(
(state) => ({ personArr: state.person, count: state.count }),
{
increment: increment,
decrement: decrement,
syncIncrement: incrementAsync,
}
)(Count);
5)Person组件 Person.jsx
import React, { Component } from "react";
import { nanoid } from "nanoid";
import { connect } from "react-redux";
import { addPerson } from "../../redux/actions/person";
// UI组件
class Person extends Component {
addPerson = () => {
const name = this.nameNode.value;
const age = this.ageNode.value;
const personObj = { id: nanoid(), name, age };
this.props.addPerson(personObj);
this.nameNode.value = ""; // 添加后清空数据
this.ageNode.value = ""; // 添加后清空数据
};
render() {
return (
<div>
<h2>总人数为:{this.props.personArr.length}</h2>
<h3>上方组件求和结果为:{this.props.count}</h3>
<input
ref={(c) => (this.nameNode = c)}
type="text"
placeholder="请输入姓名"
/>
<input
ref={(c) => (this.ageNode = c)}
type="text"
placeholder="请输入年龄"
/>
<button onClick={this.addPerson}>添加</button>
<ul>
{this.props.personArr.map((item) => {
return (
<li key={item.id}>
姓名:{item.name}---年龄:{item.age}
</li>
);
})}
</ul>
</div>
);
}
}
// 创建并暴露一个Person的容器组件
export default connect(
(state) => ({ personArr: state.person, count: state.count }),
{
addPerson: addPerson,
}
)(Person);
6)store文件 store.js
// 引入createStore用于创建store对象、applyMiddleware用于支持异步action
import { createStore, applyMiddleware } from 'redux'
// 引入redux-thunk用于支持异步action
import thunk from 'redux-thunk'
// 引入redux-devtools-extension 用于支持开发者工具
import { composeWithDevTools } from 'redux-devtools-extension'
// 引入合并后的reducer
import allReducers from './reducers'
// 对外暴露store
// export default createStore(allReducers, composeWithDevTools())
// export default createStore(allReducers, applyMiddleware(thunk)) //异步action
export default createStore(allReducers, composeWithDevTools(applyMiddleware(thunk)))
7)reducer汇总文件
// 该文件用于汇总所有的reducer
// 引入combineReducers用于合并所有reducer
import { combineReducers } from 'redux'
// 引入reducer
import count from './count'
import person from './person'
// 合并reducer到一个对象里
export default combineReducers({ count: count, person: person })
8)countReducer
import { INCREMENT, DECREMENT } from "../constant";
const initState = 0
// Reducer函数接收两个参数:之前的状态、动作对象
// preState=initState 形参默认值
export default function countReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
// 初始化
return preState
}
}
9)personReducer
import { ADD_PERSON } from "../constant";
const initState = [{ id: '1001', name: 'tom', age: 18 }]
// Reducer函数接收两个参数:之前的状态、动作对象
export default function personReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case ADD_PERSON:
return [data, ...preState]
default:
return preState
}
}
10)count Action
import { INCREMENT, DECREMENT } from "../constant";
// 为Count组件生成action对象
export const increment = value => ({ type: INCREMENT, data: value })
export const decrement = value => ({ type: DECREMENT, data: value })
// 异步action。里面会调用同步action
export const incrementAsync = (value, time) => {
// action的值为函数
return (dispatch) => {
setTimeout(() => {
dispatch(increment(value));
}, time);
}
}
11)person Action
import { ADD_PERSON } from '../constant'
// 为Person组件生成action对象
export const addPerson = personObj => ({ type: ADD_PERSON, data: personObj })
12)常量文件
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const ADD_PERSON = 'add_person'
八、项目打包运行
1、构建项目
npm run build 执行完毕后生成一个文件夹build
2、安装serve(可以快速生成服务器)
全局安装serve npm i serve -g
3、进入build目录下,启动serve serve 或者使用 serve 目录/
4、如果后续代码有修改,重新执行一遍(相当于重新发布上线):npm run build serve 目录/
九、React扩展
setState
setState更新状态的两种写法:
setState(stateChange, [callback])-- 对象式的setState- stateChange 为状态改变对象(该对象可以体现出状态的更改)
- callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用
setState(updater, [callback])-- 函数式的setState- updater 为返回stateChange对象的函数
- updater 可以接收到state和props
- callback 是可选的回调函数,它在状态更新完毕、界面也更新后(render调用后)才被调用
import React, { Component } from "react";
export default class StateDemo extends Component {
state = { count: 0 };
addNumber = () => {
// setState(stateChange, [callback]) -- 对象式的setState
// const { count } = this.state;
// // this.setState({ count: count + 1 });
// this.setState({ count: count + 1 }, () => {
// console.log(this.state.count);
// });
// setState(updater, [callback]) -- 函数式的setState
this.setState(
(state, props) => {
console.log("传递过来的props是:", props);
return { count: state.count + 1 };
},
() => {
console.log(this.state.count);
}
);
};
render() {
return (
<div>
<h2>当前求和结果为:{this.state.count}</h2>
<button onClick={this.addNumber}>点击加一</button>
</div>
);
}
}
总结:
- 对象式的setState是函数式的setState的简写方式(语法糖)
- 使用原则:
- 如果新状态不依赖于原状态(比如设置成一个固定值) ===> 使用对象方式
- 如果新状态依赖于原状态(比如在原状态基础上设置新值) ===> 使用函数方式
- 如果需要在setState()执行后获取最新的状态数据,需要在第二个callback函数中读取
路由组件的lazyLoad
import React, { Component, lazy, Suspense } from "react";
import { NavLink, Route, Routes } from "react-router-dom";
// import About from "./About";
// import Home from "./Home";
import "./load.css";
// 懒加载
const About = lazy( () => import('./About') )
const Home = lazy( () => import('./Home') )
export default class Demo extends Component {
render() {
return (
<div>
<div>
<div>
<div>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/about">About</NavLink>
<NavLink className={({ isActive }) => (isActive ? "demostyle" : "")} to="/home">Home</NavLink>
</div>
</div>
<div
style={{
border: "1px dashed orange",
width: "400px",
height: "100px",
}}
>
{/* 注册路由。<Routes>相当于5.x版本的<Switch> */}
<Suspense fallback={<h1>loading...</h1>}>
<Routes>
<Route path="/about" element={<About />} />
<Route path="/home" element={<Home />} />
</Routes>
</Suspense>
</div>
</div>
</div>
);
}
}
注意:Loading组件不使用懒加载
Hooks
Hook是React 16.8.0版本中新增的特性,可以在函数组件中使用state以及其他的React特性。三个常用的Hook:
- State Hook:React.useState()
- Effect Hook: React.useEffect()
- Ref Hook:React.useRef()
1)State Hook
State Hook让函数组件也可以有state状态,并进行状态数据的读写操作。useState第一次初始化指定的值在内部做缓存。
语法:const [xxx, setXxx] = React.useState(initValue)
setXxx的两种写法:
setXxx(newValue):参数为非函数值,直接指定新的状态值,内部用它覆盖原来的状态值setXxx(value => newValue):参数为函数,接收原来的状态值,返回新的状态值,内部用它覆盖原来的状态值
export default function Demo() {
const [count, setCount] = React.useState(0);
function add() {
// setCount(count + 1) // 第一种写法
setCount((count) => count + 1); // 第二种写法
}
return (
<div>
<h2>当前求和结果为:{count}</h2>
<button onClick={add}>点击+1</button>
</div>
);
}
2)Effect Hook
Effect Hook可以在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)。可以理解为:
EffectHook = componentDidMount() + componentDidUpdate() + componentWillUnmount()
React中的副作用操作:
- 发ajax请求数据获取
- 设置订阅/启动定时器
- 手动更改真实DOM
Effect Hook语法:
useEffect(()=>{
// 在此可以执行任何带副作用的操作
...
return ()={ // 将会在组件卸载前执行
// 在此做一些收尾工作,比如清除定时器、取消订阅等
...
}
},[stateValue]) // 如果指定的是[],回调函数只会在第一次render()后执行
export default function Demo() {
const [count, setCount] = React.useState(0);
// 这里相当于挂载组件时调用
// React.useEffect(() => {
// setInterval(() => {
// setCount((count) => count + 1);
// }, 1000);
// },[]);
React.useEffect(() => {
let timer = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
function add() {
setCount((count) => count + 1);
}
function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
return (
<div>
<h2>当前求和结果为:{count}</h2>
<button onClick={add}>点击+1</button>
<button onClick={unmount}>点击卸载组件</button>
</div>
);
}
3)Ref Hook
Ref Hook可以在函数组件中存储/查找组件内的标签或任意其他数据
语法:const refContainer = React.useRef();
作用:保存标签对象,功能与React.createRef()一样
export default function Demo() {
const myRef = React.useRef();
function show() {
alert(myRef.current.value);
}
return (
<div>
<input type="text" ref={myRef}></input>
<button onClick={show}>点击提示输入信息</button>
</div>
);
}
Fragment
作用:可以不必有一个真实的DOM根标签了
使用方式:<Fragment></Fragment> 或者 <></>
export default class Demo extends Component {
render() {
return (
// <Fragment key={1}>
// <input type="text" />
// <input type="text" />
// </Fragment>
<>
<input type="text" />
<input type="text" />
</>
);
}
}
Context
一种组件间通信方式,常用于祖组件与后代组件间的通信。Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
使用方式:
1)创建Context容器对象:
const XxxContext = React.createContext()
2)渲染子组件时,外面包裹xxxContext.Provider,通过value属性给后代组件传递数据
<XxxContext.Provider value={数据}>
子组件
</XxxContext.Provider>
3)后代组件读取数据:
// 第一种方式:仅适用于类组件
static contextType = xxxContext // 声明接收context
this.context // 读取context中的value数据
// 第二种方式:函数组件与类组件都可以使用
<xxxContext.Consumer>
{
value=>( // value就是context中的value数据
要显示的内容
)
}
</xxxContext.Consumer>
import React, { Component } from "react";
const UserNameContext = React.createContext();
export default class A extends Component {
state = { username: "Tom", age: 18 };
render() {
return (
<div style={{ border: "1px solid orange", padding: "8px" }}>
<h2>A组件</h2>
<h5>A组件里面的年龄是:{this.state.username}</h5>
<UserNameContext.Provider value={{ username: this.state.username, age: this.state.age }}>
<B />
</UserNameContext.Provider>
</div>
);
}
}
class B extends Component {
render() {
return (
<div style={{ border: "1px solid blue", padding: "8px" }}>
<h2>B组件</h2>
<C />
</div>
);
}
}
class C extends Component {
// 声明接收context
static contextType = UserNameContext;
render() {
return (
<div style={{ border: "1px solid red", padding: "8px" }}>
<h2>C组件</h2>
<h5>C组件里面接收到A的名字是:{this.context.username},年龄:{this.context.age}</h5>
<D />
</div>
);
}
}
function D(){
return (
<div style={{ border: "1px solid black", padding: "8px" }}>
<h2>D组件</h2>
<h5>D组件里面接收到A的名字是:
<UserNameContext.Consumer>
{
value => {return `${value.username},年龄:${value.age}`}
}
</UserNameContext.Consumer>
</h5>
</div>
)
}
组件优化
Component的两个问题:
1)只要执行setState(),即使不改变状态数据,组件也会重新render()
2)只当前组件重新render(),就会自动重新render子组件(包括没有用父组件任何数据时),这样效率比较低
解决办法:
只在当组件的state或者props数据发生改变时才重新render()
原因:
Component中的shouldComponentUpdate()总是返回true
解决办法:
- 办法1:重写shouldComponentUpdate()方法
- 比较新旧state或props数据,如果有变化才返回true,如果没有返回false
- 办法2:使用 PureComponent
- PureComponent重写了shouldComponentUpdate(),只有state或者props数据发生改变时才返回true
- 注意:只是进行state和props数据的浅比较,如果只是数据对象内部数据变了,返回false
- 不要直接修改state数据,而是要产生新数据(this.setState({ carName: "xxx" })就会产生新数据)
- 项目中一般使用 PureComponent 来优化
import React, { Component, PureComponent } from "react";
import "./index.css";
export default class Parent extends PureComponent {
state = { carName: "奔驰" };
changeCar = () => {
this.setState({ carName: "保时捷911" })
// this.setState({});
};
render() {
console.log("Parent-render()调用");
const { carName } = this.state
return (
<div className="parent">
<h3>Parent组件</h3>
<span>父组件:{carName}</span>
<button onClick={this.changeCar}>点击换车</button>
<Child carName={carName} />
</div>
);
}
}
class Child extends PureComponent {
render() {
console.log("Child-render()调用");
return (
<div className="child">
<h3>Child组件</h3>
<span>子组件接收:{this.props.carName}</span>
</div>
);
}
}
RenderProps
如何向组件内部动态传入带内容的结构(标签)?
- vue中:使用slot插槽技术,也就是通过组件标签体传入结构
- React中:
- 使用children props:通过组件标签体传入结构
- 使用render props:通过组件标签属性传入结构,一般用render函数属性
children props:
<A>
<B>xxxx</B>
</A>
问题:如果B组件需要A组件中的数据,是做不到的
import React, { Component } from "react";
import "./index.css";
export default class Parent extends Component {
render() {
return (
<div className="parent">
<h3>Parent组件</h3>
{/* <A>Parent组件说:hello A</A> */}
<A>
<B />
</A>
</div>
);
}
}
class A extends Component {
state = { name: "Tom" };
render() {
// console.log(this.props); // {children: 'hello A'}
return (
<div className="a">
<h3>A组件</h3>
{/* {this.props.children} */}
{/* <B /> */}
{this.props.children}
</div>
);
}
}
class B extends Component {
render() {
return (
<div className="b">
<h3>B组件</h3>
</div>
);
}
}
render props:
<A render = {(data) => <C data={data}></C>}> </A>
A组件:{this.props.render(内部state数据)}
C组件:读取A组件传入的数据并展示 {this.props.data}
import React, { Component } from "react";
import "./index.css";
export default class Parent extends Component {
render() {
return (
<div className="parent">
<h3>Parent组件</h3>
{/* <A>Parent组件说:hello A</A> */}
<A render={(name) => <B name={name}/>} />
</div>
);
}
}
class A extends Component {
state = { name: "Tom" };
render() {
// console.log(this.props); // {children: 'hello A'}
return (
<div className="a">
<h3>A组件</h3>
{/* {this.props.children} */}
{/* <B /> */}
{this.props.render(this.state.name)}
</div>
);
}
}
class B extends Component {
render() {
return (
<div className="b">
<h3>B组件</h3>
<span>A组件传递过来的状态:{this.props.name}</span>
</div>
);
}
}
ErrorBoundary错误边界
错误边界(ErrorBoundary):是用来捕获后代组件错误,渲染出备用页面
特点:只能捕获后代组件生命周期产生的错误(一般也是用来捕获render中出现的错误) ,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
使用方式:getDerivedStateFromError 配合 componentDidCatch
export default class Parent extends Component {
state = {
errorMessage: "", // 用于标识子组件的错误
};
// 当Parent的子组件出现报错时,会触发此方法的调用,并携带过来错误信息
static getDerivedStateFromError(error) {
console.log(error);
// 返回新的state,在render之前触发
return { errorMessage: error };
}
// 出现错误时,调用此钩子
componentDidCatch(error, info) {
console("渲染组件出错次数,这里可以发送给服务端", error, info);
}
render() {
return (
<div>
<h3>Parent组件</h3>
{this.state.errorMessage ? <h2>子组件错误啦- - !</h2> : <Child />}
</div>
);
}
}
组件间通信方式总结
组件间的关系有:父子组件、兄弟组件(非嵌套组件)、祖孙组件(跨级组件)
几种通信方式:
- props
- children props
- render props
- 消息订阅发布
- pub-sub 、event等
- 集中式管理
- redux 、 dva 等
- context
- 生产者-消费者模式
比较好的搭配方式:
- 父子组件:props
- 兄弟组件: 消息订阅发布、集中式管理
- 祖孙组件(跨级组件):消息订阅发布、集中式管理、context(一般开发用的少,封装插件用的多)