文章简介

  • 定义:介绍高阶组件的概念。
  • 实例:通过小实例巩固高阶组件的概念。
  • 二次封装 Antd弹窗组件,通过函数方法的形式调用。

    正文开始

    敲业务敲到一定量的时候,就需要发起一个“质变”。而这个“质变”需要一个契机,那就是要明白如何将新获取的高级知识结合到自己的项目中来。大道理少说几句,大家心里应该都有点逼数的。下面就开始扯一扯React高阶组件这个知识点。

    定义

    高阶组件英文全称:Higher Order Component。简称:HOC。
    用我自己的理解就是:高阶组件是一个函数,它接受组件作为参数,返回值为一个新的组件(函数组件或类组件)。
    React组件是将props转换为UI,而高阶组件(HOC)是将组件转换为另一个组件。
    dfbaab732edb6729a51a567e347eba60.gif

    实例

    需求分析

    编写两个数据类型相似的列表组件,并且展示在父组件中。
    首先,通过yarn create vite新建一个React项目,在项目src目录下新建两个文件,Alist.jsxBlist.jsx,代码如下所示: ```jsx // Alist.jsx import { useEffect, useState } from ‘react’ // 笔者手动生成的一个静态JSON数据 const url = ‘http://image.chennick.wang/1644916550730-0-a.json

const Alist = () => { const [data, setData] = useState([])

useEffect(() => { // 发起请求获取列表数据 fetch(url).then(res => { return res.json() }).then(({ data }) => { setData(data) }) }, [])

return

{ data.map((item, index) =>
姓名:{item.name};年龄:{item.age}
) }
}

export default Alist

  1. ```jsx
  2. // Blist.jsx
  3. import { useEffect, useState } from 'react'
  4. // 笔者手动生成的一个静态JSON数据
  5. const url = 'http://image.chennick.wang/1644917014491-1-b.json'
  6. const Blist = () => {
  7. const [data, setData] = useState([])
  8. useEffect(() => {
  9. // 发起请求获取列表数据
  10. fetch(url).then(res => {
  11. return res.json()
  12. }).then(({ data }) => {
  13. setData(data)
  14. })
  15. }, [])
  16. return <div style={{ padding: 20, border: '1px solid #e9e9e9', display: 'inline-block' }}>
  17. {
  18. data.map((item, index) => <div key={index}>
  19. <div>名称:{item.name};价格:{item.price} 元</div>
  20. </div>)
  21. }
  22. </div>
  23. }
  24. export default Blist

最后将两个列表组件展示在页面中,修改App.jsx文件如下:

  1. import { useState } from 'react'
  2. import Alist from './Alist'
  3. import Blist from './Blist'
  4. import './App.css'
  5. function App() {
  6. return (
  7. <div className="App">
  8. <Alist></Alist>
  9. <Blist></Blist>
  10. </div>
  11. )
  12. }
  13. export default App

页面展示效果如下所示:
image.png

实现高阶组件

实现上述代码之后,我们先别忙着激动,冷静思考一番之后,你会发现上述代码中有业务逻辑重复的部分,那就是数据状态datauseEffect钩子函数中的请求逻辑。利用高阶组件将它们的公有部分提取出来单独维护。

高阶组件通常都是以With开头的,比如路由组件的WithRouter、状态组件的WithStore等。
我们也照葫芦画瓢,在src目录下新建一个WithData.jsx文件,添加内容如下。
首先,它是一个函数,并且返回一个新的组件:

  1. // WithData.jsx
  2. const WithData = () => {
  3. const WithDataComponent = () => {
  4. // do something
  5. }
  6. return WithDataComponent
  7. }
  8. export default WithData

将两个列表组件的公有部分抽离出来,写入上述返回的组件WithDataComponent中,并且WithDataComponent组件需要将传入的列表组件返回,如下:

  1. import { useEffect, useState } from 'react'
  2. // WithData.jsx
  3. // Component 为传入的组件,url 为每个组件对应的请求地址
  4. const WithData = (Component, url) => {
  5. const WithDataComponent = () => {
  6. const [data, setData] = useState([])
  7. useEffect(() => {
  8. fetch(url).then(res => res.json()).then(({ data }) => {
  9. setData(data)
  10. })
  11. }, [])
  12. // 将请求返回的 data 属性以 props 的形式返回给传入的 Component 组件
  13. return <Component data={data} />
  14. }
  15. return WithDataComponent
  16. }
  17. export default WithData

上述代码中,提取了请求的逻辑,并且将请求后的data数据,通过组件传入的形式传递给Component。那么此时,便可以在AlistBlist组件中,通过函数参数的形式,获取到相应的data数据。修改Alist.jsxBlist.jsx如下所示:

  1. // Alist.jsx
  2. import WithData from './WithData' // 引入高阶组件
  3. const url = 'http://image.chennick.wang/1644916550730-0-a.json'
  4. const Alist = ({ data }) => {
  5. return <div style={{ padding: 20, border: '1px solid #e9e9e9', display: 'inline-block' }}>
  6. {
  7. data.map((item, index) => <div key={index}>
  8. <div>姓名:{item.name};年龄:{item.age}</div>
  9. </div>)
  10. }
  11. </div>
  12. }
  13. export default WithData(Alist, url) // 传入 Alist 和 url 作为参数
  1. // Blist.jsx
  2. import WithData from './WithData' // 引入高阶组件
  3. const url = 'http://image.chennick.wang/1644917014491-1-b.json'
  4. const Blist = ({ data }) => {
  5. return <div style={{ padding: 20, border: '1px solid #e9e9e9', display: 'inline-block' }}>
  6. {
  7. data.map((item, index) => <div key={index}>
  8. <div>名称:{item.name};价格:{item.price} 元</div>
  9. </div>)
  10. }
  11. </div>
  12. }
  13. export default WithData(Blist, url) // 传入 Alist 和 url 作为参数

AlistBlist两个列表组件,经过WithData的包裹之后,抛出一个高阶组件内返回的新组件<Component data={data} />。所以,在App.jsx中,相当于如下:
image.png
所以在Alist.jsxBlist.jsx中,可以在函数的参数中,拿到data参数。最终渲染结果还是没变:
image.png

扩展知识点

此时若是我在父组件App.jsx中,在Alist组件中传入一个参数,如下所示:

  1. import { useState } from 'react'
  2. import Alist from './Alist'
  3. import Blist from './Blist'
  4. import './App.css'
  5. function App() {
  6. return (
  7. <div className="App">
  8. <Alist title='我是父组件传入的title参数'></Alist>
  9. <Blist></Blist>
  10. </div>
  11. )
  12. }
  13. export default App

Alist.jsx中打印这个属性,如下所示:

  1. ...
  2. const Alist = ({ data, title }) => {
  3. console.log('title', title)
  4. ...
  5. }

结果如下所示:
image.png
原因是,在父组件App.jsx中引入的Alist组件,是经过高阶组件WithData包裹后返回的函数组件,也就是WithDataComponent组件,我们看看代码如下:

  1. import React, { useState, useEffect } from 'react'
  2. const WithData = (Component, url) => {
  3. const WithDataComponent = () => {
  4. const [data, setData] = useState([])
  5. useEffect(() => {
  6. fetch(url).then(res => res.json()).then(({ data }) => {
  7. setData(data)
  8. })
  9. }, [])
  10. return <Component data={data} />
  11. }
  12. return WithDataComponent
  13. }
  14. export default WithData

WithDataComponent组件中,传入的title并没有带给Component,最终导致在Alist.jsx中,title属性丢失了。
那么,我们需要将父组件传进来的props全部都传给Component,修改如下:

  1. const WithDataComponent = (props) => {
  2. const [data, setData] = useState([])
  3. useEffect(() => {
  4. fetch(url).then(res => res.json()).then(({ data }) => {
  5. setData(data)
  6. })
  7. }, [])
  8. return <Component {...props} data={data} />
  9. }
  10. return WithDataComponent

然后我们重新运行项目,观察Alist.jsx下的打印信息,结果如下:
image.png

结论

至此,一个简单的高阶组件就写完了。当然,复杂业务情况下,高阶组件远不止这么简单,需要对业务的高度理解,以及项目结构的统筹规划,将很多相似、类似的结构一一提取出来,封装成高阶组件。

二次封装 Ant Design 弹窗组件

利用高阶组件的特性,我们来封装一个Ant Design的弹窗以方法调用的形式。

需求分析

我们在业务开发中,经常会使用到Ant Design为我们提供的Modal弹窗组件。正常情况下使用它,需要通过visible属性来控制显示或隐藏,如果在一个页面中存在多个弹窗需求,则会遇到下面这样的情况:

  1. const [visible1, setVisible1] = useState(false)
  2. const [visible2, setVisible2] = useState(false)
  3. const [visible3, setVisible3] = useState(false)
  4. const [visible4, setVisible4] = useState(false)
  5. const [visible5, setVisible5] = useState(false)
  6. ...

N 多个visible状态在同一个页面中进行管理,眼花缭乱。
于是,为了解决这个困境,利用高阶组件优化一波,将弹窗的visible状态封装到高阶组件中控制。

实现逻辑

首先,在上述项目的基础上,通过指令安装Ant Design

  1. yarn add antd

修改main.jsx全局引入antd样式如下所示:

  1. ...
  2. import 'antd/dist/antd.css'

正常使用visible控制弹窗,在src目录下添加组件DialogRename.jsx组件,如下所示:

  1. import { Input, Modal } from 'antd'
  2. const DialogRename = ({ visible, onCancel }) => {
  3. return <Modal
  4. title="我是弹窗"
  5. visible={visible}
  6. onOk={onCancel}
  7. onCancel={onCancel}
  8. >
  9. <>
  10. <label>名称:</label>
  11. <Input style={{ width: 400 }} onChange={(e) => {
  12. setValue(e.target.value)
  13. }} placeholder='请输入名称' />
  14. </>
  15. </Modal>
  16. }
  17. export default DialogRename

上述代码中,从父组件接受visible状态,和控制弹窗隐藏的方法onCancel。然后在App.jsx入口页引入弹窗组件,代码如下:

  1. import { useState } from 'react'
  2. import { Button } from 'antd'
  3. import DialogRename from './DialogRename'
  4. import './App.css'
  5. function App() {
  6. const [visible, setVisible] = useState(false) // 控制 DialogRename 组件显示或隐藏
  7. return (
  8. <div className="App">
  9. <Button onClick={() => setVisible(true)}>打开它</Button>
  10. <DialogRename visible={visible} onCancel={() => setVisible(false)} />
  11. </div>
  12. )
  13. }
  14. export default App

效果如下所示:
你好,React 高阶组件(HOC)懂不懂? - 图7
接下来,我们来完成一个高阶组件,新建WithDialog.jsx,代码如下所示:

  1. // WithDialog.jsx
  2. import React from 'react'
  3. import { render, unmountComponentAtNode } from 'react-dom'
  4. export default class WithDialog {
  5. constructor(Component) {
  6. this._ele = null
  7. this._dom = <Component onCancel={this.close} />
  8. this.show()
  9. }
  10. show = () => {
  11. this._ele = document.createElement('div')
  12. render(this._dom, document.body.appendChild(this._ele))
  13. }
  14. close = () => {
  15. unmountComponentAtNode(this._ele)
  16. this._ele.remove()
  17. }
  18. }

声明一个WithDialog类组件,构造函数constructor接受Component参数为需要包装的组件,比如此次我们需要包装的DialogRename组件。声明一个全局属性this._ele用于后续创建一个弹窗需要挂载的目标div标签。this._dom用于挂载传入的弹窗组件。最后默认执行this.show方法,挂载弹窗。

show方法,创建一个div标签,通过react-dom包提供的render方法,将弹窗组件this._dom挂载到页面中。

close方法,通过react-dom方法提供的unmountComponentAtNode卸载组件方法,将this._ele挂载弹窗组件的节点卸载,最后再remove移除。

改造DialogRename.jsx组件如下:

  1. import { useState } from 'react'
  2. import { Input, Modal } from 'antd'
  3. import WithDialog from './WithDialog'
  4. const DialogRename = ({ ...props }) => {
  5. const [value, setValue] = useState('') // 输入框的值
  6. const handleOk = () => {
  7. props.onCancel()
  8. }
  9. return <Modal
  10. title="我是弹窗"
  11. visible
  12. onCancel={props.onCancel}
  13. onOk={handleOk}
  14. >
  15. <>
  16. <label>名称:</label>
  17. <Input style={{ width: 400 }} onChange={(e) => {
  18. setValue(e.target.value)
  19. }} placeholder='请输入名称' />
  20. </>
  21. </Modal>
  22. }
  23. export default () => new WithDialog(DialogRename)

首先明确一点,在父组件App.jsx中,是需要通过调用方法的形式,发起弹窗组件的。所以我们在DialogRename.jsx中,抛出一个函数,export default () => new WithDialog(DialogRename)。经过WithDialog包裹之后,DialogRename组件可以接收到在WithDialog.jsx中传入的onCancel方法,这里通过解构的形式获取,赋值给ModalonCancel属性。

之后,我们在App.jsx中通过方法的形式调用组件,如下所示:

  1. import { Button } from 'antd'
  2. import DialogRename from './DialogRename'
  3. import './App.css'
  4. function App() {
  5. const handleOpen = () => {
  6. DialogRename()
  7. }
  8. return (
  9. <div className="App">
  10. <Button onClick={handleOpen}>打开它</Button>
  11. </div>
  12. )
  13. }
  14. export default App

重启项目,效果如下所示:
Kapture 2022-02-16 at 17.49.24.gif
当点击弹窗的确认组件时,需要执行一些方法,所以我们可以在DialogRename方法中传入onOk方法,如下所示:

  1. import { Button } from 'antd'
  2. import DialogRename from './DialogRename'
  3. import './App.css'
  4. function App() {
  5. const handleOpen = () => {
  6. DialogRename({
  7. onOk: (val) => {
  8. console.log('onOk:', val)
  9. }
  10. })
  11. }
  12. return (
  13. <div className="App">
  14. <Button onClick={handleOpen}>打开它</Button>
  15. </div>
  16. )
  17. }
  18. export default App

然后在DialogRename.jsx中接受onOk赋值给Modal组件的onOk,修改DialogRename.jsx如下所示:

  1. import { useState } from 'react'
  2. import { Input, Modal } from 'antd'
  3. import WithDialog from './WithDialog'
  4. const DialogRename = ({ onOk, ...props }) => {
  5. const [value, setValue] = useState('')
  6. const handleOk = () => {
  7. onOk(value)
  8. props.onCancel()
  9. }
  10. return <Modal
  11. title="我是弹窗"
  12. visible
  13. onOk={handleOk}
  14. {...props}
  15. >
  16. <>
  17. <label>名称:</label>
  18. <Input style={{ width: 400 }} onChange={(e) => {
  19. setValue(e.target.value)
  20. }} placeholder='请输入名称' />
  21. </>
  22. </Modal>
  23. }
  24. export default () => new WithDialog(DialogRename)

执行onOk(value)方法,将输入框的值回调给App.jsx中的onOk方法,如下所示:
image.png
点击OK按钮之后,出现如上图所示的报错。原因很简单,当DialogRenameWithDialog组件包裹的时候,传给它的onOk方法丢失了,此时需要在构造方法中传入父组件传递给DialogRenameprops,如下所示:

  1. // DialogRename.jsx
  2. ...
  3. export default (props) => new WithDialog(DialogRename, props)

然后前往WithDialog.jsx,将props透传给Component,如下所示:

  1. // WithDialog.jsx
  2. ...
  3. constructor(Component, props) {
  4. this._ele = null
  5. this._dom = <Component onCancel={this.close} {...props} />
  6. this.show()
  7. }

此时你才能真正的拿到传递进来的onOk方法,如下所示:
image.png
你可以尝试在App.jsx中再传入一个属性,覆盖掉Modal组件的title属性,如下所示:

  1. // App.jsx
  2. // ...
  3. const handleOpen = () => {
  4. DialogRename({
  5. onOk: (val) => {
  6. console.log('onOk:', val)
  7. },
  8. title: '我是App传入的title'
  9. })
  10. }

弹窗的标题就会被覆盖掉,效果如下:
image.png
最后,英文看起来怪怪的,我们引入全局中文包,修改WithDialog.jsx如下:

  1. // WithDialog.jsx
  2. ...
  3. import zhCN from 'antd/lib/locale/zh_CN'
  4. import { ConfigProvider } from 'antd'
  5. ...
  6. constructor(Component, props) {
  7. this._ele = null
  8. this._dom = <ConfigProvider locale={zhCN}>
  9. <Component onCancel={this.close} {...props} />
  10. </ConfigProvider>
  11. this.show()
  12. }

效果如下所示:
image.png

总结

掌握好高阶组件,对业务开发会有更深刻的理解。这是初中级前端进阶为高级前端的必经之路,专注业务的优化,也是衡量一个前端开发者技术好坏的评判标准。