https://zh-hans.react.dev/learn
From 黑马B站的视频(React 18.2.0 )
1.简介
React由Meta公司研发,是一个用于构建Web和原生交互界面的库
较于其它前端框架 优势: 丰富的生态,跨平台支持
2.搭建开发环境
npx create-react-app react-basic
// npx - Node.js工具命令,查找并执行后续的包命令
// create-react-app - 核心包(固定写法),用于创建React项目
// react-basic React项目的名称(可以自定义)
如果提示:输入y 其它情况失败自行排查 比如node版本等
Need to install the following packages:
create-react-app@5.0.1
Ok to proceed? (y) y
我这里报错 失败【可能是我个人电脑问题 】我各种尝试npm镜像之类还是失败了
第二天的时候 尝试了npm install -g react , npm install -g react-dom, npm install -g react-scripts 再去create-react-app first-demo 其实npm install -g react-scripts仍然报错network connectivity的错误。但总算成功了。
进入项目文件夹 npm start 启动项目 。但是我又localhost refused to connect. 用解决了
localhost:3000 拒绝访问解决办法-CSDN博客
index.js文件介绍 -- 项目入口 从这里开始运行
App.js -- 项目根组件
3.JSX
JSX是JavaScript和XMl(HTML)的缩写,表示在JS代码中编写HTML模版结构,它是React中构建UI的方式: 既有HTML的声明式模版,也有JS的可编程能力
3.1JSX的本质
是 JS的语法扩展,浏览器本身不能识别,需要通过解析工具做解析之后才能在浏览器中使
Babel · The compiler for next generation JavaScript
3.2 jsx中使用js表达式
通过 大括号语法{}
识别JavaScript中的表达式,比如常见的变量、函数调用、方法调用
注意:if语句、switch语句、变量声明不属于表达式,不能出现在{}中
3.3 JSX实现列表渲染
使用原生js种的map方法
实现列表渲染。 给独一无二的key(字符串或者number)解决报错问题——react内部使用提升更新性能
3.4 JSX实现条件渲染
3.4.1 逻辑与运算符 &&
3.4.2 三元表达式 ? :
3.4.3 复杂条件渲染
自定义函数 + 判断语句
4.React的事件绑定
on + 事件名称 = { 事件处理程序 }
,整体上遵循驼峰命名法
使用事件参数:事件回调函数中设置形参e即可
传递自定义参数:事件绑定的位置改造成箭头函数的写法,在执行clickHandler实际处理业务函数的时候传递实参 (注意:不能直接写函数调用)
同时传递事件对象和自定义参数:在事件绑定的位置传递事件实参e和自定义参数,clickHandler中声明形参,注意顺序对应
5.组件使用
一个组件就是一个用户界面的一部分,它可以有自己的逻辑和外观,组件之间可以互相嵌套,也可以复用多次
在React中,一个组件就是首字母大写的函数(普通、箭头均可),内部存放了组件的逻辑和视图UI, 渲染组件只需要把组件当成标签书写即可
5.1 基础样式处理
5.1.1 行内样式(不推荐)
const fontStyle = {
fontStyle: 'italic'
}
function App() {
return (
<div className="App">
// 行内样式多单词要采用驼峰命名, 样式对象可以写成变量的形式
<span style={{...fontStyle,color:'red',fontSize:'22px'}}>is red</span>
</div>
);
}
5.1.2 class类名
className ='xxx'
// index.css
.foo {
color: blue;
}
// 使用
import './index.css'
function App() {
return (
<div className="App">
<span className ='foo'>class foo</span>
</div>
);
}
tab页点击高亮展示样式
const [activeTab , setactiveTab] =useState('hot')
const tabClick = (type)=>{
setactiveTab(type)
}
{tabs.map(item=> <span key={item.type} className={`nav-item ${activeTab === item.type && 'active'}`} onClick={()=> tabClick(item.type)}>{item.text}</span>)}
5.1.3 classnames优化类名控制
npm install classnames 然后对应的组件 import classnames from 'classnames' 就可以使用了
// 静态类名直接写'xxx' 类名之间, 动态的用{ xxx : 条件}
className = {classnames('nav-item', {active: activeTab === item.type})}
5.2 组件状态管理-useState
useState 是一个 React Hook(函数)【返回的是一个数组】,它允许我们向组件添加一个状态变量
, 从而控制影响组件的渲染结果
和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)
import { useState } from "react";
function App() {
//调用useState添加一个状态变量 count-状态变量 setCount-修改状态变量的方法
const [ count, setCount ] = useState(0)
const [ form, setForm ] = useState({
name: 'jack'
})
// 点击事件回调
const handleCkick = ()=>{
// 不能直接count++
// 用传入的新值修改count 用新的count 渲染UI
setCount(count + 1)
}
const changeForm = ()=>{
setForm({
...form,
name:'irene'
})
}
return (
<div className="App">
<button onClick={ handleCkick}>{count}</button>
<button onClick={ changeForm}>{form.name}</button>
</div>
);
}
在React中状态被认为是只读的,我们应该始终替换它而不是修改它
, 直接修改状态不能引发视图更新
5.2.1 受控表单绑定
const [comment , setcomment] =useState('')
<textarea
value={comment} // value绑定state
onChange={(e)=> setcomment(e.target.value)} // 输入框最新值给state
className="reply-box-textarea"
placeholder="发一条友善的评论"
/>
5.2.2 非受控表单绑定
通过获取DOM的方式获取表单的输入数据 使用useRef钩子函数
1.useRef生成ref对象 绑定到dom标签身上
import { useRef} from "react";
const inputRef = useRef(null)
2.dom可用时,ref.current获取dom/渲染完毕之后dom生成之后才可用
const btnClick = ()=> {
console.dir(inputRef.current);
}
<input type='text' ref={inputRef} />
<button onClick={btnClick}></button>
5.3 组件通信
5.3.1 父传子
// 2、子组件姐搜狐数据 props参数
function Son (props){
console.log(props);// 打印可知是对象 里面包含了父组件传递的所有数据
// 子组件只能读取props中的数据,不能直接进行修改, 父组件的数据只能由父组件修改
return <div> {props.name}
<div> {props.age}</div>
<div> {props.isTrue && '为真'}</div>
<div> {props.obj.name}</div>
{props.cb()}
{props.child}
</div>
}
{/* 1、父组件传递数据 在子组件的标签上绑定 xxx={} 可以传递任意的合法数据,比如数字、字符串、布尔值、数组、对象、函数、JSX */}
<Son name={name}
age={20}
isTrue={false}
list={['vue','react']}
obj={{name: 'ii'}}
cb ={()=> console.log(123)}
child = {<span> this is child</span>}
></Son>
特殊的prop children
当我们把内容嵌套在组件的标签内部时,组件会自动在名为children的prop属性中接收该内容
5.3.2 子传父
在子组件中调用父组件中的函数并传递参数
function Son ({ onGetSonMsg }) {
// Son组件中的数据
const sonMsg = 'this is son msg'
return (
<div>
<button onClick={() => onGetSonMsg(sonMsg)}>sendMsg</button>
</div>
)
}
function App () {
const [msg, setMsg] = useState('')
const getMsg = (msg) => {
setMsg(msg)
}
return (
<div>
this is App, {msg}
<Son onGetSonMsg={getMsg} />
</div>
)
}
export default App
5.3.3 兄弟组件通信
借助 状态提升
机制,通过共同的父组件进行兄弟之间的数据传递
-
A组件先通过子传父的方式把数据传递给父组件
-
拿到数据之后通过父传子的方式再传递给B组件
function A ({ onGetAName }) {
// Son组件中的数据
const name = 'this is A name'
return (
<div>
this is A compnent,
<button onClick={() => onGetAName(name)}>send</button>
</div>
)
}
function B ({ name }) {
return (
<div>
this is B compnent,
{name}
</div>
)
}
function App () {
const [name, setName] = useState('')
const getAName = (name) => {
setName(name)
}
return (
<div>
this is App
<A onGetAName={getAName} />
<B name={name} />
</div>
)
}
5.3.4 跨层组件通信
① 使用 createContext
方法创建一个上下文对象Ctx
② 在顶层组件(App)中通过 Ctx.Provider
组件提供数据
③ 在底层组件(B)中通过 useContext
钩子函数获取消费数据
import { createContext, useContext, useState } from "react";
//APP → A → B
// 1. createContext方法创建一个上下文对象
const MsgContext = createContext()
function App () {
const msg = 'this is app msg'
return (
<div>
{/* 2. 在顶层组件 通过Provider组件提供数据 */}
<MsgContext.Provider value={msg}>
this is App
<A />
</MsgContext.Provider>
</div>
)
}
function B () {
// 3. 在底层组件 通过useContext钩子函数使用数据
const msg = useContext(MsgContext)
return (
<div>
this is B compnent,{msg}
</div>
)
}
5.4 副作用管理-useEffect
useEffect是一个React Hook函数,用于在React组件中创建不是由事件引起而是由渲染本身引起的操作(副作用), 比如发送AJAX请求,更改DOM等等
useEffect(() => {} ,[]) // 参数1 是函数(副作用函数)里面写要执行的操作, 参数2 可选参数 数组 里面放依赖项 如果为[]空数组 副作用函数只会在组件渲染完毕之后执行一次
const [list , setList] =useState([])
// 渲染完毕去发送请求
useEffect(()=>{
// 注意这里写请求方法的方式 上图为练习中错误写法报错
async function getList (){
const res = await fetch('http://geek.itheima.net/v1_0/channels')
const jsonRes = await res.json() // 这里需要await
setList(jsonRes.data.channels)
}
getList()
},[])
5.4.1依赖项
依赖项 | 副作用功函数的执行时机 |
---|---|
没有依赖项 | 组件初始渲染 + 组件更新时执行 |
空数组依赖 | 只在初始渲染时执行一次 |
添加特定依赖项 | 组件初始渲染 + 依赖项变化时执行( 如果有多个值 的数组,会比较每一个值,有一个变化就执行) |
注意当没有依赖项 副作用函数操作又改变数据(setXxx())时 会无限循环
React useEffect 两个参数你用对了吗,这也许是最全的使用说明书-CSDN博客
5.4.2 清除副作用
清除副作用的函数最常见的执行时机是在组件卸载时自动执行
function Son () {
// 1. 渲染时开启一个定时器
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行中...')
}, 1000)
return () => {
// 清除副作用(组件卸载时)
clearInterval(timer)
}
}, [])
return <div>this is son</div>
}
5.5 自定义Hook实现
以 use打头的函数
,通过自定义Hook函数可以用来实现逻辑的封装和复用
// 封装hook 布尔切换的逻辑
function useToggle (){
// 可复用逻辑代码
const [value,setValue] = useState(true)
const toggle = () => setValue(!value)
// 哪些状态和回调需要在其他组件中使用 可以是对象也可以是数组
return {
value,
toggle
}
}
使用
const {value ,toggle} = useToggle()
{value && <div></div>}
<button onClick={toggle}>toggle</button>
5.6 React Hooks使用规则
-
只能在组件中或者其他自定义Hook函数中调用
-
只能在组件的顶层调用,不能嵌套在if、for、其它的函数中
抽象组件
原则:App作为“智能组件”负责数据的获取,Item作为“U组件”负责数据的渲染
6. Redux
是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
为了职责清晰,数据流向明确,Redux把整个数据修改的流程分成了三个核心概念,分别是:state、action和reducer
1. state - 一个对象 存放着我们管理的数据状态
2. action - 一个对象 用来描述你想怎么改数据
3. reducer - 一个函数 根据action的描述生成一个新的state
// 1. 定义reducer函数
// 作用: 根据不同的action对象,返回不同的新的state
// state: 管理的数据初始状态
// action: 对象 type 标记当前想要做什么样的修改
function reducer (state = { count: 0 }, action) {
// 数据不可变:基于原始状态生成一个新的状态
if (action.type === 'INCREMENT') {
return { count: state.count + 1 }
}
if (action.type === 'DECREMENT') {
return { count: state.count - 1 }
}
return state
}
// 2. 使用reducer函数生成store实例
const store = Redux.createStore(reducer)
// 3. 通过store实例的subscribe订阅数据变化
// 回调函数可以在每次state发生变化的时候自动执行
store.subscribe(() => {
console.log('state变化了', store.getState())
document.getElementById('count').innerText = store.getState().count
})
// 4. 通过store实例的dispatch函数提交action更改状态
store.dispatch({
type: 'INCREMENT'
})
6.1 项目中使用Redux
安装俩个其他插件 - Redux Toolkit 和 react-redux
-
Redux Toolkit(RTK)- 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式【简化store的配置方式 内置immer支持可变式状态修改 内置thunk更好的异步创建】
-
react-redux - 用来 链接 Redux 和 React组件 的中间件【获取 更新】
npm i @reduxjs/toolkit react-redux
新建store文件夹 里面新建一个i入口文件index.js【组合所有子模块 导出store】 一个modules目录
6.1.1 使用React Toolkit 创建 counterStore
// index.js
import { configureStore } from "@reduxjs/toolkit";
// 导入子模块reducer
import couterReducer from './modules/counterStore'
const store = configureStore({
reducer:{
counter: couterReducer
}
})
export default store
// counterStore.js
import { createSlice } from "@reduxjs/toolkit";
const counterStore = createSlice({
name:'counter',// 独一无二
// 初始化state
initialState:{
count: 0
},
// 修改状态方法 同步 支持直接修改
reducers:{
increment(state){
state.count++
},
decrement(state){
state.count--
},
}
})
// 解构出actionCreater函数
const { increment, decrement} = counterStore.actions
// 获取reducer
const couterReducer = counterStore.reducer
// 按需导出actionCreater
export {increment, decrement}
// 默认导出reducer
export default couterReducer
6.1.2 为React注入store
react-redux 内置 Provider组件 通过 store 参数把创建好的store实例注入到应用中,链接正式建立
//index.js
//react必要的两个核心包
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 导入store
import store from './store'
// 导入store提供组件Provider
import { Provider } from 'react-redux'
// 把App根组件渲染到id为root的dom节点上---在public/index.html
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
// 提供store数据
<Provider store={store}>
<App />
</Provider>
);
6.1.3 组件使用store中的数据
用到一个钩子函数 - useSelector,它的作用是把store中的数据映射到组件中
const { count } = useSelector( state=> state.counter) // state.xxx 这个xxx是Store入口文件index.js里对应模块的名称
6.1.4 组件修改store中的数据
hook函数 - useDispatch,它的作用是生成提交action对象的dispatch函数
import { useDispatch, useSelector } from "react-redux";
import { decrement, increment } from "./store/modules/counterStore";
function App() {
const { count} = useSelector( state=> state.counter)
const dispatch = useDispatch()
return (
<div className="app">
// 执行store模块中导出的actioncreater方法 得到要提交的action对象 然后dispatch
<button onClick={()=> dispatch(increment())}>+</button>
{count}
<button onClick={()=> dispatch(decrement())}>-</button>
</div>
);
}
6.1.5 提交action传参
在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性上
// couterStore.js
const counterStore = createSlice({
name:'counter',
initialState:{
count: 0
},
reducers:{
increment(state, action){
state.count += action.payload || 1
},
}
})
// 组件中
<button onClick={()=> dispatch(increment(10))}>+10</button>
6.1.6 异步action处理
与 同步的区别主要是要单独封装一个函数,在函数内部return一个新函数,在新函数中 封装异步请求获取数据、 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
// 1、channelStore.js
const channelStore = createSlice(省略 reducers里的同步方法 setChannelList 要传action参数)
// 创建异步
const { setChannelList } = channelStore.actions //【解构出actionCreater】
const url = 'http://geek.itheima.net/v1_0/channels'
// 封装一个函数 在函数中return一个新函数 在新函数中封装异步
// 得到数据之后通过dispatch函数 触发修改
const fetchChannelList = () => {
return async (dispatch) => {
const res = await axios.get(url)
dispatch(setChannelList(res.data.data.channels))
}
}
export { fetchChannelList }
const channelReducer = channelStore.reducer // 【这里是.reducer 不是reducers】
export default channelReducer
2、组件中
import { fetchChannelList } from './store/channelStore'
const { channelList } = useSelector(state => state.channel)
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch])
7.react router
安装 npm i react-router-dom
新建router文件夹,新增index.js入口 ,其中引入组件 用createBrowserRouter创建router实例并导出
// router/index.js
import Login from '../page/Login'
import Article from '../page/Article'
import { createBrowserRouter } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/login',
element: <Login />
},
{
path:'/article',
element: <Article />
}
])
export default router
//index.js
import { RouterProvider } from 'react-router-dom'
import router from './router'
ReactDOM.createRoot(document.getElementById('root')).render(
<RouterProvider router={router}/>
)
7.1路由导航
路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信
7.1.1 声明式导航
通过 <Link/>
组件描述出要跳转到哪里去,to属性指定要跳转到路由path,组件会被渲染为浏览器支持的a链接,如果需要传参直接通过字符串拼接的方式拼接参数即可
import { Link } from "react-router-dom"
<Link to="/article">tiaozhuan</Link>
7.1.2 编程式导航
通过 useNavigate
钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,更加灵活.
import { useNavigate } from "react-router-dom"
const navigate = useNavigate()
return <div>我是home
<button onClick={()=> navigate('/article')}>tiaozhuan</button>
</div>
7.1.3 路由传参
1、searchParams传参
navigate('/article?id=1001&name=jack')
import { useSearchParams } from "react-router-dom"
const [params] = useSearchParams()
let id = params.get('id')
2、 params传参
navigate('/article/1001/jack')
路由配置参数占位符
path:'/article/:id/:name',
import { useParams } from "react-router-dom"
const params = useParams()
let id = params.id
7.2 嵌套路由
在一级路由中又内嵌了其他路由,这种关系就叫做嵌套路由,嵌套至一级路由内的路由又称作二级路由
// 使用 children属性配置路由嵌套关系
const router = createBrowserRouter([
{
path:'/',
element: <Layout />,
children:[
{
index:true, // 默认二级路由 去掉path 设置index:true
element: <Home />
},
{
path:'article',
element: <Article />
},
]
}
])
//
import { Outlet } from "react-router-dom"
// 使用 <Outlet/> 组件配置二级路由渲染位置
<Outlet></Outlet>
7.3 404路由配置
// 准备一个NotFound组件
const NotFound = ()=>{
return <div>404</div>
}
export default NotFound
//在路由表数组的末尾,以*号作为路由path配置路由
{
path:'*',
element:<NotFound />
}
7.4 俩种路由模式
history模式和hash模式, ReactRouter分别由 createBrowerRouter 和 createHashRouter 函数负责创建
路由模式 | url表现 | 底层原理 | 是否需要后端支持 |
---|---|---|---|
history | url/login | history对象 + pushState事件 | 需要 |
hash | url/#/login | 监听hashChange事件 | 不需要 |
8. 开发
8.1 其他常用hooks
省略useState useDispath useEffect useSelector 等
8.1.1 useMemo
缓存用的,只有当一个依赖项改变的时候才会发生变化,
// 参数1:是一个创建依赖项函数 参数2:这个是个数组,存放着被依赖的项,只有当这个数组里面的变量发生了变化,才会调用参数1的函数
useMemo(()=>{
// return 计算之后的值
},[])
useMemo是在useEffect前
useEffect可看成class组件里面的componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合,
而useMemo则可以看作是componentWillUpdate函数。
顺序大致如下:useMemo → render → useEffect
8.1.2 获取当前路由路径 useLocation
const pathUrl = useLocation() 【注意hooks顶层调用】
const selectedKey = pathUrl.pathname
对应的Menu 组件【antd】 selectedKeys={selectedKey} 可以实现菜单高亮
8.1.3 useNavigate
传参见7.1.3
import { useNavigate } from "react-router-dom";
const navigate = useNavigate()
navigate("/session-timed-out");
8.1.4 useReducer 管理多个相对关联的状态数据
import { useReducer } from "react"
import { Button } from "antd"
// 1、定义reducer函数,根据不同的action返回不同的新状态
function reducer(state,action){
switch(action.type){
case 'INC':
return state+1
case 'DEC':
return state - 1
case 'SET':
return action.payload
default:
return state
}
}
function App () {
// 2、组件中调用useReducer(reducer函数,初始值) 得到[state状态,dispatch修改状态方法]
const [state , dispatch] = useReducer(reducer,0)
return <div>
//3、dispatch函数分派一个action对象里面有type 通知reducer产生一个新的状态 使用这个新状态更新UI
<Button onClick={()=> dispatch({type:'DEC'})}> — </Button>
{state}
<Button onClick={()=> dispatch({type:'INC'})}> + </Button>
// 可以传参
<Button onClick={()=> dispatch({type:'SET',payload:100})}> +到100 </Button>
</div>
}
export default App
8.1.5 useCallback
组件多次渲染时缓存函数 保证在App重新渲染的时候保持引用稳定
const onGetSonMessage = useCallback((message) => {
console.log(message)
}, [])
8.1.6 useImperativeHandle
通过ref暴露子组件内部的方法
import { forwardRef, useImperativeHandle, useRef } from 'react'
const MyInput = forwardRef(function Input(props, ref) {
// 实现内部的聚焦逻辑
const inputRef = useRef(null)
const focus = () => inputRef.current.focus()
// 暴露子组件内部的聚焦方法
useImperativeHandle(ref, () => {
return {
focus,
}
})
return <input {...props} ref={inputRef} type="text" />
})
function App() {
const ref = useRef(null)
const focusHandle = () => ref.current.focus()
return (
<div>
<MyInput ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}
export default App
8.2 项目整体开发梳理
8.2.1、CRA 脚手架创建项目
npx create-react-app react-jike
进入项目文件夹 安装依赖 启动
目录结构:【默认的可删除的有App.css; App.test.js;logo.svg;reportWebVitals.js;
setupTests.js 整理App.js 和index.js】
-src
-apis 项目接口函数
-assets 项目资源文件,比如,图片等
-components 通用组件
-pages 页面组件
-router 路由
-store 集中状态管理
-utils 工具,比如,token、axios 的封装等
-App.js 根组件
-index.css 全局样式
-index.js 项目入口
8.2.2、SCSS
预编译的 CSS,支持一些比较高级的语法【变量 嵌套等】,可以提高编写样式的效率,CRA接入
CRA接入scss 只需安装解析 sass 的包:npm i sass -D
8.2.3
、Ant Design组件库
在 create-react-app 中使用 - Ant Design
安装:npm install antd --save
导入并使用: import { Button } from 'antd'
一些常用的:
Form表单
initialValues={{ xxx: 1 }} name为xxx的初始值是1 ;
onFinish回调提交表单 有参数是收集的表单数据.;
Form.Item 的name是接口的字段 rules={[]}校验 ;
form={form} const [form ] = Form.useForm()获取实例 form.setFieldsValue(res.data)回填数据【结构不对无法回填的时候 处理一下数据】
Upload上传 maxCount={1} multiple={}控制上传数量 fileList={} 回显图片;onChange 有参数是{file:{},fileList:[]}
<Radio.Group> 单选框 onChange 有参数事件对象能拿到e.target.value
<Table rowKey="id" columns={columns} dataSource={list} pagination={{total:count,pageSize:queryParams.per_page,onChange:onPageChange}}/>onPageChange函数中参数是点击的页码 然后重新set请求参数对象 从而重新请求数据
Popconfirm气泡确认框 包在button外面
8.2.4、router 路由 看目录7
安装 npm i react-router-dom
pages 里准备 Layout
Login等
基础组件
配置路由: router/index.js 引入组件,进行配置createBrowserRouter([]),导出路由实例 入口index.js:<RouterProvider router={router} />
8.2.5、配置别名路径
8.2.5.1 webPack配置
-
路径解析配置(webpack),把 @/ 解析为 src/
-
路径联想配置(VsCode),VsCode 在输入 @/ 时,自动联想出来对应的 src/下的子级目录
craco插件—路径解析配置
CRA【create-react-app】本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件 - craco
①安装(开发环境-D) npm i -D @craco/craco
②根目录下创建配置文件 craco.config.js
③修改 scripts 命令
//craco.config.js
const path = require('path')
module.exports = {
// webpack 配置
webpack:{
// 配置别名
alias: {
// 约定使用@表示src文件所在lujing
'@': path.resolve(__dirname,'src')
}
}
}
// 修改命令 package.json
"scripts":{
"start":"craco start",
"build":"craco build"
}
联想路径配置 —路径联想配置(VsCode)
根目录下新增配置文件 jsconfig.json
// jsconfig.json
{
"compilerOptions":{
"baseUrl":"./",
"paths":{
"@/*":[
"src/*"
]
}
}
}
8.2.5.2 Vite的路径别名配置
1、让Vite做路径解析(真实的路径转换)
2、让VSCode做智能路径提示(开发者体验)
npm i @types/node -Dnpm i @types/node -D 【ts代码会报错 需要安装node类型包】
vite.config.ts
import path from 'path'
//defineConfig里加入
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
修改tsconfig.app.json文件
"compilerOptions":{} 里加入
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
8.2.6、 登录
表单校验: Form.item 组件上绑定name 校验规则rules ,表单提交回调等
const onFinish=(values) => {
console.log('Success:', values); // 可以拿到表单填写的数据
};
<Form
validateTrigger={['onBlur']} // 统一设置字段触发验证的时机 默认是onChange
onFinish={onFinish} // 提交表单且数据验证成功后回调事件
>
<Form.Item
name="mobile" // 保持与接口字段一致
// 多条校验规则 通过之后再校验下一条
rules={[
{ required: true, message: '请输入手机号!' },
{ pattern:/^1[3-9]\d{9}$/ , message: '请输入正确手机号!' },
]}
>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
// 省略其他
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
8.2.7、 请求封装
安装axios: npm i axios
utils/request.js 文件 :创建 axios 实例,配置 baseURL,超时时间 ,请求拦截器(注入token),响应拦截器
import axios from "axios";
const request = axios.create({
baseURL: 'http://geek.itheima.net/v1_0', // 根域名配置
timeout: 5000 //超时时间
})
// 添加请求拦截器 请求发送之前拦截 插入自定义配置(参数)
request.interceptors.request.use((config)=> {
//1.获取token
const token = getToken()
//2.按照后端有要求拼接token
if(token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, (error)=> {
return Promise.reject(error)
})
// 添加响应拦截器 处理返回的数据
request.interceptors.response.use((response)=> {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error)=> {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
// 监控401 token
if(error.response.status === 401) {
// 清除token 跳转登录
clearToken()
router.navigate('/login')
window.location.reload()//强制刷新
}
return Promise.reject(error)
})
export {request}
utils/index.js 中,统一导出request
import { request } from './request'
export { request }
apis/新建各模块的.js 统一维护
import { request } from "@/utils";
export function loginAPI (formData){
return request({
url:'/authorizations',
method: 'POST',
data:formData
})
}
axios和ts配合
axios提供了request泛型方法, 方便我们传入类型参数推导出接口返回值的类型。泛型参数 Type 的类型决定了 res.data 的类型
1根据接口文档创建一个通用的泛型接口类型(多个接口返回值的结构是相似的)
2. 根据接口文档创建特有的接口数据类型(每个接口有自己特殊的数据格式)
3. 组合1和2的类型, 得到最终传给request泛型的参数类型
export function getListAPI(params: ListParams){
return request.request<ResType<ChannelRes>>({
url:'/articles',
params,
})
}
8.2.8、redux 管理token
安装 省略 具体看目录6
store/modules/user.js
// 用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils'
const userStore = createSlice({
name: 'user',// 模块名
// 数据状态
initialState: {
token:''
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.userInfo = action.payload
}
}
})
// 解构出actionCreater
const { setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
// 发送异步请求
const res = await http.post('/authorizations', loginForm)
// 提交同步action进行token 存入
dispatch(setUserInfo(res.data.token))
}
}
export { fetchLogin }
export default userReducer
store/index.js
// 组合子模块 导出store实例
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({
reducer: {
// 注册子模块
user: userReducer
}
})
index.js
import {Provider} from 'react-redux'
import store from './store'
const root = createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<RouterProvider router={router}>
</RouterProvider>
</Provider>
)
pages/login/index.js 登录并跳转页面
import { useDispatch} from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { fetchLogin } from '../../store/modules/user'
// 表单提交登录
const dispatch = useDispatch()
const navigate = useNavigate()
const onFinish= async (values) => {
await dispatch(fetchLogin(values))
navigate('/')
message.success('登录成功')
};
token持久化
基于Redux的存储方式又是基于内存的,刷新就会丢失
①存token时 localStorage也存一份
②初始化数据的时候 先查询localStorage
封装存取方法 :因为很多模块都要用到
utils/token.js
const TOKENKEY = 'token_key'
function setToken (token) {
return localStorage.setItem(TOKENKEY, token)
}
function getToken () {
return localStorage.getItem(TOKENKEY)
}
function clearToken () {
return localStorage.removeItem(TOKENKEY)
}
export {
setToken,
getToken,
clearToken
}
utils/index.js 中转导出
store/modules/user.js
import { setToken as _setToken,getToken} from '@/utils'
initialState: { // 数据状态
token: getToken() || ''
},
reducers:{ // 同步修改方法
setToken(state,action){
state.token = action.payload
_setToken(action.payload)
}
}
8.2.9 路由鉴权
封装 AuthRoute
路由鉴权高阶组件,判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login
components/AuthRoute/index.jsx
import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'
const AuthRoute = ({ children }) => {
const isToken = getToken()
if (isToken) {
return <>{children}</>
} else {
return <Navigate to="/login" replace />
}
}
export default AuthRoute
src/router/index.jsx
import AuthRoute from '@/components/Auth'
// 登录时,直接渲染相应页面组件 未登录时,重定向到登录页面
element: <AuthRoute><Layout /></AuthRoute>,
二级路由 嵌套子路由 children: [] 和Layout
中配置二级路由出口<Outlet/> 省略
路由菜单交互实现useNavigate() 省略
8.2.10 获取个人信息,退出登录,token失效
获取个人信息:①在Redux的store中编写获取用户信息的相关逻辑 ②在Layout组件中触发action的执行 ③使用store中的数据进行渲染
退出登录:①添加确认回调事件 ②store
中新增退出登录的action函数,在其中删除token 用户信息 ③回调事件中,调用userStore中的退出action ④返回登录页面
token失效:一段时间不做任何操作,超时之后应该清除所有过期用户信息跳回到登录
请求接口返回401 监控这个状态 具体看8.2.7
8.2.11 echarts
获取 ECharts - 入门篇 - 使用手册 - Apache ECharts
安装之后 封装组件
import * as echarts from "echarts"
import { useEffect, useRef } from "react"
const BarChart = ({title,xData, sData, style = { width: '400px', height: '300px' }})=> {
const chartRef = useRef(null)
useEffect(()=>{// useEffect保证dom可用 才进行图表的渲染
//1. 获取渲染图标的dom节点 要有宽高
const chartDom = chartRef.current
// 2. 图表初始化生成图表实例对象
const myChart = echarts.init(chartDom)
// 3.准备图表参数
const option = {
title: {
text:title
},
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
series: [
{
data: sData,
type: 'bar'
}
]
}
myChart.setOption(option)
},[])
return <div ref={chartRef} style={style}></div>
}
export default BarChart
使用
<BarChart
title={'三大框架数量'}
xData={['Vue', 'React', 'Angular']}
sData={[2000, 5000, 1000]} />
8.2.12 打包
打包指的是将项目中的源代码和资源文件进行处理,生成可在生产环境中运行的静态文件的过程打包
命令:npm run build
8.2.12.1 项目本地预览
-
全局安装本地服务包
npm i -g serve
该包提供了serve命令,用来启动本地服务器 -
在项目根目录中执行命令
serve -s ./build
在build目录中开启服务器 -
在浏览器中访问:
http://localhost:3000/
预览项目
8.2.12.2 打包优化-配置路由懒加载
路由的JS资源只有在被访问时才会动态获取,目的是为了优化项目首次打开的时间
// router/index.js
import { lazy , Suspense} from "react";
// import Artical from "../page/article";
const Artical = lazy(()=> import("@/page/article")) //1. 使用 lazy 方法导入路由组件
2. 使用内置的 Suspense 组件渲
element: (
<Suspense fallback={'加载中'}>
<Article />
</Suspense>
)
如果有报错:Import in body of module; reorder to top import/first
将导入语句移至模块顶部并置于所有业务代码之前。 所以将const Artical = lazy(()=> import("@/page/article"))放在所有import代码之后
打开控制面板-network-选择js 可以看到路由切换时加载的
8.2.12.3 打包优化-包体积可视化分析
①安装分析打包体积的包:npm i source-map-explorer
②在 package.json 中的 scripts 标签中,添加分析打包体积的命令
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
}
③对项目打包:npm run build
(如果已经打过包,可省略这一步)
④运行分析命令:npm run analyze
⑤通过浏览器打开的页面,分析图表中的包体积
8.2.12.4 打包优化-CDN优化
CDN是一种内容分发网络服务,当用户请求网站内容时,由离用户最近的服务器将缓存的资源内容传递给用户
哪些资源可以放到CDN服务器?
体积较大的非业务JS文件,比如react、react-dom
1.体积较大,需要利用CDN文件在浏览器的缓存特性,加快加载时间
2.非业务JS文件,不需要经常做变动,CDN不用频繁更新缓存
实践:
1.把需要做CDN缓存的文件排除在打包之外(react、react-dom)
2.以CDN的方式重新引入资源(reac、react-dom)
craco.config.js
const path = require('path')
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src')
},
// 配置webpack
// 配置CDN
configure: (webpackConfig) => {
let cdn = {
js:[]
}
whenProd(() => {
// key: 不参与打包的包(由dependencies依赖项中的key决定)
// value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
webpackConfig.externals = {
react: 'React',
'react-dom': 'ReactDOM'
}
// 配置现成的cdn资源地址
// 实际开发的时候 用公司自己花钱买的cdn服务器
cdn = {
js: [
'https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js',
]
}
})
// 通过 htmlWebpackPlugin插件 在public/index.html注入cdn资源url
const { isFound, match } = getPlugin(
webpackConfig,
pluginByName('HtmlWebpackPlugin')
)
if (isFound) {
// 找到了HtmlWebpackPlugin的插件
match.userOptions.files = cdn
}
return webpackConfig
}
}
}
public/index.html
<body>
<div id="root"></div>
<!-- 加载第三发包的 CDN 链接 -->
<% htmlWebpackPlugin.options.files.js.forEach(cdnURL => { %>
<script src="<%= cdnURL %>"></script>
<% }) %>
</body>
8.2.13 react.memo优化组件渲染
允许组件在props没有改变的情况下跳过重新渲染【默认机制:顶层组件发生重新渲染,这个组件树的子级组件都会被重新渲染 】
memo 进行包裹之后,返回一个新的组件MemoSon, 只有传给MemoSon的props参数发生变化时才会重新渲染
const MemoSon = React.memo(function Son() { // React.memo可以改为memo(引入)
console.log('子组件被重新渲染了')
return <div>this is span</div>
})
<MemoSon />
props的比较机制:进行的是‘浅比较’,底层使用 Object.is
进行比较,针对于对象数据类型,只会对比俩次的引用是否相等,如果不相等就会重新渲染,Object([], []) => false 有变化, 当父组件的菌数重新执行时,实际上形成的是新的数组引用
如果就想让引用确定, 那么可以用useMemo缓存
8.2.14 forwardRef
允许组件使用ref将一个DOM节点暴露给父组件
import { forwardRef, useRef } from 'react'
// forwardRef包裹 子组件接收 props, ref
const MyInput = forwardRef(function Input(props, ref) {
return <input {...props} type="text" ref={ref} />//对应的DOM上绑定ref
}, [])
function App() {
const ref = useRef(null)
const focusHandle = () => {
// 可以获取
ref.current.focus()
}
return (
<div>
<MyInput ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}
export default App
9. clssAPI(已不推荐)
// class API
import { Component } from 'react'
// 继承react的基类Component
class Counter extends Component {
// 状态变量state
state = {
count: 0,
}
// 事件回调
clickHandler = () => {
// 修改状态变量this.setState() 触发UI组件渲染
this.setState({
count: this.state.count + 1,
})
}
// UI模版render(){}函数
render() {
return <button onClick={this.clickHandler}>+{this.state.count}</button>
}
}
function App() {
return (
<div>
<Counter />
</div>
)
}
export default App
9.1 类组件生命周期
9.2 类组件通信(依赖this)
父传子:直接通过prop子组件标签身上绑定父组件中的数据即可
<Son msg={this.state.msg}/> 子:this.props.msg
子传父:在子组件标签身上绑定父组件中的数,子组件中调用这个函数传递参数
onGetSonMsg = (msg) => { this.setState({ msg }) } 子组件标签上onGetSonMsg={this.onGetSonMsg} 子组件中const { onGetSonMsg } = this.props onClick={() => onGetSonMsg('this is son msg')}
10 zustand 简洁状态管理工具
import { create } from 'zustand'
// 创建store 函数参数必须返回一个对象 对象内部写状态数据和方法
const useStore = create((set) => {
return {
count: 0,
inc: () => {
// 必须调用set来修改数据 两种写法①参数是函数 需要用到老数据时
set(state => ({ count: state.count + 1 }))
// ②参数是一个对象
set({ count: 1 })
},
channelList:[],
// 支持异步
getList: async ()=>{
const res = await fetch(URL)
const resJson = await res.json()
set({ channelList: resJson.data.channels})
}
}
})
export default useStore
使用:
import useStore from './store/useCounterStore.js'
// useStore() 返回的是一个对象 解构出状态变量和方法
const { count, inc,channelList, getList} = useStore()
<button onClick={inc}>{count}</button>}
切片模式
场景:当我们单个store比较大的时候,可以采用一种切片模式
进行模块拆分再组合
import { create } from 'zustand'
// 创建counter相关切片
const createCounterStore = (set) => {
return {
// 省略
}
}
// 创建channel相关切片
const createChannelStore = (set) => {
return {
// 省略
}
}
// 组合切片
const useStore = create((...a) => ({
...createCounterStore(...a),
...createChannelStore(...a)
}))
使用:同不切分
11 Vite
是一个框架无关的前端工具链, 可以快速的生成一个 React + TS 的开发环境, 并且可以提供快速的开发体验
npm create vite@latest react-ts-pro -- --template react-ts
① npm create vite@latest 固定写法 (使用最新版本vite初始化项目)
② react-ts-pro 项目名称 (可以自定义)
③ -- --template react-ts 指定项目模版位react+ts
12 Typescript
12.1 useState
自动推导:通常React会根据传入useState的默认值来自动推导类型(setXxx的时候也会校验参数类型),不需要显式标注类型
传递泛型参数:useState本身是一个泛型函数,可以传入具体的自定义类型
type User = {
name: string
age: number
}
//1 限制useState函数参数的初始值必须满足类型为: User | () => {return User}
const [user, setUser] = React.useState<User>({
name: 'jack',
age: 18
})
// 2 执行setUser 限制参数必须满足类型为:User | ()=> User | undefined
setUser(newUser)
// 这里newUser对象只能是User类型
3. 使用user状态数据具备User类型相关的类型提示
初始值为null: 当我们不知道状态的初始值是什么,将useState的初始值为null是一个常见的做法,可以通过具体类型联合null来做显式注解useState<User l null>(null)
在使用的时候 {user?.age} ?. 是可选链运算符 当做类型守卫 null
或者 undefined
) 的情况下不会引起错误
12.2 props
为组件prop添加类型,本质是给函数的参数做类型注解,可以使用type对象类型或者interface接口来做注解
// type Props = {
// className:string
// }
interface Props {
className: string
}
function Button(props:Props){
return <button className={props.className}>确定</button>
}
父组件 Button组件只能传入名称为className的prop参数,类型为string, 且为必填
<Button className='test'></Button>
12.2.1 为children添加类型:
是一个比较特殊的prop, 支持多种不同类型数据的传入,需要通过一个内置的ReactNode类型来做注解
type Props = {
className:string
children: React.ReactNode
}
function Button(props:Props){
return <button className={props.className}>{props.children}</button>
}
使用:children可以是多种类型,包括:React.ReactElement 、string、number、
React.ReactFragment 、React.ReactPortal 、boolean、 null 、undefined
<Button className='test'>ceshi</Button>
12.2.2 事件
type Props = {
onGetMsg?:(msg:string)=> void
}
function Button(props:Props){
const {onGetMsg} = props
const clickHandle = ()=>{
onGetMsg?.("msg") // onGetMsg非必须 所以需要用?. 不然报错:不能调用可能是“未定义”的对象
}
return <button onClick={clickHandle}>子传父</button>
}
父组件:
// 需要遵守类型的约束,参数传递需要满足要求 不然提示参数“msg”隐式具有“any”类型
const getMsgHandler =(msg:string)=>{
console.log(msg);
}
// 1、绑定prop时如果绑定内联函数直接可以推断出参数类型,
<Button onGetMsg={(msg)=>console.log(msg)}></Button>
// 2、否则需要单独注解匹配的参数类型
<Button onGetMsg={getMsgHandler}></Button>
12.3 useRef
获取dom的场景,可以直接把要获取的dom元素的类型当成泛型参数传递给useRef,可以推导出.current属性的类型
function Foo() {
// 尽可能提供一个具体的dom type, 可以帮助我们在用dom属性时有更明确的提示
// divRef的类型为 RefObject<HTMLDivElement> input的话是<HTMLInputElement>
const inputRef = useRef<HTMLDivElement>(null)
useEffect(() => {
inputRef.current?.focus()
},[])
return <div ref={inputRef}>etc</div>
}
引用稳定的存储器:把useRef当成引用稳定的存储器使用的场景可以通过泛型传入联合类型来做,比如定时器的场景(不是很明白)
const timeId = useRef<number | undefined>(undefined)
useEffect(()=>{
timeId.current = setInterval(()=>{
console.log('12');
},1000)
return ()=> clearInterval(timeId.current)
},[])
13 视频案例用到的库 工具等
13.1. lodash
13.1.1orderBy
lodash.orderBy | Lodash中文文档 | Lodash中文网
安装 npm i --save lodash
yarn add lodash
使用 import _ from 'lodash'
// 第一个参数Array|Object 第二个参数 按什么排序 第三个参数 正序倒序
_.orderBy(users, ['user'], ['desc']);
13.1.2 groupBy分组
// 参数1:Array|Object 用来迭代的集合
// 参数2 这个迭代函数用来转换key(根据什么来分组)
_.groupBy([], '');
举例
_.groupBy(['one', 'two', 'three'], 'length');
// => { '3': ['one', 'two'], '5': ['three'] }
13.2. uuid
生成独一无二id
安装: npm install uuid
import { v4 as uuidv4 } from 'uuid';
uuidv4()
13.3. day.js
轻量化处理时间
安装 npm install dayjs
使用:
import dayjs from 'dayjs'
dayjs().format('MM-DD HH:-mm') // 10-30 09:07
13.4. json-server 工具
数据Mock:前端可以在没有实际后端接口的支持下先进行接口数据的模拟,进行正常的业务功能开发
方式:
①前端直接写假数据(纯静态,没有服务)
②自研Mock平台
③ json-server等工具
是一个快速以json文件作为数据源模拟接口服务的工具
安装 npm install json-server -D
根目录下准备一个db.json 文件
package.json中scripts里添加
"serve": "json-server db.json --port 3004"
npm run serve 保持服务不关闭
13.5. 谷歌调试插件
①点击谷歌右侧竖着的三个点→更多工具(more tools)→ 扩展extensions
②直接打开chrome://extensions/
③谷歌右侧有扩展的图标 点击 之后再点击Manage extensions
打开开发者模式Developer mode
安装办法
第一种、谷歌商店 搜索安装
第二种、极简插件 搜索 React Developer Tools 和 Redux DevTools 下载→解压→ 将.crx文件拖到前面打开的扩展页面→ 弹出框内选择添加 → 重启谷歌
Redux DevTools 会提示No store found. Make sure to follow the instructions.
https://github.com/zalmoxisus/redux-devtools-extension#usage 查看这个知道还要做一些配置 但是这里用的是 redux toolkit 不知如何处理
13.6. Ant Design Mobile
npm install --save antd-mobile
13.6.1 主题定制
// 1、全局
// 根目录加一个theme.css
:root:root {
--adm-color-primary: #a062d4;
}
// 入口文件index.js导入
import './theme.css'
//2、局部
.purple-theme {
--adm-color-primary: #a062d4;
}
<div className='purple-theme'>
<Button color='primary'>Purple</Button>
</div>
13.6.2 List无限滚动
1. 滑动到底部触发加载下一页动作
<InfiniteScroll/> loadMore回调函数加上async
滚动条件:① hasMore为true 2 距离底部距离<threshold
2. loadMore加载下一页数据
3. 把老数据和新数据做拼接处理
[...oldList, ... newList]
4. 停止监听边界值
没有数据了将hasMore设置为false
13.7. 样式reset normalize.css
安装: npm install normalize.css
入口文件中引入:import 'normalize.css';
13.8. 富文本编辑器 react-quill
npm i react-quill@2.0.0-beta.2 【特定版本兼容react18 如果失败尝试npm i react-quill@2.0.0-beta.2 --legacy-peer-deps】
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
<ReactQuill
className="publish-quill"
theme="snow"
placeholder="请输入文章内容"
/>
13.9. 引入汉化包 时间选择器显示中文
import locale from 'antd/es/date-picker/locale/zh_CN'
{/* 传入locale属性 控制中文显示*/}
<RangePicker locale={locale}></RangePicker>