React
React前言
官方文档
- React 18 中文文档1(国内社区):https://react.docschina.org/
- React 18 中文文档2(官方):https://zh-hans.reactjs.org
- React 18 英文文档:https://reactjs.org
- React 17 英文文档:https://17.reactjs.org
react概述
-
React 是一个用于动态构建用户界面的 Js 库
-
起源于 Facebook ,并于 2013 年 5 月开源
-
当前最新版本是18的版本
- 2020升级为17版本(变化特别大,添加hook语法)
- 2022升级为18版本(变化不大)
-
React本身只关注界面,其它如:前后台交互、路由管理、状态管理等都由其它插件搞定
react特点
1.声明式
react利用JSX 语法来声明描述动态页面, 数据更新界面自动更新
React.createElement() 是命令式
声明式&命令式
-
声明式
只需更新数据,具体操作不需要写
声明式不用我们亲自操作原生DOM就能去做页面的动态初始显示和更新显示,使用 JSX 语法来描述页面,只要更新状态数据界面就会自动更新
-
命令式
声明式的反面是命令式
命令式即具体如何操作需要我们自己定义
2.组件化
- 将一个较大较复杂的界面拆分成几个可复用的部分,封装成多个组件, 再组合使用
- 组件可以被反复使用
3.一次学习 随处编写
- 不仅可以开发 web 应用(react-dom)
- 还可以开发原生安卓或ios应用(react-native)
4.高效
- React虚拟DOM
- React 虚拟 DOM Diff 算法(高效更新,更新时不是全部更新)
React基础
基本使用
步骤
-
引入两个JS文件
⚠注意文件引入顺序
react-dom.development.js基于react.development.js,顺序不可改
-
react17版本
<!-- 提供了React对象 --> <script src="./lib/17/react.development.js"></script> <!-- 提供了ReactDOM对象 --> <script src="./lib/17/react-dom.development.js"></script>
-
react18版本
<!-- 引入react库,提供React对象 --> <script src="./lib/react18/react.development.js"></script> <!-- 引入react-dom库,提供ReactDOM对象 --> <script src="./lib/react18/react-dom.development.js"></script>
-
-
在html定义一个根容器标签
一般id值为root或app
<div id="root"></div>
-
创建react元素
react元素类似html元素
中括号代表参数是可选的,可以不传
React.createElement( type, //标签名 [props], //包含所有标签属性的对象 children1, children2... //任意多个子节点,可能是字符串,也可能是react元素 ) const element = React.createElement( 'h1', {title: '你好, React!'}, 'Hello React!' ) //有标签子节点,在children处创建新的react元素 //没有属性,则传空对象{}或者null const element = React.createElement( "h1", { title: "你好, React!" }, React.createElement("p", {}, "Hello1"), React.createElement("p", null, "Hello2") );
-
渲染 react 元素
react18 与 react17 版本在渲染react元素的API上不一样
新项目更多的使用18,而旧的项目可能用的就是17的版本
-
react17版本
ReactDOM.render( element, //react元素 container //页面渲染元素的容器元素 ) ReactDOM.render(element, document.getElementById('root'))
-
react18版本
// 4.1 创建指定了页面容器元素的root对象 const root = ReactDOM.createRoot(container); // 4.2 渲染react元素 root.render(element) ReactDOM.createRoot(document.getElementById('root')).render(element)
-
⚠ReactDOM中的DOM全部是大写
特殊属性-类名
在react里设置类class属性时,应写className,而不是class
class属性最终要转换为DOM元素的className属性 => 标签的class属性
const element = React.createElement(
'h1',
{className: "active"},
'Hello React!'
)
React元素
React元素也称为虚拟 DOM (virtual DOM 简写vdom) 或虚拟节点(virtual Node简写vnode)
React元素是一个普通的 JS 对象,不是真实 DOM 元素对象
虚拟DOM&真实DOM
虚拟 DOM | 真实 DOM | |
---|---|---|
差异 | 较轻的对象(属性少) | 较重的对象(属性多) |
占用内存少,创建快 | 占用内存多,创建慢 | |
关系 | 在渲染时,虚拟DOM最终都会转换为真实DOM(通过root.render(vnode)转换) |
React元素属性
{
type: , "h1"//标签名
props: {
title: '你好, React!' //n个标签属性,
children: : 'Hello React!' //字符串 / vnode / vnode的数组
},
key: 1//节点的唯一标识
}
-
标签名 => type: “h1”
-
标签属性 => props: {title: ‘你好, React!’}
-
子节点 => props: {children: ‘Hello React!’}
-
key => 节点的唯一标识
JSX
创建的子元素多后,React.createElement()的语法会变得复杂,使用JSX
const element = React.createElement( "div", {}, React.createElement("h2", { title: "北京疫情" }, "123"), React.createElement("p", { className: "active" }, "321") ); ReactDOM.createRoot(document.querySelector("#root")).render(element);
基本使用
JSX 是一种JS 的扩展语法, 用来快速创建 React 元素(虚拟DOM/虚拟节点)
- JSX形式上类似HTML标签,且标签内部可以套JS表达式
const h1 = <h1 className="active">哈哈哈</h1>
//可以用括号括起内容
const h1 = (<h1 className="active">哈哈哈</h1> )
- 浏览器无法识别JSX,需要引入babel将jsx 编译成React.createElement的形式
- babel作用
- es6 => es5
- jsx => js
- babel作用
- babel编译 JSX 语法的包为:@babel/preset-react
- 运行时编译可以直接使用babel的完整包:babel.js
JSX注意点
-
不要加引号,否则是字符串而不是JSX
let vnode = <p>aaa</p> ✅ let vnode = "<p>aaa</p>" ❌
-
必须有结束标签
<input type="text"> ❌ <input type="text" /> ✅ <span><span> ❌ <span></span> ✅
-
整个JXS只能有一个根标签
✅ let vnode = ( <p> <span>aaa</span> </p> ) ❌ let vnode = ( <p> <span>aaa</span> </p> <p>bbb</p> )
-
空标签可以自闭合
JSX使用JS表达式
jsx表达式用于显示动态数据
JSX中使用JS表达式的语法:{表达式}
-
{表达式}
使用:- 标签体文本
- 标签属性值
-
可以是js表达式,但不能是js语句
<p>---{title.toUpperCase()}----</p>✅ <p>---{title.toUpperCase();}----</p>❌
⚠有分号即为语句
-
基本数据中的null、undefined、布尔值在页面中不做任何显示,不会报错
-
可以是数组,会自动遍历数组进行显示
<p>{[1,2,3]}</p>
-
可以是react元素对象,但不能是非react元素的js对象
-
react元素对象可以显示
<p>{<span>aaa</span>}</p>✅
-
js对象无法显示,会报错
例外:设置行内样式必须使用js对象
<p>{{name:"asa",age:20}}</p>❌
-
-
style属性值必须是一个包含样式的js对象
<p style="color:'red'">aaa</p>❌ <p style={{color:"red"}}>aaa</p>✅
-
jsx注释代码
{/* 注释内容 */}
条件渲染
1.满足条件显示界面1,否则显示界面2
需求1: 如果loading为真显示提示loading界面, 否则显示结果页面
let loading = false
let vdom
1.if else
if (loading) {
vdom = <h2>正在加载中...</h2>
} else {
vdom = <p>结果页面</p>
}
2.三目表达式
vdom = loading ? <h2>正在加载中222...</h2> : <p>结果页面222</p>
2.只有满足条件才显示界面
不满足条件也不会报错,只是不显示界面
&&
需求2: 只有loading为真才显示界面
vdom = loading && <h3>loading...</h3>
列表渲染
利用react会自动遍历数组元素显示的特性
-
根据数组生成标签数据
arr.map()
<ul>{arr.map((item) => <li>{item}</li>)}</ul>
⚠不是花括号,是大括号
- 列表中的vnode都需要有一个唯一的key属性值
const arr = [
{ id: 1, name: "React" },
{ id: 3, name: "Vue" },
{ id: 5, name: "小程序" },
];
const vdom = (
<div>
<h2>前端框架课程列表</h2>
<ul>
{courses.map((c) => ( //⚠不是花括号,是大括号
<li key={c.id}>{c.name}</li>
//⚠此处的li是JSX,而不是html
))}
</ul>
</div>
);
ReactDOM.createRoot(document.getElementById("root")).render(vdom);
script>
样式处理
行内样式
- style属性值必须是一个包含样式的js对象
- 样式属性名使用小驼峰命名法 camelCase
- 如果样式是数值,可以省略px单位
<h2 style={{color: 'red', fontSize: 30}}>React style</h2>
<h2 style={{color: 'red', fontSize: '30px'}}>React style</h2>
类名样式
- 必须用className,不能用class
- 比起行内样式更推荐使用类名添加css样式,效率更高些
<h2 className="title">React class</h2>
事件处理
绑定事件
React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:
- 命名采用小驼峰式(camelCase),比如:onClick、onFocus 、onMouseEnter
- 使用 JSX 语法时需要传入一个函数作为事件处理函数,而不是一个字符串(用大括号包裹,而不是引号)
const div = <div onClick={事件函数}></div>
React事件特点
React事件不是原生DOM事件,而是合成事件(即包装原生事件产生的对象)
-
React事件有原生事件的所有重要属性+新属性nativeEvent(原生事件对象)
-
react事件与原生事件基本都是对应关系,一个例外——表单事件的change事件❗
- react事件的change监听在输入过程中触发
- 因为react事件的onChange事件内部绑定的是原生input事件
- 而原生是在失去焦点才触发
- react事件的change监听在输入过程中触发
-
处理好了浏览器的兼容性问题
React 根据 W3C 规范来自定义的合成事件,屏蔽了底层浏览器的细节差异,保证了行为的一致性
-
React事件阻止事件默认行为
return false 不起作用
e.preventDefault()
React脚手架
使用官方react脚手架原因:
-
JSX 转 JS 和 ES6 转 ES5 语法运行时编译效率低
-
webpack打包环境搭建复杂且没有质量保证, 效率低
使用脚手架创建react项目
使用脚手架工具 create-react-app
来创建React项目
方式一 npm全局下载
全局下载后(只需下载一次),再通过其命令创建项目
这种方式下载版本不一定是最新的,推荐学习时使用
npm i create-react-app -g
create-react-app 项目名称
方式二 npx最新下载
利用 npx 临时下载最新的,同时运行其命令创建项目
在公司开发时,最好使用npx,而不用全局下载的方式 =》 能保证用的是最新的
npx create-react-app 项目名称
npx 做的事情:
- 先全局下载 create-react-app
- 执行 create-react-app 命令, 创建 react 项目
- 自动将 create-react-app 从全局中删除掉(保证每次都下载最新的
最后cd到项目文件夹,npm strart
开始项目
⚠引入React & ReactDOM
import React from "react"; //react18 import ReactDOM from "react-dom/client"; //react17 import ReactDOM from "react-dom";
安装chrome调试工具
安装谷歌插件React Developer Tools
功能
- 访问react项目时,插件图标会亮
- 多了调试选项: Components,查看应用中组件组成和各个组件的相关数据(props/state)
国内安装谷歌插件
- 极简插件: https://chrome.zzzmh.cn/#/index
- 解压出chrome插件文件: .crx文件
- 进入谷歌扩展程序列表页面
- 将crx文件拖拽到列表页面,当页面显示安装时再松手
React组件
类组件目前不怎么用,都用函数组件
组件与组件化
组件
-
组件:实现一个局部功能界面的所有代码的集合(jsx(js+html)、css、img)
即相当于jsx(js+html)、css、img组合而成的标签
-
因此使用时也是标签的形式
比如APP是一个组件,使用时==
<APP/>
==即可 -
虽然函数组件返回的是一个函数,但不能由我们自己调用,而是由react来调用,所以不是
APP()
而是<APP/>
-
-
一个组件中可能会用到多个js模块
-
优点:可以复用
一个React应用就是由多个React组件组合而成的
-
一个组件可以有内部动态数据(state状态), 也可以接收外部传入的动态数据(props)
组件化
组件化是一种项目的编码方式
将界面的功能界面拆分成多个组件来实现
定义组件的方式
定义注意点
-
组件名首字母必须大写
react以组件名来区分自定义组件元素和一般元素
-
组件必须有返回值
返回内容就是组件呈现的结构
返回值为null,表示不渲染任何内容(undefined、布尔值也一样,但一般用null)
-
return的根标签只能有一个
export default function App(){ //js代码写在return外面 console.log("aaa") return ( <div> <img src={logo} alt="" /> <p>123</p> </div> ) }
1.函数组件
// 定义组件
function App() {
// return null 表示不渲染任何内容
return <div>App</div>
}
// 渲染组件标签
ReactDOM.createRoot(document.querySelector('#root')).render(<App />)
-
函数组件在组件标签渲染时调用,但不会new产生实例对象
类组件在组件渲染时会new产生实例对象
-
函数组件没有state状态,但可以通过hooks语法提供
2.类组件
// 定义类组件
import React from "react"
class App extends React.Component {
render () {
return <div>App Component</div>
}
}
// 渲染组件标签
ReactDOM.createRoot(document.querySelector('#root')).render(<App />)
- 类组件继承React.Component父类,从而使用Component类中提供的方法或属性
- 类组件中必须声明一个render函数,来返回组件界面的虚拟DOM元素
- 类组件在组件标签渲染时调用,new 产生实例对象
- 类组件有state状态
react便捷操作vscode插件
快速创建类组件
rcc
react class component
快速创建函数组件
rfc
react class component
类组件状态state
概述
-
状态(state)是内部动态数据 ,是组件内部的私有数据,只能在组件内部使用
-
函数组件又叫无状态组件,类组件又叫有状态组件
函数组件没有state,只能根据外部传入的数据(props)动态渲染
类组件有自己的state数据,一旦更新state数据, 组件界面就会自动更新
-
组件对象state属性的属性值为对象,可以在state对象中保存多个数据
state = { name:"asa", age:20 }
使用
初始化state
-
方式一:推荐
state = {xxx: value}
-
方式二:在构造器中初始化
- 子类有构造器必须调用super()
- 初始化state
constructor () { // 1.必须调用super() super() // 2.初始化state this.state = { xxx: value }
读取state
this.state.xxx
更新state
使用方法**setState
**进行更新
this.setState({xxx: newValue})
⚠不能使用this.state.xxx = newValue
进行更新,页面并不会更新显示
实例
class StateTest extends React.Component {
state = {
count: 0,
xxx: 'abc'
}
render () {
// 读取state数据
const {count} = this.state
return <div onClick={() => {
// 通过setState()更新state => 界面会自动更新
this.setState({
count: count + 1
})
}}>点击的次数: {count}</div>
}
}
事件回调this问题
为了提高代码的性能和阅读性,最好把事件函数定义在return的组件界面结构外面
而要读取类组件的state,this.state.xxx
的this必须指向组件对象
但在严格模式下,事件函数的this指向undefined
事件回调都不是组件对象调用的,是事件触发后直接调用的
handleCLick () {
this.setState({
count: this.state.count + 1
})
}
<button onClick={this.handleCLick}>有问题</button><br/>
解决办法
方式一和方式二都利用了箭头函数的this穿透问题
-
方式一:将事件函数定义为箭头函数
handleCLick2 = () => { this.setState({ count: this.state.count + 3 }) } <button onClick={this.handleCLick2}>方式一</button>
-
方式二:用箭头函数包裹事件函数
<button onClick={() => this.handleCLick()}>方式二</button> //这种方法可以传递参数 <button onClick={(e) => this.handleCLick(e, 'abc')}>方式二</button>
-
方式三:bind更改事件函数this指向
<button onClick={this.handleCLick.bind(this)}>方式三</button>
方式的选择:
- 一般用箭头函数方式,编码简洁
- 如果要参数,用包裹箭头函数方式
组件props
概述
-
props是外部传入动态数据
组件是封闭的,接收外部数据通过 props 来实现,
props是父组件向子组件传递的数据
-
props可以给组件传递任意类型的数据
-
props是只读对象,子组件只能读取属性的值,不要修改props
-
如果父组件传入的是动态state数据,那一旦父组件更新state数据,子组件也会更新
使用
父组件传递
父向子传入数据:给组件标签添加属性
<APP name1="asa" name2={"asa"} age1="20" age2={20}/>
//name1、name2、age1为字符串
//age2为数字
//一次传递多个属性
<Child {...obj}>
子组件读取
- 函数组件:通过
参数props
接收数据 - 类组件:通过
this.props
接收数据
更新props
更新只能在父组件更新,而不在子组件更新
如果父组件传入的是动态的state数据,那一旦父组件更新state数据,子组件也会更新
实例
-
父页面
import { Son1, Son2 } from "./son"; export default class Classb extends Component { state= { user:{ name:"asa", age:20 } } fn=()=>{ this.setState({user:{name:"oooo",age:10000}}) } render() { let {name,age} = this.state.user return ( <div> father: name:{name} age:{age} <Son1 name={name} age={age}></Son1> <Son2 name={name} age={age}></Son2> //简洁传法: <Son2 {...user}></Son2> <button onClick={this.fn}>更新</button> </div> ) } }
-
子页面 son.jsx
export class Son1 extends Component { render() { let {name,age}=this.props; return ( <div>Son1:name:{name} age:{age}</div> ) } } export function Son2(props) { const {name,age} = props; return ( <div>Son2:name:{name} age:{age}</div> ) }
props校验&默认值-了解
不怎么用
函数组件函数体外指定
类组件的类体内指定
props校验
指定并检查接收prop的类型和必要性
-
导入 prop-types 包 (内置包
import PropTypes from 'prop-types'
-
指定校验
//函数组件 FunPropsCheck.propTypes = { myName: PropTypes.string.isRequired, age: PropTypes.number, } //类组件 static propTypes = { myName: PropTypes.string.isRequired, age: PropTypes.number, } //myName属性: 字符串类型, 必须有 //age属性: 数值类型, 不是必须的
props默认值
如果prop没有传入,指定默认值是多少
//函数组件
FunPropsCheck.defaultProps = {
age: 0
}
//类组件
static defaultProps = {
age: 0
}
//age属性:默认值为0
类组件生命周期
生命周期图
生命周期图谱:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
生命周期三大阶段
挂载阶段
流程:constructor => render => componentDidMount
触发:render()渲染组件元素
更新阶段
流程:render => componentDidUpdate
触发:setState() 、forceUpdate()、 组件接收到新的props
卸载阶段
流程:componentWillUnmount
触发:不再渲染组件、条件渲染不再满足条件
生命周期钩子
constructor
- 只执行一次:创建组件对象挂载时第一个调用
- 用于初始化state属性或其它实例属性方法(可以简写到类体中)
render
- 执行多次:挂载调用一次 + 每次state/props更新都会调用
- 用于返回要初始显示或更新显示的虚拟DOM界面
render的调用(高效)
- 第一次调用render => 得到虚拟DOM =>生成真实DOM
- 重新调用render => 得到新虚拟DOM => 进行新旧虚拟DOM差异比较 => 最小化的真实DOM的更新
componentDidMount
- 执行一次:在第一次调用render且组件界面已显示之后调用
- 用于初始执行一个异步操作:发ajax请求/启动定时器等
componentDidUpdate
- 执行多次:组件界面更新(真实DOM更新)之后调用
- 用于数据变化后, 就会要自动做一些相关的工作(比如: 存储数据/发请求)
componentWillUnmount
- 执行一次:在组件卸载前调用
- 用于做一些收尾工作,如:清除定时器
React Hooks
官方hook
概述
- Hook 是 React 16.8 的新增特性。
- Hook也叫钩子,本质就是函数,能让你使用 React 组件的状态和生命周期函数…
- Hook语法基本代替了类组件的语法,可以在不编写类组件的情况下使用 state 以及其他的 React 特性
Hook规则
- 只在最顶层使用 Hook,不要在条件或循环中
- hook按照创造hook的顺序排列
- 只在React组件函数内部中调用 Hook,不要在组件函数外部调用
useState()❗
用来定义状态数据
可以多次调用,产生多个状态数据
1.初始执行useState
useState(初始值)
所做的事:
- React创建一个state数据,将state数据初始值定为传入的参数
- 同时定义一个更新state数据的函数
- 返回一个数组:
[state数据,更新state数据的函数]
useState()以数组形式返回
const [xxx, setXxx] = useState(initValue)
//定义一个数据
const [name, setName] = useState("asa")
//定义多个数据
//1.方式一:调用2次useState()
const [name, setName] = useState("asa")
const [age, setAge] = useState(20)
//2.方式二:使用对象???
const [name, setName] = useState("asa")
2.更新state数据
更新内部的状态数据,并触发组件的更新,即重新调用组件函数(重新渲染组件)
setXxx的2种调用方式
-
setXxx(newValue)
更新为指定某个值时,这种方式更占优势,如点击直接变成5
-
setXxx(value => newValue)
- value是内部保存的状态数据值,总是内部存储的最新值
- newValue指定更新后的值
- 更适合用于基于原数据更新数据的情况,如每次加一
⚠在异步函数里必须使用这种方式,因为val总是内部存储的最新值
详见useEffect的调用一注意点
setXxx的"异步"问题❗
参考
- setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的
- setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
3.再次执行useState
返回内部存储的状态数据和更新函数的数组
实例
//需要引入useState方法
import React, { useState } from 'react';
export default function App() {
const [name,setName] = useState("asa")
return (
<div>
<p>{name}</p>
<button onClick={()=>setName(name+"o")}>click1</button>
<button onClick={()=>setName(value => value+"o")}>click2</button>
</div>
)
}
useEffect()
模拟类组件的生命周期,相当于componentDidMount、componentDidUpdate、componentWillUnmount的组合
用于执行一些带副作用的操作(异步、数据存储、更新DOM,更新状态数据)
可以在一个组件中多次使用
模拟周期 | 说明 | |
---|---|---|
useEffect(() => {}, []) | componentDidMount | 只在初始化执行一次 可以在回调中执行异步操作:启动定时器、发ajax请求等 没有监视任何数据变化 |
useEffect(() => {}) | componentDidMount + componentDidUpdate | 1.初始化执行一次 2.任意更新后都会执行 |
useEffect(() => {}, [xxx]) | componentDidMount + xxx更新的componentDidUpdate | 初始化执行一次 2.xxx更新后执行 |
useEffect(() => { return () => {} }, []) | componentWillUnmount | return的函数中的内容为卸载前调用执行 |
调用一
useEffect(() => {}, [])
相当于componentDidMount
只在初始化执行一次
可以在回调中执行异步操作: 启动定时器,发ajax请求
⚠注意点
useRef()
用于获取标签对象或自定义数据
获取标签对象
-
创建一个ref容器
const inputRef = useRef();
-
将ref容器交给标签
<input ref={inputRef}/>
-
通过ref容器获取标签
const input = inputRef.current
export default function App() {
const inputRef = useRef();
function fn(){
const input = inputRef.current
alert(input.value)
}
return (
<div>
<input type="text" ref={inputRef}/>
<button onClick={fn}>获取input输入值</button>
</div>
)
}
获取自定义数据
⚠ref用来存储标识,更新数据后可立即获取,无需等到重新渲染组件函数后才能获取最新数据
-
创建ref容器
const myRef = useRef(初始值)
-
存储数据
myRef.current = 数据
-
读取数据
myRef.current
export default function App() {
const msgRef = useRef("asa");
const timer
//创建msgRef容器,并设置msgRef初始值为asa,若此处不传初始值,读取时为undefined
return (
<div>
<button onClick={()=>{
msgRef.current = "ooo"
}}>更改msgRef值为ooo</button>
<button onClick={()=>{
alert(msgRef.current)
}}>读取并弹窗msgRef值</button>
<button onClick={()=>{
msgRef.current = "bbb"
}}>更改msgRef值为bbb</button>
</div>
)
}
useState和useRef存储数据的区别
useState
用于显示在界面上,会更新
useRef
不用于显示在界面上,仅开发人员自己存储数据使用(如定时器的id等
useContext()
读取context提供的数据
收集表单数据
受控组件&非受控组件都是包含表单的组件
非受控组件无法进行表单校验(只能提交时才能校验)
受控组件可以实现实时表单校验(在输入过程中就进行校验)
非受控组件
输入的数据不与state数据关联同步,需要手动读取表单元素的值
借助于 useRef,使用原生 DOM 方式来获取表单元素值
export default function App() {
const submit = (e) => {
e.preventDefault();
let name = nameRef.current.value;
alert(name);
};
const nameRef = useRef();
return (
<form>
用户名: <input type="text" ref={nameRef} /> <br />
<input type="submit" value="登 陆" onClick={submit} />
</form>
);
}
受控组件
输入数据与状态数据同步,双向绑定:
优势:受控组件可以实时表单校验(在输入过程中就进行校验)
实现步骤
- 在 state 中添加一个数据,作为表单元素的value值
- 给表单元素绑定 change 事件,将表单元素的值设置为 state 的值
export function App2() {
const submit = (e) => {
e.preventDefault();
alert(name);
};
const [name, setName] = useState();
const fn = (e) => {
setName(e.target.value);
};
return (
<form onSubmit={submit}>
用户名: <input type="text" value={name} onChange={fn} /> <br />
<input type="submit" value="登 陆" />
</form>
);
}
组件化编码
基本流程
1.拆分组件
- 拆分界面,定义组件
- 为每个组件分别创建一个文件夹用来存储jsx和css代码
2.实现静态组件界面
在组件中编写html、css
(可以先用一个静态页面实现,再拆分到每个组件中)
3.实现动态组件
-
动态初始化显示
-
设计状态数据
- 数据的类型/结构
- 数据名称
- 数据存在于哪个组件:看该state数据是什么组件需要
- 只有一个组件需要——存于该组件本身
- 多个组件需要——存于这些组件的父组件
-
传递给相应组件进行显示
通过标签属性props进行传递
-
-
实现交互功能
如:添加/删除/勾选
-
state数据在哪个组件,更新状态数据的行为/函数就定义在哪个组件
-
如果子组件需要更新父组件的state,将更新state的函数通过props传递给子组件调用
-
步骤
- 在父组件中定义更新状态数据的函数
- 通过props将函数传递给子组件
- 子组件中调用接收的函数去更新父组件的状态数据
-
案例
功能描述
- 动态显示初始列表
- 添加一个 todo
- 删除一个 todo
- 反选一个 todo
- todo 的全部数量和完成数量
- 全选/全不选 todo
- 删除完成的 todo
- todo列表持久化
实现步骤
- 拆分组件:拆分界面,定义组件
组件通讯
组件通讯
组件间的数据交互,一个组件向另一个组件发送数据
组件间的关系
- 父子
- 祖孙
- 兄弟或其它
props通讯
通过标签属性传递数据进行通讯
缺点
无法跨层通讯,只能逐层多次传递
传递方式
-
父向子:通过子组件标签传递非函数属性
-
子向父:通过子组件标签传递函数属性
-
祖向孙:通过子组件标签传递非函数属性(逐层多次传递)
-
孙向祖:通过子组件标签传递函数属性(逐层多次传递)
-
兄弟间:借助父组件进行通讯
将state数据定义在父组件,再将state数据和更新state数据的函数分别传递给兄弟子组件使用
context通讯
context可以实现跨层直接通信
传递方式
- 祖向孙
- 孙向祖
使用
-
创建一个context容器对象
const context = React.createContext()
-
最外层组件向后代组件提供数据
import context from './context' <context.Provider value={要传递的数据}> <Child1 /> </context.Provider>
-
后代组件读取数据
import context from './context' const data = useContext(context)
pubsub-js
消息发布订阅的js库
实现任意组件间的直接通讯
配置
安装
npm i pubsub-js
引入
import PubSub from 'pubsub-js'
订阅消息
PubSub.subscribe('消息名', (msgName, data) => {})
- 参数
- msgName:消息名,没用
- data:发布的数据
- 返回值
- 返回一个标识token,用于取消订阅
- token为唯一的
发布消息
PubSub.publish('消息名', 数据)
取消订阅
PubSub.unsubscribe(token/消息名)
-
token
token即订阅消息时返回的token
通过token取消订阅,只能取消一个订阅
-
消息名
由于一个消息可以被多个订阅
通过消息名取消订阅,可能取消多个订阅
Ajax
测试接口
- 接口1: https://api.github.com/search/repositories?q=re&sort=stars
- 接口2: https://api.github.com/search/users?q=a
说明
在 react 项目开发中,基本都是使用 axios 库来发送 ajax请求与后台通信
发送Ajax的情况
-
初始化发一次请求
在useEffect(() => {}, [])中发送
-
在事件回调函数中发送请求
-
在某个state或props数据发生改变后发送请求
在 useEffect(() => {}, [数据]) 中发送
⚠effect函数不能是async函数
需要用箭头函数包裹一层
()=>( async function( await axios.get() ) )
react配置代理
react/vue的开发环境中有一个专门处理ajax跨域的代理服务器:webpack-dev-server
-
webpack-dev-server的作用
将项目代码在内存中打包,生成内存中的打包文件,并启动服务运行项目,就可以通过虚拟地址来访问项目
-
http-proxy-middleware
- webpack-dev-server中的一个代理服务器工具包
- 作用:将对当前项目的所有404的请求转发给指定的跨域的后台接口
-
react配置代理
在package.json中追加配置,当请求了当前域不存在的资源时,会将该请求转发给proxy设置的域名进行请求
"proxy": "url"
//当请求了当前域不存在的资源时,将请求转发给4000端口
"proxy": "http://localhost:4000"
⚠此时需要跨域请求的url,前面不应该带域名
axios.get{ url:"http://localhost:3000/serach/users"❌ url:"/serach/users"✅ }
⚠不能配置多个代理
Fragment
Fragment是原生DOM中,内存中用来保存多个DOM节点对象的容器
如果将Fragment添加到页面中,其本身不会进入页面但它的所有子节点会进行页面
可以使用Fragment作为根标签(前提:根标签没有样式)
Fragment可简写为空标签==<>jsx代码</>
==
function Hello(){
return (
// 这样就只会渲染h1和p
<React.Fragment>
<h1>fragment</h1>
<p>hello react</p>
</React.Fragment>
)
}
js补充
高阶函数&函数柯里化
高阶函数
参数是函数或返回值是函数的函数
- 参数是函数
- 数组遍历api
- Promise及其的then、finally等
- setInterval & setTimeout
- 返回值是函数
- bind
- 返回函数的闭包
函数柯里化
柯里化函数是一种特别的高阶函数
将一次传递参数的方式变为多次传递参数,函数就会嵌套返回,整个函数即称为柯里化函数
柯里化函数特点——懒计算
外层调用时先不进行计算,最内层的函数调用时才进行计算处理
function fn1(a, b, c) {
return a + b + c;
}
console.log(fn1(1, 2, 3));
function fn2(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}
console.log(fn2(1)(2)(3));
纯函数&副作用函数
纯函数
多次调用传入相同的参数,结果一样
函数执行不影响外部环境:外部变量/外部存储/远程存储/异步
function fn(a,b){
return a+b;
}
副作用函数
也称为不纯的函数
副作用函数的执行会影响周围环境,影响其他人
let c = 3;
function fn(a,b){
//1.使用了外部数据
return a+b+c;
//2.更新了外部数据
c = 5
//3.存储了数据
session.setItem("key",val)
//4.异步操作
}
区别并实现bind和call
区别
call | bind | |
---|---|---|
同 | 都是用来改变函数中的this的 | |
异 | call立即调用 | bind只有当调用bind返回的函数时才会调用 |
选择 | 看函数是否是需要立即调用, 如果不是,只能用bind |
实现
实现bind
Function.prototype.bind = function (thisObj, ...args) {
console.log('bind')
// bind中的this是原函数
return (...args2) => {
return this.call(thisObj, ...args, ...args2)
}
}
实现call
Function.prototype.call = function (thisObj, ...args) {
if(thisObj===undefined || thisObj===null) {
thisObj = window
}
// 将原函数添加为thisObj的方法
thisObj.tempFn = this
// 通过thisObj来调用方法
const result = thisObj.tempFn(...args)
// 删除这个方法
delete thisObj.tempFn
return result
}
路由工具-React Router6
前言
单页应用
SPA (single page application):单页应用
- 整个应用只有一个整体页面,页面切换只是局部页面更新
- 点击路由链接切换页面时,本身不发请求,进行纯前台的DOM更新
- 优点:加载数据少,基本不会出现切换页面时空白的情况
路由
路由:一套 key: value 的映射组合
-
后端路由:path 与处理请求的回调及请求方式的映射组合
app.get('/getUser', () => {})
-
前台路由:path 与 路由组件的映射组合
<Route path='/about' element={<About/>}></Route>
路由工具概述
React Router有三个不同的包:
- react-router:路由的核心库,提供了很多的:组件、钩子。
- react-router-dom:开发网页端使用的路由库
- react-router-native:开发移动端使用的路由库
路由基本使用
路由组件一般放在pages或views文件夹中
安装
npm i react-router-dom
基本使用
-
使用路由器标签包裹App
ReactDOM.createRoot(document.getElementById("root")).render(<BrowserRouter><App /></BrowserRouter>);
-
定义路由导航链接及文本
<NavLink to="/about">About</NavLink> <NavLink to="/home">Home</NavLink>
⚠NavLink会给当前所在的路由链接添加一个类名: active
-
注册渲染路由组件
<Routes> <Route path="/about" element={<About/>} /> <Route path="/home" element={<Home/>} /> <Route path="/" element={<Navigate to="/home"/>} /> {/* <Navigate to="/home"/> 位于/时自动重定向到/home */} </Routes>
路由表
路由表:应用中所有路由信息的数组
将路由的注册信息集合在一个js模块进行单独管理,不与组件混在一起
定义路由表
const routes = [
{path: '/xxx', element: <Xxx/>},
{path: '/xxx', element: <Navigate to="/xxx"/>}
]
根据路由表注册路由
利用useRoutes根据路由表注册路由,显示路由组件
{
useRoutes(routes)
}
嵌套路由
-
定义子路由导航链接(和路由主无差
<NavLink to="news">News</NavLink> <NavLink to="/home/msg">Msg</NavLink>
-
在路由表中注册子路由
- 子路由需写在父路由的children键中
- 子路由的path有2种写法
path:"/父路由/子路由"
path:"子路由"
{ path: "/home", element: <Home />, children: [ { path: "news", element: <News />, }, { path: "/home/message", element: <Message />, }, ], },
-
使用
<Outlet />
渲染显示子路由
内置组件
组件总览
功能 | |
---|---|
BrowserRouter | 用于包裹整个应用<APP/> |
HashRouter | 用于包裹整个应用<APP/> |
Link | 点击修改url,跳转至对应路由,不发送网络请求 |
NavLink | 1. 点击修改url,跳转至对应路由,不发送网络请求 2. 对应路由的NavLink添加特殊类名-active |
Routes & Route | 注册渲染路由组件 必须要用包裹 |
Navigate | 重定向url |
Outlet | 渲染子路由 |
BrowserRouter & HashRouter区别❗
BrowserRouter | HashRouter | |
---|---|---|
前台路由路径 | 不带#http://loacalhost:3000/home | 带#http://loacalhost:3000/#/home |
请求url | http://loacalhost:3000/home | http://loacalhost:3000 始终请求根路径 |
在某个路由路径下刷新 | 浏览器携带路由路径进行请求,服务器没有对应资源(index.html)返回,就会404 但由于react内部设置了webpack配置: devServer: {hostoryApiFallback: true} 当404时,给浏览器返回index界面(fallback 备用计划) | 不会携带hash路径,浏览器总是请求项目根路径,服务器返回对应的index界面 |
引入静态文件 | 1.相对路径./ 引入静态文件http://loacalhost:3000/home/css/style.css 在子路由刷新时文件会加载错误 2.最好使用 / ,保证通过根路径发送请求 | 相对路径./ 引入静态文件,不会出错http://loacalhost:3000/css/style.css (因为HashRouter始终以根路径发送请求) |
内置hooks
说明 | |
---|---|
useRoutes() | 根据路由表,动态创建和useRoutes(routes) |
[useParams()](#方式一: params) | 获取路由的params参数let { id } = useParams() |
[useSearchParams()](#方式二: query) | 获取or更新路由的query参数 1. 返回一个数组 [query参数, 更新query参数的方法] 2. 读取参数 search.get('key') |
[useLocation()](#方式三: state) | 获取当前location信息const x = useLocation() |
useNavigate() | 实现编程式导航 |
路由组件间通讯(传参)
方式一: params
-
定义路径时指定params参数占位
{ path: "detail/:id/:title", element: <Detail />, }
-
定义路径链接,指定to携带params参数
<Link to=`detail/1/post1`>post1</Link> <Link to={`detail/${item.id}/${item.title}`}> {item.title} </Link>
-
目标路由组件中读取参数
const {id,title} = useParams();
useParams()返回一个params参数对象
方式二: query
query参数也称为search参数
⚠query参数传参不需要指定占位
-
定义路径链接,指定to携带query参数
<Link to=`detail?id=1&title=post1`> <Link to={`detail/?id=${item.id}&title=${item.title}`}> {item.title} </Link>
-
目标路由组件中读取参数
const [serch,setSearch] = useSearchParams(); const id = serch.get("id"); const title = serch.get("title");
useSearchParams()
返回一个数组[search参数, 更新search参数的方法]
- 读取参数:
search.get("key")
- 更新参数:
setSearch('id=008&title=哈哈')
- 读取参数:
方式三: state
- state默认值为null
- state参数可以携带任意类型的数据
- state参数不会出现在地址栏,但刷新还在
⚠使用前提:使用
<BrowserRouter/>
包裹
-
定义路径链接,携带state参数
<Link to="detail" state=`detail/1/post1`>post1</Link> <Link to="detail" state={`detail/${item.id}/${item.title}`}> {item.title} </Link>
-
目标路由组件中读取参数
通过
useLocation()
获取location信息中的stateconst x = useLocation() const {state: {id, title, content}} = x return ( <> <li>消息编号:{id}</li> <li>消息标题:{title}</li> </> )
打印案例中的x(location信息)
{ hash: "", key: "ah9nv6sz", pathname: "/login", search: "?name=zs&age=18", state: {id: '001', title: 'm1', content: 'abcd'} }
传参方式选择
- 一般情况下,优先选择params参数
- 参数数据较多时,使用state参数,前提是必须使用
<BrowserRouter/>
路由导航/跳转
路由导航/跳转编码模式
声明式
通过路由链接进行路由跳转
<Link to="/home">home</Link>
编程式
手动调用路由跳转函数进行路由跳转
利用**useNavigate()
**返回一个跳转函数用来实现编程式导航:
-
获取跳转函数
const navigate = useNavigate()
-
调用跳转函数
navigate(path:"路由path", {配置项})
- path中可以携带params参数和query参数
- state参数需写在配置项中
路由导航/跳转方式
push模式(默认 | replace模式 |
---|---|
跳转路由,记录当前路由并跳转到新路由 | 跳转路由,替换当前路由路径为新路由 |
有历史记录,能回退到上一个路由 | 没有历史记录,不能回退到上一个路由 |
push模式
默认为push模式
replace模式
切换为replace模式
声明式
当属性值是true时,可以省略属性值
<Link replace/>
编程式
navigate(path, {replace: true})
路由懒加载
默认所有路由组件都会被打包到一起,初始访问就会全部加载 => 初始要加载的打包比较大
路由懒加载:路由组件仅在需要时才发送请求加载
开始只加载(请求)当前路由所对应的打包文件,其它路由组件的打包文件只有初始访问时才加载
解决-在路由表注册路由时操作:
-
lazy
懒加载动态引入组件import {lazy} from 'react' // 懒加载动态引入组件 const About = lazy(() => import('../pages/About'))
动态引入
import( '模块路径')
- 返回promise,promise的value为模块对象
- 被引入的模块会被单独打包
-
注册路由组件时,需要通过
Suspense
来指定Loading界面由于一开始组件不加载会导致报错,通过Suspense来指定备用界面,避免加载组件时报错
import {lazy} from 'react' const routes = [ { path: "/about", element: ( <Suspense fallback={<h2>loading...</h2>}> <About /> </Suspense> ), }, ]
状态管理工具1-Redux
- Redux:集中式状态管理工具
- 官方文档: https://cn.redux.js.org/
- 做项目不用Redux
- 下载:
npm i redux
redux三大原则
- 单一数据源
- State是只读的
- 使用纯函数来执行产生新state
三大核心概念
- action:提出想法
- reducer:实现想法
- store:管理state数据和reducer,与组件进行通信
Action
action是一个原生js对象,描述要执行的行为和条件数据
type需大写
{type:"INCREMENT", payload:2}
Action creator
使用函数来创建 action,将该函数统称为 action creator
// action creator
const increment = (num) => ({ type: 'INCREMENT', payload: num })
// 创建action
increment()
Reducer
reducer是一个函数
接收state和action,计算返回新的state
const counterReducer = (state={count: 0}, action) => {
switch (action.type) {
case 'INCREMENT':
return {count: state.count + action.payload}
case 'DECREMENT':
return {count: state.count - action.payload}
default: // ⚠必须默认返回当前state
return state
}
}
⚠不要直接修改 state,而要产生新的 state返回
⚠必须默认返回当前state
⚠reducer必须是一个纯函数
Store
Redux的state 仓库,一个redux应用只有一个store
一般回专门创建一个store文件夹用来存放
作用
- 存储state数据,并提供更新state数据的方法
- store是组件与redux通信的桥梁
创建store
内部调用reducer函数,,产生初始状态值
import {createStore} from 'redux';
const store = createStore(reducer函数)
读取state
store.getState().count
更新state
根据传入的action对象, 调用reducer函数产生新的state
store.dispatch(Action)
//1.直接传入action
store.dispatch({type: 'INCREMENT', payload: 1})
//2.通过action创建器传入action
const incrementAction = (num) => ({type: 'INCREMENT', payload: num})
store.dispatch(incrementAction(1))
监听state变化
监听state变化,读取最新的state数据,从而去更新组件
store.subscribe(() => {//state变化时执行该回调函数})
⚠redux的state数据更新之后,不会自动更新组件,需使用store.subscribe()来监听数据更新
Redux基本使用
import {createStore} from 'redux'
// reducer函数: 根据老的state和指定的action, 返回一个新的state
const countReducer = (state={count: 1}, action) => {
console.log('countReducer', state, action)
switch (action.type) {
case 'INCREMENT':
// state.count += action.payload // 不能直接修改
return {count: state.count + action.payload} // 载荷 数据
case 'DECREMENT':
return {count: state.count - action.payload}
default:
return state
}
}
// 创建store
const store = createStore(countReducer) // 内部会调用reducer函数, 产生初始状态值
// 读取状态值
console.log(store.getState().count) // 1
// 在index.js中监视状态数据的变化,重新渲染页面
store.subscribe(() => {
root.render(<App/>)
})
// action creator action对象的创建器(工厂函数)
const incrementAction = (num) => ({type: 'INCREMENT', payload: num})
const decrementAction = (num) => ({type: 'DECREMENT', payload: num})
// 更新状态数据
store.dispatch(incrementAction(2)) // => countReducer调用产生新的状态 => 更新状态 => 调用监视的回调
store.dispatch(decrementAction(3)) // => countReducer调用产生新的状态 => 更新状态 => 调用监视的回调
工具
调试工具
-
安装谷歌扩展调试工具:Redux DevTools
-
项目配置
-
下载
npm i redux-devtools-extension -D
-
引入并启用redux调试工具
import {composeWithDevTools} from 'redux-devtools-extension' // 创建store时, 启用redux调试工具 const store = createStore(counter, composeWithDevTools())
-
异步工具
Redux-thunk
用于实现异步数据请求的redux中间件
状态管理工具二-Mobx
官网:https://cn.mobx.js.org/
Mobx调试工具:MobX Developer Tools
前言
概述
Mobx是简单、可扩展的状态管理工具
作用: 对多组件共享的状态数据进行集中式的统一管理
三大核心概念
observable
状态数据属性
action
更新状态数据的方法
computed
基于状态数据计算属性
-
计算属性多次读取,只会计算一次(内部在第一次计算后,会缓存结果
-
什么时候重新计算:依赖数据发生改变后,再次读取时重新计算
将对象中的属性、方法和计算属性变为响应式
方式一: 简洁写法(多用 makeAutoObservable(this) 方式二: 原始写法 makeObservable(this, { count: observable, // 标识count是一个响应式的可观察的属性 oddOrEven: computed, // 标识oddOrEven是一个响应式的计算属性 increment: action, // 标识increment是一个响应式的行为函数 })
Mobx vs. Redux
Mobx | Redux | |
---|---|---|
侵入性 | 弱 无模板代码,非常简洁 | 强 有严格的工作流程,需要写大量模板代码 |
store个数 | 多个store,更方便 | 单个store |
自身是否支持异步 | 自身支持异步 | 自身不支持异步,需要中间件处理异步 |
状态数据是否可变 | 状态数据是响应式的 可直接对状态数据进行任意修改,变化更方便 | 强调状态数据不可变 不能直接更新状态数据,只能产生并返回一个新状态数据 |
适合项目 | 简单、规模不大的应用 | 约束性强,更适合大型多人协作开发 |
⚠复杂的应用也不是说不能用,需要合理的理清业务逻辑关系,合理的使用
基本使用
安装
npm i mobx mobx-react
使用
-
定义管理状态数据的store
-
为模块创建一个js文件,用于配置store
-
定义类, 在类中定义状态属性、更新状态属性的方法、计算属性
⚠Mobx不建议在action函数外直接更新状态数据——runInAction
-
向外暴露类的实例对象
⚠暴露的是实例对象new class(),而不是类本身
-
引入makeAutoObservable并在构造器调用,将当前对象变为响应式对象
-
[引入autorun并在构造器调用,监听依赖数据的改变]
//引入makeAutoObserverable,[autorun] import { autorun, makeAutoObservable } from "mobx"; //定义类 class TodosStore { //定义状态属性 todos = []; //定义计算属性 get doneCount() { return todos.length; } //构造器中调用makeAutoObservable,[autorun] constructor() { //将当前counter对象变为响应式对象 makeAutoObservable(this); //监听依赖数据的改变 autorun(() => { localStorage.setItem("todos", JSON.stringify(this.todos)); }); } //定义更新状态属性的方法 addItem = (todo) => { this.todos.unshift(todo); }; } //向外暴露类的实例对象 export default new TodosStore();
-
-
读取状态数据组件使用
读取状态显示:
TodosStore.todos
-
更新状态数据组件使用
更新状态数据:
TodosStore.更新方法
⚠最好不要解构store中的函数,会改变this的指向
-
若组件需渲染store中的内容
- 引入observer高阶组件函数,,监听mobx的变化,一旦变化就重新渲染组件
- 使用observer包裹需要渲染store内容的组件
- 将组件变为一个观察者
export default observer(函数组件名)
import { observer } from "mobx-react"; import React from 'react' function List() { return ( <>组件</> ) } export default observer(List)
runInAction
Mobx不建议在action函数外直接更新状态数据
-
异步操作更新状态数据会收到警告
addItem = (todo) => { setTimeout(()=>{ this.todos.unshift(todo); },1000) };
-
解决:使用runInAction包裹异步函数中更新状态数据的操作
import {runInAction} from 'mobx' runInAction(() => {更新状态数据}) addItem = (todo) => { setTimeout(()=>{ runInAction(() => { this.todos.unshift(todo); }) },1000) };
高阶组件
高阶组件用于关联mobx
高阶组件不是组件,是函数,是特殊的高阶函数
成为高阶组件的条件
- 接收的参数是组件函数
- 返回值是一个新的组件函数
监听属性变化-了解
autorun
import {autorun} from 'mobx'
autorun(() => {
//依赖数据以及依赖数据的内部数据更新时执行
})
执行autorun
autorun只需调用执行一次(即绑定监听一次),后面会自动监听依赖数据更新
推荐做法
放在constructor中
当实例化一个对象时,会自动调用执行一次constructor里的代码
autorun的执行
- 初始化调用一次
- 数据更新时调用
⚠autorun是深度监听
依赖数据以及依赖数据的内部数据更新时都触发(todos或todos.id改变时)
reaction
初始不调用,依赖数据更新时调用
不是深度监视,只在依赖数据本身更新时触发(只有todos改变时)
import {reaction} from 'mobx'
reaction(
() => 指定数据,
(newValue, oldValue) => {
//指定数据本身更新时执行;
}
)
autorun vs. reaction
autorun | reaction | |
---|---|---|
同 | 都是用于监视状态数据变化的 | 一旦被监视数据改变了,自动调用回调函数 |
调用情况 | 1.初始化调用一次 2.依赖数据更新时调用 | 1.初始不调用 2.依赖数据更新时调用 |
监听类型 | 深度监视 1.依赖数据变化 2.依赖数据的内部数据变化时都触发 | 不是深度监视 只在依赖数据本身变化时触发 |
axios扩展
axios请求封装函数直接调用
-
创建api文件夹的文件,将axios请求都放进去,封装成函数导出
-
使用时直接调用函数
-
命名以req开头,便于识别其为axios请求函数
/*
包含n个接口请求函数的模块
每个接口返回一个promise对象
*/
import axios from 'axios'
export const reqSearchUsers = (keyword) => axios({
url: '/search/users',
params: {q: keyword}
})
TS版React项目环境
区别create-react-app与vite
create-react-app | vite | |
---|---|---|
开发环境运行启动速度 | 慢 需要对所有项目代码进行打包处理 | 快 直接运行服务器,运行时动态按需打包处理 |
修改配置文件后是否需要重新运行 | 需要重新运行 | 自动重新加载运行 |
内部实现使用语言 | js语言,运行相对慢 | go语言,运行速度快 |
使用vite创建react项目
- vite官网: https://cn.vitejs.dev/
- 创建react项目
# 创建项目
npm create vite@latest
# 快捷创建名为vite-react的ts版的react项目
npm create vite@latest vite-react -- --template react-ts
- 启动项目
# 下载依赖
cd vite-react
npm i
# 运行
npm run dev
配置模块路径别名
目标
- 引入模块时避免 …/的编码: 比如用@来对应src
- 在使用@开始写模块路径时, vscode能提示补全
配置别名
-
vite.config.ts
⚠为了ts能识别,需安装包npm i @types/node -D
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' // npm i @types/node -D // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], // 配置别名和省略扩展名 resolve: { alias: { "@": path.resolve(__dirname, 'src') }, extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], }, })
-
tsconfig.json
"compilerOptions": { ... ... "baseUrl": "./", "paths": { "@/*": ["src/*"] } },
改造为ts版本
- js => ts
- jsx => tsx(文件中有标签、jsx的语法的,必须为tsx
- 添加必要的类型约束
- 路由: 通过RouteObject来约束路由表中路由配置
- mobx:
- 为状态数据定义数据类型接口,并进行约束
- useState
- 为action函数的参数指定相应的类型
- 为状态数据定义数据类型接口,并进行约束
- 组件:
- 为接收的props参数定义类型约束 注意不能给解构的形参变量指定类型
- 不能单独约束props参数中的某一个键值对,需要对props整体进行约束
- function Item({todo}: {todo: Todo}){}
- 为事件回调的event对象定义类型约束 不能用原生事件的类型,得用react内置的事件类型
- import type {KeyboardEvent} from ‘react’
- 为接收的props参数定义类型约束 注意不能给解构的形参变量指定类型
- ajax请求
- 为响应数据定义对应的interface/type
- 为axios指定请求成功的响应数据结构:
- 响应拦截器成功回调返回的是response.data: axios.get<any, 响应体数据类型>()
- 响应拦截器成功回调不是返回response.data: axios.get<响应体数据类型>()
UI组件库
UI组件库:封装了一些通用的界面功能效果的组件, 简化开发编码
流行的React UI 组件库
- 国内-Ant Design:https://ant-design.antgroup.com/index-cn
- 国外-Material UIhttps://www.mdui.org/design/
antd下载
npm i antd
//目前最新版本为5,开发版本一般用4
//样式需要引用antd的样式合集,见使用
npm i antd@4
组件按需打包
问题
- antd默认已经实现了组件的按需要打包
- 但默认没有实现样式的按需打包
解决
利用 vite-plugin-style-import 实现 antd样式的按需打包
- 下载工具包
npm i -D vite-plugin-style-import #自动打包组件样式
npm i -D consola # 内部使用consola
npm i -D less # antd内部使用less编写的样式
- 配置: vite.config.ts
...
...
import { createStyleImportPlugin, AntdResolve } from 'vite-plugin-style-import'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
...
...
// 自动打包antd组件对应的样式配置1
createStyleImportPlugin({
resolves: [AntdResolve()]
})
],
// 自动打包antd组件对应的样式配置2
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript
javascriptEnabled: true,
},
}
},
...
...
})
- 不再引入antd的样式文件
// import 'antd/dist/antd.css'
自定义主题
问题
antd提供的默认主色是蓝色, 而我们项目需要的主色调可能是其它颜色, 比如绿色(#25b864)
解决
通过vite给antd配置新的主色
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript
javascriptEnabled: true,
// 重写 less 变量,自定义主题样式
modifyVars: {
'@primary-color': '#25b864',
},
},
}
},
⚠配置好后要删除node_modules下的.vite
否则,主色不会改变
因为.vite中缓存着前面打包的antd, 默认不进行重新打包