文章会涉及到react本身的基础知识(包括组件通讯、生命周期、路由管理、状态管理等方面),相信你认认真真看完这篇文章以后,你会对react开发有个大致的了解,并且能够快速入门。这篇文章也可用作面试复习 react 基础,并且这篇文章会持续更新,废话不多说,so ,Let's go!!!





组件通信

props

适用于父子组件通信

父组件->子组件

父组件将需要传递的参数通过key={xxx}方式传递至子组件,子组件通过this.props.key获取参数.

import React from 'react'import Son from './son'class Father extends React.Component {  constructor(props) {    super(props)    this.handleChange = this.handleChange.bind(this)  }  state = {    info: '父组件',  }  handleChange = (e) => {    this.setState({      info: e.target.value,    })  }  render() {    return (       {  constructor(props) {    super(props)  }  render() {    return (



{this.props.info}




) }}export default Son


子组件->父组件

利用 props callback 通信,父组件传递一个 callback 到子组件,当事件触发时将参数放置到 callback 带回给父组件.

// 父组件import React from 'react'import Son from './son'class Father extends React.Component {  constructor(props) {    super(props)  }  state = {    info: '',  }  callback = (value) => {    // 此处的value便是子组件带回    this.setState({      info: value,    })  }  render() {    return (



{this.state.info}




) }}export default Father// 子组件import React from 'react'interface IProps { callback: (string) => void}class Son extends React.Component { constructor(props) { super(props) this.handleChange = this.handleChange.bind(this) } handleChange = (e) => { // 在此处将参数带回 this.props.callback(e.target.value) } render() { return (


Context

适用于跨层级组件之间通信

// context.jsimport React from 'react'const { Consumer, Provider } = React.createContext(null) //创建 context 并暴露Consumer和Provideexport { Consumer, Provider }// 父组件import React from 'react'import Son from './son'import { Provider } from './context'class Father extends React.Component {  constructor(props) {    super(props)  }  state = {    info: 'info from father',  }  render() {    return (



{this.state.info}




) }}export default Father// 子组件import React from 'react'import GrandSon from './grandson'import { Consumer } from './context'class Son extends React.Component { constructor(props) { super(props) } render() { return ( {(info) => ( // 通过Consumer直接获取父组件的值


父组件的值:{info}




)} ) }}export default Son// 孙子组件import React from 'react'import { Consumer } from './context'class GrandSon extends React.Component { constructor(props) { super(props) } render() { return ( {(info) => ( // 通过 Consumer 中可以直接获取组父组件的值


组父组件的值:{info}




)} ) }}export default GrandSon


特别注意

如果需要消费多个 Context,则 React 需要使每一个 consumer 组件的 context 在组件树中成为一个单独的节点。

// provider...



{this.state.info}




{this.state.theme}




// consumer ... {(info: string) => ( // 通过Consumer直接获取父组件的值 {(theme: string) => (


父组件info的值:{info}




父组件theme的值:{theme}




)} )}


很多优秀的 React 组件的核心功能都通过 Context 来实现的,比如 react-redux 和 react-router 等,所以掌握 Context 是必须的。

OnRef

OnRef 的原理很简单,本质上就是通过 props 将子组件的组件实例(也是我们常说的 this)当作参数,通过回调传到父组件,然后在父组件就可以拿到子组件的实例,拿到了它的实例就可以调用它的方法(为所欲为)了。

// 父组件import React from 'react'import Son from './son'import { Button } from 'antd'class Father extends React.Component {  child: any  constructor(props) {    super(props)  }  sonRef = (ref) => {    this.child = ref // 在这里拿到子组件的实例  }  clearSonInput = () => {    this.child.clearInput()  }  render() {    return (



清空子组件的input


) }}export default Father// 子组件import React from 'react'import { Button } from 'antd'interface IProps { onRef: any}class Son extends React.Component { constructor(props) { super(props) } componentDidMount() { this.props.onRef(this) // 在这将子组件的实例传递给父组件this.props.onRef()方法 } state = { info: 'son', } handleChange = (e) => { this.setState({ info: e.target.value, }) } clearInput = () => { this.setState({ info: '', }) } render() { return (


{this.state.info}




) }}export default Son


ref

ref是react提供给我们的一个属性,通过它,我们可以访问 DOM 节点或者组件.

// 父组件import React from 'react'import Son from './son'import { Button } from 'antd'class Father extends React.Component {  son: any  constructor(props) {    super(props)    this.son = React.createRef() // 在此处创建ref  }  clearSonInput = () => {    const { current } = this.son // 注意,这里必须通过 this.son.current拿到子组件的实例    current.clearInput()  }  render() {    return (



清空子组件的input


) }}export default Father// 子组件import React from 'react'import { Button } from 'antd'class Son extends React.Component { constructor(props) { super(props) } state = { info: 'son', } handleChange = (e) => { this.setState({ info: e.target.value, }) } clearInput = () => { this.setState({ info: '', }) } render() { return (


{this.state.info}




) }}export default Son


值得注意的是,我们必须通过 this.childRef.current才能拿到子组件的实例。 使用 ref 常见的场景有管理焦点,文本选择或媒体播放、触发强制动画、集成第三方 DOM 库等。

第三方工具

events(发布订阅)

这种方式适用于没有任何嵌套关系的组件通信。其原理就是使用事件订阅。即是一个发布者,一个或者多个订阅者。 使用之前需要先安装:

yarn add events
// event.tsimport { EventEmitter } from 'events'export default new EventEmitter()// 发布者 通过emit事件触发方法,发布订阅消息给订阅者import React from 'react'import Son1 from './son1'import Son2 from './son2'import { Button } from 'antd'import emitter from './event'class Father extends React.Component {  son: any  constructor(props) {    super(props)  }  handleClick = () => {    //emit事件触发方法,通过事件名称找对应的事件处理函数info,将事件处理函数作为参数传入    emitter.emit('info', '我是来自father的 info')  }  render() {    return (



点击按钮发布事件


) }}export default Father// 订阅者1// 通过emitter.addListener(事件名称,函数名)方法,进行事件监听(订阅)。// 通过emitter.removeListener(事件名称,函数名)方法 ,进行事件销毁(取消订阅)import React from 'react'import { Button } from 'antd'import emitter from './event'class Son1 extends React.Component { constructor(props) { super(props) } state = { info: '', } componentDidMount() { // 在组件挂载完成后开始监听 emitter.addListener('info', (info) => { this.setState({ info: 'Son1收到消息--' + info, }) }) } componentWillUnmount() { // 组件销毁前移除事件监听 emitter.removeListener('info', (info) => { this.setState({ info: 'Son1即将移除事件监听--' + info, }) }) } render() { return (


{this.state.info}




) }}export default Son1// 订阅者2import React from 'react'import { Button } from 'antd'import emitter from './event'class Son2 extends React.Component { constructor(props) { super(props) } state = { info: '', } componentDidMount() { // 在组件挂载完成后开始监听 emitter.addListener('info', (info) => { this.setState({ info: 'Son2收到消息--' + info, }) }) } componentWillUnmount() { // 组件销毁前移除事件监听 emitter.removeListener('info', (info) => { this.setState({ info: 'Son2即将移除事件监听--' + info, }) }) } render() { return (


{this.state.info}




) }}export default Son2


路由

随着前端工程的复杂度越来越高,所以路由管理在现在的前端工程中,也是一个值得注意的点,vue提供了vue-router来管理路由。相似的,react则提供了react-router来管理路由。

react-router

react-router 包含 3 个,分别为react-router、react-router-dom 和 react-router-native。

react-router提供最基本的路由功能,但是实际使用的时候我们不会直接安装 react-router,而是根据应用运行的环境来选择安装 react-router-dom和react-router-native。因为react-router-dom 和 react-router-native 都依赖 react-router,所以在安装时,react-router 也会⾃自动安装。

其中react-router-dom 在浏览器器中使⽤,而react-router-native在 react-native 中使用。

在 react-router 里面,一共有 3 种基础组件,他们分别是

  • 路由组件(router components) 比如 和
  • 路由匹配组件(route matchers components) 比如 和
  • 导航组件(navigation components) 比如 , , 和

路由组件

对于 web 项目,react-roruter-dom 提供了 和 两个路由组件。

  • BrowserRouter:浏览器的路由方式,也就是使用 HTML5 提供的 history API ( pushState , replaceState 和 popstate 事件) 来保持 UI 和 url 的同步。这种方式在react开发中是经常使用的路由方式,但是在打包后,打开会发现访问不了页面,所以需要通过配置 nginx 解决或者后台配置代理。
  • HashRouter:在路径前加入#号成为一个哈希值,Hash 模式的好处是,再也不会因为我们刷新而找不到我们的对应路径,但是链接上面会有#/。在vue开发中,经常使用这种方式。

要使用路由组件,我们只需要确保它是在根组件使用,我们应该将包裹在路由组件下面:

import { BrowserRouter } from 'react-router-dom';......

匹配组件

有两种路由匹配组件: 和

这两个路由匹配组件通常在一起使用,在里面包裹多个,然后它会逐步去比对每个的path属性 和浏览器当前location的pathname是否一致,如果一致则返回内容,否则返回null。

{/* 如果当前的URL是`/about`,即 location = { pathname: '/about' } ,那么About组件就应该被渲染,其余的Route就会被忽略 */

值得注意 ⚠️ 的是: 只会匹配 URL的开头,而不是整个 URL。简单的来说就是它不是精确匹配 ,例如 和 它永远都只能匹配到,他们开头都有'/'。 在这里我们有两种解决方法:

  • 将此放在的最后一个位置
  • 另一种解决方案是添加'exact' 实现精确匹配:

所以组件只会 render 第一个匹配到的路由,像上面我们说的,如果没有设置 path,则一定会匹配,我们可以用来实现 404 的功能:

{/* 当上面的组件都没有匹配到的时候, 404页面 就会被 render */}



404页面


} />


导航组件

导航组件有, , 和 。

当我们使用的时候,在 html 页面会被渲染为一个a标签:

Home// Home

是一种特殊的 ,当中的地址和浏览器地址匹配成功的时候,会添加一个 style 样式,如下:

About

在 html 页面当中,它会被渲染为:

About

但是有时你可能想要强制跳转到某个页面,比如未登录不能进入首页,这个时候你可以使用

复制代码

状态管理

前端工程的复杂度越来越高,状态管理也是一个很重要的点。在 react 生态中,现在最火的状态管理方案就是redux。

redux

我们都知道,react 是单向的数据流,数据几乎都是通过 props 依次从上往下传:





一个组件的状态有两种方式改变:

  • 来自父组件的 props 改变了,那么这个组件也会重新渲染
  • 自身有 state,自身的 state 可以通过this.setstate方法改变

现在假如我们组件树的层级比较深,有很多子组件需要共享状态,那么我们只能通过状态提升来改变状态,将状态提升到顶级父组件改变,当顶级父组件的状态改变了,那么旗下的所有子节点都会重新渲染:





当出现这种情况当时候,你就该使用redux了。那么使用redux之后,就会变成这样:





以上 gif 动图很生动的展示了 redux 解决的问题,下面我们来介绍一下 redux 相关的知识点:

Store

在 redux 里面,只有一个Store,整个应用需要管理的数据都在这个Store里面。这个Store我们不能直接去改变,我们只能通过返回一个新的Store去更改它。redux提供了一个createStore来创建state

import { createStore } from 'redux'const store = createStore(reducer)
action

这个 action 指的是视图层发起的一个操作,告诉Store 我们需要改变。比如用户点击了按钮,我们就要去请求列表,列表的数据就会变更。每个 action 必须有一个 type 属性,这表示 action 的名称,然后还可以有一个 payload 属性,这个属性可以带一些参数,用作 Store 变更:

const action = {  type: 'ADD_ITEM',  payload: 'new item', // 可选属性}

上面这个例子就定义了一个名为ADD_ITEM的Action,它还携带了一个payload的参数。 Redux 可以用 Action Creator 批量来生成一些 Action。

reducer

在上面我们定义了一个Action,但是Action不会自己主动发出变更操作到Store,所以这里我们需要一个叫dispatch的东西,它专门用来发出action,不过还好,这个dispatch不需要我们自己定义和实现,redux已经帮我们写好了,在redux里面,store.dispatch()是 View发出 Action 的唯一方法。

store.dispatch({  type: 'ADD_ITEM',  payload: 'new item', // 可选属性})

当 dispatch 发起了一个 action 之后,会到达 reducer,那么这个 reducer 用来干什么呢?顾名思义,这个reducer就是用来计算新的store的,reducer接收两个参数:当前的state和接收到的action,然后它经过计算,会返回一个新的state。(前面我们已经说过了,不能直接更改state,必须通过返回一个新的state来进行变更。)

const reducer = function(prevState, action) {  ...  return newState;};

这个 reducer 是一个纯函数。纯函数的意思是说,对于相同的输入,只会有相同的输出,不会影响外部的值,也不会被外部的值所影响。纯函数属于函数式编程的概念,如果你想了解更多纯函数的概念,请看这里

可以看到,我们在创建store的时候,我们在createStore里面传入了一个reducer参数,在这里,我们就是为了,每次store.dispatch发送一个新的action,redux都会自动调用reducer,返回新的state。

那么当项目特别大特别复杂的时候,state 肯定是非常大的一个对象,所以我们需要写很多个 reducer,那么在这里,我们就需要把 reducer 进行拆分。每个 reducer 只负责管理 state 的一部分数据。那么我们如何统一对这些 reducer 进行管理呢?redux 给我们提供了 combineReducers 方法,顾名思义,就是将所有的子 reducer 合成一个 reducer,方便我们管理。

import { combineReducers } from 'redux'import listReducer from './listReducer/reducers'import detailReducer from './detailReducer/reducers'import aboutReducer from './aboutReducer/reducers'const rootReducer = combineReducers({  listReducer,  detailReducer,  aboutReducer,})export default rootReducer
中间件

熟悉koa的朋友们,应该知道中间件的概念。中间件的意思简单理解就是,在某两个操作之间,我们需要进行某些操作。那么在 redux,我们为什么要引入中间件呢?到目前为止,我们来捋一下我们刚刚已经进行的步骤:

  1. 创建 store
import { createStore } from 'redux'const store = createStore(reducer)
  1. 发出 action
store.dispatch({  type: 'ADD_ITEM',  payload: 'new item', // 可选属性})
  1. reducer 计算返回新的 state
const reducer = function(prevState, action) {  ...  return newState;};

我们发现,我们这次发起的变更,都是同步操作,那么问题来了。假如我们state里面有一个列表:list,用户根据在view上面点击了一些筛选条件,发起请求,然后变更state里面list的值。在这里,有异步请求,但是我们变更 redux 的过程都是同步的,显然是不支持异步的,所以这里就用到中间件了。那么我们应该将异步请求放在以上哪个步骤去执行呢?显然第 1 步和第 3 步不可能,其中第 1 步只是在创建 store,第 3 步 reducer 是纯函数,根本不可能加入异步操作。所以我们很自然的想到,就是在 store.dispatch 的之后,到达reducer之前进行异步操作:

store.dispatch = function(prevAction) async{  console.log("发请求啦");  // 异步操作执行完成之后才派发action  const list = await getList();  // 把 list 放到action里面  const newAction = {    type: prevAction.type,    payload:list  }  store.dispatch(newAction);};

就是给store.dispatch再包裹一层,这就是中间件的原理。 redux 常见的中间件有redux-thunx、redux-promise、redux-saga。相关的详细用法在这里不再赘述(下面会介绍dva-core的用法)。 redux 应用中间件的方法:

import { applyMiddleware, createStore } from 'redux'import myMiddleware from './myMiddleware'const store = createStore(reducer, applyMiddleware(myMiddleware))
通知变更

那么到这一步了,我们变更了 state,下一步是将变更通知给 view 了。在 redux 里面,提供了store.subscribe(listener)这个方法,这个方法传入一个listener,比如在 react 里面,listener可以是this.setState(xxx),每当 redux 里面的state改变了,通过store.subscribe(listener)我们的页面也会重新渲染。意思是我们每个页面都得手动去store.subscribe(listener),这也太麻烦了吧,对吧。

react-redux 和 redux

为了解决上述的痛点问题,更好的将 redux 和 react 结合,官方给我们提供了react-redux这个包(可能你到现在脑子有点乱了,我刚开始也是)。那么现在,我们需要明确一个概念:redux 和 react 是两个八竿子不着的人。redux 只是一个状态管理框架,react 只是一个前端应用框架。redux 可以用于前端任何框架,例如 vue,甚至纯 javaScript 都可以。后来 react-redux 出现了,他把 redux 和 react 撮合在一起了,于是他们两强强联合,风云合璧,所向披靡,好了不扯了。说了这么多就是想说明 react-redux 这个包的作用。

在详细说明react-redux的作用之前,我们先介绍以下知识点: react-redux将 react 组件划分为容器组件和展示组件,其中

  • 展示组件:只是负责展示 UI,不涉及到逻辑的处理,数据来自父组件的props;
  • 容器组件:负责逻辑、数据交互,将 state 里面的数据传递给展示组件进行 UI 呈现

容器组件是react-redux提供的,也就是说,我们只需要负责展示组件,react-redux负责状态管理。

我们知道,redux提供了一个大的state。这里我们需要面对两个问题,第一个问题,如何让我们react项目里面的所有组件都能够拿到state?;第二个,每当state变更之后,组件如何收到变更信息?

Provider

针对第一个问题,react-redux提供了Provider组件。用这个Provider包裹根组件,将redux导出的state,作为参数往下面传

import React from "react";import { Provider } from "react-redux";import App from "./App";import { store } from "./store"; // 这个store由redux导出···;···return

这样所有的组件都能拿到state了。这个 Provider 组件原理就是通过react的Context来实现的,我们可以看看源码:

....const Context = context || ReactReduxContext;return {children};....

这里的contextValue就包裹了我们传入的store,很明显,它创建了 Context,通过{children}这种方式将我们传入的store提供给了react所有组件。

connect

在上面我们知道怎么将 redux 暴露出来的 state 提供给 react 组件的,那么接下来,我们在某个子组件里面如何收到 state 的变更呢?react-redux给我们提供了connect方法。这个方法可以传入两个可选参数:mapStateToProps和mapDispatchToProps,然后会返回一个容器组件,这个组件可以自动监听到 state 的变更,将 state 的值映射为组件的 props 参数,之后我们可以直接通过 this.props 取到 state 里面的值。

const mapStateToProps = (state) => ({  goodsList: state.goodsList,  totalCount: state.totalCount,});export default connect(  mapStateToProps, // 可选// mapDispatchToProps, // 可选(GoodsList);

mapStateToProps就是将 state 的值映射为组件的props,mapDispatchToProps就是将store.dispatch映射为props。如果我们不传mapDispatchToProps的话,connect会自动将 dispatch 注入到 props 里面,我们在组件里可以直接通过this.props.dispatch发起一个action给reducer。

connected-react-router 和 redux

当我们在项目中同时用了react-router 和 redux的时候,我们可以把他们两个深度整合。我们想要在store里面拿到router,甚至可以操作router,还可以记录router的改变。例如我们把用户是否登录的状态存在redux里面,在登录之后会进行页面的跳转。正常的操作是我们在发起请求之后,得到一个状态,此时我们需要dispatch一个action去改变redux的状态,同时我们需要进行路由的跳转,类似于这样:store.dispatch(replace('/home'))。想要实现这种深度整合,我们需要用到 connected-react-router和history两个库。

首先需要history生成history对象,结合connected-react-router的connectRouter生成routerReducer,同时利用connected-react-router的routerMiddleware实现dispatch action导航,也就是我们刚刚说的store.dispatch(replace('/home')):

// APP.tsxconst createHistory = require('history').createBrowserHistoryexport const history = createHistory()// reducer/index.tsconst routerReducer = connectRouter(history)const routerMiddlewareForDispatch = routerMiddleware(history)const middleware = [routerMiddlewareForDispatch]

接着利用redux的combineReducers将我们自己的reducer和routerReducer合并起来,组成rootReducer,然后利用 createStore创建store并暴露出去:

// reducer/index.tsexport default function geneGrateSotore(history: any) {  const routerReducer = connectRouter(history)  const routerMiddlewareForDispatch = routerMiddleware(history)  const middleware = [routerMiddlewareForDispatch]  //合并routerReducer  const rootRuder = combineReducers({    info: infoRuder,    router: routerReducer,  })  const store = createStore(rootRuder, applyMiddleware(...middleware))  return store}

最后我们在App.tsx导入刚刚我们创建的这个方法,生成store,接着将我们创建的history对象传入connected-react-router的ConnectedRouter组件作为props,并用它包裹我们的Router组件:

// App.tsximport React from 'react'import { Provider } from 'react-redux'import { ConnectedRouter } from 'connected-react-router'import geneGrateSotore from './store'import Router from './router'import './App.css'const createHistory = require('history').createBrowserHistoryconst history = createHistory()const store = geneGrateSotore(history)const f: React.FC = () => {  return (      )}export default f

这样我们就将connected-react-router 和 redux整合起来了。现在当我们在View利用Link进行路由跳转的时候,会通过react-router-dom进行路由跳转,并且也会通过connected-react-router发起一个action去更新redux state里面的router对象,以记录路由的变化。同时现在我们在状态管理的时候,也可以直接通过connected-react-router提供的push、replace等方法了,他们是从 redux 出发,也就是说先发起一个action,然后再进行路由跳转。

小结一下

看了上面的这些东西,是不是感觉脑子贼乱,什么react、redux、react-redux、react-router、react-router-dom、connected-react-router,这些概念是真的多,我刚开始接触的时候直接看懵逼。。所以 react 的可组合性比 vue 高很多,所以说 vue 真的是自动挡、react 是手动挡。但是我们只需记住,前端的一切概念,都是纸老虎而已。静下心来捋一捋,很快就理解了。好了,现在来看看这个图:





结合上面介绍的知识点,把思路捋顺,咱们再继续

dva

通过上面的这些工具,我们已经可以搭建一个很棒的基于react和redux的工程。如果单纯使用redux,当项目越来越大的时候,我们的redux的操作也会变得繁杂,代码和文件的组织会变得臃肿,我们就必须把action、reducer、createActions、actionType等等文件放在不同的目录下面,当我们需要使用的时候,就要打开文件目录去翻半天,我们需要在这些文件里面不停切换,简直抓狂。所以我们不禁会想:要是所有的操作都放在一个文件该多好!没错,它来了。它就是dva。

dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

因为我们前面已经自己组织了react-router,所以我们只使用dva-core,有了这个,我们就可以将 reducers, effects 等等组织在一个model文件里面了。以前我们组织代码的方式是createAction.ts、actionType.ts、reducer/xxx.ts,但是通过dva,我们就可以把这些都写在一个文件里面。接下来我们来看看如何配置dva-core:

首先我们需要通过dva create暴露一个dva app:

// dva/index.tsximport React from 'react'import { create } from 'dva-core'import { Provider } from 'react-redux'export default function(options: any) {  const app = create(options)  options.models.forEach((model: any) => app.model(model))  app.start()  const store = app._store  app.start = (container: any) => {    return () => {container}  }  app.getStore = () => store  return app}

上面的 options 即是我们的 model 文件。然后我们需要利用这个 app 重新配置我们的 store:

/// store/index.tsimport { connectRouter, routerMiddleware } from 'connected-react-router'import dva from './dva'import models from '../models'export default function geneGrateSotore(history: any) {  const routerReducer = connectRouter(history)  const routerMiddlewareForDispatch = routerMiddleware(history)  const app = dva({    models,    initState: {},    extraReducers: { router: routerReducer },    onAction: [routerMiddlewareForDispatch],  })  return app}

然后一样的,我们在App.tsx使用它即可:

import React from 'react'import { ConnectedRouter } from 'connected-react-router'import geneGrateSotore from './store'import Router from './router'import './App.css'const createHistory = require('history').createBrowserHistoryconst history = createHistory()const app = geneGrateSotore(history)const f: React.FC = app.start(  )export default f

这和之前配置 redux 对比起来,我们已经省了很多的文件了,我们的 store 文件清净多了。下面我们要来编写model文件了。例如有个模块叫model1:

// model/model1.tsexport const namespace = 'model1'interface IDvaState {  info1: string}const state: IDvaState = {  info1: 'init info1',}const model1 = {  namespace,  state,  effects: {    *changeInfo1({ payload }, { put }) {      try {        const { text } = payload        yield put({          type: 'setState',          payload: { info1: text },        })        // eslint-disable-next-line no-empty      } catch (error) {}    },  },  reducers: {    setState(state: IDvaState, { payload }) {      return { ...state, ...payload }    },  },}export default [model1]

这个文件可能有的小伙伴一下子看不明白,接下来我们来一起捋一捋。

  • namespace: 命名空间(我们组件可以通过这个 namespace 拿到我们的 model,从而取值和进行操作)
  • state:这个就是我们需要管理的状态(存放状态的地方)
  • effects:一些操作,我们可以把同步操作和异步操作都放到effects里面,简单来说就是我们的业务逻辑。这里会详细介绍,咱们先明白它是做什么的
  • reducers: reducer这个概念就是跟redux的reducer是同一个意思(返回一个newState,更新state) 好了有了上面这些概念,接下来我们来看看如何把组件和model联系起来。
// home.tsximport React, { Component } from 'react'import { connect } from 'react-redux'import { Button } from 'antd'import { namespace } from '../models/model1'interface Iprops {  info1: string  dispatch: any}export class Home extends Component {  changeFormHome = () => {    const { dispatch } = this.props    dispatch({      type: `${namespace}/changeInfo1`,      payload: {        text: '从Home改变的info1',      },    })  }  render() {    console.log(this.props.info1)    return (



我是home页




{this.props.info1}


home点击更改redux


) }}const mapStateToProps = (model: any) => ({ info1: model[namespace].info1,})export default connect(mapStateToProps)(Home)


看了上面的代码,你是不是瞬间理解了,咱们还是通过react-redux的connect来获取model的,只不过我们通过namespace指定了具体是哪个model。到目前为止,咱们已经知道,组件如何从model里面取值。那么我们在组件里面如何改变 model里面的 state 呢?

现在我们的home.tsx页面上有一个按钮,点击这个按钮会dispatch一个action:

... dispatch({      type: `${namespace}/changeInfo1`,      payload: {        text: '从Home改变的info1',      },    })...

可以看到,这个 action 的 type 为我们的model/effects/changeInfo1。看到这里相信你已经理解得差不多了,对的,就是通过 dispatch 一个和 effects 里面的一个同名方法,即可找到 effects 对应的方法。 接下来到effect对应的changeInfo1看看是如何改变 state。

... *changeInfo1({ payload }, { put }) {      try {        const { text } = payload        yield put({          type: 'setState',          payload: { info1: text },        })        // eslint-disable-next-line no-empty      } catch (error) {}    }...

我们通过 put ,发起了一个action,到最终的reducer,其中的payload来自我们传入的text,然后state被修改了,同时页面也刷新了:





除了put,还有call(用于发起异步请求)、select(用于取出 state 里面的值),都是redux-saga提供的,这里不过多叙述,具体使用的方法请查阅redux-saga文档。

我这里整理了两个图,对比redux和dva的流程操作:







简单总结一下:dva 把 action -> reducer 拆分成了,action -> model(reducer, effect)我们在写业务的时候,再也不用到处去找我们的reducer、action了。dva真的给我们提供了极大的便利。

生命周期(新旧对比)

旧版生命周期

旧版生命周期 指的是 React 16.3 及其之前的版本。





请看下面两段代码

// 父组件import React, { Component } from 'react'class Parent extends Component {  static defaultProps = {      info:'parent info'  }  constructor(props){    super();    //初始化默认的状态对象    this.state = {      name:'parent life'    };    console.log('1. constructor 初始化 props 和 state')  }  componentWillMount(){    console.log('2. componentWillMount 组件将要挂载')  }  //一般在componentDidMount执行副作用,如异步请求  componentDidMount() {    console.log('4. componentDidMount 组件挂载完成')    this.fetch() //异步请求  }  shouldComponentUpdate(nextProps,nextState){    console.log('5. shouldComponentUpdate 询问组件是否需要更新, 返回true则更新,否则不更新')    return true;  }  componentWillUpdate(nextProps, nextState){    console.log('6. componentWillUpdate 组件将要更新')  }  componentDidUpdate(prevProps, prevState)){    console.log('7. componentDidUpdate 组件更新完毕')  }  changeName = ()=>{      this.setState({number:this.state.number})  };  render() {    console.log('3.render渲染')    return (      const { info } = this.props      const { name } = this.state



{this.props.info}:{this.state.name}


更改name


) }}export default Parent// 子组件import React, { Component } from 'react'class Son extends Component { constructor(props) { super(props) this.state = { info: 'son info', } } componentWillUnmount() { console.log('Son componentWillUnmount 组件将要挂载') } //调用此方法的时候会把新的属性对象和新的状态对象传过来 shouldComponentUpdate(nextProps, nextState) { if (nextProps.parentName !== 'parent info') { return true } else { return false } } //componentWillReceiveProp 组件收到新的属性对象 componentWillReceiveProps() { console.log('1.Son组件 componentWillReceiveProps') } render() { console.log(' 2.Son组件 render') return (


{this.props.parentName}




) }}export default Son


react 父子组件的渲染顺序遵循洋葱模型

新版生命周期





static getDerivedStateFromProps

  • static getDerivedStateFromProps(nextProps,prevState):接收父组件传递过来的 props 和组件之前的状态,返回一个对象来更新 state 或者返回 null 来表示接收到的 props 没有变化,不需要更新 state.
  • 该生命周期钩子的作用: 将父组件传递过来的 props 映射 到子组件的 state 上面,这样组件内部就不用再通过 this.props.xxx 获取属性值了,统一通过 this.state.xxx 获取。映射就相当于拷贝了一份父组件传过来的 props ,作为子组件自己的状态。注意:子组件通过 setState 更新自身状态时,不会改变父组件的 props
  • 配合 componentDidUpdate,可以覆盖 componentWillReceiveProps 的所有用法
  • 该生命周期钩子触发的时机:
  1. 在 React 16.3.0 版本中:在组件实例化、接收到新的 props 时会被调用
  2. 在 React 16.4.0 版本中:在组件实例化、接收到新的 props 、组件状态更新时会被调用
// 根据新的属性对象派生状态对象  // nextProps:新的属性对象 prevState:旧的状态对象  static getDerivedStateFromProps(nextprops, state) {    console.log('props',nextprops);    // 返回一个对象来更新 state 或者返回 null 来表示接收到的 props 不需要更新 state    if (nextProps.parentName !== 'parent info') {      return {        info: nextprops.parentName,        // 注意:这里不需要把组件自身的状态也放进来      };    }    return null;  }

getSnapshotBeforeUpdate

  • getSnapshotBeforeUpdate(prevProps, prevState):接收父组件传递过来的 props 和组件之前的状态,此生命周期钩子必须有返回值,返回值将作为第三个参数传递给 componentDidUpdate。必须和 componentDidUpdate 一起使用,否则会报错。
  • 该生命周期钩子触发的时机 :被调用于 render 之后、更新 DOM 和 refs 之前
  • 该生命周期钩子的作用: 它能让你在组件更新 DOM 和 refs 之前,从 DOM 中捕获一些信息(例如滚动位置)
  • 配合 componentDidUpdate, 可以覆盖 componentWillUpdate 的所有用法
  • demo:每次组件更新时,都去获取之前的滚动位置,让组件保持在之前的滚动位置
getSnapshotBeforeUpdate() {    // 返回更新内容的高度    return this.wrapper.current.scrollHeight;  }componentDidUpdate(prevProps, prevState, prevScrollHeight) {// 在这里拿到高度    this.wrapper.current.scrollTop =      this.wrapper.current.scrollTop +      (this.wrapper.current.scrollHeight - prevScrollHeight);  }

版本迁移

  • componentWillMount,componentWillReceiveProps,componentWillUpdate 这三个生命周期因为经常会被误解和滥用,所以被称为 不安全(不是指安全性,而是表示使用这些生命周期的代码,有可能在未来的 React 版本中存在缺陷,可能会影响未来的异步渲染) 的生命周期。
  • React 16.3 版本:为不安全的生命周期引入别名 UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate。(旧的生命周期名称和新的别名都可以在此版本中使用)
  • React 16.3 之后的版本:为 componentWillMount,componentWillReceiveProps 和 componentWillUpdate 启用弃用警告。(旧的生命周期名称和新的别名都可以在此版本中使用,但旧名称会记录 DEV 模式警告)。

此段总结均来自你真的了解 React 生命周期吗

性能优化

我们都知道,react 是数据驱动视图的变化,即是通过reder来渲染视图,当数据(即状态)变化时,我们的页面就应当重新渲染。但是应用复杂之后就会出现这种情况:一个父组件 A 下面包含了多个子组件 B、C、D。假如 B、C 组件用到了父组件 A 的某个属性,子组件 D 却没有用到这个属性,当父组件的这个属性改变的时候,他下面的子组件 B、C 组件重新渲染,但是子组件 D 本不需要重新渲染,但是他没办法,他也被重新渲染了。这就造成了性能浪费了。说这么多,不如我们来看个例子:

// 父组件import React, { Component } from 'react'import { Button } from 'antd'import Son1 from './son1'import Son2 from './son2'import Son3 from './son3'interface Istate {  info1: string  info2: string}export class Parent extends Component {  state: Istate = {    info1: 'info1',    info2: 'info2',  }  info1Change = () => {    this.setState({      info1: 'info1被改变了...',    })  }  render() {    return (



父组件


点击更改info1


) }}export default Parent// 子组件1import React, { Component } from 'react'interface Iprops { info1: string}class Son1 extends Component { render() { console.log('son1重新渲染了....') return (


我是son1




{this.props.info1}




) }}export default Son1// 子组件2import React, { Component } from 'react'interface Iprops { info2: string}class Son2 extends Component { render() { console.log('son2重新渲染了....') return (


我是son2




{this.props.info2}




) }}export default Son2


上面这个例子,父组件提供了两个值:info1 和 info2,其中 Son1 组件只用到了 info1,Son2 组件只用到了 info2。我们在父组件中,点击了按钮改变了 info1 的值,父组件必须重新渲染,因为它自身状态改变了,Son1 也应该重新渲染,因为它依赖于 info1,而 Son2 是否应该重新渲染呢?按道理,它不应该重新渲染,因为 info2 没有改变,但是当我们每次点击按钮改变 info1 的时候,Son1 和Son2 都重新渲染了,这就明显存在问题了。

shouldComponentUpdate

在上面 生命周期章节,我们讲到了shouldComponentUpdate这个生命周期钩子,它接收两个参数,一个是下一次的 props 和下一次的 state,在这里,我们拿到下一次的 props(nextProps)和当前的 props 进行比较,根据我们的场景,返回一个 bool 变量,返回 true,则表示要更新当前组件,返回 false 则表示不更新当前组件。

import React, { Component } from 'react'interface Iprops {  info2: string}class Son2 extends Component {  // 利用生命周期 shouldComponentUpdate 进行比较  shouldComponentUpdate(nextProps: Iprops, nextState: any) {    if (nextProps.info2 === this.props.info2) return false    return true  }  render() {    console.log('son2重新渲染了....')    return (



我是son2




{this.props.info2}




) }}export default Son2


当我们再次点击按钮更改info1的值,发现Son2就不会再重新渲染了。

PureComponet

react为我们提供了PureComponet的语法糖,用它也可以用作组件是否渲染的比较。它的原理就是内部实现了shouldComponentUpdate。让我们用PureComponet来改造一下刚刚的Son2组件:

import React, { PureComponent } from 'react'interface Iprops {  info2: string}class Son2 extends PureComponent {  render() {    console.log('son2重新渲染了....')    return (



我是son2




{this.props.info2}




) }}export default Son2


再次点击按钮改变info1的值,发现Son2也不会渲染了。

虽然PureComponent帮我们很好的实现了shouldComponentUpdate,但是它也是有缺点的。它只能用作对象的浅层比较,也就是它只会进行一层比较,当我们的数据是嵌套的对象或者数组的时候,它就无法比较了。所以PureComponent最好只用于展示型组件。

除了以上缺点以外,PureComponent还有一些另外值得我们注意的地方:

当我们给PureComponent包裹的子组件传入一个立即执行函数的时候,父组件的状态改变的时候,这个子组件始终会重新渲染:

{}} />

这个问题的出现是因为 change 这个函数每次都会执行,所以导致 Son2 组件每次都会重新渲染。这个问题的解决方法很简单,有两种方法:

  • 第一种,将这个立即执行函数,抽取到类方法上,并且在constructor bind this:
constructor(props: any) {    super(props)    this.change = this.change.bind(this)  }  state: Istate = {    info1: 'info1',    info2: 'info2',  }  info1Change = () => {    this.setState({      info1: 'info1被改变了...',    })  }  change() {}  render() {    return (



父组件


点击更改info1


) }


  • 第二种,利用箭头函数将立即函数抽取成类属性:
state: Istate = {    info1: 'info1',    info2: 'info2',  }  info1Change = () => {    this.setState({      info1: 'info1被改变了...',    })  }  change = () => {}  render() {    return (



父组件


点击更改info1


) }


Memo

刚刚我们介绍了PureComponent,但是这只是用于class组件,当我们用函数组件时,react 也给我们提供了一种方式:memo

import React, { memo } from 'react'interface Iprops {  info2: string}const Son3: React.FC = (props) => {  console.log('son3重新渲染了....')  return (



我是Son3




{props.info2}




)}export default memo(Son3)


不过使用 memo 的时候也有PureComponent的限制,我们仍然需要注意。

看到了这里,肯定很多同学都在问,hooks 哪去了?别急呢,马上就来

hooks

随着react 16.8版本的出现,hooks也问世了。hooks解决了class组件饱受诟病的众多问题,比如绑定this的问题、组件的逻辑复用的问题等等。其实起初我对hooks并不十分感冒,因为那个时候笔者连class写法都还没掌握,再学个这玩意简直徒增烦恼。后来没办法,团队里的小伙伴都开始使用hooks,所以我也被动学习了一波。这不写不知道,一写就真香!!!所以赶紧学起来吧

useState

记住一个点,useState返回两个参数,一个是state(也就是我们的state)、一个是用于更新state的函数。其实你叫啥名都行,不过咱们为了给 hooks 一个标志,大多采用如下写法:

const [name, setName] = useState(initState)

好了,掌握这个就行了,我们来使用一下:(别忘记了,我们现在只需要写函数式组件了哦)

import React, { useState } from 'react'import { Button } from 'antd'const Home: React.FC = ({ dispatch, goodsList }) => {  const [info, setInfo] = useState('init info')  return (



{info}


setinfo('改变info')}> 点击更改info


)}export default Home


当我们初次进入 home 页面时,页面上会显示 info 的初始值:init info,然后当我们点击按钮,调用 setInfo,然后 info 的值就被改变了。就是这么简单。

useEffect

useEffect 其实只比复杂了那么一点。它合成了 calss 组件中的componentDidMount、componentDidUpdate、 componentWillUnmount。 我们很容易就明白,它是用来执行副作用的,最最常见的副作用就是异步请求。

下面我们用它来实现class组件的componentDidMount的用法:

const Home: React.FC = ({ dispatch, goodsList }) => {  // 获取商品列表  const getList = () => {    dispatch({      type: `${namespace}/getGoodsList`,    })  }  useEffect(() => {    getList() // 调用方法发起异步请求  }, [])  return (



{goodsList.map((item, index) => { return }> })}




)}const mapStateToProps = (model) => ({ goodsList: model[namespace].goodsList,})export default connect(mapStateToProps)(Home)


上面的getList就是咱们发起异步请求的方法,我们在useEffect里面使用了它,同时我们还传入了一个[],就表示我们只需在页面初始化的时候发起请求,这样使用就相当于class组件的componentDidMount。

接下来我们再来实现class组件的componentDidUpdate的用法:

import React, { useState, useEffect } from 'react'import { connect } from 'react-redux'import { Button, Card } from 'antd'import { namespace } from '../models/model1'interface Iprops {  goodsList: any[]  dispatch: any}const Home: React.FC = ({ dispatch, goodsList }) => {  const [info, setInfo] = useState('init info')  // 获取商品列表  const getList = () => {    dispatch({      type: `${namespace}/getGoodsList`,    })  }  useEffect(() => {    getList()  }, [info])  return (



我是home页




{info}


setInfo('改变info')}> 点击更改info


{goodsList.map((item, index) => { return }> })}




)}const mapStateToProps = (model) => ({ goodsList: model[namespace].goodsList,})export default connect(mapStateToProps)(Home)


看上面,我们希望点击按钮时改变 info 时,它会自动再去发起请求,从而刷新页面(也就是说,goodsList 的数据依赖于 info)。可以看见,我们这里还是利用了useEffect的第二个参数,只不过这次我们传入的是[info],意思就是告诉useEffect,如果 info 的值发生改变了,就去发起请求。这相当于我们在class组件的componentDidMount钩子。

接下来还有最后一个class组件的componentWillUnmount的用法了。这个就更简单了,我们只需要在useEffect return 一个回调函数,就可以用来清除上一次副作用留下的副作用了:

.... useEffect(() => {    getList()    return () => dispatch({ type: `${namespace}/clearData` })  }, [])....

useRef

这个hook更简单了,它就是用来拿到子组件的实例的,相当于class组件的React.createRef():

import React, { useState, useEffect, useRef } from 'react'import { connect } from 'react-redux'import { Button, Card } from 'antd'import { namespace } from '../models/model1'import Son from './components/son'interface Iprops {  goodsList: any[]  dispatch: any}const Home: React.FC = ({ dispatch, goodsList }) => {  const sonRef = useRef(null) // 在这里新建一个子组件的ref  const [info, setInfo] = useState('init info')  // 获取商品列表  const getList = () => {    conson.log(sonRef.current) // 在这里就可以通过sonRef拿到子组件    dispatch({      type: `${namespace}/getGoodsList`,    })  }  useEffect(() => {    getList()  }, [info])  return (



我是home页




{info}


setInfo('改变info')}> 点击更改info


{goodsList.map((item, index) => { return }> })}




)}const mapStateToProps = (model) => ({ goodsList: model[namespace].goodsList,})export default connect(mapStateToProps)(Home)


useContext

useContext这个hook的作用也很简单,它可以让我们在函数组件中使用Context,而且它还解决了以前我们需要利用Consumer包裹组件的问题:

// context.jsimport React from 'react'const { Provider, Consumer } = React.createContext(null) //创建 context 并暴露Provider和Consumerexport { Provider, Consumer }// 父组件import React from 'react'import Son from './son'import { Provider } from './context'class Father extends React.Component {  constructor(props) {    super(props)  }  state = {    info: 'info from father',  }  render() {    return (          )  }}export default Father

在 class 组件里面,我们要想拿到 Context 里面的值,必须通过 Consumer 包裹组件:

// 子组件import React from 'react'import { Consumer } from './context'class Son extends React.Component {  constructor(props) {    super(props)  }  render() {    return (              {(info) => (          // 通过Consumer直接获取父组件的值



父组件的值:{info}




)} ) }}export default Son


有了 useContext,就只需要这样:

// 子组件import React from 'react'funcion Son() {  const info = useContext(Context)  render() {    return (

父组件的值:{info}


) }}export default Son


我们可以看到上面直接使用 React.useContext(Context) 就可以获得 context,而在之前的版本中需要像这样才能获取 ({vlaue} => {}) ,这极大的简化了代码的书写。

useMemo

先来看看官网给出的用法:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

根据官网的解释和这个用法可以看出,在 a 和 b 的变量值不变的情况下,memoizedValue的值不变。即是:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的(有点像vue的计算属性)。那它有什么用呢?通常来说可以用作性能优化的手段。我们来看一个例子:

// 父组件import React, { useState } from 'react'import { Input } from 'antd'import Son1 from './son1'interface Iprops {}const Home: React.FC = () => {  const [info, setInfo] = useState('')  const [visible, setVisible] = useState(true)  const onVisible = () => {    setVisible((visible) => !visible)  }  const changeInfo = (e: React.ChangeEvent) => {    const value = e.target.value    setInfo(value)  }  return (



{info}


changeInfo(e)}>


)}export default Home// 子组件import React from 'react'import { Button } from 'antd'interface Iprops { onVisible: () => void}const Son1: React.FC = ({ onVisible }) => { console.log('我被重新渲染了....') return (


onVisible()}>button


)}export default Son1


在父组件中,有个Input输入框,每次输入新的值,父组件的info的值就会发生改变,同时我们发现子组件每次都会重新渲染,即使我们子组件没用到info的值,那是因为setInfo导致父组件重新渲染了,也导致onVisible每次都变成一个新的值,所以引起子组件重新渲染。那么有的同学就会说,可以利用React.memo,我们来试一试:

import React, { memo } from 'react'import { Button } from 'antd'interface Iprops {  onVisible: () => void}const Son1: React.FC = ({ onVisible }) => {  console.log('我被重新渲染了....')  return (



onVisible()}>button


)}export default memo(Son1)


然后我们随便在输入框输入新的值,我们发现,子组件仍然会重新渲染,为什么呢?那是因为这里的props.onVisible是一个函数,它是一个引用类型的值,当父组件重新渲染onVisible 这个函数也会重新生成,这样引用地址变化就导致对比出新的数据,子组件就会重新渲染。所以我们需要缓存onVisible这个函数,即是:我们只需要创建一遍这个函数,以后父组件重新渲染的时候,onVisible的值仍然是第一次渲染的值,这样子组件才不会重新渲染。这个时候我们就用到了useMemo:

import React, { useState } from 'react'import { Input } from 'antd'import Son1 from './son1'interface Iprops {}const Home: React.FC = () => {  const [info, setInfo] = useState('')  const [visible, setVisible] = useState(true)  const onVisible = useMemo(() => {    return () => {      setVisible((visible) => !visible)    }  }, [])  const changeInfo = (e: React.ChangeEvent) => {    const value = e.target.value    setInfo(value)  }  return (



{info}


changeInfo(e)}>


)}export default Home


可以看到,我们利用useMemo将onVisible缓存起来了,我们在useMemo的第二个参数传入了一个[],表明它只会在渲染时执行一次,这里的用法跟useEffect一样,[]传入依赖项,当依赖项改变时,我们缓存的值才会重新计算。再次在输入框输入新的值,我们发现子组件不渲染了。

useMemo 一般用于计算比较复杂的场景

useCallback

如果掌握了useMemo,那掌握 useCallback简直不在话下。我们先来看看定义:

const memoizedCallback = useCallback(() => {  doSomething(a, b)}, [a, b])

在 a和b 的变量值不变的情况下,memoizedCallback 的引用不变。即:useCallback 的第一个入参函数会被缓存,从而达到渲染性能优化的目的。是不是跟useMemo很像?useMemo是缓存值,useCallback一个是缓存函数的引用。也就是说 useCallback(fn, [deps]) 相当于 useMemo(() => fn, [deps])。我们现在用 useCallback 来改造一下刚刚上面 那个例子:

....const Home: React.FC = () => {  const [info, setInfo] = useState('')  const [visible, setVisible] = useState(true)  // const onVisible = useMemo(() => {  //   return () => {  //     setVisible((visible) => !visible)  //   }  // }, [])  const onVisible = useCallback(() => {    setVisible(visible => !visible)  }, [])  const changeInfo = (e: React.ChangeEvent) => {    const value = e.target.value    setInfo(value)  }  return (



{info}


changeInfo(e)}>


)}export default Home


我相信你肯定已经看懂了,什么?没看懂?那再看一遍!

自定义 hook

借助于react提供的基础hook,我们通常也可以自定义hook,react规定我们自定义hook时,必须以use开头。我们来尝试自定义一个控制对话框的hook:

import { useState } from 'react'type returnd = [boolean, (visible?: boolean) => void]const useVisible = (initVisible = false): returnd => {  const [visible, setVisible] = useState(initVisible)  function onVisible(value?: boolean) {    const newValue = value === undefined ? !visible : value    setVisible(newValue)  }  return [visible, onVisible]}export default useVisible

首先我们利用useState声明了visible和setVisible,然后我们定义了onVisible这个函数用来更改visible,接着我们返回[visible, onVisible]。然后我们来看看如何使用:

import { Button, Modal } from 'antd'import useVisible from '../hooks/useVisible'const Home: React.FC = () => {  const [visible, setVisible] = useVisible(false)  const modalShow = (value: boolean) => {    setVisible(value)  }  return (



modalShow(true)}> Open Modal modalShow(false)} onCancel={() => modalShow(false)}>

Some contents...




Some contents...




Some contents...




)}export default Home


就像我们使用其他hook一样方便。我们在写业务(搬砖)的过程中,我们可以尝试去将一些可复用的逻辑或者操作封装为我们自己的hook,这才是hooks的强大之处。

项目配置

我们在开发react的时候,不免需要一些配置,例如别名、跨域等等。vue给我们提供了一个vue.config.js用于配置,那么react项目呢?我们需要用到react-app-rewired和customize-cra:

yarn add react-app-rewired -Dyarn add customize-cra -D

安装完之后,我们需要更改一下我们的package.json文件:

....  "scripts": {    "start": "react-app-rewired start",    "build": "react-app-rewired build",    "test": "react-app-rewired test --env=jsdom",    "eject": "react-scripts eject"  },....

接着我们需要在项目的根目录新建一个config-overrides.js文件。接下来我们对项目进行一些配置:

/* config-overrides.js */const path = require('path')const { override, addWebpackResolve, fixBabelImports, overrideDevServer } = require('customize-cra')const { addReactRefresh } = require('customize-cra-react-refresh')// 配置开发环境跨域const devServerConfig = () => (config) => {  return {    ...config,    port: 3000,    proxy: {      '/mock/158/airi': {        target: 'https://api.guaik.org',        changeOrigin: true,        secure: false,      },    },  }}module.exports = {  webpack: override(    // 热加载    addReactRefresh(),    // 配置路径别名    addWebpackResolve({      alias: {        '@': path.resolve(__dirname, 'src'),      },    }),    // antd 按需加载    fixBabelImports('import', {      libraryName: 'antd',      libraryDirectory: 'es',      style: true,    })  ),  devServer: overrideDevServer(devServerConfig()),}

更多的配置请查看这里:customize-cra

总结

react 的知识点真的很多,对不?本文只是一个入门概览,还有很多很多的知识点,希望小伙伴们通过这篇文章对 react 有个大致了解。