文章目录
一、脚手架的创建与安装
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会被调用渲染页面。
   当props和state发生变化时,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组件:
 使用context名.Consumer读取数据。
 Context.Consumer标签需要 函数作为子元素(function as child);
 这个函数接收当前的 context 值,返回一个 React 节点
import ThemeContext from "./context/theme-context"
function HomeBanner () {
  return (
    <div>
      {/* 函数式组件中使用Context共享的数据 */}
      <ThemeContext.Consumer>
        {
           {/* value就是context的值*/}
          value => {
            return <h3>颜色:{value.color}</h3>
          }
        }
      </ThemeContext.Consumer>
    </div>
  )
}
 
(2) 组件读取多个Context的数据
在类组件中,组件名.contextType = xxx只能指定一种context类型,如果该组件想读取多个context的数据,可以借助Context.Consumer
 
(3) defaultValue
什么时候使用默认值defaultValue呢?当组件未被Context组件包裹,但又想使用该Context的数据时。比如Profile组件
 
 步骤一: 在context文件里指定默认值
 
 步骤二: 组件中使用
 
 控制台打印结果:{color: 'blue', price: 10}
(4) ContextAPI总结
-  
React.createContext
创建一个需要共享的Context对象:ThemeContext = React.createContext({ color: "blue", price: 10 })
defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值 -  
Context.Provider
每个 Context 对象都会返回一个 Provider React 组件;Provider 接收一个 value 属性,传递给消费组件(Consumer);<ThemeContext.Provider value={{ color: 'red', price: '50' }}>
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染; -  
Class.contextType
挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
Profile.contextType = ThemeContext -  
Context.Consumer
这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
这里需要 函数作为子元素(function as child);
这个函数接收当前的 context 值,返回一个 React 节点;

 
什么时候使用Context.Consumer呢?
1.当使用value的组件是一个函数式组件时;
 2.当组件中需要使用多个Context时;










