React快速入门(三)动画、状态与路由
React过渡动画
-
npm install react-transition-group --save
-
react-transition-group主要包含四个组件:
- Transition
- 该组件是—个和平台无关的组件(不—定要结合CSS)
- CSSTransition
- 在前端开发中,通常使用CSSTransition来完成过渡动画效果
- 三种状态:
- 第一类:开始状态:对应的类是-appear、-enter、exit;
- 第二类:执行动画:对应的类是-appear-active、-enter-active、-exit-active;
- 第三类:执行结束:对应的类是-appear-done、-enter-done、-exit-done;
- 常见对应的属性
- in:触发进入或者退出状态
- 如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉;
- 当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
- 当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
- classNames:动画class的名称
- timeout:过渡动画的时间
- appear:是否在初次进入添加动画
- unmountOnExit:退出后卸载组件
- in:触发进入或者退出状态
- 钩子函数
- onEnter:在进入动画之前被触发;
- onEntering:在应用进入动画时被触发;
- onEntered:在应用进入动画结束后被触发;
//style.css .why-appear, .why-enter { opacity: 0; } .why-appear-active, .why-enter-active { opacity: 1; transition: opacity 2s ease; } /* 离开动画 */ .why-exit { opacity: 1; } .why-exit-active { opacity: 0; transition: opacity 2s ease; } //app.jsx export class App extends PureComponent { constructor(props) { super(props) this.state = { isShow: true } this.sectionRef = createRef() } render() { const { isShow } = this.state return ( <div> <button onClick={e => this.setState({ isShow: !isShow })}>切换</button> {/* { isShow && <h2>哈哈哈</h2> } */} <CSSTransition nodeRef={this.sectionRef} in={isShow} unmountOnExit={true} classNames="why" timeout={2000} appear onEnter={e => console.log("开始进入动画")} onEntering={e => console.log("执行进入动画")} onEntered={e => console.log("执行进入结束")} onExit={e => console.log("开始离开动画")} onExiting={e => console.log("执行离开动画")} onExited={e => console.log("执行离开结束")} > <div className='section' ref={this.sectionRef}> <h2>哈哈哈</h2> </div> </CSSTransition> </div> ) } }
- SwitchTransition
- 两个组件显示和隐藏切换时,使用该组件
- SwitchTransition中主要有一个属性:mode,有两个值
- in-out:表示新组件先进入,旧组件再移除;
- out-in:表示旧组件先移除,新组件再进入;
- SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;
- SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性;
render() { const { isLogin } = this.state return ( <div> <SwitchTransition mode='out-in'> <CSSTransition key={isLogin ? "exit": "login"} classNames="login" timeout={1000} > <button onClick={e => this.setState({ isLogin: !isLogin })}> { isLogin ? "退出": "登录" } </button> </CSSTransition> </SwitchTransition> </div> ) }
- TransitionGroup
- 将多个动画组件包裹在其中,一般用于列表中元素的动画
render() {
const { books } = this.state
return (
<div>
<h2>书籍列表:</h2>
<TransitionGroup component="ul">
{
books.map((item, index) => {
return (
<CSSTransition key={item.id} classNames="book" timeout={1000}>
<li>
<span>{item.name}-{item.price}</span>
<button onClick={e => this.removeBook(index)}>删除</button>
</li>
</CSSTransition>
)
})
}
</TransitionGroup>
<button onClick={e => this.addNewBook()}>添加新书籍</button>
</div>
)
}
React中的CSS
内联样式
- style接受一个采用小驼峰命名属性的JavaScript对象,而不是CSS字符串
- 内联样式,样式之间不会有冲突;可以动态获取当前state中的状态
- 大量样式导致代码混乱,编写时没有提示,写法上需要驼峰标识
<p style={{color: "blue", fontSize: "20px"}}>我是内容, 哈哈哈</p>
普通CSS
- 编写一个单独的文件,之后再进行引入,最大的问题是会出现样式层叠掉
//App.css
.title {
font-size: 32px;
color: green;
}
.content {
font-size: 22px;
color: orange;
}
//App.jsx
import "./App.css"
<div>
<h2 className='title'>我是标题</h2>
<p className='content'>我是内容, 哈哈哈哈</p>
</div>
css modules
- React的脚手架已经内置了css modules的配置:
- .css/.less/.scss等样式文件都需要修改成**.module.css/.module.less/.module.scss**等;之后就可以引用并且进行使用了;
- css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。
- 这种方案也有自己的缺陷:
- 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
- 所有的className都必须使用{style.className}的形式来编写;
- 不方便动态来修改某些样式,依然需要使用内联样式的方式;
//App.module.css
.title {
font-size: 32px;
color: green;
}
.content {
font-size: 22px;
color: orange;
}
//App.jsx
import appStyle from "./App.module.css"
export class App extends PureComponent {
render() {
return (
<div>
<h2 className={appStyle.title}>我是标题</h2>
<p className={appStyle.content}>我是内容, 哈哈哈哈</p>
</div>
)
}
}
CSS in JS
-
事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态
-
CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修改状态等等
-
虽然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点
-
所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案
-
目前比较流行的CSS-in-JS的库:
-
styled-components
- styled-components的本质是通过函数的调用,最终创建出一个组件:这个组件会被自动添加上一个不重复的class;styled-components会给该class添加相关的样式
- 它支持类似于CSS预处理器一样的样式嵌套:支持直接子代选择器或后代选择器,并且直接编写样式;可以通过&符号获取当前元素;直接伪类选择器、伪元素等;
- props可以传递,添加attrs属性,…
import styled from "styled-components" import { primaryColor, largeSize } from "./style/variables" export const SectionWrapper = styled.div.attrs(props => ({ tColor: props.color || "blue" }))` border: 1px solid red; .title { font-size: ${props => props.size}px; color: ${props => props.tColor}; &:hover { background-color: purple; } } .content { font-size: ${largeSize}px; color: ${primaryColor}; }
-
emotion
-
glamorous
-
ES6标签模板字符串
- 正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外—种调用方式
- 如果我们在调用的时候插入其他的变量:
- 模板字符串被拆分了;
- 第一个元素是数组,是被模块字符串拆分的字符串组合;
- 后面的元素是—个个模块空符串传入的内容:
// 标签模板字符串的使用
function foo(...args) {
console.log(args)
}
foo("why", 18, 1.88)
foo`my name is ${name}, age is ${age}`//'my name is ',', age is ','',
classnames
- 用于动态添加classnames的一个库
Redux使用详解
纯函数
- 纯函数的维基百科定义:在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等
- 总结:确定的输入,一定会产生确定的输出;函数在执行过程中,不能产生副作用
- 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;副作用是产生bug的“温床”
- 举例:
- slice: slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组**(纯函数)**
- splice: splice截取数组,会返回一个新的数组,也会对原数组进行修改**(非纯函数)**
- 所有 React组件都必须像纯函数一样保护它们的props不被更改
redux的必要性
- JavaScript需要管理的状态越来越多,越来越复杂;状态之间相互会存在依赖,一个状态的改变会引起另一个状态的变化;当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎样的变化,会变得非常难以控制和追踪
- Redux就是一个帮助我们管理 State 的容器: Redux 是 JavaScript 的状态容器 ,提供了可预测的状态管理
redux的核心理念
- Store – 存储数据
- action – 派发(dispatch)action来更新数据
- action是一个普通的 JavaScript 对象 ,用来描述这次 更新的 type 和 content
- reducer – 将传入的state和action结合生成一个新的state
- 三大原则:
- 单一数据源
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个store中
- Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;
- State是只读的
- 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State
- 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题
- 使用纯函数来执行修改
- 通过reducer将旧state和actions联系在一起,并且返回一个新的State
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用;
- 单一数据源
react-redux
- redux官方帮助我们提供了react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效
// 具体页面 -- 映射
import { connect } from "react-redux"
const mapStateToProps = (state) => ({
counter: state.counter.counter,
})
const mapDispatchToProps = (dispatch) => ({
addNumber(num) {
dispatch(addNumberAction(num))
},
subNumber(num) {
dispatch(subNumberAction(num))
}
})
export default connect(mapStateToProps, mapDispatchToProps )(Category)
- 网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构
- redux进行异步的操作可以使用中间件;中间件可以帮助我们在请求和响应之间嵌入一些操作的代码
- redux引入了中间件(Middleware)的概念:这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;比如日志记录、调用异步接口、添加代码调试功能等等
- 官网推荐的、包括演示的网络请求的中间件是使用redux-thunk
//1、安装 yarn add redux-thunk
//store.js
import { createStore, applyMiddleware, compose } from "redux"
import thunk from "redux-thunk"
import reducer from "./reducer"
// 先安装redux-devtools,再继承
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
//2、在创建store时传入应用了middleware的enhance函数
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
export default store
//3、定义返回一个函数的action
//actionCreators.js
export const fetchHomeMultidataAction = () => {
// 如果是一个普通的action, 那么我们这里需要返回action对象
// 问题: 对象中是不能直接拿到从服务器请求的异步数据的
return function(dispatch, getState) {
// 异步操作: 网络请求
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
dispatch(changeBannersAction(banners))
dispatch(changeRecommendsAction(recommends))
})
}
}
- Reducer进行代码拆分,文件拆分后,可以通过combineReducers函数可以方便的让我们对多个reduce进行合并
- combineReducers实现原理:
- 将我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于之前的reducer函数)
- 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state
- 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新
import { createStore, applyMiddleware, compose, combineReducers } from "redux"
import thunk from "redux-thunk"
import counterReducer from "./counter"
import homeReducer from "./home"
import userReducer from "./user"
// 将两个reducer合并在一起
const reducer = combineReducers({
counter: counterReducer,
home: homeReducer,
user: userReducer
})
ReduxToolkit
ReduxToolkit概念
- 安装Redux Toolkit:
npm install @reduxjs/toolkit react-redux
- Redux Toolkit的核心API主要是如下几个:
- configureStore:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的slice reducer,添加你提供的任何Redux中间件,redux-thunk默认包含,并启用Redux DevTools Extension.
- reducer,将slice中的reducer可以组成一个对象传入此处;
- middleware:可以使用参数,传入其他的中间件(自行了解);devTools:是否配置
- devTools工具,默认为true;
- createSlice:接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。
- createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的thunk
- pending : action被发出,但是还没有最终的结果
- fulfilled:获取到最终的结果(有返回值的结果)
- rejected:执行过程中有错误或者抛出了异常
- configureStore:包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的slice reducer,添加你提供的任何Redux中间件,redux-thunk默认包含,并启用Redux DevTools Extension.
//counter.js
import { createSlice } from "@reduxjs/toolkit"
//createSlice返回值是一个对象,包含所有的actions
const counterSlice = createSlice
createSlice返回值是一个对象,包含所有的actions({
//用户标记slice的名词
name: "counter",
//初始化值
initialState: {
counter: 888
},
//相当于reducer函数
reducers: {
addNumber(state, { payload }) {
state.counter = state.counter + payload
},
subNumber(state, { payload }) {
state.counter = state.counter - payload
}
}
})
export const { addNumber, subNumber } = counterSlice.actions
export default counterSlice.reducer
//home.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
export const fetchHomeMultidataAction = createAsyncThunk(
"fetch/homemultidata",
async (extraInfo, { dispatch, getState }) => {
// console.log(extraInfo, dispatch, getState)
// 1.发送网络请求, 获取数据
const res = await axios.get("http://123.207.32.32:8000/home/multidata")
// 2.取出数据, 并且在此处直接dispatch操作(可以不做)
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
dispatch(changeBanners(banners))
dispatch(changeRecommends(recommends))
// 3.返回结果, 那么action状态会变成fulfilled状态
return res.data
})
const homeSlice = createSlice({
name: "home",
initialState: {
banners: [],
recommends: []
},
reducers: {
changeBanners(state, { payload }) {
state.banners = payload
},
changeRecommends(state, { payload }) {
state.recommends = payload
}
},
//写法1
// extraReducers: {
// [fetchHomeMultidataAction.pending](state, action) {
// console.log("fetchHomeMultidataAction pending")
// },
// [fetchHomeMultidataAction.fulfilled](state, { payload }) {
// state.banners = payload.data.banner.list
// state.recommends = payload.data.recommend.list
// },
// [fetchHomeMultidataAction.rejected](state, action) {
// console.log("fetchHomeMultidataAction rejected")
// }
// }
//写法2
extraReducers: (builder) => {
// builder.addCase(fetchHomeMultidataAction.pending, (state, action) => {
// console.log("fetchHomeMultidataAction pending")
// }).addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
// state.banners = payload.data.banner.list
// state.recommends = payload.data.recommend.list
// })
}
})
export const { changeBanners, changeRecommends } = homeSlice.actions
export default homeSlice.reducer
- Redux Toolkit底层使用了immerjs的一个库来保证数据的不可变性
- https://mp.weixin.qq.com/s/hfeCDCcodBCGS5GpedxCGg
- 为了节约内存,又出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性数据结构);
- 用一种数据结构来保存数据;
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费;
ReactRouter
-
事实上,路由器主要维护的是一个映射表,映射表会决定数据的流向
-
hash模式兼容性好,在老版IE中都可以运行,但是缺陷是包含一个#,显得不像一个真实的路径
-
history接口是H5新增的,兼容性一般,但是看起来更加美观,有6种模式改变URL而不刷新页面
-
安装React Router:
npm install react-router-dom
;提供了相关组件BrowserRouter/HashRouter- Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件
- BrowserRouter使用history模式
- HashRouter使用hash模式
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(
<StrictMode>
<HashRouter>
<Suspense fallback={<h3>Loading...</h3>}>
<App/>
</Suspense>
</HashRouter>
</StrictMode>
)
路由映射配置
- **Routes:**包裹所有的Route,在其中匹配一个路由
- Route: Route用于路径的匹配;
- path属性:用于设置匹配到的路径
- element属性:设置匹配到路径后,渲染的组件
- exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
路由配置和跳转
- Link和NavLink
- 通常路径的跳转是使用Link组件,最终会被渲染成a元素;NavLink是在Link基础之上增加了一些样式属性
- to属性:Link中最重要的属性,用于设置跳转到的路径
Navigate导航
- Navigate用于路由的重定向,当这个组件出现时,就会执行跳转到对应的 to 路径中
Not Found页面
- 如果用户随意输入一个地址,该地址无法匹配,那么在路由匹配的位置将什么内容都不显示。
- 很多时候,我们希望在这种情况下,让用户看到一个Not Found的页面。
- 这个过程非常简单:
- 开发一个Not Found页面;
- 配置对应的Route,并且设置path为*即可;
const About = React.lazy(() => import("../pages/About"))
const Login = React.lazy(() => import("../pages/Login"))
const routes = [
{
path: "/",
element: <Navigate to="/home"/>
},
{
path: "/home",
element: <Home/>,
children: [
{
path: "/home",
element: <Navigate to="/home/recommend"/>
},
{
path: "/home/recommend",
element: <HomeRecommend/>
}
]
},
{
path: "/about",
element: <About/>
},
{
path: "/login",
element: <Login/>
},
{
path: "*",
element: <NotFound/>
}
]
路由参数传递
- 动态路由的方式
- search的方式
路由的配置文件
-
我们希望将所有的路由配置放到一个地方进行集中管理:
- 在早期的时候,Router并且没有提供相关的API,我们需要借助于react-router-config完成
- 在Router6.x中,为我们提供了useRoutes API可以完成相关的配置
-
如果对某些组件进行了异步加载(懒加载),那么需要使用Suspense进行包裹