文章目录
- 一、脚手架的创建与安装
- 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会被调用渲染页面。
当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组件:<