第一次学习(失败):挖坑系列
React简介
React是什么?
一个用于构建用户界面的JavaScript库。
强调将数据渲染为HTML视图
React由谁开发的?
Meta(Facebook)
原生JS的问题
- DOM-API操作UI效率低
- JS直接操作DOM会导致浏览器进行大量重绘重排操作
- 原生JS代码复用率低
React的特点
- 声明式编码,组件化模式
- 命令式编码: 你先坐电梯下楼左转,走到自动贩卖机前,买一杯水,按原路返回给我
- 声明式编程: 我渴了
- React Native支持移动端开发
- 虚拟DOM+Diffing算法,减少与DOM的交互,性能好
React入门
Hello React
需要引入依赖:
- babel.js
- react.js
- react-dom.js
- prop-types.js
hello_react.html
依赖的引入需要按照一定顺序:
1 2 3 4 5 6
| <script src="../React-js/react.development.js"></script>
<script src="../React-js/react-dom.development.js"></script>
<script src="../React-js/babel.min.js"></script>
|
script标签的src一定要是text/babel
1 2 3 4 5 6 7 8 9
| <script type="text/babel">
const VDOM = <h1>Hello React</h1> ReactDOM.render(VDOM,document.getElementById('test')) </script>
|
JSX
jsx与js的对比
jsx能够简化js的dom操作
但简化操作只针对编码者而言,实际由babel编译后的jsx依旧是按部就班的在操作dom
jsx
1 2 3 4 5 6 7 8
| <script type="text/babel"> let VDOM = ( <h1> <span>Hello World</span> </h1> ) ReactDOM.render(VDOM,document.getElementById('test')) </script>
|
js
1 2 3 4 5 6 7 8
| <script> const VDOM = React.createElement( 'h1', {id:'title'}, React.createElement('span',{},'Hello World') ) ReactDOM.render(VDOM,document.getElementById('test')) </script>
|
虚拟DOM与真实DOM对比
真实DOM
1 2 3 4 5
| <div id="test"> <h1> <span>Hello World</span> </h1> </div>
|
虚拟DOM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| { "type": "h1", "key": null, "ref": null, "props": { "children": { "type": "span", "key": null, "ref": null, "props": { "children": "Hello World" }, "_owner": null, "_store": {} } }, "_owner": null, "_store": {} }
|
JSX语法规则
- 标签中混入js表达式时使用 花括号{} 包裹
1 2
| let name = 'John' let VDOM = <h1>{name}</h1>
|
- 类名指定使用className
之所以避开class,是由于class与ES6类定义的关键字有冲突
1
| let VDOM = <h1 className='title'>Hello</h1>
|
- 内联样式需要注意以下要点:
- style = {{样式内容需要使用双括号包裹}}
- 样式的赋值采用键值对格式
- 样式值采用字符串格式
- 样式名采用驼峰标识
1 2 3 4 5 6 7 8 9
| let VDOM = ( <h1 style={{ color:'#fff', border:"1px solid #000", fontSize:"14px" }}> Hello World </h1> )
|
jsx内的遍历案例
1 2 3 4 5 6 7 8 9
| const data = ['AAA', 'BBB', 'CCC'] const VDOM = ( <div> <ul> {data.map(item => (<li key={item}>{item}</li>))} </ul> </div> ) ReactDOM.render(VDOM, document.getElementById('test'))
|
React组件
组件定义
React提供两种定义组件的方式:
组件使用函数形式定义,函数名首字母一定要大写
1 2 3 4
| function MyComponent(){ return <h1>My Component</h1> } ReactDOM.render(<MyComponent/>,document.getElementById('root'))
|
定义组件的函数内部,this指向哪里?
答: this指向undefined。
这是因为代码经过babel翻译之后,会进行严格模式规范:
严格模式禁止自定义函数的this指向window。
组件类一定要继承React.Component类
1 2 3 4 5 6 7 8
| class MyComponent extends React.Component{ render(){ return ( <h1>Hello MyComponent</h1> ) } } ReactDOM.render(<MyComponent/>, document.getElementById('root'))
|
类定义流程
- 找到MyComponent组件类
- 创建新实例
- 通过创建出来的实例调用原型的render方法
- 将render返回的虚拟DOM转为真实DOM呈现在页面上
类组件中的this指向哪里?
答: 组件类实例
组件实例三大属性
state
state中用于存放组件中的一些状态信息
1 2 3 4 5 6 7 8 9
| class MyComponent extends React.Component { constructor(props) { super(props) this.state = { country: "UK", color: '#00ffff' } } }
|
在render中使用state中的数据,
注意: 每次更新state数据都会重新调用一次render
1 2 3 4 5 6
| render(){return( <h1 style={{color:this.state.color}}> {this.state.country === 'UK'?'Hello':'こにちは'} </h1> ) }
|
组件中事件函数的声明方式
dom事件对应的回调函数并非由组件实例调用,
因此会导致回调函数中的this实际上指向的是undefined
因此需要先一步指定事件触发的回调方法的上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class MyComponent extends React.Component{ constructor(props){ super(props) this.state = { country:"UK", color:'#00ffff' } this.sayHello = this.sayHello.bind(this) } render(){ return( <h1 style={{color:this.state.color}} onClick={this.sayHello}> {this.state.country === 'UK'?'Hello':'こにちは'} </h1> ) } sayHello(){ if(this.state.country == 'UK'){ this.state.country = 'Japan' }else{ this.state.country = 'UK' } this.setState({ country:this.state.country }) } }
|
state语法糖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class MyComponent extends React.Component{ state = { country:'UK', color:'#00ffff' } sayHello = ()=>{ if(this.state.country == 'UK'){ this.state.country = 'Japan' }else{ this.state.country = 'UK' } this.setState({ country:this.state.country }) } render(){ return( <h1 style={{color:this.state.color}} onClick={this.sayHello}> {this.state.country === 'UK'?'Hello':'こにちは '} </h1> ) } } ReactDOM.render(<MyComponent/>,document.getElementById('root'))
|
props
props用于存放需由外界传入组件的动态数据
基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const sayHello = ()=>console.log('sayHello') class Person extends React.Component{ render(){ return ( <ul> <li onClick={this.props.sayHello}>Name:{this.props.name}</li> <li>Age:{this.props.age+1}</li> <li>Sex:{this.props.sex}</li> </ul> ) } } React.render( <Person name={"aaa"} age={19} sayHello={sayHello}/>, document.getElementById('root') )
|
传入组件的参数还可以通过解构赋值实现更简洁的写法
1 2 3 4 5 6 7 8 9
| let p = { name:'bbb', age:20, sex:'male' } React.render( <Person {...p}/>, document.getElementById('root') )
|
babel在对JSX做转换的时候会对下面这种语法做特殊处理:
实际上就相当于将obj的键值对作为Component组件的属性值
props限制
需要引入prop-types.js类型检验库
1 2 3 4 5 6
| Person.propTypes = { name:PropTypes.string.isRequired, age:PropTypes.number, sex:PropTypes.string, sayHello:PropTypes.func }
|
类型检验需要放在组件类的propTypes属性中:
1 2 3
| Person.propTypes = { }
|
类型配置之前需要加上PropTypes类的引用:
1 2 3
| { age:PropTypes.number }
|
多个限制使用链式声明:
1 2 3
| { name:PropTypes.string.isRequired }
|
数据类型标志与关键字的冲突避免:
- 为防止关键字与数据类型的冲突,将类型首字母小写处理:
- PropTypes.number
- PropTypes.string
- PropTypes.bool
- PropTypes.object
- PropTypes.array
- function由于是关键字,因此简写处理:
1 2 3
| { sayHello:PropTypes.func }
|
定义属性默认值
使用组件类的defaultProps属性定义默认值:
1 2 3
| Person.defaultProps = { sex: 'female' }
|
props语法糖
注意:props一旦传入类组件后即为只读属性,不可修改
props的语法糖实际上就是使用static静态属性给类本身定义属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Character extends React.Component { static propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number, sex: PropTypes.string, makeQitatapu: PropTypes.func } static defaultProps = { sex: 'male' }
render() { return ( <ul> <li onClick={this.props.makeQitatapu}>Name:{this.props.name}</li> <li>Age:{this.props.age + 1}</li> <li>Sex:{this.props.sex}</li> </ul> ) } } function makeQitatapu(){ } ReactDOM.render(<Person name="aaa" age={19} sayHello={sayHello} />, document.getElementById('root'))
|
函数式组件的props
组件函数只能用props,并且propTypes和defaultProps只能定义在函数外
(这是因为函数内的this指向undefined)
函数组件无法使用state和refs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function MyComponent(props){ return ( <div> <h1>Hello {props.name}</h1> <h2>{props.age}</h2> </div> ) } MyComponent.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number } MyComponent.defaultProps = { age: 20 } ReactDOM.render(<MyComponent name={'Jerry'} />,document.getElementById('root'))
|
refs
ref用于标识dom容器,
一般有三种定义形式:
- 字符串形式
- 内联回调函数形式
- 官方更为推荐的方法
- 但是重绘时会导致每次render调用两次
- 第一次:传入参数null
- 第二次:传入当前dom元素
- 绑定回调函数形式
- createRef
字符串形式的ref
1 2 3 4 5 6 7 8 9 10 11 12 13
| class MyComponent extends React.Component{ alertInput = ()=>{ this.alert(this.refs.input.value) } render(){ return( <div> {/* 定义字符串格式的ref */} <input ref='input' onBlur={this.alertInput}/> </div> ) } }
|
回调格式的ref
有内联和绑定两种定义格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class MyComponent extends React.Component{ bindRef = (currentNode)=>{ this.input2Ref = currentNode } render(){ return ( <div> {/% 内联样式 %/} <input ref={currentNode=>this.input1Ref = currentNode}/> {/% 绑定样式 %/} <input ref={this.bindRef}/> </div> ) } }
|
createRef
React.createRef调用后返回一个容器,存储被ref标识的节点,
每个容器中只能存储一个dom元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class MyComponent extends React.Component{ state = { value:'' } myRef = React.creaeRef() myRef2 = React.createRef() setValue = ()=>{ this.setState({ value:this.myRef.current.value }) } render(){ return( <div> <input ref={myRef} onBlur={this.setValue} /> <input ref={myRef2} /> <h1>{this.state.value}</h1> </div> ) } }
|
受控与非受控组件
非受控组件
组件中的数据现用现取,一般需要依赖ref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| myRef = React.createRef() state = {value:''}
inputInfo = ()=>{ this.setState({ value:this.myRef.current.value }) } render(){ return( <form> <input ref={this.myRef} type="text"/> <button onClick={this.inputInfo}>Click me</button> </form> ) }
|
受控组件
state中与组件相关联的数据实时更新,类似Vue的双向绑定组件,
受控组件可以减少ref的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| state = {value:''}
inputInfo = (e)=>{ this.setState({ value:e.target.value }) } render(){ return( <form> <input ref={this.myRef} onChange={this.inputInfo} type="text"/> </form> ) }
|
虚拟DOM的Diffing算法
React中使用key属性对虚拟DOM进行唯一标识,
每次调用render重新渲染前,会将用最新数据生成的虚拟dom和旧的虚拟dom进行比较,
比较是将key值相同的两个dom元素拿来对比其中的内容,
如果内容改变,就用新的虚拟dom代替旧dom,
因此,dom元素的key值必须唯一并且尽量可以保持不变
使用变化key值和不变key值更新dom时的对比:
将key值全部更换
1 2 3 4 5 6 7
| <ul ref='list'> { inputList.map((item, index)=>{ return <li key={index}>{item}=<input type="text"/></li> }) } </ul>
|
使用唯一标识作为key值
1 2 3 4 5 6 7
| <ul ref='list'> { inputList.map((item, index)=>{ return <li key={item}>{item}=<input type="text"/></li> }) } </ul>
|
React生命周期
旧生命周期
父组件更新子组件视角
componentWillReceiveProps
两种引发组件更新的操作
两种操作会引发组件更新:
- setState 更新state
- forceUpdate 强制更新
新生命周期
React17弃用的钩子函数
- ComponentWillMount
- ComponentWillUpdate
React17对弃用的旧钩子函数做了兼容,需要在函数名前加上 UNSAFE_ 前缀
- UNSAFE_componentWillMount
- UNSAFE_componentWillUpdate
新添加的钩子函数
三种会触发getDerivedStateFromProps的操作:
- New props 传入新参数
- setState 修改状态
- forceUpdate 强制更新
getDerivedStateFromProps主要用于处理组件的state依赖props的情况
getDerivedStateFromProps1 2 3
| static getDerivedStateFromProps(nextProps,prevState){ return nextState }
|
将更新前页面的状态传递到更新后的生命周期函数里
getDerivedStateFromProps1 2 3 4 5 6 7
| getSnapshotBeforeUpdate(){ return snapshot } ComponentDidUpdate(prevProps,prevState,snapshot){ }
|
React脚手架
create-react-app
全局安装react脚手架依赖包
1
| npm i -g create-react-app
|
使用脚手架创建新项目
默认提供四个运行脚本:
- start 运行项目,默认端口3000
- build 项目打包,使用webpack项目管理工具
- test 前端测试
- eject 将隐藏的webpack配置文件暴露出来,此过程不可逆
public文件夹
public文件夹存放一些静态文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>
|
网页做app套壳时的配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" }
|
网页爬取内容的相关文件
1 2 3
| # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow:
|
src文件夹
src为开发使用到的的主要目录,
初始化的src目录结构如下:
- App.css 根组件样式文件
- App.js 根组件js文件
- App.test.js 根组件测试文件
- index.css 全局样式
- index.js 项目入口js文件
- reportWebVitals.js 用于记录页面性能,依赖web-vitals库
- setupTests.js 测试文件,依赖jest-dom库
index.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' import App from './App' import reportWebVitals from './reportWebVitals'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render( <React.StrictMode> <App /> </React.StrictMode> )
reportWebVitals();
|
React Ajax
跨域
本机3000端口服务需要请求本机5000端口数据,
可以直接请求吗?
1
| axios.get('http://localhost:5000/simData')
|
答: 不能,因为跨域,
到底是在哪一步跨域失败呢?
跨域的解决
解决方法实际上就是在3000(Client) 和 5000(Server) 之间,
再配置一台代理服务器(同样在3000端口),
3000端口同时提供微型代理功能 和 客户端功能。
解决方式1:package.json配置
适合的业务场景:代理目标唯一
配置位置:package.json1
| "proxy":"http://localhost:5000"
|
这样项目中所有发送到3000端口的请求都会被转发到5000端口
解决方式2:http-proxy-middleware
适合的业务场景:需要代理多个目标地址时
配置位置:src/setupProxy.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| let {createProxyMiddleware} = require('http-proxy-middleware') module.exports = function(app){ app.use( createProxyMiddleware('/api1',{ target: 'http://localhost:5000', changeOrigin: false, pathRewrite:{ "^/api1" : "" } }), createProxyMiddleware( '/api2',{ target: 'http://localhost:5001', changeOrigin: false, pathRewrite:{ "^/api2" : "" } } ) ) }
|
changeOrigin
changeOrigin用于决定是否在服务器端暴露请求源
changeOrigin = true 不暴露请求源
此时3000请求5000,服务器端收到响应头中Host如下:
1
| HOST = localhost:5000(服务器端地址)
|
changeOrigin = false 暴露请求源
此时3000请求5000,服务器端收到响应头中Host如下:
1
| HOST = localhost:3000(客户端地址)
|
pathRewrite
表示是否对url的部分路径进行重写,
被替换的部分往往是用于进行代理转发识别的路径段,
比如配置如下:
1 2 3 4 5 6
| createProxyMiddleware('/api1', { target: 'http://localhost:5000', changeOrigin: false, pathRewrite: { '^/api1':'' })
|
则当请求的url为 http://localhost:3000/api1/simData 时,
url会经历如下处理:
第二次学习(失败):快速系列
1
| npx create-react-app app-name
|
- 数据注入
- 列表渲染
- 事件
- 状态处理 useState
- 参数传递
- 父传子:props
- 子传父:函数参数裹挟值传递
- 同级传递:父组件中转
- 多级传递:Context Hook
参数传递场景练习
一个用户评论列表,有三层结构:
List 列表
ListItem 列表项
Button 点赞按钮
context状态项:
theme 控制样式风格
filterOption 筛选项
- userName 用户名
- isSort 是否按照点赞数排序
第三次学习(完结):面向Vue基础系列
b站教程
jsx基础
1 2 3
| function FunHelloComp(){ return <div>Func Comp</div> }
|
1 2 3 4 5 6 7 8
| class ClassHelloComp extends React.Component{ constructor(prop) { super(prop); } render(){ return <div>Class Comp</div> } }
|
react中创建组件的2种方法:
- jsx:需要由babel将jsx翻译为js
- React.createElement 实际实现jsx的接口
事件传递
react事件绑定的函数被触发时,
如果不做处理,this的指向是undefined
有如下方式使得this指向当前组件:
- 箭头函数定义事件方法
- 事件绑定时用bind函数处理事件方法
- 行内定义事件方法
传参方式:
- 高级函数
- 使用bind获取新函数
- 第一个参数为this
- 最后一个参数为事件对象event
响应式数据
react响应式原理
- vue:监听响应式变量的get和set方法
- react:手动调用setState进行更新
- 往setState内传入一个新对象,其中包含需要更新的数据
- 将传入的新对象与旧state进行合并
- 使用合并后的state对页面视图进行更新
响应式的优化
无论数据有没有发生变化,直接调用setState都会触发页面的重绘,
要想优化这一点可以使用PureComponent:
1
| class App extends React.PureComponent{}
|
使用PureComponent时,如果响应式数据没有被更新,就不会触发多余的页面重绘。
注意:
使用PureComponent是对数据更新的一种优化
会监听变量的内存地址的变化
因此仅仅是数组和对象的成员变化是监听不到的
必须要对原数组或者对象进行拷贝更新
组件间参数传递
props
react不需要在子组件内对prop声明,
只要传入的参数, 都可以直接拿来用
- prop类型验证:propTypes
- 类型验证库:proptypes
npm install proptypes –save
1 2 3 4 5 6 7 8 9
| ChildComp.propTypes = { mes: function(props){ if(typeof(props.mes) !== 'string'){ throw new Error("Mes must be a string"); } }, count: PropTypes.number }
|
1 2 3
| ChildComp.defaultProps = { mes:"Default Mes" }
|
插槽
- 默认插槽
- 具名插槽
- 使用HtmlElement元素作为props参数传递
- 作用域插槽哦
- 返回HtmlElement元素的props函数
- 由父组件创建,能获取到子组件值的插槽
样式
- 引用.css后缀的样式文件,作用域为全局。
- 引用.module.css后缀样式文件,相当于vue中的scoped,对全局作用域做了限制
1 2 3
| import childStyle from "child.module.css";
<div className={childStyle.title}></div>
|
classnames库
使用classnames库,能够快速用js变量控制class类名的toggle切换
npm install classnames –save
1 2 3 4 5 6 7 8 9 10 11 12
| import classnames from "classnames";
state={ hasClass2:false }
<div className={classnames({ class1:true, class2:this.state.hasClass2 })}> </div>
|
也可以通过classnames.bind,将样式文件使用这种方式进行控制
1 2 3 4 5 6 7 8
| import classnames from 'classnames/bind' import childStyle from "./child.module.css" const childBindClassNames = classnames.bind(childStyle)
<div className={childBindClassNames({ class1: true, class2: false })}></div>
|
生命周期
- React.StrictMode模式下,生命周期执行两次
- shouldComponentUpdate
- 表示是否应该更新
- PureComponent优化的原理
- 根据获取到的新值判断是否进行更新
- componentDidMount
对比vue和react的更新
- vue
- 在get和set函数中触发更新
- 在get函数中进行依赖收集
- 在数据更新后,只更新用到该数据的地方
- react
增删改查案例
ref && context
ref
父组件访问子组件参数
1 2
| const childRef = React.createRef(); childRef.current.addCount()
|
context
- 使用 React.createContext 创建context
- 使用ContextName.Provider进行value的传递
- 使用ContextName.Consumer接受参数
- 并用回调函数获取到value值
1 2 3 4 5 6 7 8 9 10 11 12 13
| export const Context1 = React.createContext(); export default class Parent extends React.PureComponent { render(){ return ( <> <Context1.Provider value={{key:'123',label:'外太空'}} > <Child ref={childRef}></Child> </Context1.Provider> </> ) } }
|
1 2 3 4 5 6
| export default class Parent extends React.PureComponent { render(){ return <GrandSon/> } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import {Context1} from "./Parent.js"; export default class GrandSon extends React.PureComponent { render(){ return ( <Context1.Consumer> {value=>{return ( <div> <p>{value.key}</p> <p>{value.label}</p> </div> )}} </Context1.Consumer> ) } }
|
函數组件
和类组件的区别
- 无生命周期
- 不用考虑this
- 通过hook完成操作
- 函数体相当于render函数
- props作为函数的第一个参数
Hook
React内置Hook
- useState
- useEffect
- useMemo
- useCallback
- useRef
- useContext
- 更方便获取Context.Provider中提供的值
高阶组件
和高阶函数(返回函数的函数)概念相似,
高阶组件相当于一个返回组件的函数,
主要用于:
- 在多个组件上装载相同的逻辑片段
- 对多个组件进行相同的生命周期处理
- 场景:修改父组件的state数据时,对子组件进行选择性更新(shouldComponentUpdate)
使用一套逻辑封装的鼠标定位组件:
关于性能优化
React的性能优化需要开发者手动去实现,
如果不做任何处理,
一个父组件的更新往往会触发它所有子组件的更新。
- React.memo 对组件尽心优化的高阶组件,避免不必要的组件更新
- useMemo 对静态类进行优化
- useCallback 对静态方法进行优化
React-router
npm依赖
- React-router 服务端渲染SSR
- React-router-dom 浏览器渲染
- React-router-native ReactNative混合开发
React-router-dom
首先要给需要路由的组件添加最外层标签包裹:
两种包裹方式:
1 2 3
| <HashRouter> <App/> </HashRouter>
|
路由跳转
- Routes/Route 声明路由跳转的页面
- NavLink 多用于导航栏,最近被点击的navlink会多出active类
- Link 纯路由跳转用标签
路由重定向
子路由
1 2 3 4
| <Route path="/wish" element={token?<Wish/>:<Navigate to="/home"></Navigate>}> <Route path="createWish" element={<CreateWish></CreateWish>}></Route> <Route path="destroyWish" element={<DestroyWish></DestroyWish>}></Route> </Route>
|
1 2 3 4 5 6 7 8
| export default function Wish(props){ return ( <div> Best Wish: <Outlet></Outlet> </div> ) }
|
异步加载路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const LazyKill = lazy(()=>{return import ("./pages/Kill");}) function App(){ let token = 12 return ( <div className="App"> <ul className="nav-ul"> <li className="nav-link"><NavLink to="/home">HOME</NavLink></li> <li className="nav-link"><NavLink to="/mine">MINE</NavLink></li> <li className="nav-link"><NavLink to="/kill">KILL</NavLink></li> <li className="nav-link"><NavLink to="/wish/createWish">Create</NavLink></li> <li className="nav-link"><NavLink to="/wish/destroyWish">Destroy</NavLink></li> <li className="nav-link"><NavLink to="/work/123">Work</NavLink></li> </ul> {/* v5:Switch v6:Routes*/} <Suspense fallback={<h1>加载中。。。</h1>}> <Routes> <Route path="/home" element={<Home></Home>}></Route> <Route path="/mine" Component={token?Mine:Home}></Route> <Route path="/kill" element={<LazyKill></LazyKill>}></Route> <Route path="/wish" element={token?<Wish/>:<Navigate to="/home"></Navigate>}> <Route path="createWish" element={<CreateWish></CreateWish>}></Route> <Route path="destroyWish" element={<DestroyWish></DestroyWish>}></Route> </Route> <Route path="/work/:id" Component={Work}></Route> </Routes> </Suspense> </div> ) }
|
路由参数
params参数
useParams 直接返回params参数
1 2 3
| <Routes> <Route path="/work/:id" Component={Work}></Route> </Routes>
|
1 2 3 4 5 6 7 8
| export default function Work(){ let params = useParams() return ( <div> Work No. {params.id} </div> ) }
|
query参数
useSearchParams 返回类似useState格式的数据
/mine?name=LilyAndAndy
1 2 3 4 5 6 7 8 9 10 11 12
| export default function Home(){ const [searchParams, setSearchParams] = useSearchParams() console.log(searchParams.get("name")) setSearchParams({ name:"NiceBoy" }) return ( <div className="Home app-page"> mine mine mine </div> ) }
|
location
路由跳转:useNavigate
返回路由跳转时的一些数据:
1
| const location = useLocation()
|
路由跳转时,可以跳过params和query方式,传递数据:
1 2 3 4
| {} <button onClick={()=>goto("/nextPage",{ state:{mes:"hello world"} })}>Go to kill</button>
|
权限控制
- Route.element/Component中做判断:
- 使用Navigate重定向
1
| <Route path="/wish" element={token?<Wish/>:<Navigate to="/home"></Navigate>}>
|
异步路由
1 2
| const LazyKill = lazy(()=>{return import("./pages/Kill")})
|
1 2 3 4 5
| <Suspense fallback={<h1>加载中。。。</h1>}> <Routes> <Route path="/kill" element={<LazyKill></LazyKill>}></Route> </Routes> </Suspense>
|
全局状态管理
Redux
需要手动实现store逻辑的修改,比较麻烦:
reducer仓库创建
- state:管理的状态
- action:操作相关的自定义参数
- 约定俗成type为操作类型,payload为目标值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import {legacy_createStore as createStore} from "redux";
function mesReducer(state={mes:"hello"}, action){ switch(action.type){ case "changeMes": state.mes = action.payload; return {...state} case "resetMes": state.mes = "hello"; return {...state} default: return state } }
let store = createStore(reducer) export default store
|
拆分成模块合并包装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import {combineReducers} from "redux"; function mesReducer(state={mes:"hello"}, action){}
function numReducer(state={version:1}, action){ switch(action.type){ case "addVersion": state.version ++; return {...state} case "resetVersion": state.version = 1; return {...state} default: return state } }
const reducer = combineReducers({ mesReducer, numReducer })
|
store.state的双向绑定
Redux中维护的状态并不会像state或props一样,
被监听是否改变并刷新页面。
- 思路一:store.subscribe(不推荐)
- 使用store.subscribe进行监听,每当有数据被修改时,重新渲染组件
- 思路二:使用react-redux
- react-redux提供connect函数,能够在state和props之间建立映射关系,从而实现响应式更新
react-redux使用步骤:
- 对根组件进行Provider包装
- 在调用状态的组件中使用connect进行关联
- connect是一个返回包装后组件的高级组件
- connect关联的状态在新组件中的props中包裹
- connect的2个参数:
- 参数1:对state进行映射返回
- 参数2:对dispatch进行包装返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import {connect} from "react-redux"; const connectApp = connect( (state=>{ console.log(state) return { mes:state.mesReducer.mes } }), (dispatch=>{ return { dispatch, customFun:()=>console.log("print something...") } }) )(App)
export default connectApp;
|
@reduxjs/toolkit
toolkit是redux的优化版,不同点在于:
- 状态管理模块创建方式不同:
- redux创建reducer函数,legacy_createStore对store进行包装
- toolkit使用createSlice创建切片slice
- 调用方式不同
- redux中dispath传递的actions.type类型要亲自定义可选操作和逻辑
- toolkit中根据切片的name和reducers中的成员名进行调用
创建状态切片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let mesSlice = createSlice({ name:"mes", initialState:{ mes:"hello", }, reducers:{ changeMes(state,action){ console.log(state,action) state.mes = action.payload }, resetMes(state,action){ state.mes = "hello" } } })
|
组合切片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| let mesSlice = createSlice({ name:"mes", initialState:{}, reducers:{} }) let numSlice = createSlice({ name:"num", initialState:{}, reducers:{} }) let store = configureStore({ reducer:{ mesReducer:mesSlice.reducer, numReducer:numSlice.reducer, } })
|
切片reducer的调用
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default function App(){ const state = useSelector((state=>{ return state.numReducer })) const dispatch = useDispatch() return ( <div> Version: {state.version} <br/> <button onClick={()=>dispatch({type:"num/addVersion"})}></button> </div> ) }
|
响应式修改状态的几种方式
- connect函数
- 通过将props和store.state进行映射关联
- Hook
- useState:组件的state和store.state进行映射关联
- useDispatch:获取到store的可操作方法
useDispatch不需要用action调用到目标方法,可以直接将方法从切片中暴露出来:
1 2 3
| export const {changeMes} = mesSlice.actions export const {addVersion,asyncAddVersion} = numSlice.actions
|
1 2 3 4
| import {addVersion} from "./store/toolkit_index"; const dispatch = useDispatch(); <button onClick={()=>dispatch(addVersion())}>新增版本号(引入版本)</button>
|
异步修改全局状态
redux的action中是不支持直接执行异步修改状态的操作的,
要想异步修改状态,必须对异步的三个状态(pending、failing、fulfilled)各自的操作进行定制(好麻烦)
其间需要使用到createAsyncThunk,创建异步执行对象:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
export let changeNumThunk = createAsyncThunk('numSlice/asyncAddVersion', async (params)=>{ return await new Promise((res,rej)=>{ console.log(params) setTimeout(()=>{ res(999) },1000) }) })
|
执行对象需要包裹在切片中的extraReducers中,
这是一个专门处理切片内部逻辑(不用于暴露到dispatch)的reducers定义模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| let numSlice = createSlice({ name:"num", initialState:{}, reducers:{}, extraReducers:(chunk)=>{ chunk.addCase(changeNumThunk.pending,()=>{ console.log("pending") }).addCase(changeNumThunk.fulfilled,(state, action)=>{ console.log("fulfilled") state.version = action.payload }) } })
export const {asyncAddVersion} = numSlice.actions
|
异步操作的使用:
1 2 3
| import {asyncAddVersion} from "./store/toolkit_index"; <button onClick={()=>dispatch(changeNumThunk(456))}>新增版本号(异步+引入版本)</button>
|
路由权限案例
案例描述
- 根据用户登录身份,服务端返回不同的路由
- 客户端根据返回的路由生成路由映射
- 当用户访问没有生成的路由时,重定向到默认页面
React生态
React生态常用库
- UI组件:Ant Design / Ant Design Mobile
- 应用框架:Umi
官方文档阅读
Hook
memo/useCallback/useMemo
memo
React子组件更新的触发方式有几种:
- 父传子的props改变
- 基础变量类型props(memo包装)
- 函数类型props(useCallback)
- 引用类型props(useMemo)
- 自身state改变
- 内部用到的context改变
优化意见
- 需要将props的变化范围控制在最小
- 如果不是绘图级别细粒度的组件更新,memo优化程度不大
- 数据视图的展示放在父组件,修改数据方法用useCallback包装传入子组件
useContext
搭配实现组件对context的订阅
- context支持基础类型、引用类型
- 防止Provider结构过于冗杂,可以将Provider封装成组件
- context可用于在传递对象和函数时进行优化重新渲染
useReducer
useReducer旨在对大量state相关操作进行逻辑集中书写,
reducer函数需要两个参数:
- state:被操作的state数据
- action:选择的操作
reducer函数的返回值就是state被修改的结果
比如需要对同一个列表list进行增删改查操作,
action一共有4种:增删改查
reducer中根据action的不同,集中实现四种操作对应的逻辑,
对外只暴露出4种action
使用reducer往往出于以下几点考虑:
useEffect
一般用于将组件和外部组件同步
- 常常将重复的useEffect用自定义Hook的方式单独拉出来封装
- 可以定义setup和cleanup操作
- 常用场景
- 封装和第三方组件之间的接口
- 挂载和清理Event事件
- 请求数据
useEffect可以指定依赖项:
- 依赖为[],只执行一次
- 不加依赖数组,每次useEffect内容都会重新执行
- 有依赖项,依赖项改变,useEffect内容会重新执行
forwardRef && useImperativeHandle
用forwardRef进行包装的子组件,可以将ref节点暴露给父组件,
相当于vue中的defineExpose:
这种暴露可以跨辈获取。
配合useImperativeHandle可以自定义子组件暴露的内容
useLayoutEffect
在浏览器重新绘制屏幕之前触发
使用场景:
网页内容的布局有时会根据显示内容而改变,
当希望用页面上一帧的信息计算下一帧的布局时,
就可以在useLayoutEffect中进行
useOptimistic
允许在异步操作时更新state,
增强用户体验,
使用场景:
类似于数据未加载到客户端时的loading作用
useSyncExternalStore
主要是为了在组件中,响应式展示第三方数据源。
需要2个参数:
- subscribe
- 参数为触发组件渲染的函数
- 返回值为clenaup函数
- getSnapshot
使用场景:
- 类似store的用法,作为全局状态管理器
- 监听浏览器的变量变化
组件
- Fragment 用于组合多个组件的空标签
- Profiler 测量组件渲染性能
- StrictMode 严格模式
- Suspense 在组件加载完成之前提供替代渲染内容
API
- createContext 创建上下文
- forwardRef 创建可以将内容暴露给父组件的组件
- memo 创建根据props/state/context进行缓存的组件(而非强制根据父组件的重新渲染而重新渲染)
- lazy 组件懒加载
- startTransition 不阻塞UI的情况下更新state
lazy
用于实现组件懒加载
可以配合Suspense实现组件灵敏加载
综合练习
Hook综合练习
一个聊天软件界面:
- 鼠标移入时,光标变为圆点
- 点击人,输入邮件内容,发送邮件
- 聊天内容模块,提供跳转到最早聊天记录/查看最新聊天记录按钮
- 父组件调用子组件方法 forwardRef useImperativeHandle
- 2个主要组件:
- 联系人列表
- prop1 联系人列表
- context1 当前发送信息的联系人
- 聊天界面
- 全局存储:
- 监听网络:
- useSyncExternalStore监听浏览器网络连接
组件纯粹性
React提倡组件要保持纯粹,
意思就是组件函数无论被调用创建多少次,渲染结果应当都是一样的,
就和纯函数一样,只要输入值不变,输出值永远相同,
这就意味着不应当引入全局变量等可能出现mutation(突变)的内容来污染组件
这也是为什么在开发环境、严格模式下,
React会将每个组件内容执行2次的原因,
就是为了检查组件的纯粹性
React18 + Redux + React Router
环境配置
react开发依赖安装
安装下面的依赖包:
1 2 3 4 5 6 7 8
| { "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^9.1.2", "react-router": "^6.26.2", "react-router-dom": "^6.26.2", "redux": "^5.0.1" }
|
配置路径别名
首先安装node库的ts声明配置(为了在ts文件中使用path)
在vite.config.ts中配置路径别名:
1 2 3 4 5 6 7 8 9 10
| export default defineConfig({ plugins: [react()], resolve:{ alias:{ '@': path.resolve(__dirname, './src') } } })
|
在tsconfig.json中配置,
使编译器中提示路径别名下的目录结构:
配置方法
1 2 3 4 5 6 7 8
| { "baseUrl": ".", "paths": { "@/*": [ "src/*" ] } }
|
antd
记住组件和图标是分开安装的就ok
1
| npm install antd @ant-design/icons --save
|
路由配置
方法1:组件式路由配置
在src目录下创建router文件,其中创建路由组件index.tsx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import {BrowserRouter, Route, Routes} from "react-router-dom"; import App from '@/App.tsx' import Home from "@/views/Home"; import About from "@/views/About";
const baseRouter = () => ( <BrowserRouter> <Routes> <Route path="/" element={<App/>}> <Route path="/home" element={<Home/>}></Route> <Route path="/about" element={<About/>}></Route> </Route> </Routes> </BrowserRouter> )
export default baseRouter
|
其中将根路径的组件设置为App,
其它的组件都是App下的子组件,
因此需要在App组件内添加Outlet组件作为子组件容器:
1 2 3 4 5 6 7 8
| function App(){ return ( <div> 下面是子组件: <Outlet/> </div> ) }
|
在入口组件main.tsx中把Router组件嵌入:
1 2 3 4 5 6
| import Router from '@/router' ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <Router/> </React.StrictMode>, )
|
方式2:路由表配置
这种方式类似于vue-router的路由配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import Home from "@/views/Home" import About from "@/views/About"; import {Navigate} from "react-router";
const routes = [ { path:"/", element:<Navigate to="/home"></Navigate> }, { path:'/home', element:<Home/>, }, { path:'/about', element:<About/> } ]
export default routes;
|
入口文件中的App照常引入,并用Router包裹:
1 2 3 4 5 6 7 8 9 10 11 12
| import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' import {BrowserRouter} from "react-router-dom";
ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <BrowserRouter> <App/> </BrowserRouter> </React.StrictMode>, )
|
在App.tsx中使用useRouter引入router:
1 2 3 4 5 6 7 8 9 10 11
| function App() { const outlet = useRoutes(router) return ( <div> {outlet} </div> ) }
export default App
|
路由懒加载
使用路由懒加载lazy+Suspense组,
可以对loading页面进行封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const withLoadingComponent = (comp: JSX.Element) => ( <React.Suspense fallback={<div>Loading...</div>}> {comp} </React.Suspense> ) const routes = [ { path:"/", element:<Navigate to="/home"></Navigate> }, { path:'/home', element:withLoadingComponent(<Home/>) }, { path:'/about', element:withLoadingComponent(<About/>) } ]
|
子路由
和vue-router中同样使用children属性:
1 2 3 4 5 6 7 8 9 10 11 12
| const routes = [ { path:'/home', element:withLoadingComponent(<Home/>), children:[ { path:'page1', element:withLoadingComponent(<Page1/>) }, ] }, ]
|
全局匹配路由
在路由配置表最后添加一个配置项:
1 2 3 4 5 6 7
| const routes = [ { path:'*', element:<Navigate to="/home/page1"/> } ]
|
Redux
具体内容参考这里
使用浏览器插件Redux DevTools可以协助进行开发
在src目录下创建store文件夹,其中存放创建redux仓库的代码。
store/index.ts
1 2 3 4 5 6 7
| import {legacy_createStore} from "redux"; import reducer from './reducer' const store = legacy_createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ) export default store
|
store/reducer.ts
1 2 3 4 5 6 7 8 9 10 11
| const defaultState = { num: 20 }
let reducer = (state = defaultState) => { const newState = JSON.parse(JSON.stringify(state)) return newState }
export default reducer
|
还需要在根组件main.tsx中,添加一个全局数据提供组件对App组件进行包裹:
1 2 3 4 5 6 7 8 9 10
| import {Provider} from 'react-redux' import store from '@/store'
ReactDOM.createRoot(document.getElementById('root')!).render( <Provider store={store}> <BrowserRouter> <App/> </BrowserRouter> </Provider>, )
|
在组件中使用仓库数据,需要用到useSelector
1 2 3 4 5 6 7 8 9 10 11 12
| import {useSelector} from "react-redux";
function Page1(){ const {num} = useSelector(state=>({ num:state!.num })) return ( <div>num = {num}</div> ) }
export default Page1;
|
使用useDispatch调用仓库里定义的方法,
在reducer回调添加action参数,用于判断选择操作的类型,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| let reducer = ( state = defaultState, action:{type:string} ) => { const newState = JSON.parse(JSON.stringify(state))
switch(action.type){ case 'addOne': newState.num++ break case 'addTen': newState.num+=10 break }
return newState }
|
在页面使用useDispatch调用操作:
1 2 3 4
| const dispatch = useDispatch() const addNum = (type) => { dispatch({type}) }
|
关于全局仓库ts数据类型的定义
在全局类型声明文件vite-env.d.ts中添加声明:
在src下创建types目录用于存放一些类型声明文件,
创建store.d.ts:
1 2 3 4 5 6 7 8 9
|
type RootState = ReturnType<typeof import('@/store').getState>
interface Window{ __REDUX_DEVTOOLS_EXTENSION__:function; }
|
引用store的数据时,将对应的数据类型定义写上:
1 2 3 4
| const {num} = useSelector((state:RootState)=>({ num:state.num }))
|
合并多个store
将多个store抽取到单独的文件夹下,
在store/index下将多个store集中返回
目录结构:
1 2 3 4 5 6 7 8
| - store - status_1 - index - reducer - status_2 - index -reducer - index.ts
|
使用combineReducers将2个reducer结合起来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import {combineReducers, legacy_createStore} from "redux"; import NumStatusReducer from '@/store/NumStatus/reducer.ts' import ArtStatusReducer from '@/store/ArtStatus/reducer.ts'
const reducers = combineReducers({ NumStatusReducer, ArtStatusReducer })
const store = legacy_createStore( reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() )
export default store
|
本文产自🐙足八桑🐙肚子里了剩无几的墨水,转载请注明出处