React基础

学习要求:

  • 深度学习原则
  • 站在React立场上去理解React的设计理念
  • 理解API设计的初衷
  • 阅读官方文档

概念

问题:React是什么?

构建用户界面的JavaScript库

问题:React的主观意愿是什么?

  1. React仅仅是负责View层渲染
  2. React是一个视图渲染的工具库,不做框架的事情,不做自定义指令,不做数据类型强处理

问题:为什么要这么设计React?

对比与vue option横向拆分(data,methods写法区域较为固定)组件的方式只适用中小型应用(简单页面数据展示),一旦数据庞大时组件间事件传递数据传递,不管是否用vuex都会是一个繁琐的操作

对比react只是关注视图,逻辑的写法偏向纵向,可以容易拆分,也可以结合各种设计思想进行模块之间的设计与编写,开发者思想清晰时可以对组件更加好的拆分,代码非常的干净,方法可以单独封装后进行组件内部使用,事件传递和数据传递也简单,也更加好的管理和维护,场景上更适合更加复杂的应用场景,后台系统,开发社区也较为成熟

使用

简单使用React

如何负责视图渲染?

  1. //1.添加根容器
  2. <div id="app"></div>
  3. //2.引入cdn脚本
  4. //注意:开发环境
  5. //注意:script标签新增属性crossorigin src="..."以保持请求时允许跨域
  6. <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js" ></script>
  7. <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" ></script>
  8. //3.创建React组件
  9. class MyButton extends React.Component {
  10. constructor(props) {
  11. super(props);
  12. //创建state 相当于 vue data
  13. this.stata = {
  14. openStatus: false
  15. }
  16. }
  17. //渲染视图必须放入render函数里
  18. render() {
  19. //并且要返回视图模板
  20. return `Hello React!`;
  21. }
  22. }

知识点:

  • React提供了ReactAPI专门处理视图的API集合
  • ReactDOM: 从render函数到虚拟DOM节点到真实DOM节点需要用的库

React.createElement方法可以手动创建一个React元素

  1. //React提供了React API专门处理视图的API集合
  2. //ReactDOM: 从render函数到虚拟DOM节点到真实DOM节点需要用的库
  3. /**
  4. * ReactDOM.render(){}
  5. * @param react元素
  6. * @param 挂载的容器节点
  7. */
  8. ReactDOM.render(
  9. //创建react元素变成虚拟节点然后再变成真实节点
  10. /**
  11. * React.createElement(){}
  12. * @param 标签名称
  13. * @param 新增属性
  14. * @param 标签文本内容/子节点标签
  15. */
  16. React.createElement('div', {
  17. 'data-tag': 'div'
  18. }, 'This is my first React experience'),
  19. document.getElementById('app')
  20. );
  21. /**
  22. * 页面element:
  23. * <div id="app">
  24. * <div data-tag="div">This is my first React experience</div>
  25. * </div>
  26. */
  1. //手动新增子节点的写法:
  2. //注意:
  3. //1.新增子节点必须带有key属性
  4. //2.class写法是className
  5. //3.子节点插入到[]
  6. var span = React.creatElement('span', {
  7. className: 'text',
  8. key: 1
  9. }, 'This is a span');
  10. ReactDOM.render(
  11. React.createElement('div', {
  12. 'data-tag': 'div'
  13. }, [span]),
  14. document.getElementById('app')
  15. );
  16. /**
  17. * 页面element:
  18. * <div id="app">
  19. * <div data-tag="div">
  20. * <span class="text">This is a span</span>
  21. * </div>
  22. * </div>
  23. */

React.createElement方法也可以接受React类组件作为参数去创建一个真实节点

  1. ReactDOM.render(
  2. React.createElement(MyButton),
  3. document.getElementById('app')
  4. );
  5. //页面element:<div id="app">Hello React!</div>

关于React组件:

  1. 继承React.Component
  2. render函数返回视图

搭建

如何创建React?

工程化创建一个React

  1. //创建一个脚手架
  2. //脚手架内部的工程化实现:babel/webpack
  3. //npx命令: npm5.2+ 版本新增命令
  4. npx create-react-app my-react-app
  5. //运行项目
  6. npm run dev

JSX

通常来说,浏览器无法处理JSX语法,可以通过包编译,也可以在vite的项目中运行.jsx文件

问题:JSX是什么?

  • 是一种标签语法,在JavaScript基础上的语法扩展,既有HTML XML的形态,又有JavaScript的逻辑
  • 不是字符串,不是HTML标签
  • 是描述UI呈现与交互直观的表现形式
  • 生成React元素
  1. //通过JSX创建react元素
  2. const rEl = <h1>This is my first JSX experience.</h1>

createElement与JSX对比:

  1. //createElement
  2. const rEl = React.createElement('h1', null, 'This is h1');
  3. //JSX
  4. const rEl = <h1>This is my first JSX experience.</h1>
  5. ReactDOM.render(rEl, document.getElementById('app'));

命名:

JSX遵循JS命名规范,一般使用camelCase小驼峰命名

  • class -> className
  • tabindex -> tabIndex

问题:为什么React不区分开视图和逻辑?

  1. 渲染和UI标记是有逻辑耦合的
  2. 即使是这样的耦合,也能实现关注点分离
  1. //渲染和UI标记是有逻辑耦合的
  2. render(){
  3. return (
  4. <button onClick={ statusChange() }></button>
  5. );
  6. }

问题:JSX插值表达式是什么?

一切有效的(符合JS编程逻辑的)表达式都写在{}里面,JSX有编译过程,被编译以后转化成React元素,实际上是一个普通的对象

  1. const rEl = <h1 className="title">This is a title part.</h1>
  2. /**
  3. * 打印react对象
  4. * console.log(rEl);
  5. * {
  6. * $$typeof: Symbol(react.element),
  7. * key: null,
  8. * props: {className: 'title', children: 'This is a title part.'},
  9. * ref: null,
  10. * type: "h1",
  11. * _owner: null,
  12. * _store: {validated: false},
  13. * _self: null,
  14. * _source: null
  15. * }
  16. */

渲染

ReactDOM

根节点内的所有的内容都是由ReactDOM进行管理的

ReactDOM.render函数

将react元素渲染到根节点中

  1. //接收函数组件/类组件
  2. function Title(){
  3. return (<h1>This is h1</h1>);
  4. }
  5. ReactDOM.render(
  6. <Title/>,
  7. document.getElementById('app')
  8. );

总结:如果是组件渲染,ReactDOM.render的第一个参数一定要是一个React元素

  1. 组件使用JSX语法
  2. 使用React.createElement将组件转为React元素

问题:React基本的更新逻辑有哪些?

  • React元素是不可变的对象

    • 不能添加属性
    • 不能修改属性
    • 不能删除属性
    • 不能枚举

重点:

渲染更新的规律,观察element中节点的更新状况,发现ReactDOM.render会深度对比新旧标签元素的状态,只会做必要的真实DOM更新,像虚拟节点对比算法

  • 渲染之前每个React元素组成一个虚拟DOM的对象结构然后去渲染
  • 更新之前形成新的虚拟DOM的对象结构,对比新旧的虚拟DOM节点,分析出不同的地方,形成一个DOM更新的补丁,打补丁到真实DOM去更新

组件与Props

问题:组件是什么?

在前端,组件是视图的片段,组件包含视图标记,事件,数据,逻辑,外部的配置

问题:props的作用是什么?

组件是封闭的,要接收外部数据勇敢通过props来实现,props接收传递给组件的数据

问题:数据是什么?

组件一般是内部管理数据集合(状态),外部传入配置集合(props)

  1. //类组件
  2. class Test extends React.Component{
  3. //接收外部传入的属性配置在props里保存
  4. constructor(props){
  5. super(props);
  6. //内部私有属性
  7. this.state = {...};
  8. }
  9. //逻辑
  10. render(){
  11. //视图标记
  12. return (...);
  13. }
  14. }
  15. //外部绑定属性传入配置xxx
  16. ReactDOM.render(
  17. <Title title="xxx"/>,
  18. document.getElementById('app')
  19. );
  1. //函数组件
  2. //利用hooks来做
  3. function Test(props){
  4. const [ title, setTitle ] = React.useState(props.title);
  5. //视图
  6. return (
  7. <div>
  8. <h1>{ title }</h1>
  9. <button onClick={ ()=>setTitle('This is title') }></button>
  10. </div>
  11. );
  12. }
  13. ReactDOM.render(
  14. <Title title="xxx"/>,
  15. document.getElementById('app')
  16. );

组件渲染的过程:

  1. React主动调用组件
  2. 将属性集合转换成对象
  3. 将对象作为props传入组件
  4. 替换JSX中的props或者state中的变量
  5. ReactDOM将最终React元素通过一系列的操作转化成真实DOM进行渲染

注意:

使用类组件时,如写了构造函数,应该将props传递给super(),否则无法在构造函数中获取props

组件调用规范:

  • 视图标记时HTML标签
  • 大驼峰写法作为一个React元素
  • 组件转换React元素

组合组件:

几个子组件放入到父组件里(返回的视图中组合)

问题:组件props可以传递什么类型的数据?

  1. <List
  2. //字符串
  3. name="rose"
  4. //数值
  5. age={19}
  6. //数组
  7. colors={['red', 'green', 'blue']}
  8. //返回结果的函数
  9. fn={() => consolo.log('this is a fn')}
  10. //React元素
  11. tag={<p>this is a p.</p>}
  12. />

属性props和数据状态state的区别:

  1. state叫数据池对象,组件内容的管理数据的容器,可写读
  2. props叫配置池对象,外部使用(调用)组件时ReactDOM.render第二参数传入的属性集合,组件内部只读

问题:为什么属性props对象不可写(会报错)?

组件内部是不应该有权限修改的组件外部的数据

关于props校验:

允许在创建组件的时候,就指定props的类型,格式等

  1. //安装包
  2. npm i -S props-types
  3. //引入
  4. import PropTypes from 'prop-types';
  5. //添加校验规则
  6. App.propTypes = {
  7. a: PropTypes.number,
  8. fn: PropTypes.func.isRequired,
  9. tag: PropTypes.element,
  10. filter: PropTypes.shape({
  11. area: PropTypes.string,
  12. price: PropTypes.number
  13. })
  14. }

约束规则:

常见类型:array,bool,function,number,object,string

  1. //常见类型
  2. optionFunc: PropTypes.func
  3. //必选
  4. requiredFunc: PorpTypes.func.isRequired
  5. //特定结构的对象
  6. optionalObjectWithShape: PropTypes.shape({
  7. color: PropTypes.string,
  8. fontSize: PropTypes.number
  9. })

关于props的默认值:

使用场景在分页组件中的每页显示条数

  1. //App.defaultProps = {
  2. pageSize: 10
  3. }
  4. return(<div>{props.pageSize}</div>)

state

stata是React的核心,是一个组件私有的状态数据池

使用:

  1. class List extends React.Component{
  2. contructor(){
  3. super();
  4. this.state = { count: 0 }
  5. }
  6. }

总结:

  1. 如果想使用组件的时候,传入数据props组件配置
  2. 如果是组件使用的数据,使用私有数据状态state

注意事项:

  1. 必须使用setState方法来更改state
  2. 多个setState是会合并调用
  3. propsstate更新数据要谨慎,避免直接依赖他们,他们俩很有可能是在异步程序中更新的
  4. setState操作合并的原理是浅合并

单向数据流(One-Way Data Flow)

从父组件到子组件由上而下的传递流动的数据状态,叫单向数据流

关于组件中的state

  • state是组件内部特有的数据封装
  • 其他组件时无法读写修改该组件的state
  • 组件可以通过其他组件调用的时候传入属性来传递state的值
  • props虽然是响应式的,但在组件内部是只读的,所以仍然无法修改其他组件的state
  • 安全影响范围:state只能传递给自己的子组件,说明state只能影响当前组件的UI的内部的UI
  • 组件可以没有状态,有没有状态,组件间都不受嵌套影响,有无状态是可以切换的

setState

setState()是异步更新数据的,可以多次调用,但只会触发一次渲染

关于setState()方法:

状态是可变的,它的作用在于

  1. 修改state状态
  2. 更新UI

关于setState方法的推荐调用语法:

此写法也是异步更新的

  1. //state:最新的state
  2. //props:最新的props
  3. setState((state, props) => {
  4. return {
  5. //要更改的状态
  6. count: state.count + 1;
  7. }
  8. });

关于setState方法的第二个参数:

在状态更新后(页面完成重新渲染)立即执行某个操作

  1. this.setState(
  2. (state, props) => {},
  3. () => { console.log('这个回调函数会在状态更新后立即执行') }
  4. );

事件

事件处理函数绑定与事件对象

React元素也是采用了类似于DOM0标准中的事件属性定义的方法

  1. //JSX写法:
  2. //onClick小驼峰
  3. <button onClick={ this.doSth }>点击</button>
  1. //直接创建React元素方法的写法:
  2. React.createElement(
  3. 'button',
  4. {
  5. onClick: { this.doSth }
  6. },
  7. 'click'
  8. );

React的事件对象:

  1. console.log(e);
  2. //SyntheticBaseEvent{...}
  3. //合成基础事件对象是React重新定义的
  4. //这个对象遵守W3C事件对象的规范,不存在任何的浏览器兼容性问题

问题:为什么React要将事件处理直接在React元素上绑定?

React一直认为事件处理跟视图是有程序上的直接关系的,事件处理和视图写在一起可以更加直观的表述视图与逻辑的关系,更加好维护

关于this指向:

  • 默认处理函数的thisundefined
  • ES6class模块默认是不对事件处理函数进行this的再绑定

解决办法:

  1. 可以在构造器中bind(this)
  2. 可以在视图中bind(this)
  3. 利用回调加箭头函数onClick= { ()=> this.doSth() }

回调加箭头函数改变指向的缺点:

每次render函数执行时会创建新的回调,给子组件的属性进行传递函数的时候,每次都要新创建一个回调,子组件每次都会接收一个新的函数,就会有可能触发子组件的render渲染

事件对象传参:

事件对象都是在最后一个参数

  1. //回调
  2. <button onClick={ (e)=> this.doSth(1, 2, 3) }>点击</button>
  3. //显示传入事件对象
  4. doSth(p1, p2, p3, e){...}
  1. //bind
  2. <button onClick={ this.doSth.bind(this, 1, 2, 3) }>点击</button>
  3. //隐式传入事件对象
  4. doSth(p1, p2, p3, e){...}

列表渲染

关于key值:

  • 列表中的每个子元素都必须一个唯一的key属性值
  • key是React查看元素是否改变的一个唯一标识
  • key必须在兄弟节点中唯一,确定的(兄弟结构是在同一列表中的兄弟元素)
  • 不建议使用index作为key值
  • 建立在列表顺序改变,元素增删的情况下
  • 列表项增删或顺序改变,index的对应项就会改变
  • key对应的项还是之前列表情况的对应元素的值
  • 导致状态混乱,查找元素性能会变差

解决做法:

  • 如果列表是静态不可操作的,可以选择index作为key,也不推荐
  • 有可能这个列表在以后维护的时候有可能变更为可操作的列表
  • 避免使用index
  • 可以用数据的ID
  • 使用动态生成一个静态ID 如通过包nanoid

注意:

  • key是不会作为属性传递给子组件的,必须显示传递key
  • 防止开发者在逻辑中对key值进行操作

状态组件

函数组件叫无状态组件,类组件又叫有状态组件

问题:状态是什么?

状态state即数据

  • 函数组件没有自己的状态,只负责数据展示(静态)
  • 类组件有自己的状态,负责更新UI,页面交互

受控组件

react有两种表单处理方式:

  • 受控组件
  • 非受控组件

关于htmlreact中状态的冲突:

  • html中的表单元素是可输入的,有自己的可变状态
  • react中可变状态一般保存在state

react希望所有的可变状态都由state去管理,所有存在冲突

问题:react如何解决以上冲突?

state与表单中的value绑定在一起,由state值来控制表单元素的值

问题:什么是受控组件?

控制表单输入行为取值的方式的组件,跟input表单相关的渲染数据必须保存在自己的state数据里

  1. <input type="text" value={this.state.txt} />

受控使用步骤:

  1. state中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
  2. 给表单元素绑定change事件,将表单元素的值设置为state的值(控制表单元素值的变化)

常见的表单元素:

  • 文本框:操作
  • 富文本框:操作
  • 下拉框:操作
  • 复选框:操作

表单操作:

通过修改页面的内容从而更改state数据

事件绑定e.target.value/e.target.checked

React基础 - 图1

  1. //事件绑定后 更改视图里输入框内容时,会看到调试栏Components里的state里面的txt 被改为输入的内容
  2. class ExerciseComponent extends React.Component {
  3. constructor() {
  4. super();
  5. this.state = {
  6. txt: 'Please write something in inputarea',
  7. content: 'Please write something in textarea',
  8. city: 'beijing',
  9. isCheck: true
  10. };
  11. }
  12. //文本框
  13. handleTxtChange = (e) => {
  14. this.setState({ txt: e.target.value });
  15. };
  16. //富文本框
  17. handleContentChange = (e) => {
  18. this.setState({ content: e.target.value });
  19. };
  20. //下拉菜单
  21. handleCityChange = (e) => {
  22. this.setState({ city: e.target.value });
  23. };
  24. //复选框
  25. handleIsCheckChange = (e) => {
  26. this.setState({ isCheck: e.target.checked });
  27. };
  28. render() {
  29. return (
  30. <div>
  31. <h1>This is form demo</h1>
  32. <hr />
  33. {/* 文本框 */}
  34. <h4>文本框:</h4>
  35. <input
  36. type="text"
  37. value={this.state.txt}
  38. onChange={this.handleTxtChange}
  39. />
  40. <hr />
  41. {/* 富文本框 */}
  42. <h4>富文本框:</h4>
  43. <textarea
  44. value={this.state.content}
  45. onChange={this.handleContentChange}
  46. ></textarea>
  47. <hr />
  48. {/* 下拉菜单 */}
  49. <h4>下拉菜单:</h4>
  50. <select value={this.state.city} onChange={this.handleCityChange}>
  51. <option value="shanghai">上海</option>
  52. <option value="beijing">北京</option>
  53. <option value="shenzhen">深圳</option>
  54. </select>
  55. <hr />
  56. {/* 复选框 */}
  57. <h4>复选框:</h4>
  58. 爱好:
  59. <input
  60. type="checkbox"
  61. checked={this.state.isCheck}
  62. onChange={this.handleIsCheckChasnge}
  63. />
  64. 篮球
  65. <hr />
  66. </div>
  67. );
  68. }
  69. }

以上受控组件多表单元素优化写法:

  • 使用一个事件处理程序同时处理多个表单元素
  1. //1.给表单元素添加name属性,名称与state相同
  2. {/* 文本框 */}
  3. <h4>文本框:</h4>
  4. <input
  5. name="txt"
  6. type="text"
  7. value={this.state.txt}
  8. onChange={this.handleTxtChange}
  9. />
  10. //2.根据表单元素类型获取对应值
  11. this.handleChange = (e) => {
  12. const target = e.target;
  13. const name = target.name;
  14. const value = target.type === 'checkbox'
  15. ? target.checked
  16. : target.value;
  17. //3.根据name设置对应state
  18. this.setState({
  19. [name]: value
  20. });
  21. };

受控组件和非受控组件的区别:

  • 受控组件(推荐使用):

    • 视图表单数据受控于state状态数据组件
    • 控制表单操作并且同步state
  • 非受控组件:视图表单数据是只读的

案例:用户信息提交表单

受控组件方式实现一个表单带有:

  • 用户
  • 密码
  • 文本区域
  • 选择菜单
  • 单选按钮/多选按钮

源码地址:

https://gitee.com/kevinleeeee/react-form-submit-demo

案例:评论列表

填写评论人名称和评论内容点击发布评论展示评论列表

案例展示图:

React基础 - 图2

功能:

  • 暂无评论(条件渲染)
  • 评论列表渲染(列表渲染)
  • 获取评论信息,评论人,内容(受控组件)
  • 发布评论,更新评论列表(setState())

写法:

  • 类组件写法
  • 受控组件方式写法

源码地址:

https://gitee.com/kevinleeeee/react-comment-form-list-demo

非受控组件

问题:什么是非受控组件?

不受控于state, 使用React中的ref从DOM节点中获取表单数据得到组件

问题:如何不通过state数据状态去保存表单标签里面的值?

通过ref可以保存 ,在标签里定义ref="xxxRef", 通过this.refs.xxx.value访问到保存的值

也可以创建引用挂载到视图上React.createRef()

默认值:

在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值。在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。在一个组件已经挂载之后去更新 defaultValue 属性的值,不会造成 DOM 上值的任何更新

form field默认值在组件挂载完毕后进行更新,不会导致DOM的任何更新

  • select标签通过defaultValue属性拿到默认值
  • radio单选框/checkbox复选框标签通过defaultCheck属性拿到默认值

使用步骤:

  1. //1.调用React.createRef()方法创建一个ref对象
  2. constructor(){
  3. super();
  4. this.txtRef = React.createRef();
  5. }
  6. //2.将创建好的ref对象添加到文本框中
  7. <input type="text" ref={this.txtRef} />
  8. //3.通过ref对象获取文本框的值
  9. console.log(this.txtRef.current.value);

文档:受控组件和非受控组件的使用

地址:https://zh-hans.reactjs.org/docs/uncontrolled-components.html

问题:如何选择?

非受控input表单很像传统HTML表单input,它会记录用户输入内容,用户可以通过ref属性来获取相应的值,只要你需要的时候可以直接从input里拉取想要的值,当点击按钮提交时可以拿到值

  1. class Form extends Component{
  2. handleSubmitClick = () => {
  3. const name = this._name.value;
  4. }
  5. render(){
  6. return (
  7. <div>
  8. <input ref={ input => this._name = input } />
  9. <button onClick={ this.handleSubmitClick }>Sign up</button>
  10. /div>
  11. );
  12. }
  13. }

组件通信

父传子:

提供要传递的state数据,在子组件绑定属性即可

子传父:

利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数

  1. 父组件提供一个回调函数(用于接收数据)
  2. 将该函数作为属性的值,传递给子组件
  3. 子组件通过props调用回调函数
  1. class Father extends React.Component{
  2. //1.定义父组件的回调函数方法
  3. getChildMsg = (msg) => {
  4. console.log('接收到子组件的数据', msg);
  5. }
  6. render(){
  7. return(
  8. //2.将整个回调函数传递给子组件
  9. <Child getChildMsg={this.getChildMsg} />
  10. );
  11. }
  12. }
  13. class Son extends React.Component{
  14. state = { childMsg: '子组件私有数据' }
  15. handleClick = () => {
  16. //通过`props`调用回调函数
  17. return this.props.getChildMsg(this.state.childMsg);
  18. }
  19. render(){
  20. return(
  21. //3.执行自己的方法
  22. <button onClick="this.handleClick">点击</button>
  23. );
  24. }
  25. }

兄弟传:

将共享状态数据提升到最近的公共父组件中,由公共父组件管理这个状态(状态提升思想)

  • 公共父组件负责:

    1. 提供共享状态
    2. 提供操作共享状态的方法
  • 要通信的子组件只需要通过props接收状态或操作状态的方法
  1. class Counter extends React.Componet{
  2. //提供共享状态
  3. state = { count: 0 }
  4. //提供修改状态的方法
  5. onIncrement = () => { ... }
  6. render(){
  7. return (
  8. <div>
  9. <Child1 count=(this.state.count) />
  10. <Child2 onIncrememnt={this.onIncrement} />
  11. </div>
  12. );
  13. }
  14. }
  15. const Child1 = props => { return <h1>计算器:{props.count}}</h1> };
  16. const Child2 = props => { return <button onClick = () => props.onIncrememnt()>+1</button> };

状态提升

父组组件数据关系与状态提升

无父子关系的两个组件同享一个数据并且同步数据变化

单向数据流

数据的流动都是从父到子通过props向下传递

关键点:props是只读属性,不能去操作,它对应的数据操作交给父组件完成,数据由父组件来管理

状态提升:本应该是子组件的数据的状态交给父组件来保存操作,然后通过props传递给子组件

问题:如何解决两个组件需要共享同一状态并且状态同步的情况?

  1. //类组件调用(实例化)的时候,组件内部的状态是独立且唯一的
  2. //组件一,
  3. class Info extentds React.Component { //业务逻辑1 }
  4. //组件二
  5. class UserNameInput extentds React.Component {
  6. //业务逻辑2
  7. //使用了组件1
  8. render(){
  9. return (
  10. <Info />
  11. );
  12. }
  13. }
  14. //父组件
  15. class App extends React.Component {
  16. //使用了两次组件2
  17. render(){
  18. return (
  19. //向各自的子组件传值
  20. //这样传值结果是:两个组件的state状态数据是不同步的,相互独立的
  21. <UserNameInput inputNum={ 1 }/>
  22. <UserNameInput inputNum={ 1 }/>
  23. );
  24. }
  25. }
  26. ReactDOM.render(
  27. <App />,
  28. document.getElementById('app')
  29. );

解决办法:用函数写法可以实现不同步

  1. //组件嵌套与调用,和类组件还是函数组件没有关系
  2. function Info(props){ ... }
  3. //类组件与函数组件相互是可以调用的

将子组件定义的状态提升到父组件去传值使用,实现两个组件同一状态同步

组合继承

包含组合

关于children属性:

表示组件标签的子节点,当组件标签有子节点时,props就会有props.children属性

  1. <App>我是子节点</App>
  1. //1.如果Container内部有内容, React会在props内部增加children属性
  2. //2.如果Container内部有非元素内容, children: 非元素内容
  3. //3.如果Container内部有单个元素内容, children:React元素对象
  4. //4.如果Container内部有多个元素内容, children: [...(React元素对象)]
  5. class Container extends React.Component {
  6. render(){
  7. console.log(this.props);
  8. }
  9. return (
  10. //包含组合
  11. <div className="container">
  12. { this.props.children }
  13. </div>
  14. );
  15. }
  16. class App extends React.Component {
  17. render(){
  18. return (
  19. //<Container>123</Container>
  20. <Container>
  21. <h1>Title</h1>
  22. </Container>
  23. );
  24. }
  25. }
  1. class Container extends React.Component {
  2. render(){
  3. console.log(this.props);
  4. }
  5. return (
  6. <div className="container">
  7. <div className="header">
  8. { this.props.header }
  9. </div>
  10. <div className="sidebar">
  11. { this.props.sidebar }
  12. </div>
  13. <div className="main">
  14. { this.props.main }
  15. </div>
  16. </div>
  17. );
  18. }
  19. class Header extends React.Conponent {...}
  20. class SideBar extends React.Conponent {...}
  21. class Main extends React.Conponent {...}
  22. class App extends React.Component {
  23. render(){
  24. return (
  25. <Container>
  26. header = { <Header/> }
  27. sidebar = { <SideBar/> }
  28. main = { <Main/> }
  29. </Container>
  30. );
  31. }
  32. }

问题:为什么JSX还可以通过props传递视图React元素?

JSX本质上都会转成React元素(Object对象),视图通过props传递的机制比较像vue的插槽,但是React没有插槽的概念的定义,React本身就允许props传递任何类型的数据到子组件

多层组合

  1. //给Header组件底下的Select组件组合属性和方法
  2. //仅仅传给Header组件就能实现多层嵌套组件传值
  3. render(){
  4. return (
  5. <div>
  6. <Header
  7. text={this.state.headerTitle}
  8. citySelector={
  9. //组合
  10. <Selector
  11. cityData={this.state.cityData}
  12. changeCity={this.changeCity.bind(this)}
  13. ></Selector>
  14. }
  15. ></Header>
  16. </div>
  17. );
  18. }

问题:组件如何做继承关系?

React目前还没有发现有需要组件继承的需求,因为通过children或者传递视图React元素的方式完全可以解决组件组合的问题,props可以传递任何类型的数据,所以组合的方式完全可以替代继承方案

问题:如何处理逻辑部分需要继承性或者公用性?

这个需要开发者自己去写逻辑抽离的模块,函数,类,单独进行模块导入使用

组件更新机制

组件通过setState()方法执行更新渲染

父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件)

React基础 - 图3

性能优化

问题:组件性能如何优化?

  • 减轻state,只存储跟组件渲染相关的数据(如:count/列表数据/loading等)
  • 避免不必要的重新渲染,如避免不必要的子组件渲染(解决:shouldComponentUpdate()钩子函数,通过它返回值决定是否true,false重新渲染)

注意:不用做渲染的数据不要放在state中,比如定时器id等,可以放在构造器定义的this

  1. //关于shouldComponentUpdate
  2. //触发时机:更新阶段的钩子函数,组件重新渲染render前执行
  3. class AComponent extends Component{
  4. //newxProps -> 最新的props
  5. //nextState -> 最新状态
  6. //this.state -> 当前状态(更新前)
  7. shouldComponentUpdate(nextProps, nextState){
  8. //根据条件,决定是否重新渲染
  9. if(xxx){
  10. return true;
  11. }else{
  12. return false;
  13. }
  14. //写法1:通过nextState判断
  15. //nextState.number !== this.state.number -> true
  16. return nextState.number !== this.state.number;
  17. }
  18. }
  19. //写法2:通过nextProps判断
  20. //给子组件传值
  21. <Child number={this.state.number}>
  22. //在子组件中的shouldComponentUpdate进行判断

CSS Module

CSS模块化:将css当成模块传递到组件内部用JS逻辑去调用样式

如何调用?

  1. //index.module.css -> vite
  2. //引入模块
  3. import styles from './index.module.css';
  4. //在index.module.css文件中定义样式
  5. .container{ ...; }
  6. //组件使用
  7. <div className={ style.container }>

代码分割

做生产的时候,需要做到代码分割

打包的时候会整体打包成一个bundle的一个JS文件,会存在一些代码或模块加载的时候不需要,增加bundle体积的问题,将代码或模块进行分割出来,形成单独的文件块chunk

问题:代码分割有什么好处?

  • 模块可以懒加载
  • 减少应用的体积
  • 减少加载时的体积

关于导入模块import:

import是一个ES6的模块化关键字,不是一个函数

它分为静态的导入(static import)import xxx from 'xxx'和动态的导入(dynamic import)import('xxx')

import是可以被调用的,但是它和普通的函数是不一样的,import不是一个对象,它是一个关键字import xxx/ import(xxx)类似typeof(xxx)/ typeof xxx

区别:

  • static import是模块的静态导入,特点是导入并加载时,导入的模块会立即被编译,然后不是按需编译的
  • dynamic import模块的动态导入,根据条件或事件触发按需的模块导入

问题:为什么不能滥用动态加载?

因为静态导入是有利于初始化依赖的,静态的程序分析或tree shaking动态导入是难以工作的

应用场景:

  • 动态:

    • 模块太大了,使用可能性很低的模块,模块不需要马上加载的
    • 模块的导入占用大量的系统内存
    • 模块需要异步获取
    • 导入模块时需要动态的构建路径(说明符)
    • 动态说明符:import('./' + a + b + '.js')'
    • 静态说明符:static import只支持静态说明符
    • 模块中的代码需要程序触发了某些条件才运行的

使用import的要求:

  • 如果使用create react app的方式创建工程是直接可以使用动态导入import()
  • 如果手动做webpack的配置时,查看webpack代码分割的指南
  • 如果使用babel解析import()时,安装依赖@babel/plugin-syntax-dynamic-import

懒加载

代码分割的两个方法有lazy方法Suspense是React内置的组件,挂载到React

问题:lazy是什么?

是React提供给开发者的懒(动态)加载组件的方法React.lazy(参数:函数必须接收一个支持Promise的动态导入组件),好处是减少打包体积,对初次渲染不适用的组件延迟加载,它依赖一个内置组件Suspense,给lazy加上loading提示器组件的一个容器组件

  1. //loading.jsx
  2. class Loading extends React.Component {
  3. render(){
  4. return <div>Loading...</div>
  5. }
  6. }
  7. export default Loading;
  1. //main.jsx
  2. class Main extends React.Component {
  3. render(){
  4. return <div>Main</div>
  5. }
  6. }
  7. export default Main;
  1. import Loading from './loading.jsx';
  2. //lazy接收一个动态导入组件的函数
  3. //该函数返回一个Promise
  4. //Promise会resolve一个默认导出的React组件如export default xxx;
  5. //Suspense目前只和lazy配合实现组件等待加载指示器的功能
  6. //服务端渲染不支持,改用loadable Components
  7. const MainComponent = React.lazy(() => import('./main.jsx'));
  8. class App extends React.Component {
  9. render(){
  10. return (
  11. <React.Supense fallback={ <Loading /> }>
  12. <div>
  13. <MainComponent />
  14. </div>
  15. </React.Supense>
  16. );
  17. }
  18. }

路由懒加载

  1. //入口文件
  2. //安装
  3. npm i react-router -S
  4. npm i react-router-dom -S
  5. //导入浏览器路由
  6. import { BrowserRouter } from 'react-router-dom';
  7. ReactDOM.render(
  8. <React.StrictMode>
  9. <BrowserRouter>
  10. <App />
  11. </BrowserRouter>
  12. </React.StrictMode>,
  13. document.getElementById('root')
  14. );
  1. //App.js
  2. import { Switch,Route } from 'react-router';
  3. render(){
  4. return (
  5. <Suspense fallback = { <Loading /> }>
  6. <div className="app">
  7. <Switch>
  8. <Route path="/page1" component={ lazy(() => import('./views/Page1')) } />
  9. <Route path="/page2" component={ lazy(() => import('./views/Page2')) } />
  10. <Route path="/page2" component={ lazy(() => import('./views/Page3')) } />
  11. </Switch>
  12. </div>
  13. </Suspense>
  14. );
  15. }

错误边界

React在16版本时新增的,防止某个组件的UI渲染错误导致整个应用崩溃,子组件发生JS错误,有备用的渲染UI,错误边界其实是一个组件,只能用class类组件来写

  1. class ErrorBoundary extends React.Component{
  2. state = {
  3. hasError: false
  4. }
  5. //是一个生命周期函数
  6. //参数:子组件抛出的错误
  7. //返回值:新的state
  8. //获取捕获错误状态,修改错误状态
  9. //作用:渲染备用的UI
  10. //渲染阶段调用,不允许出现操作DOM,异步操作等副作用
  11. static getDerivedStateFromError(error){
  12. //返回一个新的状态
  13. return { hasError: true }
  14. }
  15. //是组件原型上的方法
  16. //作用:边界组件错误捕获异常并进行后续处理
  17. //在组件抛出错误后调用
  18. //参数:
  19. // error:抛出的错误
  20. // info:组件引发错误相关的信息 组件栈
  21. //componentDidCatch(error, info){
  22. //副作用
  23. //}
  24. render(){
  25. if(this.state.hasError){
  26. return (
  27. //返回新的备用的UI方案
  28. <h1>This is something wrong.</h1>
  29. );
  30. }else{
  31. //显示ErrorBoundary组件里包含的state状态
  32. return this.props.children;
  33. }
  34. }
  35. }

有一些无法捕获的场景如:

  • 事件处理函数
  • 异步代码setTimeout,ajax
  • 服务端渲染
  • 错误边界组件内部有错误

错误边界组件捕获错误的时机有哪些:

  • 渲染时
  • 生命周期函数中
  • 组件树的构造函数中

如果多个嵌套错误边界组件,那么从最里层错误出发向上冒泡触发捕获

上下文

context时一个容器,里面可以装载很多数据,这些数据可以给程序的多个地方传递,它是一个程序在执行的时候可以访问的容器

问题:context有什么作用?

给整个组件树共享全局的数据

context最适合的场景是:

  • 杂乱无章的组件都需要同一些数据的时候
  • 不合适在单纯的为了不层层传递属性
  • 它会弱化及污染组件的纯度导致组件复用性降低
  1. //子组件
  2. class Select extends React.Component {
  3. //将上下文的类型指定为CityContext
  4. //this.context -> 访问到cityInfo
  5. //会向上找最近的CityContext的Provide,并取值为cityInfo
  6. static contextType = CityContext;
  7. render() {
  8. return (
  9. <select
  10. value={ this.context.name }
  11. ></select>
  12. );
  13. }
  14. }
  1. //管理标题主题的上下文
  2. //创建上下文 默认为黑色标题
  3. const ThemeContext = React.createContext('black');
  4. //ThemeContext带有Provider供应方和Consumer使用方
  5. //父组件使用上下文的提供Provide
  6. class App extends React.Component {
  7. state = {
  8. theme: 'black'
  9. };
  10. render() {
  11. return (
  12. //给子组件提供value
  13. <ThemeContext.Provider value={this.state.theme}>
  14. <Main></Main>
  15. </ThemeContext.Provider>
  16. );
  17. }
  18. }
  19. //子组件使用
  20. class Header extends React.Component {
  21. render() {
  22. return (
  23. <ThemeContext.Consumer>
  24. //拿到父组件传递的value
  25. {(theme) => {
  26. console.log(theme);
  27. //black
  28. }}
  29. </ThemeContext.Consumer>
  30. );
  31. }
  32. }

案例:移动端底部导航栏切换

技术:vite + context + react

实现:

  • 点击导航栏按钮切换显示页面
  • 点击按钮显示不同标题颜色背景(一键切换皮肤)
  • 点击按钮显示不同底部导航栏子项颜色(一键切换皮肤)

React基础 - 图4

  1. //项目目录:
  2. ├─index.html
  3. ├─package.json
  4. ├─src
  5. | ├─App.jsx - App类/父组件管理state里的theme/上下文包裹Main组件
  6. | ├─context.js - 管理标题主题的上下文
  7. | ├─index.jsx - 入口文件/渲染APP组件到页面
  8. | ├─Main.jsx - 页面组件/管理标题主体底部导航栏/私有导航栏数据
  9. | ├─components
  10. | | ├─Header - 标题组件
  11. | | | ├─index.jsx
  12. | | | index.scss
  13. | | ├─BottomNav - 底部导航栏组件
  14. | | | ├─index.jsx
  15. | | | ├─index.scss
  16. | | | Item.jsx - 子项/遍历

源码地址:https://gitee.com/kevinleeeee/react-context-bottomnav-demo

Context API

设置上下文组件名称方便于调试

问题:Context的作用是什么?

当你不想再组件树中通过逐层传递props或者state方式来传递数据时,可以使用Context来实现跨层级的组件数据传递

问题:如何使用Context?

需要用到两种组件:

  • 生产者Provider,通常时一个父节点
  • 消费者Consumer,通常是一个或多个子节点
  • 还需要声明静态属性ContextType提供给子组件的Context对象的属性
  1. //方式一:常规定义
  2. import PropTypes from 'prop-types';
  3. class App extends React.Component {
  4. //定义的是类型而不是值
  5. static childContextTypes = {
  6. color: PropTypes.string,
  7. num: PropTypes.number
  8. }
  9. //必须使用这个方法来创建一个对象
  10. getChildContext() {
  11. return {
  12. color: 'red',
  13. num: 1
  14. }
  15. }
  16. render() {
  17. return (
  18. <div>
  19. <h1>React Context</h1>
  20. </div>
  21. );
  22. }
  23. }
  24. class Main extends React.Component{
  25. //子组件必须声明类型才能拿到父组件传递过来的值
  26. static contextType = {
  27. color: PropTypes.string
  28. }
  29. render() {
  30. return (
  31. <div>
  32. <p>Title的页面---{ this.context.color}</p>
  33. </div>
  34. )
  35. }
  36. }
  1. //方式二: createContext API
  2. const AContext = React.createContext('默认值');
  3. AContext.displayName = 'MyAContext';
  4. //使用
  5. class App extends React.Component{
  6. render(){
  7. return (
  8. <AContext.Provider>
  9. <div>123</div>
  10. </AContext.Provider>
  11. );
  12. }
  13. }
  14. //在浏览器React dev tool里显示
  15. MyAContent.Provider
  16. //没有使用时默认显示
  17. Context.Provider

关于React.createContext

  • 创建一个指定的Context对象
  • 组件会找离自己最近的Provider获取其value(在state里定义的)
  1. class Test extends React.Component{
  2. render(){
  3. return (
  4. //注意:
  5. //1.没有写value属性会显示undefined
  6. //2.一般会写value为state里的数据
  7. //3.当value写undefined/null会显示undefined/null
  8. //4.当没有嵌套<AContext.Consumer>标签会找默认值
  9. <AContext.Consumer value={ this.state.a }>
  10. {
  11. (value) => (
  12. <div>{ value }</div>
  13. )
  14. }
  15. </AContext.Consumer>
  16. );
  17. }
  18. }

问题:什么时候默认值生效?

如果没有匹配到Provider就使用默认值( 在React.createContext('默认值')中定义的),其他情况均不使用默认参数

关于Context.Provider:

  • 它是通过React.createContext创建的上下文对象里的一个组件
  • Provider组件可以插入其他组件的目的是可以订阅这个Context
  • 通过Providervalue属性来将数据传递给其他Consumer组件

注意:当value发生变化时,插入Provider的组件会重新渲染

关于Context.Consumer:

  • 它使用的是Provider提供的value
  • 最大的作用是订阅context变更
  • Consumer内部使用函数作为子元素(专题:function as a child)
  • 有一种组件的内部是使用函数作为子元素
  • 特点是函数接收context最近的Provider提供的value
  • 如果没有写Provider会找默认值

关于contextType:

  • class类内部的一个静态属性(相当于ES3中给构造函数新增属性Selector.contextType)
  • 它必须指向一个由React.createContext执行后返回的Context对象
  • 给当前环境下的context重新指定引用
  • 指定后父组件上下文会有数据,不指定会显示空对象(context: {})
  • 在生命周期函数和render函数中都可以访问
  1. class Test extends React.Component{
  2. //在组件内部里内置声明一个conetextType
  3. //目的:可以获取一个上下文state里定义的数据
  4. static contextType = React.createContext('默认值');
  5. render(){
  6. //可以获取一个组件上下文state里定义的数据
  7. console.log(this.context);
  8. //{name: 'hangzhou', text:'杭州'}
  9. return ( ... );
  10. }
  11. }

问题:在组件数据共享下,Provide/ConsumercontextType上如何选择?

  • 推荐使用Provide/Consumer,因为更具有语义化
  • 在代码阅读上contextType较为难以理解

动态context嵌套

案例:context跨级共享应用

三个显示区域有按钮带有各自的样式,可以选择不同的样式同时更改按钮颜色,深入了解跨级应用

案例展示图:

React基础 - 图5

实现:

  • 改变按钮颜色

  • 登录与未登录显示

  1. //项目目录:
  2. ├─index.html
  3. ├─package.json
  4. ├─Readme.md
  5. ├─src
  6. | ├─App.jsx - 定义应用需要的属性和方法/嵌套的方式提供共享数据
  7. | ├─index.jsx
  8. | ├─views
  9. | | Home.jsx - 管理Home页面的布局
  10. | ├─context
  11. | | index.js - 实现全局文件数据共享/初始化默认值
  12. | ├─config
  13. | | index.js - 配置按钮样式
  14. | ├─components
  15. | | ├─Main - 使用Consumer使用提供的数据
  16. | | | index.jsx
  17. | | ├─Header - 使用Consumer使用提供的数据
  18. | | | index.jsx
  19. | | ├─Footer - 使用Consumer使用提供的数据
  20. | | | index.jsx
  21. | | ├─Button - 使用Consumer使用提供的数据
  22. | | | index.jsx

总结:

父组件统一管理状态,嵌套的方式把不同的数据传入共享给相应的子组件,子组件将这些需要的数据渲染即可

源码地址:https://gitee.com/kevinleeeee/react-context-button-demo

Fragment

它是在React下的一个组件,文档碎片不会占用真实的节点,原则上React每个组件都需要根节点

关于React.Fragment

这个组件创建了一个文档碎片

简写:

使用短语法<>...</>声明一个React.Fragment碎片

注意:短语法不支持key

  1. //现阶段,Fragment除了key属性,不支持其他任何属性
  2. <React.Fragment key={ id }>
  3. <dt>{id}:{name}</dt>
  4. <dd>{desc}</dd>
  5. </React.Fragment>

应用场景:

一般在表格上使用解决没有根节点的问题

  1. class App extends React.Component{
  2. state = {
  3. headers: [
  4. 'Name',
  5. 'ID',
  6. 'Age'
  7. ]
  8. }
  9. render(){
  10. return(
  11. <table border="1">
  12. //<caption> 标签定义表格的标题
  13. <caption>Private Infomation</caption>
  14. <thead>
  15. <tr>
  16. <TableHeaders headers={this.state.headers}/>
  17. </tr>
  18. </thead>
  19. <tbody>
  20. <tr></tr>
  21. </tbody>
  22. </table>
  23. );
  24. }
  25. }
  1. //使用fragment碎片避免每次新增th时都会套用div,达到不用div也可以包裹里面的元素内容
  2. //同时也会报错: th不能作为div的子元素
  3. class TableHeaders extends React.Component{
  4. render(){
  5. return (
  6. <React.Fragment>
  7. {
  8. this.props.headers.map((item, index) => (
  9. <th key={ index }>{ item }</th>
  10. ))
  11. }
  12. </React.Fragment>
  13. );
  14. }
  15. }

生命周期

问题:为什么需要知道组件的生命周期?

有助于理解组件的运行方式,完成更复杂的组件功能,分析组件错误等原因,组件在被创建到挂载到页面中运行,再到组件不用时卸载的过程

问题:什么是钩子函数?

生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数

问题:钩子函数有什么作用?

它为开发者在不同阶段操作组件提供了时机

注意:只有类组件才有生命周期

生命周期的三个阶段:

  1. 创建时(挂载阶段)

    1. 执行时机:组件创建时(页面加载时)
    2. 执行顺序:

      1. constructor()
      2. render()
      3. componentDidMount
  2. 更新时(更新阶段)

    1. 执行时机:

      1. setState()
      2. forceUpdate()
      3. 组件接收到新的props
    2. 以上三种任意一种变化,组件就会重新渲染
    3. 执行顺序:

      1. render()
      2. componentDidUpdate()
  3. 卸载时(卸载阶段)

    1. 执行时机:组件从页面消失

      1. componentWillUnmount

注意:不要在render中调用setState()方法

钩子函数 触发时机 作用
constructor 创建组件时,最先执行 1. 初始化state 2. 为事件处理程序绑定this
render 每次组件渲染都会触发 渲染UI
componentDidMount 组件挂载(完成DOM渲染)后 1. 发送网络请求 2.DOM操作

三种导致组件更新的方式:

  • 子组件接收新的props属性渲染render
  • 执行setState()方法渲染render
  • 执行forceUpdate()方法渲染render

注意:

componentDidUpdate生命周期里的if条件执行setState()方法,否则导致递归更新,栈溢出的报错

钩子函数 触发时机 作用
render 每次组件渲染都会触发 渲染UI(与挂载阶段是同一个render)
componentDidUpdate 组件更新后(完成DOM渲染) 1. 发送网络请求 2. DOM操作
  1. //在componentDidUpdate里执行setState()的正确写法:
  2. //做法:比较更新前后的props是否相同,来决定是否重新渲染组件
  3. componentDidUpdate(prevProps){
  4. if(prevProps.count !== this.props.count){
  5. this.setState();
  6. }
  7. }
钩子函数 触发时机 作用
componentWillUnmount 组件卸载(从页面消失) 执行清理工作(清除定时器等)

Portals

它提供一种将子节点渲染到存在于父节点以外DOM节点的方案

  1. ReactDOM.createPortal(React子元素, 真实DOM);

问题:reactvue插槽的区别?

类似

问题:react事件冒泡的方式是什么?

嵌套组件向上传递

应用场景:

需要子组件能够在视觉上跳出其容器

  • 对话框
  • 模态框
  • 悬浮卡
  • 提示框

问题:react portals的用法是什么?

使用:

在渲染返回的父级容器里放入定义的子组件,子组件就会按照事件冒泡的方式逐级向上传递

  1. //1.定义不相干的兄弟节点
  2. <div id="modal-root"></div>
  3. //2.获取该节点
  4. const modalRoot = document.getElementById('modal-root');
  5. //3.定义Modal组件
  6. class Modal extends Component{
  7. //创建新节点
  8. el = document.createElment('div');
  9. //挂载时插入到modalRoot节点
  10. componentDidMount(){
  11. modalRoot.appendChild(this.el);
  12. }
  13. //卸载时移除节点
  14. componentWillUnmount(){
  15. modalRoot.removeChild(this.el);
  16. }
  17. //渲染时返回新增的Portal
  18. render(){
  19. return ReactDOM.createPortal(
  20. this.props.children,
  21. this.el
  22. );
  23. }
  24. }
  25. //4.Modal组件包裹子组件Sub
  26. <Modal><Sub /></Modal>

异步加载

利用懒加载的方式实现异步加载

  1. import React, { lazy, Suspense } from 'react';
  2. const Sub = lazy(() => import('./Sub'));
  3. //Suspense方式包装一下,否则页面无法正常加载组件
  4. //fallback指定一个UI
  5. <Suspense fallback={<div>loading</div>}>
  6. <Sub />
  7. </Suspense>

路由

路由介绍

现代前端应用大多数都是SPA(单页应用程序),只有一个HTML页面应用程序,因为它的用户体验更好,对服务器压力更小

为了有效使用单个页面来管理多页面功能,前端路由应运而生

  • 路由功能:从一个视图页面导航到另一个视图页面
  • 映射规则:在React中,是URL路径与组件的对应关系

基本使用

  1. //1.安装
  2. npm i react-router-dom -S
  3. //2.引入路由三个核心组件
  4. //BrowserRouter or HashRouter
  5. import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
  6. //3.使用Router组件包裹整个应用
  7. <Router>
  8. <div className="app">...</div>
  9. </Router>
  10. //4.使用Link组件作为导航菜单(路由入口)
  11. <Link to="/first">页面1</Link>
  12. //5.使用Route组件配置路由规则和要展示的组件(路由出口)
  13. <Route path="/first" component={First}></Route>
  1. //旧版本写法1:
  2. <Router>
  3. <Switch>
  4. //必须如下顺序
  5. <Route component={LoginPage} path="/login"></Route>
  6. <Route component={IndexPage} path="/"></Route>
  7. </Swithch>
  8. </Router>
  1. //旧版本写法2(嵌套子路由组件):
  2. <Router>
  3. <h1>Hello React</h1>
  4. <hr />
  5. <Switch>
  6. <Route component={LoginPage} path="/login"></Route>
  7. <Route
  8. path="/"
  9. render={(props) => (
  10. <IndexPage>
  11. <Switch>
  12. <Route component={ListPage} path="/sub/list"></Route>
  13. <Route component={DetailPage} path="/sub/detail"></Route>
  14. </Switch>
  15. </IndexPage>
  16. )}
  17. ></Route>
  18. </Switch>
  19. </Router>
  20. //IndexPage组件需写入Link组件
  21. export default class IndexPage extends Component {
  22. render() {
  23. const { children } = this.props;
  24. return (
  25. <div>
  26. <ul>
  27. <li>
  28. <Link to="/sub/list">列表页</Link>
  29. </li>
  30. <li>
  31. <Link to="/sub/detail">详情页</Link>
  32. </li>
  33. </ul>
  34. {children}
  35. </div>
  36. );
  37. }
  38. }

常用组件说明:

  • Router组件:包裹整个应用,一个React应用只需要使用一次
  • 两个常用RouterHashRouterBrowserRouter
  • HashRouter:使用URL的哈希值实现(http://localhost:3000/#/first)
  • BrowserRouter(推荐):使用H5的history API实现
  • Link组件:用于指定导航链接(<a></a>标签)
  • Route组件:展示路由展示组件相关信息
  • Switch组件:旧版本,对路由进行分支

路由执行过程:

  1. 点击Link组件(<a></a>标签),修改浏览器地址栏中的url
  2. React路由监听到地址栏url变化
  3. React路由内部遍历所有的Route组件,使用路由规则pathpathname进行匹配
  4. 当路由规则path能够匹配地址栏中的pathname时,就展示该Route组件的内容

编程式导航

通过JavaScript代码实现路由跳转

  • historyReact路由提供的,用于获取浏览器历史记录的相关信息
  • push(path):跳转到某个页面,参数path表示要跳转的路径
  • go(n):前进或后退到某个页面,参数n表示前进或后退页面数量
  1. class Login extends Component{
  2. handleLogin = () => {
  3. this.props.history.push('/home');
  4. }
  5. }

默认路由

表示进入页面时就会匹配的路由

  • 默认路由path/
  1. <Route path="/" component={Home}></Route>

匹配模式

默认情况下,React路由是模糊匹配模式

  • 模糊匹配规则:只要pathnamepath开头就会匹配成功

精确匹配

避免默认路由展示,给Route组件添加exact属性,就变为精确匹配模式

  1. <Route exact path="/" component={Home}></Route>

路由重定向

  1. {/* 默认路由配置 跳转到 /home 实现重定向到首页 / */}
  2. <Route path="/" exact render={() => <Redirect to="/home" />}></Route>

嵌套路由

问题:什么是嵌套路由?

路由内部又包含路由

  1. //使用步骤:
  2. //1.创建子路由组件
  3. //2.在父组件Home中添加一个Route作为子路由(嵌套的路由)的出口
  4. //3.设置嵌套路由的path,格式以父路由path开头(父组件展示,子组件才会展示)
  5. const Home = () => (
  6. <div>
  7. <Route path="/home/news" component={News} />
  8. </div>
  9. );

BrowserRouter实现原理:

  1. //变更当前url
  2. window.location = 'foo';
  3. //跳转到localhost:3000/foo 地址
  4. //注意:window.location会刷新页面

问题:如何跳转地址避免页面重载?

利用HTML5history对象可以取代无刷新,

关于history.pushState(state, title, url)

  • statejavascript对象,描述当前url状态
  • title:暂无使用
  • url:指定跳转的地址
  1. history.pushState({name: 'newPath'}, null, '/foo');
  2. //跳转到localhost:3000/foo 地址 且没有刷新页面

案例:todolist

使用react-router v6实现一个路由页面的todolist

实现:

  • 基本路由跳转页面
  • 路由嵌套共享UI页面(Outlet)
  • 无路由匹配
  • url传参
  • 索引路由

    • 父路由的默认路由
    • 子路由的路径都不匹配时
  • 搜索参数
  • 输入框输入内容
  • 自定义NavLink
  • 自定义导航

案例展示图:

React基础 - 图6

问题:如何获取url参数?

通过react-router里面的useParams()方法获取

  1. import { useParams } from 'react-router-dom';
  2. let params = useParams();
  3. // console.log(params);
  4. //输入地址栏http://localhost:3000/invoices/2333
  5. //{ invoiceId: '2333' }

问题:如何获取路由参数?

通过搜索参数函数useSearchParams()

  1. import { useParams, useSearchParams } from 'react-router-dom';
  2. //返回一个设置的数组里,数组保存着一个对象,和对象的方法
  3. // let [searchParams, setSearchParams] = useSearchParams();
  4. //http://localhost:3000/invoice/1995?filter=3&name=zhangsan&age=18
  5. // console.log(searchParams);
  6. /**
  7. * URLSearchParams{
  8. * append: fn,
  9. * delete: fn,
  10. * ...
  11. * }
  12. */

问题:在react-router-dom中如何获取history对象?

通过useLocation()方法

  1. import { useLocation } from 'react-router-dom';
  2. //获取history对象
  3. let location = useLocation();
  4. console.log(location);
  5. //{pathname: '/invoices/2001', search: '', hash: '', state: null, key: '33shav6y'}

问题:如何实现自定义导航,即路由跳转?

通过useNavigate()方法

  1. import { useNavigate } from 'react-router-dom';
  2. //返回一个方法
  3. let navigate = useNavigate();
  4. //实现跳转
  5. navigate('/invoices');

源码地址: https://gitee.com/kevinleeeee/react-router-v6-todolist-demo

案例

案例:todolist

实现:

  • 增删某一项
  • 刷新页面读取localStorage数据(数据持久化)

写法:

  • 类组件写法
  • hook写法

源码地址:

https://gitee.com/kevinleeeee/react-todolist-class-hook-demo

案例:JS++后台管理系统

这是腾讯课堂数据的后台管理系统

接口参照项目: https://gitee.com/kevinleeeee/crawler-puppeteer-txcourse-demo

技术:

  • react
  • react-router
  • react-router-dom
  • axioswithCredentials属性携带cookies

案例展示图:

React基础 - 图7

React基础 - 图8

React基础 - 图9

React基础 - 图10

React基础 - 图11

实现功能:

  • 登录页面(账户登录)
  • 后台首页侧边路由导航栏
  • 点击左侧边栏实现地址跳转显示右边内容卡片区域
  • 课堂管理内容(表格)展示

    • 课堂分类操作
    • 上下架操作
  • 推荐课程内容(表格)展示

    • 上下架操作
  • 轮播图管理内容(表格)展示

    • 上下架操作
  • 课堂集合管理内容(表格)展示

    • 上下架操作
  • 老师管理内容(表格)展示

    • 明星老师操作
    • 上下线操作
  • 学生管理内容(表格)展示

    • 上下线操作
  • 数据爬取内容(表格)展示

    • 各种数据爬取操作

项目启动:

  1. 启动后端:

    1. npm run dev
    2. 开启服务phpStudy里的MySQLApache
    3. 访问数据库网页(删除所有表)
    4. 同步所有数据表(重置新增表)
    5. 访问七牛空间(清空图片存储)
  2. 启动前端后台项目(重新爬取数据)

    1. npm start
  3. 启动前端前台项目

路由组件结构:

  • IndexPage首页页面<Route>

    • Header标题组件

      • Logo组件
      • Title组件
      • Logout组件
    • SideBar侧边导航组件

      • NavItem组件
    • Container组件

      • Board组件

        • CollectionPage组件<Route>

          • TableBody组件(非公共)
        • CoursePage组件<Route>
        • CrawlerPage组件<Route>
        • RecomCoursePage组件<Route>

          • TableBody组件(非公共)
        • SliderPage组件<Route>

          • TableBody组件(非公共)
        • StudentPage组件<Route>

          • TableBody组件(非公共)
        • TeacherPage组件<Route>

          • TableBody组件(非公共)
        • ErrorPage组件<Route>
  • LoginPage登录页面<Route>

    • Login登录组件

      • Logo组件
      • Form表单组件
  • common公共组件:

    • ListItem组件
    • TableHead组件
    • TableBody组件

请求接口:

项目目录:

  1. ├─src
  2. | ├─App.jsx - 应用组件/路由页面组件结构
  3. | ├─index.js - 入口文件/挂载应用
  4. | ├─utils - 工具函数
  5. | | ├─http.js - 封装axios get/post方法
  6. | | tools.js - 去空格/抽离代码/确认信息
  7. | ├─services - 请求接口服务
  8. | | ├─Collection.js
  9. | | ├─Common.js
  10. | | ├─Course.js
  11. | | ├─Crawler.js
  12. | | ├─Login.js
  13. | | ├─RecomCourse.js
  14. | | ├─Slider.js
  15. | | ├─Student.js
  16. | | Teacher.js
  17. | ├─pages - 页面组件
  18. | | ├─Index.jsx - 首页
  19. | | ├─Login.jsx - 登录页
  20. | | ├─sub - 子路由组件 /子组件名称
  21. | | | ├─Teacher
  22. | | | | ├─index.jsx
  23. | | | | ├─index.scss
  24. | | | | ├─TableBody
  25. | | | | | ├─index.jsx
  26. | | | | | index.scss
  27. | | | ├─Student
  28. | | | | ├─index.jsx
  29. | | | | ├─index.scss
  30. | | | | ├─TableBody
  31. | | | | | ├─index.jsx
  32. | | | | | index.scss
  33. | | | ├─Slider
  34. | | | | ├─index.jsx
  35. | | | | ├─index.scss
  36. | | | | ├─TableBody
  37. | | | | | ├─index.jsx
  38. | | | | | index.scss
  39. | | | ├─RecomCourse
  40. | | | | ├─index.jsx
  41. | | | | ├─index.scss
  42. | | | | ├─TableBody
  43. | | | | | ├─index.jsx
  44. | | | | | index.scss
  45. | | | ├─Error
  46. | | | | ├─index.jsx
  47. | | | | index.scss
  48. | | | ├─Crawler
  49. | | | | ├─index.jsx
  50. | | | | ├─index.scss
  51. | | | | ├─TableBody
  52. | | | | | ├─index.jsx
  53. | | | | | index.scss
  54. | | | ├─Course
  55. | | | | ├─index.jsx
  56. | | | | index.scss
  57. | | | ├─Collection
  58. | | | | ├─index.jsx
  59. | | | | ├─index.scss
  60. | | | | ├─TableBody
  61. | | | | | ├─index.jsx
  62. | | | | | index.scss
  63. | ├─config
  64. | | ├─config.js - API路径/左侧导航栏配置
  65. | | ├─crawler_config.js - 爬虫管理配置信息
  66. | | table_config.js - 表格th头部配置信息
  67. | ├─components
  68. | | ├─Login
  69. | | | ├─index.jsx
  70. | | | ├─index.scss
  71. | | | ├─Logo
  72. | | | | ├─index.jsx
  73. | | | | index.scss
  74. | | | ├─Form
  75. | | | | ├─index.jsx
  76. | | | | ├─index.scss
  77. | | | | ├─Title
  78. | | | | | ├─index.jsx
  79. | | | | | index.scss
  80. | | | | ├─LoginForm
  81. | | | | | ├─index.jsx
  82. | | | | | index.scss
  83. | | ├─Index
  84. | | | ├─SideBar
  85. | | | | ├─index.jsx
  86. | | | | ├─index.scss
  87. | | | | ├─NavItem
  88. | | | | | ├─index.jsx
  89. | | | | | index.scss
  90. | | | ├─Header
  91. | | | | ├─index.jsx
  92. | | | | ├─index.scss
  93. | | | | ├─Title
  94. | | | | | ├─index.jsx
  95. | | | | | index.scss
  96. | | | | ├─Logout
  97. | | | | | ├─index.jsx
  98. | | | | | index.scss
  99. | | | | ├─Logo
  100. | | | | | ├─index.jsx
  101. | | | | | index.scss
  102. | | | ├─Container
  103. | | | | ├─index.jsx
  104. | | | | ├─index.scss
  105. | | | | ├─Board
  106. | | | | | ├─index.jsx
  107. | | | | | index.scss
  108. | | ├─common
  109. | | | ├─TableSelect
  110. | | | | ├─index.jsx
  111. | | | | index.scss
  112. | | | ├─TableHead
  113. | | | | ├─index.jsx
  114. | | | | index.scss
  115. | | | ├─TableBody
  116. | | | | ├─index.jsx
  117. | | | | index.scss
  118. | | | ├─ListTitle
  119. | | | | ├─index.jsx
  120. | | | | index.scss
  121. | ├─assets
  122. | | ├─scss
  123. | | | ├─button.scss
  124. | | | ├─common.scss
  125. | | | iconfont.css
  126. | | ├─img
  127. | | | logo.png
  128. | | ├─fonts

项目源码:

案例:事件待办

写法:

  • 组件hooks写法
  • 事件传递(父组件绑定事件useCallback包裹传递给子组件)
  • 属性传递

功能:

  • 事件代办列表渲染
  • 点击加号显示输入框
  • 输入内容新增待办事件项(useCallback)
  • 将列表数据缓存到localStorage(useEffect)
  • 点击待办事件项复选框同步事件中横线显示(useCallback)
  • 点击查看按钮显示查看内容模态框(useCallback)
  • 点击编辑按钮修改模态框显示的内容(useCallback)
  • 点击删除按钮删除列表某一项待办事件(useCallback)

知识点:

  • 插槽的运用
  • 原生react组件数据传递
  • 认识常用的hooks钩子
  • 项目设计
  • useEffect使用

案例展示图:

React基础 - 图12

组件划分:

  • Header标题组件
  • 输入框组件
  • 列表某一项组件
  • 模态框组件(利用插槽插入内容复用)
  • 查看内容组件
  • 编辑内容组件

项目目录:

  1. ├─src
  2. | ├─App.css
  3. | ├─App.js - useState声明数据/定义本组件方法和useCallback子组件使用的方法/useEffectlocalStorage进行读写/组件视图绑定/子组件属性和方法传递
  4. | ├─index.jsx - 入口文件/引入默认样式/渲染APP组件挂载页面
  5. | ├─libs
  6. | | utils.js - 工具函数/格式化时间
  7. | ├─components
  8. | | ├─TodoItem - 列表某一项的组件
  9. | | | ├─index.jsx - 接收父组件属性和方法/绑定视图/事件传递到父组件
  10. | | | index.scss
  11. | | ├─NoDataTip - 无数据页面组件
  12. | | | ├─index.jsx
  13. | | | index.scss
  14. | | ├─Modal - 通用模态框模板组件
  15. | | | ├─index.jsx - 接收子组件(CheckModal/EditModal)属性和方法作为插槽填入/绑定视图
  16. | | | ├─index.scss
  17. | | | ├─EditModal
  18. | | | | ├─index.jsx - 返回Modal模板/传入属性和子HTML元素/绑定ref/定义提交编辑表单方法/绑定视图
  19. | | | | index.scss
  20. | | | ├─CheckModal
  21. | | | | ├─index.jsx - 返回Modal模板/传入属性和子HTML元素/绑定视图
  22. | | | | index.scss
  23. | | ├─Header - 标题组件
  24. | | | ├─index.jsx - 绑定视图/触发组件点击事件
  25. | | | index.scss
  26. | | ├─AddInput - 输入框组件
  27. | | | ├─index.jsx - 绑定视图/绑定ref/触发组件点击事件
  28. | | | index.scss
  29. | ├─assets
  30. | | ├─js
  31. | | | ├─common.js
  32. | | | fastclick.js
  33. | | ├─css
  34. | | | ├─border.css
  35. | | | ├─resets.css
  36. | | | ui.css
  37. ├─public
  38. | ├─favicon.ico
  39. | ├─index.html

总结:

  • 此项目使用的是非redux数据管理的方式通信比较麻烦
  • 理解useState的使用场景
  • 合理使用useEffectlocalStorage进行读写
  • 合理使用useCallback避免子组件多次更新渲染
  • 合理使用useRef获取DOM节点属性

源码地址:

https://gitee.com/kevinleeeee/react-todolistpro-hook-mobile-demo

项目

项目:好客租房

移动端的租房APP应用程序,实现类似链家等项目功能,解决用户租房需求

核心业务:

  • 在线找房(地图,条件搜索)
  • 用户登录
  • 房源发布

功能:

  • tabbar底栏路由

  • 首页轮播图

  • 首页搜索

  • 首页地图找房图标

  • 租房小组:根据地理位置展示不同小组信息

  • 底部tab栏图标进行条件找房

  • 收藏房源

  • 百度地图定位

  • 个人中心

  • 发布房源,房源管理

  • 城市选择(react-virtualized):切换城市,查看城市下的房源信息

  • 在百度地图中展示当前定位城市

技术:

  • React核心库:react,react-dom,react-router-dom
  • antd-mobile:一个基于React的UI组件库
  • react-virtualized:长列表的性能优化
  • formik+yup
  • react-spring
  • 百度地图API
  • react-virtualized
  • AutoSizer:高阶组件

项目展示图:

项目搭建:

  1. 本地接口部署

    1. 创建和导入数据库:数据库名称hkzf
    2. 启动接口:API目录(./server)下运行npm start
    3. 测试接口: http://localhost:8080/
  2. 脚手架初始化项目:npx create-react-app react-hkzf-mobile-project
  3. antd-mobile
  4. 路由设置
  5. 整体布局:分析两种页面布局,使用嵌套路由实现带TabBar页面布局
  6. 首页模块:租房小组结构布局,数据获取,H5底栏位置和百度地图定位等
  7. 城市模块:数据结构处理,长列表性能优化,react-virtualized,索引列表等

组件布局:

  • Home:主页面

    • TopBar:顶部导航(城市选择,搜索,地图找房)的路由跳转
    • Carousel:轮播图
    • NavMenu:导航菜单
    • GroupCard:租房小组卡片
    • InfoCard:最新资讯卡片
  • HouseList找房页面
  • News:资讯页面
  • Profile:我的页面
  • tabbar页面:首页,找房,资讯,我的
  • tabbar页面:选择城市
  • CityList页面:城市切换进入列表城市

知识点:

  • 通过判断是否路由切换实现逻辑业务(componentDidUpdate钩子里判断)
  • 路由模糊匹配路径
  • 路由重定向
  • HTML5地理位置API:通过navigator.geolocation.getCurrentPosition(position => {})获取
  • 百度地图实现地理定位和地图找房
  • 请求数据函数封装
  • 路由嵌套传值时使用render属性render={(props)=><App {...props}></App>}
  • 长列表的性能优化(react-virtualized),应用于展示大型列表和表格数据如城市列表,通信录,微博等,会导致页面卡顿,滚动不流畅等性能问题,原因是大量DOM节点的重绘和重排,优化方案是 懒渲染 或 可视区域渲染
  • (react-virtualized)只渲染页面可视区域的列表项
  • AutoSizer:高阶组件,可以让List组件占满屏幕,自动适应屏幕中的高度和宽度
  • withRouter高阶组件来获取路由信息(history),避免只有路由Route直接渲染的组件才能够获取路由信息
  • css in js:使用JavaScript编写CSS的统称,用来解决CSS样式冲突(组件间样式)覆盖等问题,实现:CSS Module(推荐,react脚手架集成),styled-components

数据获取:

源码地址: https://gitee.com/kevinleeeee/react-hkzf-mobile-project