文章目录
一、项目的创建
1、create-react-app搭建项目流程
2、vite创建项目流程
3、项目目录简介
4、自动引入
5、模块化样式
1、.module.css
2、预处理器
3、css-in-js库
二、组件的生命周期
1、常用生命周期函数简介
2、常用生命周期的执行顺序
3、不常用生命周期简介
三、PureComponent
四、Context
1、创建context
2、提供context
3、消费context
4、多个context嵌套
五、高阶组件
1、认识高阶组件
2、HOC传递属性
3、小案例
六、相关面试题
1、React生命周期
2、React组件的传值方式
3、跨域
4、React Vue样式隔离实现
一、项目的创建
1、create-react-app搭建项目流程
create-react-app是官方支持的创建react单页应用程序的脚手架,使用这个脚手架的前提是要确保你电脑安装的node版本在14以上,npm版本在5.6以上
安装脚手架
方式一
//全局安装
npm i -g create-react-app
//利用脚手架创建项目
create-react-app my-project
方式二
使用npx进行安装,npx是npm 5.2版本新增的一个工具包,为npm包的执行者,相比npm,npx会自动安装依赖包并执行某个命令。例: 我们通常使用脚手架创建项目时,我们要先安装脚手架,才能使用脚手架执行命令进行项目创建,而npx可以帮我们省略安装脚手架这一步
npx create-react-app my-project
//进入我们创建的项目
cd my-project
注:项目名不能包含大写字母,使用npx需要node版本为22以上
2、vite创建项目流程
使用vite脚手架创建项目,vite需要node版本在18以上
npm create vite@latest
安装项目依赖包
npm i
3、项目目录简介
node_modules:项目的核心模块,依赖包
public:存放静态资源文件
.ico:页签的logo
index.html:唯一的页面文件,只提供根节点
manifest.json:移动端配置文件
robots.txt:告诉爬虫者,不可爬的页面,没有实质作用只是警告
.gitignore:声明一些在git上传时需要忽略的文件
package.json:项目的说明文件,有哪些依赖,依赖了哪个版本
package-lock.json:项目依赖的安装包,一些版本不会做一些限制,进行版本锁定
Readme.md:项目的说明文件
src目录
App.css:App组件的样式文件
App.js:项目的根组件
App.test.js:自动化测试文件
index.css:全局样式文件
index.js:项目的入口文件,html只会加载这个js文件
reportWebVitals.js:谷歌推出的浏览器性能优化的库
setupTests:针对index.js的单元测试原件
注: index.js里的React.StrictMode,它可以识别一些不安全的生命周期,检测意外的副作用和过时的api
4、自动引入
在项目中组件的后缀名最好用jsx/tsx表示一个组件,如果是普通js/ts后缀就用js/ts
默认引入:jsx在引入的时候可以不加文件后缀,编辑器会自动查找.jsx后缀的文件,要是找不到的话,会自动查找后缀为.js的文件。如果直接引入的是文件夹的名字,默认查找该文件夹下的jsx文件
5、模块化样式
1、.module.css
react中想实现模块化样式,我们只需要将样式的文件名改为:文件名.module.css即可
import title from "./App.module.css"
function App() {
return <>
<p className={title.title}>567</p>
</>
}
export default App
原理:CSSModules允许通过自动创建[filename]_[classname]_[hash]格式的唯一classname来确定css的作用域
2、预处理器
css的预处理器有sass、less、stylus,在项目中我使用的是sass,所以这里我就以sass为例
先在项目中安装sass
npm i sass
//App.scss
.content {
.title {
color: aqua;
}
}
//App.jsx
import "./App.scss"
function App() {
return <>
<div className="content">
<p className="title">567</p>
</div>
</>
}
export default App
3、css-in-js库
我们还可以使用js编写的css库,如styled-components、emotion等,这种方式实现样式模块化了解即可
import styled from "styled-components"
const StyleCss=styled.div`
color:red;
font-size:20px
`
function App() {
return <>
<StyleCss>
我是react
</StyleCss>
</>
}
export default App
二、组件的生命周期
生命周期描述了组件从创建到销毁的整个过程中不同阶段调用的方法,我们可以简单理解为到特定时间节点执行的函数
注:函数组件是纯函数,没有生命周期,但是函数组件可以使用useEffect来模拟生命周期(后面的文章会讲到)
注:react16.3版本前后生命周期不同,react16.3版本后废除了一些生命周期函数,又增加了一些,这篇文章主要讲解react16.3版本的生命周期函数
1、常用生命周期函数简介
(1)挂载阶段:组件被创建并插入到dom中
constructor:组件实例化时调用,初始化状态和绑定事件方法,也就是给this.state赋值对象来初始化内部的state,为事件绑定实例(this)
render:渲染组件的方法
componentDidMount:组件挂载完成后调用,此时,组件已经插入到dom树中了,可以用于执行网络请求,订阅,手动操作dom
(2)更新阶段
componentDidUpdate:会在更新后会被立即调用,首次渲染不会执行此方法,当更改了state数据,组件接收新属性,会执行该生命周期
(3)卸载阶段
componentWillUnmount:组件卸载之前调用,用于清除定时器,取消网络请求等等
2、常用生命周期的执行顺序
这里只介绍常用的生命周期钩子的执行顺序
常用的生命周期钩子:constructor,render,componentDidMount,componentDidUpdate,componentWillUnmount
//父组件
const content = document.querySelector(".title")
const root = ReactDOM.createRoot(content)
class App extends React.Component {
constructor() {
super()
console.log("Father-constructor")
this.state = {
msg: "我是父组件"
}
}
fn=()=>{
this.setState({
msg:'子组件的数据'
})
}
componentDidMount() {
console.log("Father-componentDidMount")
}
componentDidUpdate() {
console.log("Father-componentDidUpdate")
}
componentWillUnmount(){
}
render() {
console.log("Father-render")
const { msg } = this.state
return <>
<Child msg={msg} />
<button onClick={this.fn}>按钮</button>
</>
}
}
//子组件
class Child extends React.Component {
constructor() {
console.log("child-constructor")
super()
this.state = {
info: "我是子组件"
}
}
componentDidUpdate() {
console.log("child-componentDidMount")
}
componentDidMount() {
console.log("child-componentDidMount")
}
render() {
console.log("child-render")
const { msg } = this.props
return <>
<p>{msg}</p>
</>
}
}
root.render(<>
<App />
</>)
控制台打印的结果:
3、不常用生命周期简介
挂载阶段
getDerivedStateFromProps(nextProps,nextState):在渲染之前调用,根据props来更新state,无论是否初始渲染还是更新后都会触发,返回一个对象更新state,返回null表示不需要更新
更新阶段
更新阶段也调用getDerivedStateFromProps(nextProps,nextState),与挂载阶段作用相同
shouldComponentUpdate(nextProps,nextState):性能优化的生命周期函数,是在组件更新之前触发的生命周期函数,返回一个布尔值,决定组件的输出是否受当前状态或属性的改变影响而更新,默认返回true,false则不会更新组件
为什么要使用shouldComponentUpdate?
setState存在两个不合理的问题:(1)无论是否更新了state,render函数都会重新调用
(2)父组件更新了,就算子组件没有用到父组件的数据也会重新渲染子组件
state判断当前组件也就是父组件是否要更新,props判断子组件是否要更新
//父组件
shouldComponentUpdate(nextState, nextProps) {
if (nextState.msg === this.state.msg) {
return false
} else {
return true
}
}
//子组件
shouldComponentUpdate(nextState, nextProps) {
//fatherData父组件传来的数据
if (nextprops.fatherData=== this.props.fatherData) {
return false
} else {
return true
}
}
getSnapshotBeforeUpdate():最后一次渲染输出后调用,dom更新之前调用,用来捕获一些信息
三、PureComponent
在React16.3版本后废除了shouldComponentUpdate()因为它进行性能优化要判断两次state或props是否一致,一个属性还好,要是有多个属性就比较麻烦
PureComponent是react类组件中用于性能优化的一个内置基类,它内部自动实现了一个shouldComponentUpdate()生命周期方法
class App React.PureComponent{}
注:PureComponent对于对象属性的话只能进行浅层比较,对对象的地址进行判断。如果对象包含复杂的数据结构,有可能因为无法检查到深层的差别,产生错误的比对结果,所以只能在你的props和state比较简单的时候,可以使用PureComponent,或者在深层数据结构发生变化时调用forceUpdate()来确保组件被正确的更新
四、Context
我们传统的跨组件通信的方式是通过props层层传递,这种方式比较麻烦,可以使用context进行跨组件通信,避免逐层传递props
1、创建context
创建一个context.js文件
//context.js
import React from "react";
const myContext=React.createContext("")
export default myContext
2、提供context
使用Context对象的provider组件包裹需要接收数据的子组件
类组件
//我是父组件
import React from "react";
import MyContext from "./context"
import Child from "./child"
class App extends React.PureComponent {
state={
msg:"我是父组件数据"
}
render(){
const {msg}=this.state
return<MyContext.Provider value={msg}>
<h1>我是父组件</h1>
<Child/>
</MyContext.Provider>
}
}
export default App
函数组件
//我是父组件
import React from "react";
import MyContext from "./context"
import Child from "./child"
function App() {
return<MyContext.Provider value="我是父组件数据">
<h1>我是父组件</h1>
<Child/>
</MyContext.Provider>
}
export default App
3、消费context
在子组件中通过context对象的consumer组件或useContext Hook来获取数据
注:Context.Consumer适用于类组件和函数组件,useContext()适用于函数组件
类组件
使用Context.Consumer消费数据
儿子组件
//儿子组件
import MyContext from "./context";
import React from "react";
import GrandChild from "./grandChild";
class Child extends React.PureComponent {
render() {
return <MyContext.Consumer>
{
value => <>
<h1>我是子组件{value}</h1>
<GrandChild/>
</>
}
</MyContext.Consumer>
}
}
export default Child
孙子组件
//孙子组件
import MyContext from "./context";
import React from "react";
class GrandChild extends React.PureComponent {
render(){
return<MyContext.Consumer>
{
value=><>
<h1>我是子组件{value}</h1></>
}
</MyContext.Consumer>
}
}
export default GrandChild
使用Class.contextType消费数据
通过Class.contextType直接将context对象挂载到class的contextType的属性,可以使用this.context对context对象进行使用
写法一
将contextType属性写在类组件的外面
//孙子组件
import MyContext from "./context";
import React from "react";
class GrandChild extends React.PureComponent {
render(){
return <h1>我是子组件{this.context}</h1></>
}
}
GrandChild.contextType=MyContext
export default GrandChild
写法二
利用Es6static静态属性
//孙子组件
import MyContext from "./context";
import React from "react";
class GrandChild extends React.PureComponent {
static contextType=MyContext
render(){
return <h1>我是子组件{this.context}</h1></>
}
}
export default GrandChild
注:contextType是类的属性不是类的实例,所以将contextType写在类的外面,或者使用es6中类的静态属性将contextType写在类的里面
函数组件
使用Context.Consumer消费数据
//子组件
import MyContext from "./context";
import React from "react";
import GrandChild from "./grandChild";
function Child(){
render() {
return <MyContext.Consumer>
{
value => <>
<h1>我是子组件{value}</h1>
<GrandChild/>
</>
}
</MyContext.Consumer>
}
}
export default Child
使用useContext消费数据
//子组件
import MyContext from "./context";
import React, { useContext } from "react";
import GrandChild from "./grandChild";
function Child() {
const contextValue = useContext(MyContext)
return <>
<h1>{contextValue}</h1>
</>
}
export default Child
4、多个context嵌套
在react中我们可以使用多个context嵌套,可以解决不同层级,不同侧重点的数据共享问题,保持代码的可维护性
多个context嵌套就是每个context管理特定类型的数据,如权限控制,有管理路由的context,有管理权限的context
//父组件
function App(){
return <RouterContext.Provider value={router}>
<PermissionContext.Provider value={permissions}>
<Admin/>
</PermissionContext.Provider>
</RouterContext.Provider>
}
export default App
// 子组件
function AdminButton() {
const router = useContext(RouterContext);
const permissions = useContext(PermissionContext);
}
在项目中我们可以适当的嵌套context,通常3到4层以内,避免过度嵌套。嵌套多个context可以让react实现状态的模块化管理,保持代码职责的单一性,优化组件渲染性能,提高大型应用的可维护性
五、高阶组件
1、认识高阶组件
高阶组件(HOC)是一个函数,接收一个组件,并返回一个新组件,主要用于组件的逻辑复用和共享,不是直接渲染UI界面,类似于js的函数
注:HOC应当是纯函数,没有副作用,不能在HOC内修改原始组件,而是返回一个新组件,高阶组件一般以with开头,表示为组件提供附加功能
//子组件
class Child extends React.PureComponent{
render(){
return<>
<h1>我是儿子组件</h1>
</>
}
}
//子组件
class Son extends React.PureComponent {
render() {
return <>
<h1>我是孙子组件</h1>
</>
}
}
export default GrandChild
//高阶组件
function withLog(Wrapcomponent) {
return class extends React.Component {
//复用的逻辑
componentDidMount() {
console.log("componentDidMount")
}
render() {
return <Wrapcomponent />
}
}
}
const Childh=withLog(Child)
const GrandC=withLog(Son)
class App extends React.Component {
render() {
return <>
<Childh/>
<GrandC/>
</>
}
}
export default App
2、HOC传递属性
在高阶组件返回的新组件中通过props接收父组件传来的数据,因为有多个数据,所以通过扩展运算符将传来的数据展开
function withLog(Wrapcomponent) {
return class extends React.Component {
componentDidMount() {
console.log("componentDidMount")
}
render() {
return <Wrapcomponent {…this.props}/>
}
}
}
const Childh=withLog(Child)
const GrandC=withLog(GrandChild)
class App extends React.Component {
state={
msg:"我是高阶组件"
}
render() {
const {msg}=this.state
return <>
<Childh cData={msg} oData="hi"/>
<GrandC gData={msg}/>
</>
}
}
注:高阶组件也适用于函数组件
3、小案例
发送请求显示宠物照片
这个案例用到了axios,我们要在项目中安装axios
npm i axios
class Child extends React.PureComponent{
render(){
return<>
<img src={this.props.url} alt="" />
<h1>我是猫</h1>
</>
}
}
export default Child
class GrandChild extends React.PureComponent {
render() {
return <>
<img src={this.props.url} alt="" />
<h1>我是狗</h1>
</>
}
}
export default GrandChild
//高阶组件
function withPets(Wrapcomponent,url,type){
return class extends React.PureComponent{
state={
data:""
}
getData=()=>{
axios.get(url).then(res=>{
this.setState({
data:type===1?res.data.message:res.data[0].url
})
})
}
componentDidMount(){
this.getData()
}
render(){
return<>
<Wrapcomponent url={this.state.data} {…this.props}/>
</>
}
}
}
const Childc=withPets(Child,"https://dog.ceo/api/breeds/image/random",1)
const Grandc=withPets(GrandChild,"https://api.thecatapi.com/vl/images/search",2)
class App extends React.PureComponent{
render(){
return<>
<Childc/>
<Grandc/>
</>
}
}
export default App
注:当我们使用axios发送请求时,我要处理跨域问题,解决跨域问题的方案有很多,比较常用的就是proxy代理(开发环境)
使用create-react-app创建项目跨域处理
如果请求比较少的话,我们可以直接在package.json中进行配置
请求比较多的话,先在src文件夹下创建setupProxy.js文件(文件名不能修改必须是这个)
//setupProxy.js
const {createProxyMiddleware}=require("http-proxy-middleware")
module.exports=function(app){
app.use(createProxyMiddleware("/api"),{
target:"https://api.jisuapi.com/recipe",//后端服务器地址
changeOrigion:true,
pathRewrite:{
"/^api":""
}
})
}
使用vite创建项目跨域处理
在vite.config.js文件中配置跨域就可以了
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server:{
proxy:{
"/api":{//匹配所有以`/api`开头的请求
target:"https://api.jisuapi.com/recipe",//后端服务器地址
changeOrigin:true,//修改请求源(避免CORS问题)
rewrite:(path=>path.replace(/^\\/api/,""))//移除/api请求
}
}
}
})
六、相关面试题
1、React生命周期
面试中我们可以这样回答:
React生命周期分为三个阶段:挂载阶段、更新阶段、卸载阶段,在类组件中挂载阶段会依次调用constructor,getDerivedStateFromProps,render,componentDidMount。更新阶段:会调用getDerivedStateFromProps,shouldComponentUpdate,render,getSnapshotBeforeUpdate和componentDidUpdate。卸载阶段:componentWillUnmount。
React16.3版本之后废除了一些不安全的生命周期方法,如:挂载阶段:componentWillMount。更新阶段:componentWillReceiveProps,componentWillUpdate,shouldComponentUpdate,以适应fiber架构的异步渲染特性
函数组件没有生命周期但是我们可以使用useEffect Hook来模拟生命周期
面试中还有可能会问我们:
getDerivedStateFromProps有什么作用
它是一个静态方法,在render前进行调用,可以根据props的变化来更新state,替代了不安全的componentWillReceiveProps
在函数组件中怎么实现shouldComponentUpdate
shouldComponentUpdate是一个性能优化的生命周期,在函数组件中我们可以使用高阶组件Recat.memo包裹组件进行浅层比较,或者使用react提供的性能优化的hook,useMemo,useCallback来优化计算结果和函数
2、React组件的传值方式
React组件传值方式有以下几种方式:
(1)父子组件:父组件通过props向子组件传递数据,子组件可以通过回调函数向父组件传递数据
(2)跨组件通信:父组件可以通过context提供的provider来提供数据,后代组件可以通过consumer或useContext来使用数据,这样可以避免props层层传递
(3)复杂状态我们可以使用redux等状态管理库集中管理数据,还可以使用react提供的hook useReducer
(4)对于少量的兄弟组件之间的通信我们也可以使用状态提升,需要父组件作为中间桥梁,共享状态提升到最近的共同的父组件,一些特殊场景我们还可以通过refs访问组件实例或使用事件总线
在项目中,我会根据组件关系和数据类型选择合适的方式,比如简单的父子通信,使用props就可以了,主题切换业务可以用context,登录业务和权限管理,用状态管理库和本地存储相结合的方式,可以提高程序的性能,对于表单的一些业务复杂的交互,我会根据实际需求结合多种方式来实现,以避免不必要的重复渲染,优化性能
3、跨域
在面试中我们可以这样回答:
跨域就是浏览器不能访问其他网站的js脚本,这是由浏览器的同源策略引起的,同源策略就是同端口,同协议,同域名
我们通常有以下几种方式可以解决跨域:在开发环境时,我通常使用代理服务器或配置构建工具的proxy,在生产环境中,通常后端配置CORS就是服务端设置Access-Control-Allow-Origion,前端不用设置,如果是cookie请求前后端都需要设置,也可以使用反向代理Nginx开启一个代理服务,来实现数据的转发
还有几个不常用的跨域解决方案:可以通过JSONP动态创建script,再请求一个带参网址来实现跨域,JSONP只支持get请求,安全性能比较差,所以使用的很少。WebSocket支持任何域名和端口的访问,不受同源策略的影响,但它可能会受代理服务器配置的影响。还可以使用document.domain+iframe来实现跨域,两个页面都通过js强制设置document.domain为基础的主域来实现同域,此外,h5还提供了postMessage来实现跨域,它们在真实项目中实现起来复杂度高,而且也不安全,所以不建议使用
面试中还可能会问:
开发环境为什么需要特殊处理跨域:
开发环境前端是运行在localhost本地域名上的,而后端通常在另一个端口或域名上。在生产环境下前后端通常部署在同一个域名下,我们可以通过配置正确的CORS或Nginx来解决
4、React Vue样式隔离实现
在react中对于简单的小项目我们可以使用CSS Modules构建时转换类名为哈希字符串,生成JSON映射关系,来生成唯一类名,实现样式隔离。对于一些复杂的项目,我们可以使用css-in-js的emotion,它提供了动态的主题支持,内置的隔离机制,还可以使用css预处理器sass/less
在vue项目中,我们通常使用scoped来实现样式隔离,我们在style标签上加上scoped属性, 就会在此组件的标签上随机生成data-v开头的属性,必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到。在vue中我们也可以使用css Modules,直接在style标签上添加module属性就可以了
评论前必须登录!
注册