React (三) 创建安装脚手架,类组件与函数式组件;生命周期;父子通信props;插槽;非父子通信Context

时间:2024-10-14 12:59:28

文章目录

  • 一、脚手架的创建与安装
    • 1. 认识脚手架
    • 2. 安装脚手架
    • 3. 创建react项目
    • 4. 项目结构
  • 二、从0编写
  • 三、组件化开发
    • 1. 什么是组件化开发
    • 2. 类组件
    • 3. render函数
    • 4. 函数式组件
  • 四、生命周期
    • 1. 挂载Mount
    • 2. 更新Update
    • 3. 卸载Unmount
    • 4. 不常用的生命周期
  • 五、父子组件通信
    • 1. 父传子
    • 2. 父传子数据的props类型限制
    • 3. 子传父
    • 4、{...object} 解构 传递对象数据---拓展
  • 六、插槽
    • 1. 组件子元素实现插槽效果
    • 2. props实现插槽效果
    • 3. 作用域插槽
  • 七、非父子组件通信Context
    • 1、Context上下文的使用(类组件)
    • 3、Context的使用(函数式组件)
      • (1) 函数组件读取Context数据---Context.consumer
      • (2) 组件读取多个Context的数据
      • (3) defaultValue
      • (4) ContextAPI总结

一、脚手架的创建与安装

1. 认识脚手架

  每个项目的基本工程化结构是相似的;既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生成基本的工程化模板;
   脚手架(scaffold)就是一种工具,帮我们可以快速生成一个通用的项目目录结构,并将所需的工程环境配置好。让项目从搭建到开发,再到部署,整个流程变得快速和便捷;

2. 安装脚手架

React的脚手架:create-react-app, 简称cra
(1)提前安装node环境.(这个可参考之前安装Vue的记录配置node环境
(2)执行命令安装脚手架:npm install create-react-app -g;
  运行create-react-app --version查看安装的版本,显示版本就说明脚手架安装成功。

在这里插入图片描述
(执行命令在powershell里也可以,在git bash里也可以)

3. 创建react项目

在对应的文件夹下执行命令create-react-app 项目名,创建项目。
注意:项目名称不能包含大写字母
在这里插入图片描述
运行项目:npm run start
在这里插入图片描述

4. 项目结构

|--public
|    |-- favucin.ico     // 标签页的icon图标
|    |-- index.html      // 入口文件
|    |-- logo192.png     // 在manifest.json文件里被调用
|    |-- logo512.png     // 在manifest.json文件里被调用
|    |-- manifest.json  // 和web app配置相关
|    |-- robots.txt     //指定本网站哪些文件可以或者无法被爬取
|--src
|    |-- App.css     // App组件相关的样式
|    |-- App.js      // App组件的代码文件
|    |-- App.test.js // App组件的测试代码文件
|    |-- index.css   // 全局的样式文件
|    |-- index.js    // 整个应用程序的入口文件
|    |-- logo.svg    // 启动项目时的react图标
|    |-- reportWebVitals.js  //
|    |-- setupTest.js    //测试初始化文件
|-- package.json        // 对整个应用程序的描述:应用名称、版本号、一些依赖包

logo192.png, logo512.png,manifest.json文件都与PWA相关,PWA(国内应用较少)了解即可。
在这里插入图片描述

二、从0编写

将src下的文件都删掉,运行项目,提示缺少index.js文件,新建src/index.js

import React from "react"
import ReactDOM from "react-dom/client" // 旧版是从react里导入ReactDOM

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      msg: 'HelloWorld'
    }
  }
  render () {
    const { msg } = this.state
    return (
      <h2>{msg}</h2>
    )
  }
}
// 这里的#root是index.html文件里的
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)

与之前写的一样,都是创建类组件,然后渲染到桌面上。
可将App组件拆分到App.jsx文件里:
在这里插入图片描述

三、组件化开发

1. 什么是组件化开发

  组件化就是分而治之;如果一个页面里的所有功能和逻辑处理都放在一起,就会很难维护。如果将一个页面拆分为一个个小的功能块,有利于之后页面的管理和扩展。

用组件化的思想来构建应用:

  • 一个完整的页面可以分为很多组件;
  • 每个组件都用于实现页面的一个功能块
  • 每个组件都可以继续细分,组件本身又可以进行复用

最终,任何的应用都会被抽象成一棵组件树
在这里插入图片描述

2. 类组件

定义类组件的要求:

  • 组件的名称是大写字符开头(无论类组件还是函数组件)
  • 类组件需要继承React.Component(后边优化时,可继承Pure)
  • 类组件必须实现render函数

使用class定义一个类组件:

import React from "react";  // 导入的React是个对象,里面有React.Component属性
class App extends React.Component {
  // constructor是可选的,通常在构造函数里初始化一些数据
  constructor() {
    super()
    // 维护的是组件内部的数据
    this.state = {
      msg: 'Hello World'
    }
  }
  // 组件内必须要实现的方法
  render () {
    return <h2>{this.state.msg}</h2>
  }
}

也可以这样继承: 反正继承的都是Component

import { Component } from "react";
class App extends Component {
  render () {
    return <h2>Hello</h2>
  }
}

3. render函数

(1) render函数什么时候被调用
  页面初次加载时,函数render会被调用渲染页面。
  当propsstate发生变化时,render会再次被调用。(props后面会学,state里的数据是通过调用this.setState进行修改)

(2) 返回值有哪几类

  • React元素
    通过JSX创建的就是React元素。JSX本质上就是调用React.createElement(),创建一个React的元素。
    在这里插入图片描述
  • 数组或fragments:返回多个元素
    fragments后边再学
     // 2.组件或者fragments(后续学习)
        return ["abc", "cba", "nba"]
        return [
           <h1>h1元素</h1>,
           <h2>h2元素</h2>,
           <div>哈哈哈</div>
        ]
    

在这里插入图片描述

  • Portals (还没学):可以渲染子节点到不同的DOM子树中。
  • 字符串或数值类型:在DOM中渲染为文本节点
  • 布尔类型或null:什么都不渲染
    return "Hello World" // 界面渲染 HelloWorld
    return true          // 什么都不渲染
    

4. 函数式组件

(1) 函数组件是使用function来进行定义的函数,这个函数返回的内容和类组件中render函数返回的一致。

(2) 函数组件的特点:

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
  • this关键字不能指向组件实例(因为没有组件实例)
  • 没有内部状态(state),即使自己定义了state数据,每次调用state返回的数据都是初始数据。也就是无法进行状态维护
// 函数式组件
function App () {
  const state = { name: 'tom' } // 每次调用App组件,state里的数据值都是tom
  // 返回值:和类组件中的render函数返回的值类型一致。
  return <h2>Hello World</h2>
}

四、生命周期

生命周期就是从创建到销毁的这个过程;
在这里插入图片描述
在生命周期这个过程中,可以划分为很多阶段:

  • 挂载阶段(Mount): 组件第一次在Dom树中被渲染的过程
  • 更新阶段(Update):组件状态发生变化,重新更新渲染的过程
  • 卸载阶段(Unmount):组件从Dom树中被移除的过程

React为了告诉我们当前组件处于哪些阶段,会在对应的阶段调用某些函数,这些函数就是生命周期函数

  • Constructor
    如果不初始化 state 或不进行方法绑定(为事件绑定实例this),则不需要为 React 组件实现构造函数。

  • componentDidMount
    依赖于DOM的操作可以在这里进行;
    在此处发送网络请求就最好的地方;(官方建议)
    可以在此处添加一些订阅

  • componentDidUpdate
    componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
    当组件更新后,可以在此处对 DOM 进行操作;

  • componentWillUnmount
    该生命周期函数会在组件卸载及销毁之前直接调用。
    在此方法中执行必要的清理操作 ( 比如 清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等)

谈生命周期函数,主要指的是类组件,因为函数式组件没有生命周期函数。

1. 挂载Mount

挂载步骤:
(1) 创建组件实例(constructor)。创建组件实例会先执行对应类组件里的构造函数constructor
每执行一次<HelloWordl/>,相当于new一个class HelloWorld extends Component{}的实例 (就像java一样)
在这里插入图片描述
(2) 执行render方法,渲染界面
(3) 挂载完毕,执行生命周期函数componentDidMount

class HelloWorld extends React.Component {
  // 1.执行构造方法
  constructor() {
    super()
    console.log('HW, constructor');
    this.state = { msg: 'HelloWorld' }
  }
  // 2. 执行render函数
  render () {
    console.log('HW, render');
    let { msg } = this.state
    return (
      <h2>{msg}</h2>
    )
  }
  // 3. 挂载完成
  componentDidMount () {
    console.log('HW, componentDidMount');
  }
}

挂载阶段,依次打印:HW, constructor,HW, render,HW, componentDidMount

2. 更新Update

从图里可以看出,触发更新有三种方式,现在只说setState()
(1) setState()修改数据
(2) 重新调用render函数
(3) 更新完成,调用生命周期函数componentDidUpdate

  // 组件的DOM更新完成
  componentDidUpdate () {
    console.log('HW, componentDidUpdate');
  }

在这里插入图片描述

3. 卸载Unmount

HelloWorld组件

  // 5. 组件将从DOM树中被移除:卸载组件
  componentWillUnmount () {
    console.log('HW, componentWillUnmount');
  }

App组件:点击按钮时,隐藏组件

 render () {
   let { isShow } = this.state
   return (
     <div>
       <button onClick={e => this.changeHWShow()}>切换</button>
       {isShow && <HelloWorld />}
     </div>
   )
 }
 changeHWShow () {
  this.setState({
    isShow: !this.state.isShow
  })
}

在这里插入图片描述

4. 不常用的生命周期

shouldComponentUpdate下一篇博客会说。
在这里插入图片描述
具体查看官方文档:官方文档

五、父子组件通信

1. 父传子

  • 父组件通过属性=值的形式来传递给子组件数据
  • 子组件通过props参数获取父组件传递过来的数据(不能换名,只能叫props)

需求:父组件Content给子组件Banner传递数据:

父组件Content:

 this.state = {
   banners: ['新歌曲', '新歌单', '新MV'],
   title: '轮播图'
 }
render () {
  let { banners, title } = this.state
  return (
    <div>
       <Banner banners={banners} title={title} />
    </div>
  )
}

<Banner banners={banners} />相当于在new实例时,传递了参数。所以Banner的构造函数需要接收这个参数。

子组件Banner:

export class Banner extends Component {
  constructor(props) {
  // props接收之后传给super()
    super(props)
    console.log('Banner接收:', props);
  }
  
  render () {
  // 从props属性里可以读取父组件传递过来的值
    let { banners, title } = this.props
    return (
      <div  className='banner'>
        <h3>{title}</h3>
        {banners.map(item => {
          return <li key={item}>{item}</li>
        })}
      </div>
    )
  }
}

  其实,如果没有constructor构造函数(2-6行代码),也会默认通过props帮忙接收父组件的数据。render函数里仍然可以通过this.props接收数据。
在这里插入图片描述

2. 父传子数据的props类型限制

(1) 设置数据类型限制

// 1. 引入 prop-types
import PropTypes from 'prop-types'
class Banner extends Component {
 ...
}
// 2. 限制类型 
// 要求title是字符串,且必须传值(如果没传,且Banner组件也没有设置title默认值,即使Banner不使用title,也会报错);
// 要求banners是数组类型的数据
Banner.propTypes = {
  title: PropTypes.string.isRequired,
  banners: PropTypes.array
}

PropTypes的其他限制:NPM:prop-types

(2) 设置数据默认值

  • 在组件内用static关键字;
  • 在组件外用defaultProps
class Banner extends Component {
  // 方式一:设置默认值
  static defaultProps = {
    title: '默认标题',
    banners: []
  }
}
// 方式二
Banner.defaultProps = {
  banners: [],
  title: "默认标题"
}

测试

为了方便观察,给banner添加了样式
Banner组件:
在这里插入图片描述

  <!--规范传值-->
  <Banner banners={banners} title={title} />
   <!--啥也不传-->
  <Banner />

在这里插入图片描述

3. 子传父

需求:
在这里插入图片描述
其实还是父组件给子组件传递一个函数,子组件调用这个函数,并将数据通过参数的形式传递给父组件。
在这里插入图片描述

4、{…object} 解构 传递对象数据—拓展

父组件的state里有一个对象数据要传递给子组件。

this.state = {
     info: { stuName: 'tom', age: 18 }
}
// props传递数据
 <Home stuName={info.stuName} age={info.age} />
 {/* 等价于 */}
 <Home {...info} /> 

六、插槽

比如导航区的组件NavBar;每个页面的导航组件结构大体一致(分为左中右三个部分),但内容不一样。
在Vue里可以通过插槽实现不同样式的导航组件。
在这里插入图片描述
React里并没有插槽这个概念。对于这种需要插槽的情况,React有两种方案可以实现:

  • 组件的children子元素
  • props属性传递React元素

1. 组件子元素实现插槽效果

App.jsx:

<!--将button,h2,i等标签传递给NavBar,NavBar标签包裹的就叫子元素-->
  <NavBar>
    <button>按钮</button>
    <h2>HelloWorld</h2>
    <i>斜体文字</i>
  </NavBar>

子组件可以通过this.props.children接收父组件传递过来的react元素。当有多个元素时,children是一个数组,包含这些元素;当只有一个元素时,children的值就是这个元素。

nav-bar.jsx:

// 子组件通过props接收react元素
 render () {
   let { children } = this.props
   console.log('children', children);
   return (
     <div className='nav-bar'>
       <div className="left">{children[0]}</div>
       <div className="center">{children[1]}</div>
       <div className="right">{children[2]}</div>
     </div>
   )
 }

在这里插入图片描述
在这里插入图片描述
如果只传button
在这里插入图片描述

缺点: <NAvBar>标签里子元素(react元素)的书写顺序决定了元素在children里的索引值。 子组件通过children的索引值获取传入的元素,容易出错。

2. props实现插槽效果

和之前props传值一样,只不过这次props传递的是react元素。
在这里插入图片描述
通过具体的属性名,可以在传入元素和获取元素时更加精准。

3. 作用域插槽

适用场景:结构由父组件决定,但是结构里用的值在子组件中。
子组件有标题titles数据,父组件传递结构,决定这些标题如何展示。比如说展示按钮
在这里插入图片描述
传递react元素:
在这里插入图片描述
问题是这样渲染出来的按钮内容都是哈哈哈, 所以需要子组件将标题内容传给父组件。

怎么传?还是通过回调函数传参的方式,itemType改成一个函数。

App.jsx:

 <TabControl
   itemType={(item) => <button>{item}</button>}
 />

子组件:

render () {
  let { titles } = this.state
  let { itemType } = this.props // 接收函数
  return (
    <div className='tab-control'>
      {titles.map((item, index) => {
        return (
          <div className='item' key={index} >
            {/* 调用函数并传参 */}
            {itemType(item)}
          </div>
        )
       })
      }
    </div >
  )
}

就很绝,甚至可以根据不同的title内容返回不同的页面结构

  getTabItem(item) {
    if (item === "流行") {
      return <span>{item}</span>    // 标签 
    } else if (item === "新款") {
      return <button>{item}</button> // 按钮
    } else {
      return <i>{item}</i>  // 斜体文字
    }
  }
...
itemType={item => this.getTabItem(item)}

七、非父子组件通信Context

在组件树中,顶层Provider(父组件)提供数据,下边的作为消费者Consumer(后代组件)接收数据,或者通过指定的方式接收数据。

1、Context上下文的使用(类组件)

通过props实现父给孙组件传值,会打扰到中间的组件。而且比较麻烦。
在这里插入图片描述
组件关系 App> Home> HomeInfo

Context使用步骤:

(1) 使用React.createContext创建一个context

src/context/theme-context.js:

import React from "react";
// 1. 创建一个Context
const ThemeContext = React.createContext()
export default ThemeContext

(2) 引入上下文,并为后代提供数据

App组件中:

import ThemeContext from './contex

{/* 2.通过ThemeContext中的Provider中value属性为后代提供数据 */}
<ThemeContext.Provider value={{ color: 'red', price: '50' }}>
   <Home />
 </ThemeContext.Provider>

给子组件Home包裹标签<ThemeContext>, (这里演示非父子组件传值,所以包裹Home。也可以在Home组件里给孙组件HomeInfo包裹标签,包裹到的组件及后代组件都可以用到数据)

(3) 在组件中指定要读取的Context类型,并读取数据

HomeInfo组件:

class HomeInfo extends Component {
  render () {
    // 4. 第四步:读取数据
    console.log('上下文:', this.context); 
		...
  }
}
// 3. 第三步:context可能有很多,设置组件的要读取哪一类的context
HomeInfo.contextType = ThemeContext

在这里插入图片描述

3、Context的使用(函数式组件)

(1) 函数组件读取Context数据—Context.consumer

定义函数组件HomeBanner,并在Home组件中使用
Home组件:

  render () {
    return (
      <div>
        <h2>Home组件</h2>
        <HomeBanner />
      </div>
    )
  }

注意之前在App组件中,已经用Context的组件将Home组件包裹了。

HomeBanner组件:<