# React18 **Repository Path**: laiht/react18 ## Basic Information - **Project Name**: React18 - **Description**: 看的李立超的视频 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-06-19 - **Last Updated**: 2024-06-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [TOC] # React18 ​ ---讲师:李立超 ,懂王(router部分) ## 基础复习 ### 变量的声明 > `var` VS` let` - `var`变量没有块级作用域,即代码块内部声明的变量在代码块外部也能访问。解决办法就是使用闭包 ```js for(var i=0; i<4; i++){ // 随便写点什么 } console.log(i); // 仍然可以访问到变量i输出 4 //使用闭包 (function(){ if(false){ var b=33 } }) ``` - `let`有块级作用域 - `var`有变量提升,任意位置var变量会在所有代码执行前声明。 > `const` const在JS中用来声明常量,所谓常量就是只能赋值一次的变量,当一个变量用来保存一个对象时(函数或其他对象),为了避免变量被修改通常会使用const来声明。 ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-05-28_15-03-11.png) ### 解构和展开 #### 解构 通过解构赋值可以直接将数组中的元素,对象中的属性赋值给其他的变量。 > 数组解构 ```js let a, b, rest; [a, b] = [10, 20]; // 第一个元素给a,第二个元素给b console.log(a); // 10 console.log(b); // 20 ​ [a, , b] = [10, 20, 30]; // 第一个元素给a,第三个元素给b,中间空出一个 console.log(a); // 10 console.log(b); // 30 ​ [a, b, ...rest] = [10, 20, 30, 40, 50]; // 第一个元素给a,第二个元素给b,剩下的给rest console.log(a); // 10 console.log(b); // 20 console.log(rest); // [30, 40, 50] ​ const [d, e, f] = [40, 50, 60] // 可以在赋值时直接声明变量 console.log(d); // 40 console.log(e); // 50 console.log(f); // 60 ​ [a=5, b=7] = [1]; // 赋值是可以指定默认值 console.log(a); // 1 console.log(b); // 7 //交换变量值 let a = 1; let b = 3; ​ [a, b] = [b, a]; // 交换变量a和b的值 console.log(a); // 3 console.log(b); // 1 ​ const arr = [1,2,3]; [arr[2], arr[1]] = [arr[1], arr[2]]; // 交换数组中两个元素的位置 console.log(arr); // [1,3,2] ``` > 对象解构 ```js let a, b; ({ a, b } = { a: 10, b: 20 }); //属性a赋值给变量a,属性b赋值给变量b console.log(a); // 10 console.log(b); // 20 ​ ({a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40}); //属性a赋值给变量a,属性b赋值给变量b,其余的保存到rest中 console.log(a); // 10 console.log(b); // 20 console.log(rest); // {c: 30, d: 40} ​ ({a = 10, b = 5} = {a: 3}); // 可以指定默认值 console.log(a); // 3 console.log(b); // 5 ​ const {name, age} = {name:'孙悟空', age:18}; // 对象解构时如果使用const、let、var开头,可以不加(),变量和属性名对应名字匹配 console.log(name); // 孙悟空 console.log(age); // 18 ​ const {len: width} = {len:100}; // 将对象的len属性值赋值给变量width console.log(width); // 100 ​ const {inner:{size}} = {a: 10, b: 20, inner:{size: 5}}; // 将对象中inner.size赋值给变量size console.log(size) // 5 ​ const {inner:{size:mySize}} = {a: 10, b: 20, inner:{size: 5}}; // 将对象中inner.size赋值给变量mySize console.log(mySize); // 5 ​ ``` #### 展开 展开可以在函数调用时,将数组(或字符串)展开为函数的参数,也可以在通过字面量创建数组(或对象)时,直接将其他数组(或对象)在新数组(或对象)中展开(类似于浅拷贝)。 语法: ```js myFunction(...iterableObj); // 展开数组传参 ​ [...iterableObj, '4', 'five', 6]; // 创建数组时展开其他数组 ​ let objClone = { ...obj }; // 将一个对象中的所有键值对展开到一个新对象中(浅拷贝) ``` ```js const arr = [0, 1, 2]; const newArr = [...arr, 12]; console.log(newArr); // [0, 1, 2, 12] ​ let parts = ['shoulders', 'knees']; let lyrics = ['head', ...parts, 'and', 'toes']; // ["head", "shoulders", "knees", "and", "toes"] ​ let arr1 = [0, 1, 2]; let arr2 = [3, 4, 5]; arr1 = [...arr1, ...arr2]; ​ let obj1 = { name: '孙悟空', age: 18 }; let obj2 = { name: '猪八戒', address: '高老庄' }; ​ let clonedObj = { ...obj1 }; // 浅拷贝对象 // Object { name: '孙悟空', age: 18 } ​ let mergedObj = { ...obj1, ...obj2 }; // 合并两个对象,相同属性会被后面的属性值覆盖 // Object {name: '猪八戒', age: 18, address: '高老庄'} ``` ### 箭头函数 箭头函数是传统函数表达式的简写方式,它简化了函数的编写,也带来了一些限制导致在一些场景下它无法使用。 特点: 1. 箭头函数没有自己的this 2. 箭头函数中没有arguments(当前函数的实参会保存在arguments中) 3. 不能作为构造函数调用 4. 无法通过call、apply、bind指定函数的this ```js // 基本语法 param => expression ​ // 多个参数时,参数需要使用()括起来 (param1, paramN) => expression ​ // 多条语句时,语句需要使用{}括起来,同时使用return设置返回值 param => { let a = 1; return a + param; } ​ // 返回值是一个对象时,对象需要加() params => ({foo: "a"}) ​ //多余参数、默认参数和传统函数无异 (a, b, ...args) => expression (a=400, b=20, c) => expression ``` ## 模块化 ### 导出 在创建JS模块时,我们通过export向模块外部暴露内容(函数、对象、原始值)。在其他模块中可以通过import引入这些内容。使用了export的模块会自动开启严格模式。 export导出的方式有两种: 1. 默认导出 2. 命名导出 ```js // 导出变量(命名导出) export let name1, name2, …, nameN; export let name1 = …, name2 = …, …, nameN; ​ // 导出函数(命名导出) export function functionName(){...} ​ // 导出类(命名导出) export class ClassName {...} ​ // 导出一组 export { name1, name2, …, nameN }; ​ // 重命名导出 export { variable1 as name1, variable2 as name2, …, nameN }; ​ // 解构赋值后导出 export const { name1, name2: bar } = o; ​ // 默认导出 export default expression; export default function (…) { … } // also class, function* export default function name1(…) { … } // also class, function* export { name1 as default, … }; ​ // 聚合模块 export * from …; // 将其他模块中的全部内容导出(除了default) export * as name1 from …; // ECMAScript® 2O20 将其他模块中的全部内容以指定别名导出 export { name1, name2, …, nameN } from …; // 将其他模块中的指定内容导出 export { import1 as name1, import2 as name2, …, nameN } from …; // 将其他模块中的指定内容重命名导出 export { default, … } from …; ``` ### 引入 import用来引入其他模块中导出的内容,注意!只有通过export导出的内容才能够通过import引入。和export一样,使用了import的模块会自动启用严格模式。 ```js // 引入默认导出 import defaultExport from "module-name"; ​ // 将所有模块导入到指定命名空间中 import * as name from "module-name"; ​ // 引入模块中的指定内容 import { export1 } from "module-name"; import { export1 , export2 } from "module-name"; ​ // 以指定别名引入模块中的指定内容 import { export1 as alias1 } from "module-name"; import { export1 , export2 as alias2 , [...] } from "module-name"; ​ // 引入默认和其他内容 import defaultExport, { export1 [ , [...] ] } from "module-name"; import defaultExport, * as name from "module-name"; ​ // 引入模块 import "module-name"; ``` ## React简介 使用React开发Web项目,我们需要引入两个js脚本: `react.development.js` - react 是react核心库,只要使用react就必须要引入 - 下载地址:https://unpkg.com/react@18.0.0/umd/react.development.js `react-dom.development.js` - react-dom 是react的dom包,使用react开发web应用时必须引入 - 下载地址:https://unpkg.com/react-dom@18.0.0/umd/react-dom.development.js ## 三个API `React.createElement()` - `React.createElement(type, [props], [...children])` - 用来创建React元素 - 参数: 1.元素的名称(html标签必须小写)2.标签中的属性:class属性需要使用className来设置;在设置事件时,属性名需要修改为驼峰命名法3.元素的内容(子元素) - 注意点: React元素最终会通过虚拟DOM转换为真实的DOM元素 React元素一旦创建就无法修改,只能通过新创建的元素进行替换 `ReactDOM.createRoot()` - `createRoot(container[, options])` - 用来创建React的根容器,容器用来放置React元素 `root.render()` - `root.render(element)` - 用来将React元素渲染到根元素中 - 当首次调用时,容器节点里的所有 DOM 元素都会被替换, 当重复调用render()时会使用 React 的 DOM 差分算法(DOM diffing algorithm)进行高效的更新。 - 不会修改容器节点(只会修改容器的子节点)。可以在不覆盖现有子节点的情况下,将组件插入已有的 DOM 节点中。 ## JSX JSX 是 JavaScript 的语法扩展,JSX 使得我们可以以类似于 HTML 的形式去使用 JS。JSX便是React中声明式编程的体现方式。声明式编程,简单理解就是以结果为导向的编程。使用JSX将我们所期望的网页结构编写出来,然后React再根据JSX自动生成JS代码。所以我们所编写的JSX代码,最终都会转换为以调用`React.createElement()`创建元素的代码。 注意事项: 1. 不要加引号 2. 有且只有一个根标签 3. html标签小写开头,React组件大写开头 4. 可以使用{}插入JS表达式。(表达式:有返回值的语句。JSX也是表达式) 5. 属性正常写(class使用className,style必须用{}) 6. 标签必须正常闭合 7. 布尔类型、Null 以及 Undefined 将会忽略 8. 没有返回值的语句,像if、for等语句是不能出现在JSX中的! ### JSX高频场景-JS表达式 > 在JSX中可以通过 `大括号语法{}` 识别JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等 1. 使用引号传递字符串 2. 使用JS变量 3. 函数调用和方法调用 4. 使用JavaScript对象 注意:if语句、switch语句、变量声明不属于表达式,不能出现 在{}中 ```jsx const count = 100 function getName() { return '张三' } function App() { return (
{/* 使用引号传递字符串 */} {'this is a message'} {/* 识别js变量 */} {count} {/* 函数调用*/} {getName()} {/* 方法调用 */} {new Date().getDate()} {/* 使用js对象 */}
this is a div
); } ``` ### JSX高频场景-列表渲染 ![image-20240529155829766](D:\Codes\前端学习\12-react\react18\assert\image-20240529155829766.png) > 在JSX中可以使用原生js种的`map方法` 实现列表渲染 ```jsx const list = [ {id:1001, name:'Vue'}, {id:1002, name: 'React'}, {id:1003, name: 'Angular'} ] function App(){ return ( ) } ``` 循环生成多个组件中,需要加上key值(唯一值) 在React内部就设定了一个key属性,key属性可以作为React元素的唯一标识,我们可以为列表的每一个元素指定一个唯一的key,React就可以根据key而不是位置来比较元素,这样一来无论元素的位置如何改变,都不会导致过多的元素渲染,因为有了key以后,React就不再通过位置来比较两个元素了。 设置key要求: 1. key必须在当前列表的元素中是唯一的 2. 一个元素的key最好是固定的 3. 前提是元素的顺序不会发生变化,除此之外不要用索引做key。 ### JSX高频场景-条件渲染 ![image.png](D:/Codes/前端学习/12-react/react-basic/notes/assets/05.png) > 在React中,可以通过逻辑与运算符&&、三元表达式(?:) 实现基础的条件渲染 ```jsx const flag = true const loading = false function App(){ return (
{flag && this is span} {loading ? loading...:this is span}
) } ``` ### JSX高频场景-复杂条件渲染 ![image.png](D:/Codes/前端学习/12-react/react-basic/notes/assets/06.png) > 需求:列表中需要根据文章的状态适配不同的图片展示方式 > 解决方案:自定义函数 + 判断语句 ```jsx const type = 1 // 0|1|3 function getArticleJSX(){ if(type === 0){ return
无图模式模版
}else if(type === 1){ return
单图模式模版
}else(type === 3){ return
三图模式模版
} } function App(){ return ( <> { getArticleJSX() } ) } ``` 使用`<> `标签在页面加载时该标签中的内容不会显示,其实`<>`是``的简写 ## 创建React项目 create-react-app是一个快速创建React开发环境的工具,底层由Webpack构件,封装了配置细节,开箱即用 执行命令: ```bash npx create-react-app react-basic ``` 1. npx - Node.js工具命令,查找并执行后续的包命令 2. create-react-app - 核心包(固定写法),用于创建React项目 3. react-basic React项目的名称(可以自定义) 运行项目 ``` npm start ``` ### 文件夹结构 - node_models:依赖包存放的位置 - public:公共资源文件 - src:项目源码 - App.js:项目根组件 - index.js:入口文件 ## React函数组件基础使用 ### 组件是什么 概念:一个组件就是一个用户界面的一部分,它可以有自己的逻辑和外观,组件之间可以互相嵌套,也可以复用多次 ![image-20240529160907783](D:\Codes\前端学习\12-react\react18\assert\image-20240529160907783.png) ### 组件基础使用 > 在React中,一个组件就是**首字母大写的函数**,内部存放了组件的逻辑和视图UI, 渲染组件只需要把组件当成标签书写即可 ```jsx // 1. 定义组件 function Button(){ return } // 2. 使用组件 function App(){ return (
{/* 自闭合 */}
) } ``` - React还有一种类组件的写法。 ### 组件中引入样式 直接在组件中import即可。例如:我们打算为Button组件编写一组样式,并将其存储到Button.css中。我们只需要直接在Button.js中引入Button.css就能轻易完成样式的设置。 Button.css: ```css button{ background-color: #bfa; } ``` Button.js: ```jsx import './Button.css'; const Button = () => { return ; }; export default Button; ``` 使用这种方式引入的样式,需要注意以下几点: 1. CSS就是标准的CSS语法,各种选择器、样式、媒体查询之类正常写即可。 2. 尽量将js文件和css文件的文件名设置为相同的文件名。 3. 引入样式时直接import,无需指定名字,且引入样式必须以./或../开头。 4. 这种形式引入的样式是全局样式,有可能会被其他样式覆盖。 ## React的事件绑定 ### 基础实现 > React中的事件绑定,通过语法 `on + 事件名称 = { 事件处理程序 }`,整体上遵循驼峰命名法 ```jsx function App(){ const clickHandler = ()=>{ console.log('button按钮点击了') } return ( ) } ``` 注意:clickHandler是没有加()的,如果加了()函数会在赋值时立刻执行,而赋值给onClick事件的将是函数的返回值undefined,这将导致事件的设置失效。 ### 使用事件参数 > 在事件回调函数中设置形参e即可 ```jsx function App(){ const clickHandler = (e)=>{ console.log('button按钮点击了', e) } return ( ) } ``` ### 传递自定义参数 > 语法:`事件绑定的位置`改造成箭头函数的写法,在执行clickHandler实际处理业务函数的时候传递实参 ```jsx function App(){ const clickHandler = (name)=>{ console.log('button按钮点击了', name) } return ( ) } ``` 注意:不能直接写函数调用,这里事件绑定需要一个函数引用 ### 同时传递事件对象和自定义参数 > 语法:在事件绑定的位置传递事件实参e和自定义参数,clickHandler中声明形参,注意顺序对应 ```jsx function App(){ const clickHandler = (name,e)=>{ console.log('button按钮点击了', name,e) } return ( ) } ``` ## 组件状态管理-useState ### 基础使用 > useState 是一个 React Hook(函数),它允许我们向组件添加一个`状态变量`, 从而控制影响组件的渲染结果 > 和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图) ![image-20240529172321163](D:\Codes\前端学习\12-react\react18\assert\readme.txt) ![image-20240529172357581](D:\Codes\前端学习\12-react\react18\assert\image-20240529172357581.png) ```jsx import { useState } from "react"; function App() { //调用useState添加一个状态变量,注意位置 const [count,setCount]=useState(0) return ( <>
{count}
); } export default App; ``` ### 状态的修改规则 > 在React中状态被认为是只读的,我们应该始终`替换它而不是修改它`, 直接修改状态不能引发视图更新 ![image-20240529172416297](D:\Codes\前端学习\12-react\react18\assert\image-20240529172416297.png) ### state的问题 在React中我们通过`setState()`修改状态都是异步完成的,换句话说并不是调用完`setState()`后状态立刻就发生变化,而是需要等上一段时间。 ```jsx import React, {useState} from 'react'; const Counter = () => { const [count, setCount] = useState(1); const clickHandler = ()=> { setTimeout(()=>{ setCount(count+1); }, 1000); } return (

{count}

); }; ``` 点击按钮后1秒`setCount()`才会调用,如果我们在1秒内点击按钮多次,你会发现按钮数值只会增加一次,很显然我们不希望这种情况出现。 解决方案: 在`setState()`时除了直接传递一个指定值以外,React还允许我们通过一个回调函数来修改state,回调函数的返回值就是新的state的值,使用回调函数的好处是,这个回调函数会确保上一次的`setState()`调用完成后才被调用,同时会使用最新的state值作为回调函数的第一个参数。这样一来就有效的避免了无法正确获取上一个state值的问题。 上边案例中的` setCount(count+1);`可以改成这个样子: ``` setCount(prevState => prevState+1); ``` 这样一来,函数中的prevState总是上次修改后的最新state,避免再次出现点击多次按钮只修改一次的问题。总的来说,当我们修改一个state的值而需要依赖于前边的值进行计算时,最安全的方式就是通过回调函数而不是直接修改。 ### setState()的执行流程 `setState()`的执行依赖于`dispatchSetState()`,该函数会先判断组件当前处于什么阶段,如果是渲染阶段,不会检查State值是否相同;如果是非渲染阶段会检查state值是否相同,如果值不相同,则对组件重新渲染。如果值相同,则不对组件重新渲染。如果值相同,React在一定情况下会继续执行当前组件的渲染,但是这个渲染不会触发其子组件的渲染,这次渲染不会产生实际的效果。 ### 修改对象状态 > 对于对象类型的状态变量,应该始终给set方法一个`全新的对象` 来进行修改 ![image-20240529172504242](D:\Codes\前端学习\12-react\react18\assert\image-20240529172504242.png) ```jsx function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const handlrClick=()=>{ //这样做就制造了一个mutation position.x=5 console.log(position.x);//5 } return ( <>
x:{position.x}
); } ``` 上述做法因为没有使用state设置的函数,React并不知道对象已经更改。所以React不会做出任何响应。 在这种情况下,为了真正地 [触发一次重新渲染](https://zh-hans.react.dev/learn/state-as-a-snapshot#setting-state-triggers-renders),**你需要创建一个新对象并把它传递给 state 的设置函数**: ```jsx function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const handlrClick = () => { setPosition({ x: 2, y: 3 }); } return ( <>
x:{position.x}
y:{position.y}
); } ``` 通过使用 `setPosition`,你在告诉 React: - 使用这个新的对象替换 `position` 的值 - 然后再次渲染这个组件 #### **使用展开语法复制对象** 使用 `...` [对象展开](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals) 语法,这样就不需要单独复制每个属性。 ```jsx setPosition({ ...position, x:2 }); ``` 请注意 `...` 展开语法本质是是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。 #### 使用一个事件处理函数来更新多个字段 你也可以在对象的定义中使用 `[` 和 `]` 括号来实现属性的`动态命名`。下面是同一个例子,但它使用了一个事件处理函数而不是三个: ```jsx export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleChange(e) { setPerson({ ...person, //使用[]实现动态命名 [e.target.name]: e.target.value }); } return ( <>

{person.firstName}{' '} {person.lastName}{' '} ({person.email})

); } ``` #### 更新一个嵌套对象 对于下列对象: ```jsx const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); ``` 如果你想要更新 `person.artwork.city` 的值 方法一: ```jsx const nextArtwork = { ...person.artwork, city: 'New Delhi' }; const nextPerson = { ...person, artwork: nextArtwork }; setPerson(nextPerson); ``` 方法二: ```jsx setPerson({ ...person, // 复制其它字段的数据 artwork: { // 替换 artwork 字段 ...person.artwork, // 复制之前 person.artwork 中的数据 city: 'New Delhi' // 但是将 city 的值替换为 New Delhi! } }); ``` ### 修改数组状态 > 更新存储于 state 中的数组时,需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。 当你操作 React state 中的数组时,你需要避免使用左列的方法,而首选右列的方法: ![image-20240529172524694](D:\Codes\前端学习\12-react\react18\assert\image-20240529172524694.png) #### 添加元素 > 使用`[...arr]`和`concat`方法 ```jsx import { useState } from "react"; let nextId = 0 function App() { const [name, setName] = useState(''); const [artists, setArtists] = useState([]); return ( <>

振奋人心的雕塑家们:

setName(e.target.value)} /> ); } export default App; ``` #### 删除元素 > 使用过滤的方式生成一个不包含某个数组元素的新数组。 ```jsx import { useState } from "react"; let initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'}, ]; function App() { const [artists, setArtists] = useState( initialArtists ); return ( <>

振奋人心的雕塑家们:

); } export default App; ``` #### 转换数组 > 改变数组中的某些或全部元素,可以用 `map()` 创建一个**新**数组 ```jsx let initialShapes = [ { id: 0, type: 'circle', x: 50, y: 100 }, { id: 1, type: 'square', x: 150, y: 100 }, { id: 2, type: 'circle', x: 250, y: 100 }, ]; function App() { const [shapes, setShapes] = useState( initialShapes ); function handleClick() { const nextShapes = shapes.map(shape => { if (shape.type === 'square') { // 不作改变 return shape; } else { // 返回一个新的圆形,位置在下方 50px 处 return { ...shape, y: shape.y + 50, }; } }); // 使用新的数组进行重渲染 setShapes(nextShapes); } return ( <> {shapes.map(shape => (
))} ); } export default App; ``` #### 替换数组元素 > 替换数组中一个或多个元素,可以使用`map` ```jsx let initialCounters = [ 0, 0, 0 ]; export default function CounterList() { const [counters, setCounters] = useState( initialCounters ); function handleIncrementClick(index) { const nextCounters = counters.map((c, i) => { if (i === index) { // 递增被点击的计数器数值 return c + 1; } else { // 其余部分不发生变化 return c; } }); setCounters(nextCounters); } return (
    {/* 第二个参数是索引值 */} {counters.map((counter, i) => (
  • {counter}
  • ))}
); } ``` #### 向数组中特定位置插入元素 > 将数组展开运算符 `...` 和 `slice()` 方法一起使用。`slice()` 方法从数组中切出“一片”。为了将元素插入数组,需要先展开原数组在插入点之前的切片,然后插入新元素,最后展开原数组中剩下的部分。 ```jsx import { useState } from 'react'; let nextId = 3; const initialArtists = [ { id: 0, name: 'Marta Colvin Andrade' }, { id: 1, name: 'Lamidi Olonade Fakeye'}, { id: 2, name: 'Louise Nevelson'}, ]; export default function List() { const [name, setName] = useState(''); const [artists, setArtists] = useState( initialArtists ); function handleClick() { const insertAt = 1; // 可能是任何索引 const nextArtists = [ // 插入点之前的元素: ...artists.slice(0, insertAt), // 新的元素: { id: nextId++, name: name }, // 插入点之后的元素: ...artists.slice(insertAt) ]; setArtists(nextArtists); setName(''); } return ( <>

振奋人心的雕塑家们:

setName(e.target.value)} />
    {artists.map(artist => (
  • {artist.name}
  • ))}
); } ``` #### 其他改变数组的情况 有些需求仅仅依靠展开运算符和 `map()` 或者 `filter()` 等不会直接修改原值的方法。例如,你可能想翻转数组,或是对数组排序。而 JavaScript 中的 `reverse()` 和 `sort()` 方法会改变原数组,所以你无法直接使用它们。 **然而,你可以先拷贝这个数组,再改变这个拷贝后的值。** ```jsx import { useState } from 'react'; const initialList = [ { id: 0, title: 'Big Bellies' }, { id: 1, title: 'Lunar Landscape' }, { id: 2, title: 'Terracotta Army' }, ]; export default function List() { const [list, setList] = useState(initialList); function handleClick() { const nextList = [...list]; nextList.reverse(); setList(nextList); } return ( <>
    {list.map(artwork => (
  • {artwork.title}
  • ))}
); } ``` **即使你拷贝了数组,你还是不能直接修改其内部的元素**。这是因为数组的拷贝是浅拷贝。 ## 表单数据双向绑定 ![image-20240529173838098](D:\Codes\前端学习\12-react\react18\assert\image-20240529173838098.png) ![image-20240529173858244](D:\Codes\前端学习\12-react\react18\assert\image-20240529173858244.png) 当input做输入框时,函数组件表单存在一个问题:由于state的更新是异步的,当input在做搜索框时,不能实时的根据输入的内容进行搜索,这种情况在类组件中有解决方案:` setState(newstate,callback)` 第二个参数传一个回调来处理本次状态更新后的一些其他业务 ## 对state进行保留或重置 React 会为 UI 中的组件结构构建 [渲染树](https://zh-hans.react.dev/learn/understanding-your-ui-as-a-tree#the-render-tree)。 组件上添加的state是由React保存的,React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来 - 处在不同位置的相同组件会使state重置 - 处在相同位置的相同组件会使state保留 - 处在相同位置的不同组件会使state重置 **只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。** 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。 **相同位置的相同组件会使得 state 被保留下来** 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置! ![image-20240529172543107](D:\Codes\前端学习\12-react\react18\assert\image-20240529172543107.png) ```jsx import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return (
{isFancy ? ( ) : ( )}
); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{score}

); } ``` **重置相同位置的相同组件的state** 1. 将组件渲染在不同的位置 2. 使用 `key` 赋予每个组件一个明确的身份 ```jsx import { useState } from 'react'; //原本写法会保留state export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return (
{isPlayerA ? ( ) : ( )}
); } //方法一: export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return (
{isPlayerA && } {!isPlayerA && }
); } //方法二: export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return (
{isPlayerA ? ( ) : ( )}
); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{person} 的分数:{score}

); } ``` ## 获取DOM元素 ![image-20240529202001673](D:\Codes\前端学习\12-react\react18\assert\image-20240529202001673.png) DOM可用:页面渲染完毕后dom生成之后才能可用 ## 组件通信 | 通信对象 | 通信方法 | | -------- | ---------------- | | 父->子 | props参数; | | 子->父 | props+回调函数; | | 兄弟 | 状态提升机制; | | 跨层通信 | Context机制; | | 任意组件 | Redux | ### 父传子 **props参数** 实现步骤: 1. 父组件传递数据:在子组件标签上绑定属性 2. 子组件接收数据:子组件通过props参数接收数据 ```jsx //子组件son function Son(props){ //props:对象里面包含了父组件传递过来的所有数据 console.log(props); return
this is a son, {props.name}
} //父组件 function App() { const name='this is app name' return ( <> console.log('112')} children={this is a span } /> ); } ``` - props可以传递任意的数据 - props是只读对象 **特殊的prop-children** 当我们把内容嵌套在子组件标签中时,父组件会自动在名为children的prop属性中接收该内容。 ```jsx //子组件son function Son(props){ //props:对象里面包含了父组件传递过来的所有数据 console.log(props); return
this is a son, {props.children}
} //父组件 function App() { const name='this is app name' return ( <> this is a span
this is a div
); } ``` ### 子传父 **props+回调函数** 在子组件中调用父组件传递过来的函数,并将要传递给父组件的数据以回调函数中实参的形式传递。 ```jsx //子组件son function Son({ onGetMsg }) { //props:对象里面包含了父组件传递过来的所有数据 const sonMsg = 'this is a son msg' return (
{/* 调用父组件传递过来的函数,并通过函数参数的方式传递数据 */}
) } //父组件 function App() { function getMessage(msg) { console.log(msg); } return ( <> {/* 给子组件传递函数,注意:如果父组件给子组件传递的是函数的话,命名一般为onXXX*/} ); } ``` ### 兄弟组件通信 **状态提升机制** 借助“状态提升”机制,将父组件作为桥梁,进行兄弟组件通信。 1. A组件先通过子传父的方式把数据传给父组件App 2. App拿到数据后通过父传子的方式把数据传递给B组件 ```jsx //子组件A function A({ onGetMsg }) { const AMsg = 'this is a msg from A' return (
this is A. {/* 调用父组件传递过来的函数,并通过函数参数的方式传递数据 */}
) } //子组件B function B(props) { return (
this is B. msg:{props.msg}
) } //父组件 function App() { const [msg, setMsg] = useState('') function getMessage(msg) { setMsg(msg) } return ( <> {/* 给子组件传递函数,注意:如果父组件给子组件传递的是函数的话,命名一般为onXXX*/} ); } ``` ### 跨层通信 **使用Context机制跨层级组件通信** ![image-20240529162303849](D:\Codes\前端学习\12-react\react18\assert\image-20240529162303849.png) 实现步骤: **创建context对象** 使用createContext方法创建一个上下文对象Ctx ```js import React from "react"; const TestContext = React.createContext({ name:'孙悟空', age:18, gender:'男', sayHello:()=>{ alert(this.name); } }); export default TestContext; ``` 顶层组件可以传递一个回调函数给底层组件,底层组件调用该函数,并将数据以实参的形式传递给顶层组件,从而实现底层->顶层的数据通信。 **访问context中数据** **方法一**:通过Consumer标签来访问到Context中的数据 ```jsx import React from 'react'; import TestContext from '../store/test-context'; const MyComponent = () => { return ( {(ctx)=>{ return (
  • {ctx.name}
  • {ctx.age}
  • {ctx.gender}
); }}
); }; export default MyComponent; ``` Consumer的标签体必须是一个函数,这个函数会在组件渲染时调用并且将Context中存储的数据作为参数传递进函数,该函数的返回值将会作为组件被最终渲染到页面中。这里我们将参数命名为了ctx,在回调函数中我们就可以通过ctx.xxx访问到Context中的数据。如果需要访问多个Context可以使用多个Consumer嵌套即可。 **方法二:**通过`useContext`钩子函数获取消费数据 ```jsx import React, {useContext} from 'react'; import TestContext from '../store/test-context'; const MyComponent = () => { const ctx = useContext(TestContext); return (
  • {ctx.name}
  • {ctx.age}
  • {ctx.gender}
); }; export default MyComponent; ``` 方法一和方法二方式中Context的值是写死的,并不是在组件中指定的。 **方法三:**在顶层组件(App)中通过`Ctx.Provider组件`提供数据,再在底层组件中通过`useContext`钩子函数获取消费数据(同上) 顶层组件(App)中通过`Ctx.Provider组件`提供数据: ```jsx import React from "react"; import MyComponent from "./component/MyComponent"; import TestContext from "./store/test-context"; const App = () => { return ; }; export default App; ``` Provider会设置在外层组件中,通过value属性来指定Context的值。这个Context值在所有的Provider子组件中都可以访问。当我们获取Context时,React会在它的外层查找最近的Provider,然后返回它的Context值。如果没有找到Provider,则会返回Context模块中设置的默认值。当 Provider 的 `value` 值发生变化时,它内部的所有消费组件都会重新渲染。 ## 类组件 ### 简单使用 类组件,顾名思义,也就是通过使用`ES6`类的编写形式去编写组件,该类必须继承`React.Component` 如果想要访问父组件传递过来的参数,可通过`this.props`的方式去访问 在组件中必须实现`render`方法,在`return`中返回`React`对象,如下: ```jsx import React, { Component } from 'react export default class App extends Component { render() { return (
App
) } } ``` 在官方文档中类组件不建议使用了。 ## Portal > Portal 提供了一种将子节点渲染到存在于父组件以外 DOM 节点的方案。 在React中,父组件引入子组件后,子组件会直接在父组件内部渲染。 但是,在有些场景下如果将子组件直接渲染为父组件的后代,在网页显示时会出现一些问题。比如,需要在React中添加一个会盖住其他元素的Backdrop组件,Backdrop显示后,页面中所有的元素都会被遮盖。很显然这里需要用到定位,但是如果将遮罩层直接在当前组件中渲染的话,遮罩层会成为当前组件的后代元素。如果此时,当前元素后边的兄弟元素中有开启定位的情况出现,且层级不低于当前元素时,便会出现盖住遮罩层的情况。 ### Portal的用法 1. 在index.html添加一个新的元素 2. 修改组件的渲染方式 1. 通过`ReactDOM.createPortal()`作为返回值创建元素 参数:1.jsx 2.目标位置(DOM元素) 在index.html中添加新元素: ```jsx
``` 修改Backdrop组件: ```jsx import ReactDOM from 'react-dom' const backdropDOM = document.getElementById('backdrop'); const Backdrop = () => { return ReactDOM.createPortal(
, backdropDOM ); }; ``` ## 组件的基础样式处理 > React组件基础的样式控制有俩种方式,行内样式和class类名控制 行内方式: ```jsx {/*多个单词使用驼峰的写法*/}
this is div
function App() { const [showBorder,setShowBorder]=useState(false) const style={ color:'red', fontSzie:'12px', border: showBorder?' 1px solid #fff': none//结合state控制样式 } return ( <>
this is a div
); } ``` class类名控制: ```css index.css文件下: .foo{ color: red; } ``` ```jsx import './index.css' function App(){ return (
{/* 要写className不能写class */} this is span
) } ``` ### classnames优化类名控制 classnames是一个简单的JS库,可以非常方便的通过条件动态控制class类名显示 下载classnames ``` npm i classnames ``` 引入classnames ```jsx //引入classNames import classNames from "classnames"; ``` ![image-20240602155441068](D:\Codes\前端学习\12-react\react18\assert\image-20240602155441068.png) ### CSS Module 使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一,所以使用CSS Module后,不用担心类名重复 #### 使用方式 CSS Module在React中已经默认支持了(前提是使用了react-scripts),所以无需再引入其他多余的模块。使用CSS Module时需要遵循如下几个步骤: 1. 使用CSS Module编写的样式文件的文件名必须为`xxx.module.css` 2. 在组件中引入样式的格式为`import xxx from './xxx.module.css'` 3. 设置类名时需要使用`xxx.yyy`的形式来设置 请看案例: ```jsx /* StyleDemo.module.css */ .myDiv{ color: red; background-color: #bfa; font-size: 20px; border-radius: 12px; } /* StyleDemo.js */ import styles from './StyleDemo.module.css'; const StyleDemo = () => { return (
我是Div
); }; export default StyleDemo; ``` ## 严格模式 React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次: 类组件的的 `constructor`, `render`, 和 `shouldComponentUpdate` 方法 类组件的静态方法 `getDerivedStateFromProps` 函数组件的函数体 参数为函数的`setState` 参数为函数的`useState`, `useMemo`, or `useReducer` 重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个`console.log`你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。 ## React副作用管理-useEffect() ### 概念理解 useEffect()是一个React Hook函数,用于在React组件中创建不是由事件引起而是有组件渲染本身引起的操作,比如渲染好某个组件之后立即发送AJAX请求,更改DOM,获取数据、记录日志、检查登录、设置定时器等等。 ![image-20240604170211743](D:\Codes\前端学习\12-react\react18\assert\image-20240604170211743.png) 通过使用这个Hook,我设置了React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。 ### 基础使用 > 需求:在组件渲染完毕之后,立刻从服务端获取平道列表数据并显示到页面中 ![image-20240604170245852](D:\Codes\前端学习\12-react\react18\assert\image-20240604170245852.png) 说明: 1. 参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作 2. 参数2是一个数组(可选参),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次 接口地址:http://geek.itheima.net/v1_0/channels ```jsx import { useEffect, useState } from "react"; const URL = 'http://geek.itheima.net/v1_0/channels' function App() { const [list, setList] = useState([]) useEffect(() => { //发请求获取数据 async function getList() { const res = await fetch(URL) const jsonRes = await res.json() setList(jsonRes.data.channels) } getList() }, []) return ( <>
this is app
    {list.map(item =>
  • {item.name}
  • )}
); } ``` ### useEffect依赖项说明 useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现 | **依赖项** | **副作用功函数的执行时机** | | -------------- | ------------------------------- | | 没有依赖项 | 组件初始渲染 + 组件更新时执行 | | 空数组依赖 | 只在初始渲染时执行一次 | | 添加特定依赖项 | 组件初始渲染 + 依赖项变化时执行 | ```jsx import { useEffect, useState } from "react"; function App() { const [count, setCount] = useState(0) // 1.没有依赖项 组件初始渲染 + 组件更新时执行 useEffect(() => { console.log('副作用函数执行了'); }) //2.空数组依赖 只在初始渲染时执行一次 useEffect(() => { console.log('副作用函数执行了'); },[]) // 3. 添加特定依赖项 组件初始渲染 + 依赖项变化时执行 useEffect(() => { console.log('副作用函数执行了'); },[count])//count变化执行 return ( <>
this is app
); } export default App; ``` ### 清除副作用 > 概念:在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用 ![image-20240604170322784](D:\Codes\前端学习\12-react\react18\assert\image-20240604170322784.png) 说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行 需求:在Son组件渲染时开启一个定时器,卸载时清除这个定时器。 ```jsx import { useEffect, useState } from "react" function Son () { // 1. 渲染时开启一个定时器 useEffect(() => { const timer = setInterval(() => { console.log('定时器执行中...') }, 1000) return () => { // 清除副作用(组件卸载时) clearInterval(timer) } }, []) return
this is son
} function App () { // 通过条件渲染模拟组件卸载 const [show, setShow] = useState(true) return (
{show && }
) } export default App ``` > 需求:当用户停止输入一秒之后才获取input中的value进行下一步操作 ```jsx const [keyword, setKeyword] = useState('')//存储input的值 const changeHandler = (e) => { setKeyword(e.target.value.trim()) } useEffect(() => { //降低数据过滤的次数,提高用户体验 //用户输入完了再过滤,用户输入的过程中,不要过滤 //解决方案:当用户停止输入动作1秒后,我们才做查询 //在开启一个定时器的同时应该关掉上一次的定时器 const timer = setTimeout(() => { filterFood(keyword)//根据输入的内容做过滤 }, 1000) //清理函数,在下一次effect执行前执行一次,在这里可以清除上次effet执行所带来的影响 return () => { clearTimeout(timer) } }, [keyword]) ``` ## 使用useReducer Reducer的作用就是将那些和同一个`state`相关的所有函数都整合到一起,方便在组件中进行调用。只适用于那些比较复杂的`state`(简单的没必要使用)。 语法: ```jsx const [state, dispatch] = useReducer(reducer, initialArg, init); ``` 返回值: 第一个返回值是`state`用来读取`state`的值, 第二个返回值同样是一个函数,它是一个“派发器”,通过它可以向`reducer()`发送不同的指令,控制`reducer()`做不同的操作。 参数: - `reducer()`是一个函数,也是所谓的“整合器”。它的返回值会成为新的`state`值。当我们调用`dispatch()`时,`dispatch()`会将消息发送给`reducer()`,`reducer()`可以根据不同的消息对`state`进行不同的处理。 - `reducer=(state,action)=>{}` 参数: ​ state:当前state值 ​ action:一个对象,`dispatch({...})`调用时的该对象实参会被action接收,可以通过指定该对象来控制要对state执行的哪一个操作。 ​ - `initialArg`就是`state`的初始值,和`useState()`参数一样。 基本使用: ```jsx import {useReducer, useState} from 'react'; const reducer = (state, action) => { switch(action.type){ case 'add': return state + 1; case 'sub': return state - 1; default : return state; } }; function App() { const [count, countDispath] = useReducer(reducer,1); return (
{count}
); } export default App; ``` ## React.memo React组件会在两种情况下发生重新渲染。 - 第一种,当组件自身的state发生变化时。 - 第二种,当组件的父组件重新渲染时, 第二种情况似乎并不是总那么的必要。 > `memo` 允许你的组件在 `props `没有改变的情况下跳过重新渲染。(官方) prop通常是父组件的state数据传递过来的,当父组件中除了与传递给子组件相关的数据发生变化时,父组件会重新渲染,但是此时子组件的重新渲染就没有必要了。 语法; ```jsx import { memo } from 'react'; const SomeComponent = memo(function SomeComponent(props) { // ... }); ``` 参数 - `Component`:要进行记忆化的组件。`memo` 不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括**函数组件**和 `forwardRef`组件。 - **可选参数** `arePropsEqual`:一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 `true`。否则返回 `false`。通常情况下,你不需要指定此函数。默认情况下,React 将使用 `Object.is`比较每个 prop。 因此只要其 props 没有改变,React 就不会重新渲染。即使使用 `memo`,如果它自己的 state 或正在使用的 context 发生更改,组件也会重新渲染。 ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-05_18-46-18.png) App.js ```jsx import { useState } from "react"; import A from './A' function App() { const [age, setAge] = useState(0) console.log('App渲染'); return (
我是app,我是粉色age:{age}
); } export default App; ``` A.js ```jsx import React, { useState } from 'react' import B from './B' function A() { console.log('A渲染'); const [age,setAge]=useState(0) return (
我是组件A,我是天蓝色,age:{age}
) } export default A ``` B.js ```jsx import React from 'react' function B(props) { console.log('B渲染'); return (
我是组件B,我是墨绿色 A的age:{props.Age}
) } export default React.memo(B) ``` 点击App的按钮会出现: ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-05_18-49-19.png) 点击A的按钮会出现: ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-05_18-49-40.png) ## 使用useCallback() useCallBack()是一个钩子函数,用来创建React中的回调函数。useCallback()创建的回调函数不会总在组件重新渲染时重新创建。相当于把函数缓存起来,多次渲染时只创建一次。 语法: ```jsx const cachedFn = useCallback(fn, dependencies) ``` 参数: - `fn`:想要缓存的函数。此函数可以接受任何参数并且返回任何值。在初次渲染时,React 将把函数返回给你(而不是调用它!)。当进行下一次渲染时,如果 `dependencies` 相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。 - `dependencies`:有关是否更新 `fn` 的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。并且必须像 `[dep1, dep2, dep3]` 这样编写。使用与useEffect类似 一定要将回调函数中使用的所有变量设置到依赖项中 **useCallback()和useMemo的关系:** ```jsx // 在 React 内部的简化实现 function useCallback(fn, dependencies) { return useMemo(() => fn, dependencies); } ``` ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-05_19-14-45.png) ```jsx import { useCallback, useState } from "react"; import A from './A' function App() { const [name, setName] = useState('App') console.log('App渲染'); //使用useCallback const changName = useCallback(() => { setName(pre => pre + 's') },[]) //不使用useCallback const changName = () => { setName(pre => pre + 's') } return (
); } export default App; ``` ```jsx import React, { useState } from 'react' function A(props) { console.log('A渲染'); return (
我是组件A,我是天蓝色
) } export default React.memo(A) ``` 当不使用useCallback时点击按钮,App组件和A组件都会被重新渲染,就算此时A组件使用了memo(),因为当点击按钮时App组件的state发生变化,App组件重新渲染,回调函数也会重新创建,所以A组件相关的props发生变化,A组件重新渲染。而当使用了useCallback时,回调函数的依赖项没有变化,回调函数不会重新创建,A组件不会重新渲染。 ## react中使用fetch()和await发送网络请求 Fetch是浏览器中自带的一种发送请求的方式,它是Ajax的升级版,相较于Ajax来说它使用起来更加方便,代码也更加简洁清晰。 ### 基本使用 ```jsx import { useEffect, useState } from "react"; import StudentList from "./components/StudentList"; const URL = 'http://localhost:1337/api/students' function App() { //存储数据 const [student, setStudent] = useState([]) //添加一个state来记录数据是否正在加载 const [loading, setLoading] = useState(false) //创建一个state来记录是否数据请求失败 const [errorMsg, setErrorMsg] = useState(null) useEffect(() => { //设置loading setLoading(true) setErrorMsg(null) //发送请求,fetch()需要两个参数,1.请求地址 2.请求信息 默认get请求 // then()中的回调会在请求成功时调用 //catch()中的回调会在请求失败时调用 fetch(URL) .then((response) => { if (response.ok) { return response.json()//该方法可以将响应的json直接转换为js对象,返回的是一个promise对象 } //抛出一个错误 throw new Error('数据加载失败') }).then(({ data, meta }) => {//将转换成的数据传入回调的第一个参数中 //将数据存储 setStudent(data) setLoading(false) }) .catch((error) => { //代码运行到这,说明没有成功加载到数据 setLoading(false) setErrorMsg(error.message) }) }, []) return (
{(!loading && !errorMsg) && } {loading &&

数据正在加载中.....

} {errorMsg &&

{errorMsg}

}
); } export default App; ``` ### 结合使用await ```jsx import { useEffect, useState } from "react"; import StudentList from "./components/StudentList"; const URL = 'http://localhost:1337/api/students' function App() { //存储数据 const [student, setStudent] = useState([]) //添加一个state来记录数据是否正在加载 const [loading, setLoading] = useState(false) //创建一个state来记录是否数据请求失败 const [errorMsg, setErrorMsg] = useState(null) //结合await使用 useEffect(() => { const fetchData = async () => { try { setLoading(true) setErrorMsg(null) const res = await fetch(URL) if (res.ok) { const { data, meta } = await res.json() setStudent(data) setLoading(false) } else { throw new Error('数据加载失败') } } catch (e) { setLoading(false) setErrorMsg(e.message) } } fetchData() }, []) return (
{(!loading && !errorMsg) && } {loading &&

数据正在加载中.....

} {errorMsg &&

{errorMsg}

}
); } export default App; ``` ## 自定义Hook实现 > 概念:自定义Hook是以 `use打头的函数`,通过自定义Hook函数可以用来`实现逻辑的封装和复用` ![image-20240606154230018](D:\Codes\前端学习\12-react\react18\assert\image-20240606154230018.png) 封装自定义hook通用思路 1. 声明一个以use打头的函数 2. 在函数体内封装可复用的逻辑(只要是可复用的逻辑)也可传递参数 3. 把组件中用到的状态或者回调return出去(以对象或者数组) 4. 在哪个组件中要用到这个逻辑,就执行这个函数,解构出来状态和回调进行使用 ```jsx // 封装自定义Hook // 问题: 布尔切换的逻辑 当前组件耦合在一起的 不方便复用 // 解决思路: 自定义hook import { useState } from "react" function useToggle () { // 可复用的逻辑代码 const [value, setValue] = useState(true) const toggle = () => setValue(!value) // 哪些状态和回调函数需要在其他组件中使用 return return { value, toggle } } function App () { const { value, toggle } = useToggle() return (
{value &&
this is div
}
) } export default App ``` ## React Hooks使用规则 1. 只能在组件中或者其他自定义Hook函数中调用 2. 只能在组件的顶层调用,不能嵌套在if、for、其它的函数中 ![image-20240606151613359](D:\Codes\前端学习\12-react\react18\assert\image-20240606151613359.png) ## Redux > Redux 是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行 > 作用:通过集中管理的方式管理应用的状态 > > Redux可以理解为是reducer和context的结合体 ![image-20240606165655924](D:\Codes\前端学习\12-react\react18\assert\image-20240606165655924.png) **为什么要使用Redux?** 1. 独立于组件,无视组件之间的层级关系,简化通信问题 2. 单项数据流清晰,易于定位bug 3. 调试工具配套良好,方便调试 ### Redux快速体验 #### 1. 实现计数器 > 需求:不和任何框架绑定,不使用任何构建工具,使用纯Redux实现计数器 使用步骤: 1. 定义一个 reducer 函数 (根据当前想要做的修改返回一个新的状态) 2. 使用createStore方法传入 reducer函数 生成一个store实例对象 3. 使用store实例的 subscribe方法 订阅数据的变化(数据一旦变化,可以得到通知) 4. 使用store实例的 dispatch方法提交action对象 触发数据变化(告诉reducer你想怎么改数据) 5. 使用store实例的 getState方法 获取最新的状态数据更新到视图中 代码实现: ```html 0 ``` #### 2. Redux数据流架构 > Redux的难点是理解它对于数据修改的规则, 下图动态展示了在整个数据的修改中,数据的流向 ![image-20240606183046674](D:\Codes\前端学习\12-react\react18\assert\image-20240606183046674.png) 为了职责清晰,Redux代码被分为三个核心的概念,我们学redux,其实就是学这三个核心概念之间的配合,三个概念分别是: 1. state: 一个对象 存放着我们管理的数据 2. action: 一个对象 用来描述你想怎么改数据 3. reducer: 一个函数 根据action的描述更新state ### Redux与React - 环境准备 > Redux虽然是一个框架无关可以独立运行的插件,但是社区通常还是把它与React绑定在一起使用,以一个计数器案例体验一下Redux + React 的基础使用 #### 1. 配套工具 > 在React中使用redux,官方要求安装俩个其他插件 - Redux Toolkit 和 react-redux 1. Redux Toolkit(RTK)- 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式 2. react-redux - 用来 链接 Redux 和 React组件 的中间件 ![image-20240606202832909](D:\Codes\前端学习\12-react\react18\assert\image-20240606202832909.png) #### 2. 配置基础环境 1. 使用 CRA 快速创建 React 项目 ```bash npx create-react-app react-redux ``` 2. 安装配套工具 ```bash npm i @reduxjs/toolkit react-redux ``` 3. 启动项目 ```bash npm run start ``` #### 3. store目录结构设计 ![image-20240606202915158](D:\Codes\前端学习\12-react\react18\assert\image-20240606202915158.png) 1. 通常集中状态管理的部分都会单独创建一个单独的 `store` 目录 2. 应用通常会有很多个子store模块,所以创建一个 `modules` 目录,在内部编写业务分类的子store 3. store中的入口文件 index.js 的作用是组合modules中所有的子模块,并导出store ### Redux与React - 实现counter #### 1. 整体路径熟悉 ![image-20240606202800704](D:\Codes\前端学习\12-react\react18\assert\image-20240606202800704.png) #### 2. 使用React Toolkit 创建 counterStore **counterStore.js** ```javascript import { createSlice } from "@reduxjs/toolkit"; //createSlice()创建reducer的切片 //它需要一个配置对象作为参数,通过对象的不同属性来指定它的配置 const counterStore=createSlice({ // 模块名称独一无二,用来自动生成action中的type name: 'counter', // 初始数据 initialState: { count: 1 }, // 修改数据的同步方法,支持直接修改 reducers: { increment(state,action) { /* 1.state:这个state是代理对象,可以直接修改 2.action.payload可以获取调用该方法传递的参数*/ state.count+=action.payload }, decrement(state) { state.count-- } } }) //切片对象会自动帮助我们生成action //actions中存储的是slice自动生成action创建器(函数),调用函数后会自动创建action对象(包含type和payload属性) export const {increment,decrement}=counterStore.actions // 获取reducer函数 export const counterReducer=counterStore.reducer ``` **store/index.js** ```javascript import { configureStore } from '@reduxjs/toolkit' import counterReducer from './modules/counterStore' //用来创建store对象,需要一个配置对象作为参数 export const store= configureStore({ reducer: { // 注册子模块 counter: counterReducer } }) ``` #### 3. 为React注入store > react-redux负责把Redux和React 链接 起来,内置 Provider组件 通过 store 参数把创建好的store实例注入到应用中,链接正式建立 **index.js** ```jsx 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' ReactDOM.createRoot(document.getElementById('root')).render( // 提供store数据 ) ``` #### 4. React组件使用store中的数据 > 在React组件中使用store中的数据,需要用到一个钩子函数 - useSelector,它的作用是把store中的数据映射到组件中,使用样例如下: ![image-20240607130550229](D:\Codes\前端学习\12-react\react18\assert\image-20240607130550229.png) ![image-20240607130804822](D:\Codes\前端学习\12-react\react18\assert\image-20240607130804822.png) #### 5. React组件修改store中的数据 > React组件中修改store中的数据需要借助另外一个hook函数 - useDispatch,它的作用是生成提交action对象的dispatch函数,使用样例如下: ```jsx import { useDispatch, useSelector } from "react-redux"; //导入创建action对象方法 import { increment, decrement } from "./store/modules/counterStore"; function App() { //counterStore中的state const counter = useSelector(state => state.counter) //通过useDispatch()来获取派发器对象 const dispatch = useDispatch() return ( <> {/* 调用dispatch提交action对象 */} {count} ); } export default App; ``` ### RTK Query RTK Query是一个强大的数据获取和缓存工具。RTKQ已经集成在了RTK中。首先,可以直接通过RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。其次,RTKQ在发送请求时会根据请求不同的状态返回不同的值,我们可以通过这些值来监视请求发送的过程并随时中止。 RTKQ发送请求与其他方式发送请求的最大区别有没有用缓存。RTKQ会对请求的结果进行缓存,减少请求的次数,避免发送重复的请求。 #### 1.使用createApi()创建API对象 **store/modules/student/studentApi.js** ```js //导的包要注意!!! import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' // 创建Api对象 /* createApi()用来创建RTKQ中API对象 RTKQ中所有功能都需要通过该对象来进行 需要一个对象作为参数(参数很多,暂用几个) */ const studentApi = createApi({ reducerPath: 'studentApi',//APi的标识,不能和其他API或reducer重复 /* RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装 */ /* fetchBaseQuery 简单封装过的fetch调用后会返回一个封装后的工具函数。需要一个配置对象作为参数,baseUrl表示Api请求的基本路径,指定后请求将会以该路径为基本路径。*/ baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/', prepareHeader:(header,{getState})=>{ //对请求头设置(通用) getState方法可以获取到所有store相关的state数据如获取getState().XXX.YYY(xxx-srore名,yyy数据名) header.set('Content-type','application/json') return header } }),//指定查询的基础信息,用来设置发送请求的工具 endpoints(build) { //build是请求的构建器,通过build来设置请求的相关信息 return {//Api对象中的所有端点(方法)都要在该对象中进行配置 /* 属性值要通过build对象创建,分两种情况: 查询:build.query({}) 增删改:build.mutation({}) */ getStudents: build.query({//配置该接口需要的信息 query() { //指定请求的子路径,完整路径=baseUrl+rquery()返回值 return 'students' }, transformResponse(baseQueryReturnValue) {//用来转换响应数据的格式 return baseQueryReturnValue.data } }), getStudentById: build.query({ query(id) {//调用钩子的参数会传递到这 // localhost:1337/api/students/21 //指定请求的子路径,完整路径=baseUrl+rquery()返回值 return `students/${id}` }, transformResponse(baseQueryReturnValue) {//用来转换响应数据的格式 return baseQueryReturnValue.data }, keepUnusedDataFor: 0,//设置数据缓存的时间(以秒为单位)默认60s } ), addStudent:build.mutation({ // localhost:1337/api/students query(stu){ return { url:'students', method:'post', body:{data:stu.attributes} } } }), updateStudent: build.mutation({ // localhost:1337/api/students/1 query(stu){ return { url:`students/${stu.id}`, method:'put', body:{data:stu.attributes} } } }), deleteStudent: build.mutation({ query(id) { // localhost:1337/api/students/1 return { //如果发送的请求不是get请求,需要返回一个对象来设置请求的信息 url:`students/${id}`, method:'delete' } } }) } },//用来指定Api中的各种功能,它是一个回调函数,接收一个build对象,需要一个对象作返回值 }) //API对象创建以后 ,对象会根据各种接口请求方法自动生成对应的钩子函数 //通过这些钩子函数可以向服务器发送请求 //钩子函数命名规则 getStudents->useGetStudentsQuery //从API对象中解构出钩子函数 export const { useGetStudentsQuery, useGetStudentByIdQuery,useDeleteStudentMutation,useAddStudentMutation,useUpdateStudentMutation } = studentApi //暴露APi对象给store使用/不给store使用 export default studentApi ``` #### 2.使用API对象 Api对象的使用有两种方式,一种是直接使用,一种是作为store中的一个reducer使用。 **作为store中的一个reducer使用** **store/index.js** ```js import { configureStore } from '@reduxjs/toolkit' import studentApi from './modules/student/studentApi' const store = configureStore({ reducer: { //使用studentApi的reducerPath值作为属性名 [studentApi.reducerPath]: studentApi.reducer, }, //配置一个中间件处理API的缓存 middleware: getDefaultMiddleware => getDefaultMiddleware().concat(studentApi.middleware), }) export default store ``` **index.js** ```js import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import store from './store' // 导入store提供组件Provider import { Provider } from 'react-redux' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); ``` #### 3.调用API获取数据/操作数据 ##### 3.1获取数据 ```js import { useGetStudentsQuery } from "./store/modules/student/studentApi"; function App() { //调用APi查询数据 //这个钩子函数会返回一个对象作为返回值,请求过程中相关数据都在该对象中存储 const { data, isSuccess, isLoading } = useGetStudentsQuery()//解构返回对象 console.log(data); return (
app {isLoading &&

数据加载中

} {isSuccess && data.map(item =>

{item.attributes.name}----- {item.attributes.age}----- {item.attributes.gender}----- {item.attributes.address}

)}
); } export default App; ``` ##### 3.2操作数据 以增加学生为例: ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-11_14-10-11.png) 现在有一个问题就是:当点击添加按钮后,请求会正常发送,数据库中的数据也会正常添加,但是最新的数据没有显示到页面上。解决方案在**8.给每个API打上标签** #### 4.请求时带参数 ```jsx //导的包要注意!!! import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' // 创建Api对象 /* createApi()用来创建RTKQ中API对象 RTKQ中所有功能都需要通过该对象来进行 需要一个对象作为参数 */ const studentApi = createApi({ reducerPath: 'studentApi',//APi的标识,不能和其他API或reducer重复 /* RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装 */ /* fetchBaseQuery 简单封装过的fetch调用后会返回一个封装后的工具函数。需要一个配置对象作为参数,baseUrl表示Api请求的基本路径,指定后请求将会以该路径为基本路径。*/ baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),//指定查询的基础信息,用来设置发送请求的工具 endpoints(build) { //build是请求的构建器,通过build来设置请求的相关信息 return {//Api对象中的所有端点(方法)都要在该对象中进行配置 /* 属性值要通过build对象创建,分两种情况: 查询:build.query({}) 增删改:build.mutation({}) */ getStudents: build.query({//配置该接口需要的信息 query() { //指定请求的子路径,完整路径=baseUrl+rquery()返回值 return 'students' }, transformResponse(baseQueryReturnValue) {//用来转换响应数据的格式 return baseQueryReturnValue.data } }), getStudentById: build.query({ query(id) {//调用钩子函数传递的参数会传递到这 // localhost:1337/api/students/21 //指定请求的子路径,完整路径=baseUrl+rquery()返回值 return `students/${id}` }, transformResponse(baseQueryReturnValue) {//用来转换响应数据的格式 return baseQueryReturnValue.data } } ), updateStudent: build.mutation() } },//用来指定Api中的各种功能,它是一个回调函数,接收一个build对象,需要一个对象作返回值 }) //API对象创建以后 ,对象会根据各种接口请求方法自动生成对应的钩子函数 //通过这些钩子函数可以向服务器发送请求 //钩子函数命名规则 getStudents->useGetStudentsQuery //从API对象中解构出钩子函数 export const { useGetStudentsQuery, useGetStudentByIdQuery } = studentApi //暴露APi对象给store使用/不给store使用 export default studentApi ``` ![image-20240607164549174](D:\Codes\前端学习\12-react\react18\assert\image-20240607164549174.png) #### 5.配置RTKQ缓存有效期 RTKQ发送请求与其他方式发送请求的最大区别有没有用缓存。RTKQ会对请求的结果进行缓存,减少请求的次数,避免发送重复的请求。 ![image-20240609163842746](D:\Codes\前端学习\12-react\react18\assert\image-20240609163842746.png) #### 6.调用useQuery的返回值 ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-09_17-06-06.png) - refetch:f() 一个函数,用来重新加载数据 - isFetch:false 数据是否在加载 - status;"fulfilled" 请求的状态 - isLoading: false 数据是否第一次加载 - isSuccess:true 数据是否成功 - isUninitialized:false 数据是否还没有开始发送 - isError:false 是否有错误 - error:Error() 有错误才会出现该对象 - data: 最新的数据 - currentData:当前参数的最新数据 ```js import StudentList from "./components/StudentList"; import { useGetStudentsQuery } from "./store/modules/student/studentApi"; function App() { //调用APi查询数据 //这个钩子函数会返回一个对象作为返回值,请求过程中相关数据都在该对象中存储 const res = useGetStudentsQuery()//解构返回对象 /* refetch:f() 一个函数,用来重新加载数据 isFetch:false 数据是否在加载 status;"fulfilled" 请求的状态 isLoading: false 数据是否第一次加载 isSuccess:true 数据是否成功 isUninitialized:false 数据是否还没有开始发送 isError:false 是否有错误 error:Error() 有错误才会出现该对象 data: 最新的数据 currentData:当前参数的最新数据 */ console.log(res); const { data, isSuccess, isLoading, refetch } = res return (
{isLoading &&

数据加载中

} {isSuccess && }
); } export default App; ``` #### 7.调用useQuery参数 ```js const res = useGetStudentsQuery(null, { // selectFromResult: result => {//result 默认返回结果 // if(result.data){ // result.data=result.data.filter(item=>item.attributes.age<18) // } // return result // },//用来指定useQuery返回结果 pollingInterval:0,//设置轮询的间隔,单位毫秒,如果为0则表示不轮询 skip:false,//设置是否跳过当前请求 refetchOnMountOrArgChange:false,//设置是否每次都重新加载数据(可以设置缓存有效期,单位为秒) refetchOnFocus:false,//是否在重新获取焦点时重载数据 refetchOnReconnect:false,//是否在网络重新连接后重新加载数据 }) ``` **refetchOnFocus和 refetchOnReconnect要配合setListener使用** ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-11_13-15-03.png) #### 8.给API打上标签 给API打上标签,数据的缓存就会根据标签来存储。可以在useMutation的方法中使得某些标签失效,标签失效后,对应的API请求就会重新发送获取数据。 ![](D:\Codes\前端学习\12-react\react18\assert\未命名绘图.png) ```js //导的包要注意!!! import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' // 创建Api对象 /* createApi()用来创建RTKQ中API对象 RTKQ中所有功能都需要通过该对象来进行 需要一个对象作为参数 */ const studentApi = createApi({ reducerPath: 'studentApi',//APi的标识,不能和其他API或reducer重复 /* RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装 */ /* fetchBaseQuery 简单封装过的fetch调用后会返回一个封装后的工具函数。需要一个配置对象作为参数,baseUrl表示Api请求的基本路径,指定后请求将会以该路径为基本路径。*/ baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),//指定查询的基础信息,用来设置发送请求的工具 tagTypes: ['student'],//用来指定API的标签类型 endpoints(build) { //build是请求的构建器,通过build来设置请求的相关信息 return {//Api对象中的所有端点(方法)都要在该对象中进行配置 /* 属性值要通过build对象创建,分两种情况: 查询:build.query({}) 增删改:build.mutation({}) */ getStudents: build.query({//配置该接口需要的信息 query() { //指定请求的子路径,完整路径=baseUrl+rquery()返回值 return 'students' }, transformResponse(baseQueryReturnValue) {//用来转换响应数据的格式 return baseQueryReturnValue.data }, providesTags: [{type:'student',id:'List'}]//给该API打上标签,当标签失效时会重新调用该函数 }), getStudentById: build.query({ query(id) {//调用钩子的参数会传递到这 // localhost:1337/api/students/21 //指定请求的子路径,完整路径=baseUrl+rquery()返回值 return `students/${id}` }, transformResponse(baseQueryReturnValue) {//用来转换响应数据的格式 return baseQueryReturnValue.data }, keepUnusedDataFor: 60,//设置数据缓存的时间(以秒为单位)默认60s providesTags: (result, error, id) => {//第三个参数就相当于query接收到的那个参数 return [{ type: 'student', id: id }] }//给该API打上标签,当标签失效时会重新调用该函数 } ), addStudent: build.mutation({ // localhost:1337/api/students query(stu) { return { url: 'students', method: 'post', body: { data: stu } } }, invalidatesTags:[{type:'student',id:'List'}]//使该数组中的标签失效 }), updateStudent: build.mutation({ // localhost:1337/api/students/1 query(stu) { return { url: `students/${stu.id}`, method: 'put', body: { data: stu.attributes } } }, invalidatesTags: (result,error,stu)=>{ return [{ type: 'student', id: stu.id },{type:'student',id:'List'}] }//使student中指定id的标签失效 }), deleteStudent: build.mutation({ query(id) { // localhost:1337/api/students/1 return { //如果发送的请求不是get请求,需要返回一个对象来设置请求的信息 url: `students/${id}`, method: 'delete' } }, invalidatesTags:[{type:'student',id:'List'}]//使该数组中的标签失效 }) } },//用来指定Api中的各种功能,它是一个回调函数,接收一个build对象,需要一个对象作返回值 }) //API对象创建以后 ,对象会根据各种接口请求方法自动生成对应的钩子函数 //通过这些钩子函数可以向服务器发送请求 //钩子函数命名规则 getStudents->useGetStudentsQuery //从API对象中解构出钩子函数 export const { useGetStudentsQuery, useGetStudentByIdQuery, useDeleteStudentMutation, useAddStudentMutation, useUpdateStudentMutation } = studentApi //暴露APi对象给store使用/不给store使用 export default studentApi ``` ## React Router 当前react Router的版本是v6以上,v6相对v5有很大的改动。 ### 1. 什么是前端路由 一个路径 path 对应一个组件 component 当我们在浏览器中访问一个 path 的时候,path 对应的组件会在页面中进行渲染 ![image-20240611160813477](D:\Codes\前端学习\12-react\react18\assert\image-20240611160813477.png) ### 2. 创建路由开发环境 ```bash # 使用CRA创建项目 npx create-react-app react-router-pro # 安装最新的ReactRouter包 npm i react-router-dom # 启动项目 npm run start ``` 页面路由的初始化详见**数据路由** ### 3. 路由的几种模式 #### 3.1BrowserRouter(最常用) **index.js** ```js import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import Bpp from './Bpp' import { BrowserRouter, Route, Routes } from 'react-router-dom' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( // basename属性会添加到指定路径前 {/*访问/pc/ ->App组件*/} }> {/* 访问/pc/bpp ->Bpp组件*/} }> ); ``` BrowserRouter在使用时要后台配合做一些配置,可能会出现404的问题。 #### 3.2HashRouter(了解) **index.js** ```js import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import Bpp from './Bpp' import { BrowserRouter, HashRouter, Route, Routes } from 'react-router-dom' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( // basename属性会添加到指定路径前 {/*访问/#pc/ */} }> {/* 访问/#pc/bpp */} }> ); ``` #### 3.3其他 **MemoryRouter** 使用场景:单元测试 **NativeRouter** 使用场景:安卓,ios **StaticRouter** 使用场景:NodeJS对服务端进行渲染时使用 ### 4.数据路由 #### 4.1根路由 其余的路由都将在它的内部呈现。它将作为用户界面的根布局 **index.js** ```js import React from 'react'; import ReactDOM from 'react-dom/client'; import "./index.css"; //1.导入router函数 import { RouterProvider, createBrowserRouter } from 'react-router-dom' const root = ReactDOM.createRoot(document.getElementById('root')); //2.创建router实例对象并且配置路由对应关系 const router = createBrowserRouter([ { //根路由 path: "/", element:
Hello world!
, }, ]); root.render( // 3.组件绑定 ); ``` #### 4.2抽象路由 将项目的路由配置到一个文件中,然后在应用入口文件渲染,其实就是将`createBrowserRouter()`的内容单独写成一个文件。 ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-12_17-07-22.png) **src/router/index.js** ```js import { createBrowserRouter } from "react-router-dom"; import App from "../App"; // 调用createBrowserRouter()创建router const router = createBrowserRouter([ { //根组件->根路由 path:'/', element: } ]) export default router ``` **index.js** ```js import React from 'react'; import ReactDOM from 'react-dom/client'; import { RouterProvider } from 'react-router-dom' //引入路由配置 import router from './router'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( // 3.组件绑定 ); ``` #### 4.3错误路由配置 编写错误显示页面ErrorPage组件 这里使用了`useRouteError()`钩子 ,提供了抛出的错误信息。当用户导航到不存在的路由时,你会得到一个带有 "Not Found(未找到)"的错误响应`statusText`。 ```js import { useRouteError } from "react-router-dom"; export default function ErrorPage() { const error = useRouteError(); console.error(error); return (

Oops!

Sorry, an unexpected error has occurred.

{error.statusText || error.message}

); } ``` 在路由文件配置**src/router/index.js** ```js import { createBrowserRouter } from "react-router-dom"; import App from "../App"; import ErrorPage from "../components/ErrorPage"; // 调用createBrowserRouter()创建router const router = createBrowserRouter([ { //根组件->根路由 path:'/', element:, errorElement://配置错误页面 } ]) export default router ``` #### 4.4路由导航 路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信 ##### 1. 声明式导航 > 声明式导航是指通过在模版中通过 ` ` 组件描述出要跳转到哪里去,比如后台管理系统的左侧菜单通常使用这种方式进行 ```jsx import React from 'react' import { Link, Outlet, useNavigate } from 'react-router-dom' function App() { const navigate = useNavigate() return (

App:我是根组件

基本使用

  • {/* 声明式导航 */} about组件

{/* 子路由组件的出口 */}
) } export default App ``` 语法说明:通过给组件的to属性指定要跳转到路由path,组件会被渲染为浏览器支持的a链接,如果需要传参直接通过字符串拼接的方式拼接参数即可 ##### 2. 编程式导航 编程式导航是指通过 `useNavigate` 钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在登录请求完毕之后跳转就可以选择这种方式,更加灵活 ```jsx import React from 'react' import { Link, Outlet, useNavigate } from 'react-router-dom' function App() { const navigate = useNavigate() return (

App:我是根组件

基本使用

  • {/* 编程式导航 push方式,会产生历史记录*/} {/* 编程式导航 replace方式,会替换上一条记录*/}
  • {/* 声明式导航 */} about组件

导航传参


{/* 子路由组件的出口 */}
) } export default App ``` 语法说明:通过调用navigate方法传入地址path实现跳转 **小技巧:返回功能** ```js ``` #### 4.5嵌套路由 在一级路由中又内嵌了其他路由,这种关系就叫做嵌套路由,嵌套至一级路由内的路由又称作二级路由。 1. 使用 `children`属性配置路由嵌套关系 2. 使用 `` 组件配置二级路由渲染 **1.配置路由与组件的关系** **src/router/index.js** ```js import { createBrowserRouter } from "react-router-dom"; import App from "../App"; import ErrorPage from "../components/ErrorPage"; import About from "../components/About"; import Home from "../components/Home"; // 调用createBrowserRouter()创建router const router = createBrowserRouter([ { //根组件->根路由 path: '/', element: , errorElement: ,//配置错误页面 children: [ { //配置二级路由 path: '/about', element: , }, { path: '/home', element: } ] } ]) export default router ``` **2.指定二级路由对应的组件在一级路由对应组件的出口** 通过` `来指定在根组件中子路由对应组件的出口位置。 ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-13_12-44-32.png) #### 4.6默认二级路由 当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置index属性为true。同时二级路由导航也需要修改为一级路由的path。 ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-13_12-48-32.png) #### 4.7.导航传参 导航传参有两种方式: > searchParams传参 例: /app?name=jack&id=12 搭配 ` useSearchParams()`钩子获取参数 > params传参 例:/app/1002 搭配 ` useParams()`钩子获取参数 params传递的参数要在路由配置时占位,如:`path: '/student/:id',`且使用student路由时必须要带上占位的参数 路由配置 ```js import { createBrowserRouter } from "react-router-dom"; import App from "../App"; import ErrorPage from "../components/ErrorPage"; import About from "../components/About"; import Home from "../components/Home"; import Student from "../components/Student"; // 调用createBrowserRouter()创建router const router = createBrowserRouter([ { //根组件->根路由 path: '/', element: , errorElement: ,//配置错误页面 children: [ //配置默认二级路由 { index: true, element: }, { //配置二级路由 path: '/about', element: , }, { path: '/student/:id', element: } ] } ]) export default router ``` **App组件** ```js import React from 'react' import { Link, Outlet, useNavigate } from 'react-router-dom' function App() { const navigate = useNavigate() return (

App:我是根组件

基本使用

  • {/* 编程式导航 */}
  • {/* 声明式导航 */} about组件

导航传参

    {/* params参数 */}
  • {/*Search params参数 */}

{/* 子路由组件的出口 */}
) } export default App ``` **About组件** ```jsx import React from 'react' import { useLocation, useSearchParams } from 'react-router-dom' function About() { //解构并获取searchParams参数 const [params, setParams] = useSearchParams() const id = params.get('userId') const name = params.get('name') return (

Article组件:我是二级路由

  • 张三
  • 李四
  • 王五
  • {id &&
  • {id}--{name}
  • }

) } export default About ``` **Student组件** ```jsx import React from 'react' import { useParams } from 'react-router-dom' const STU_DATA = [ { id: 1, name: '张山' }, { id: 2, name: '李四' }, { id: 3, name: '王五' }, { id: 4, name: '刘瑞' } ] function Student() { // 获取params参数 const params = useParams() const stu = STU_DATA.find(item =>item.id===+params.id) return (

Student组件:我是二级路由

{stu.id}----{stu.name}

) } export default Student ``` #### 4.8无路径路由 (看4.7导航传参student组件)在我们跳转到student组件时的路径为/student/:id,配置了一个param参数,但当传递过去的参数在STU_DATA数组中不存在时,路径找不到匹配的数据会报404错误。此时错误页面显示: ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-13_14-07-38.png) 这样根组件App也看不见了,我们希望的页面是: ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-13_14-09-36.png) 我们可以在每一个子路由中添加错误元素,但由于都是同一个错误页面,因此不建议这样做。这需要用到无路径路由,路由可以在*没有*路径的情况下使用,这样它们就可以参与用户界面布局,而不需要在 URL 中添加新的路径段。当子路由出现任何错误时,我们的新无路径路由会捕捉并呈现错误,同时保留根路由的用户界面! ```js import { createBrowserRouter } from "react-router-dom"; import App from "../App"; import ErrorPage from "../components/ErrorPage"; import About from "../components/About"; import Home from "../components/Home"; import Student from "../components/Student"; // 调用createBrowserRouter()创建router const router = createBrowserRouter([ { //根组件->根路由 path: '/', element: , errorElement: ,//配置错误页面 children: [ //配置无路径路由 { errorElement: ,//配置错误页面 children: [ //配置默认二级路由 { index: true, element: }, { //配置二级路由 path: '/about', element: , }, { path: '/student/:id', element: } ] } ] } ]) export default router ``` #### 4.9NavLink `` 是一种特殊的 `` ,它知道自己是否处于 "激活"、"待定 "或 "过渡 "状态 NavLink根据自己的状态可以设置不同的样式。设置样式有三种方式: 1. 默认激活样式 2. style设置 3. className设置 **App.css** ```css #nav a.active{ background-color: #db4949; font-weight: bold; } .heightLine{ background: #2ea9bf; font-size: 20px; } ``` **App组件** ```js import React from 'react' import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom' import './App.css' function App() { const navigate = useNavigate() return (

App:我是根组件

NaLink方式

  • {/* style设置 */} { return { fontWeight: isActive ? "bold" : "", background:isActive ? "yellow" : "", }; }}>Student组件style
  • {/* className设置 */} [ isActive ? "heightLine" : "", ].join(" ") } >Home组件

{/* 子路由组件的出口 */}
) } export default App ``` #### 4.10数据路由DataAPIs 适用场景:URL段、布局和数据往往耦合在一起组合成一个url`http://localhost:5173/contacts/1` 只有支持数据的API才能使用loader和action配置 ![image-20240612201410435](D:\Codes\前端学习\12-react\react18\assert\image-20240612201410435.png) 没有使用数据路由之前的流程: ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-12_19-44-05.png) 使用了数据路由之后的流程; ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-12_19-45-18.png) ##### **loader的使用** 1.定义当前路由组件需要用到的loader函数,并向外导出 3.获取loader函数的返回数据 **root组件** ```js import { Outlet,Link, useLoaderData } from "react-router-dom"; //1.定义当前路由组件需要用到的loader函数,向外导出 /* 返回值必须为null或一个数据对象 */ export const loader=()=>{ console.log('loader'); /* 这一段发送请求的代码 */ const data={name:'张三',age:29} return data; } export default function Root() { // 3.获取loader函数的返回数据 const res=useLoaderData() console.log(res); return ( <>
); } ``` 2.配置loader函数,每次进入该组件会调用 **src/router/index.js** ```js import { createBrowserRouter } from "react-router-dom"; import Root,{loader as rootLoader} from "../layout/root"; import ErrorPage from "../layout/errorPage"; import Student from "../layout/student"; import Person from "../layout/person"; const router = createBrowserRouter([ { //根路径'/'->根组件 path: '/', element: , errorElement: ,//配置错误页面 loader:rootLoader,//2.配置loader函数,每次进入该组件会调用 children:[ //配置二级路由 // '/student'->student组件 { path:'/student', element: }, // '/person'->person组件 { path:'/person', element: } ] } ]) export default router ``` loader函数的参数 将导航传递的`params`参数将作为键值对传递给加载器`loader`,其键与动态段匹配。 ##### action的使用(不太会) action要配合react router的组件Form使用,使用方法与loader类似。 问题就是某个组件的loader/action函数是在组件外部定义的,然后暴露出去。这样的话是不是就不能使用RTKQ了,因为钩子函数只能在组件内部或者钩子函数内使用?感觉loader和 action只能配合fetch或者axios使用。 [在开始之前 | React Router6 中文文档 (baimingxuan.github.io)](https://baimingxuan.github.io/react-router6-doc/start/tutorial) #### 4.11.useLocation() 此钩子返回当前location对象. - location对象的属性: - location.hash :当前 URL 的哈希值。 - location.key该位置的唯一密钥。 - location.pathname:当前 URL 的路径。 - location.search:当前 URL 的查询字符串。 - location.state:``或`navigate`创建的位置的状态值. **App组件** ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-13_13-35-21.png) **About组件** ```js import React from 'react' import { useLocation, useSearchParams } from 'react-router-dom' function About() { const location = useLocation() //state数据保存在该对象中 console.log(location); //解构并获取searchParams参数 const [params, setParams] = useSearchParams() const id = params.get('userId') const name = params.get('name') return (

Article组件:我是二级路由

  • 张三
  • 李四
  • 王五
  • {id &&
  • {id}--{name}
  • }

) } export default About ``` #### 4.12.useMatch() 判断当前组件的路径是否与给定的参数匹配。 ```js import React from 'react' import { useMatch, useParams } from 'react-router-dom' const STU_DATA = [ { id: 1, name: '张山' }, { id: 2, name: '李四' }, { id: 3, name: '王五' }, { id: 4, name: '刘瑞' } ] function Student() { const match=useMatch('/student/:id') console.log(match); // 获取params参数 const params = useParams() const stu = STU_DATA.find(item =>item.id===+params.id) return (

Student组件:我是二级路由

{stu.id}----{stu.name}

) } export default Student ``` ## 权限控制 **能够实现未登录时访问拦截并跳转到登录页面(路由鉴权实现)** 路由鉴权实现思路: - 封装 `AuthRoute` 路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面 - 思路为:判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login 实现步骤: 1. 在 components 目录中,创建 AuthRoute.js 文件 2. 判断是否登录 3. 登录时,直接渲染相应页面组件 4. 未登录时,重定向到登录页面 5. 将需要鉴权的页面路由配置,替换为 AuthRoute 组件包裹渲染 **AuthRoute组件(可复用)** ```js //封装 AuthRoute 路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面 import React from 'react' import { useSelector } from 'react-redux' import { Navigate } from 'react-router-dom' function AuthRouter({ children }) { //获取store的信息 const user=useSelector(store=>store.user) return ( <> {/* 是否登录,登录了就跳转到用户信息页,否则就是去登录页 */} {user.isLogged?children:} ) } export default AuthRouter ``` 路由配置 ```js import { createBrowserRouter } from "react-router-dom"; import HomePage from "../pages/HomePage"; import ProfilePage from "../pages/ProfilePage"; import Layout from "../components/layout"; import ErrorPage from "../pages/ErrorPage"; import AuthPage from "../pages/AuthPage"; import AuthRouter from "../components/AuthRouter"; // 调用createBrowserRouter()创建router const router = createBrowserRouter([ { //根组件->根路由 path: '/', element: , errorElement: , children: [ { index: true, element: }, {//通过高阶组件AuthRouter对路由进行鉴权,登陆了才能访问/profile路由 path: '/profile', element: }, { path: '/authForm', element: , // action:authAction } ] }, ]) export default router ``` 这样就实现了在未登录的情况下访问/profile会重定向到登录页面 **实现登录成功后跳转到根页面或者用户在浏览器输入的页面**(该页面由于需要登录鉴权,用户没有登录,会重定向到登陆页面,希望在登陆后能跳转到该页面) 步骤: 1.在路由鉴权的高级组件中记录用户想去的页面路径,将该路径保存在location的state里(参考**4.11useLocation()**) **AuthRouter组件** ```js //封装 AuthRoute 路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面 import React from 'react' import { useSelector } from 'react-redux' import { Navigate, useLocation } from 'react-router-dom' function AuthRouter({ children }) { //获取store的信息 const user=useSelector(store=>store.user) //获取当前路径信息 const location=useLocation() console.log(location); return ( <> {/* 是否登录,登录了就跳转到用户信息页,否则就是去登录页 */} {user.isLogged?children:} ) } export default AuthRouter ``` 2.在登录成功跳转页面时判断当前location的state中是否有路径,有就跳转到指定页面,没有就跳转到跟页面 ![](D:\Codes\前端学习\12-react\react18\assert\Snipaste_2024-06-14_15-49-02.png) ## 持久化 1. 在将用户登录时数据存储到store的同时存一份到本地。 2. 在store的数据初始化时尝试使用函数形式从localStorage加载一份先前存储的数据 **userStore.js** ```js import { createSlice } from "@reduxjs/toolkit"; //用户相关store const userStore = createSlice({ name: 'user', // 初始数据 // initialState: { // token: null,//用户token // isLogged: false,//是否登录 // user: null//用户信息 // }, initialState: () => { const token = localStorage.getItem('token') const user = JSON.parse(localStorage.getItem('user')) if (token) {//本地有数据 return { token: token, user: user, isLogged: true } } else {//本地没有数据 return { token: null,//用户token isLogged: false,//是否登录 user: null//用户信息 } } }, // 修改数据的同步方法,支持直接修改 reducers: { //登录时保存数据 login(state, action) { state.isLogged = true state.token = action.payload.token state.user = action.payload.user //存储到本地 localStorage.setItem('token', state.token) localStorage.setItem('user', JSON.stringify(state.user)) }, //退出登录时清空数据 logout(state, action) { state.isLogged = false state.token = '' state.user = null //移除本地存储数据 localStorage.removeItem('token') localStorage.removeItem('user') }, } }) export default userStore export const { login, logout } = userStore.actions ``` ## 自动退出登录 过了token失效有限期自动退出登录。还没实现。不会 ## React的钩子函数 钩子函数只能在自定钩子和组件函数中使用。 1. useState 2. useEffect 3. useContext 4. useReducer 5. useCallback 6. useRef 7. useMemo 8. useImperativeHandle 9. useLayoutEffect 10. useDebugValue(18.0新增) 11. useDeferredValue(18.0新增) 12. useTransition(18.0新增) 13. useId(18.0新增) 14. useSyncExternalStore(18.0新增) 15. useInsertionEffect(18.0新增) ### UseMemo useMemo和useCallback十分相似,useCallback用来缓存函数对象,useMemo用来缓存函数的执行结果。在组件中,会有一些函数具有十分的复杂的逻辑,执行速度比较慢。闭了避免这些执行速度慢的函数返回执行,可以通过useMemo来缓存它们的执行结果,像是这样: ``` const result = useMemo(()=>{ return 复杂逻辑函数(); },[依赖项]) ``` useMemo中的函数会在依赖项发生变化时执行,注意!是执行,这点和useCallback不同,useCallback是创建。执行后返回执行结果,如果依赖项不发生变化,则一直会返回上次的结果,不会再执行函数。这样一来就避免复杂逻辑的重复执行。 当某个组件加载比较慢时也可以将组件放到useMemo中(作用类似React.Memo) ``` const result = useMemo(()=>{ return jsx语句; },[依赖项]) ``` ### UseImperativeHandle ## 移动端适配方案 ### rem 放弃px单位,使用rem作为单位,这样在不同尺寸的设备上,通过修改根节点的`font-size`大小,实现等比例缩放 ```js documnet.documentElement.style.fontSize=100/750+'vw' ``` 将fontSize的一个大小单位设置为100%个视口的1/750 ## 配置别名路径 ### 1. 背景知识 > 1. 路径解析配置(webpack),把 @/ 解析为 src/ > 2. 路径联想配置(VsCode),VsCode 在输入 @/ 时,自动联想出来对应的 src/下的子级目录 ![image-20240602194022721](D:\Codes\前端学习\12-react\react18\assert\image-20240602194022721.png) ### 2. 路径解析配置 配置步骤: 1. 安装craco npm i -D @craco/craco 2. 项目根目录下创建配置文件 craco.config.js 3. 配置文件中添加路径解析配置 4. 包文件中配置启动和打包命令 ![image-20240602194037811](D:\Codes\前端学习\12-react\react18\assert\image-20240602194037811.png) ``` const path =require('path') module.exports={ webpack:{ alias:{ '@':path.resolve(__dirname,'src') } } } ``` ### 3. 联想路径配置 配置步骤: 1. 根目录下新增配置文件 - jsconfig.json 2. 添加路径提示配置 ```json { "compilerOptions":{ "baseUrl":"./", "paths":{ "@/*":[ "src/*" ] } } } ``` ## 图标库 ``` npm install react-icons --save ``` [React icons preview for ai (react-icons.github.io)](https://react-icons.github.io/react-icons/icons/ai/) ## 面试题 1.单页面跟多页面的区别 多页面实际上就是多个html,通过window.location相互跳转。 缺点:每个页面跳转都要数显,重新加载资源,性能会比较慢。 好处:SEO友好,适合C端项目,隔离性好,每个页面都是一个独立项目。 单页面: 好处:在一个html中进行路由跳转,实际上是通过js去控制的,比较适合B端项目,不考虑SEO。 页面跳转不需要刷新,性能更好,用户体验更好,可以实现代码复用。 缺陷:SEO不好。 2.BrowserRouter配置过程中有没有遇到一些坑? 需要后台配合,否则出现404