文章简介
- 定义:介绍高阶组件的概念。
- 实例:通过小实例巩固高阶组件的概念。
- 二次封装
Antd弹窗组件,通过函数方法的形式调用。正文开始
敲业务敲到一定量的时候,就需要发起一个“质变”。而这个“质变”需要一个契机,那就是要明白如何将新获取的高级知识结合到自己的项目中来。大道理少说几句,大家心里应该都有点逼数的。下面就开始扯一扯React高阶组件这个知识点。定义
高阶组件英文全称:Higher Order Component。简称:HOC。
用我自己的理解就是:高阶组件是一个函数,它接受组件作为参数,返回值为一个新的组件(函数组件或类组件)。React组件是将props转换为UI,而高阶组件(HOC)是将组件转换为另一个组件。
实例
需求分析
编写两个数据类型相似的列表组件,并且展示在父组件中。
首先,通过yarn create vite新建一个React项目,在项目src目录下新建两个文件,Alist.jsx、Blist.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
export default Alist
```jsx// Blist.jsximport { useEffect, useState } from 'react'// 笔者手动生成的一个静态JSON数据const url = 'http://image.chennick.wang/1644917014491-1-b.json'const Blist = () => {const [data, setData] = useState([])useEffect(() => {// 发起请求获取列表数据fetch(url).then(res => {return res.json()}).then(({ data }) => {setData(data)})}, [])return <div style={{ padding: 20, border: '1px solid #e9e9e9', display: 'inline-block' }}>{data.map((item, index) => <div key={index}><div>名称:{item.name};价格:{item.price} 元</div></div>)}</div>}export default Blist
最后将两个列表组件展示在页面中,修改App.jsx文件如下:
import { useState } from 'react'import Alist from './Alist'import Blist from './Blist'import './App.css'function App() {return (<div className="App"><Alist></Alist><Blist></Blist></div>)}export default App
实现高阶组件
实现上述代码之后,我们先别忙着激动,冷静思考一番之后,你会发现上述代码中有业务逻辑重复的部分,那就是数据状态data和useEffect钩子函数中的请求逻辑。利用高阶组件将它们的公有部分提取出来单独维护。
高阶组件通常都是以With开头的,比如路由组件的WithRouter、状态组件的WithStore等。
我们也照葫芦画瓢,在src目录下新建一个WithData.jsx文件,添加内容如下。
首先,它是一个函数,并且返回一个新的组件:
// WithData.jsxconst WithData = () => {const WithDataComponent = () => {// do something}return WithDataComponent}export default WithData
将两个列表组件的公有部分抽离出来,写入上述返回的组件WithDataComponent中,并且WithDataComponent组件需要将传入的列表组件返回,如下:
import { useEffect, useState } from 'react'// WithData.jsx// Component 为传入的组件,url 为每个组件对应的请求地址const WithData = (Component, url) => {const WithDataComponent = () => {const [data, setData] = useState([])useEffect(() => {fetch(url).then(res => res.json()).then(({ data }) => {setData(data)})}, [])// 将请求返回的 data 属性以 props 的形式返回给传入的 Component 组件return <Component data={data} />}return WithDataComponent}export default WithData
上述代码中,提取了请求的逻辑,并且将请求后的data数据,通过组件传入的形式传递给Component。那么此时,便可以在Alist和Blist组件中,通过函数参数的形式,获取到相应的data数据。修改Alist.jsx和Blist.jsx如下所示:
// Alist.jsximport WithData from './WithData' // 引入高阶组件const url = 'http://image.chennick.wang/1644916550730-0-a.json'const Alist = ({ data }) => {return <div style={{ padding: 20, border: '1px solid #e9e9e9', display: 'inline-block' }}>{data.map((item, index) => <div key={index}><div>姓名:{item.name};年龄:{item.age}</div></div>)}</div>}export default WithData(Alist, url) // 传入 Alist 和 url 作为参数
// Blist.jsximport WithData from './WithData' // 引入高阶组件const url = 'http://image.chennick.wang/1644917014491-1-b.json'const Blist = ({ data }) => {return <div style={{ padding: 20, border: '1px solid #e9e9e9', display: 'inline-block' }}>{data.map((item, index) => <div key={index}><div>名称:{item.name};价格:{item.price} 元</div></div>)}</div>}export default WithData(Blist, url) // 传入 Alist 和 url 作为参数
Alist和Blist两个列表组件,经过WithData的包裹之后,抛出一个高阶组件内返回的新组件<Component data={data} />。所以,在App.jsx中,相当于如下:
所以在Alist.jsx和Blist.jsx中,可以在函数的参数中,拿到data参数。最终渲染结果还是没变:
扩展知识点
此时若是我在父组件App.jsx中,在Alist组件中传入一个参数,如下所示:
import { useState } from 'react'import Alist from './Alist'import Blist from './Blist'import './App.css'function App() {return (<div className="App"><Alist title='我是父组件传入的title参数'></Alist><Blist></Blist></div>)}export default App
在Alist.jsx中打印这个属性,如下所示:
...const Alist = ({ data, title }) => {console.log('title', title)...}
结果如下所示:
原因是,在父组件App.jsx中引入的Alist组件,是经过高阶组件WithData包裹后返回的函数组件,也就是WithDataComponent组件,我们看看代码如下:
import React, { useState, useEffect } from 'react'const WithData = (Component, url) => {const WithDataComponent = () => {const [data, setData] = useState([])useEffect(() => {fetch(url).then(res => res.json()).then(({ data }) => {setData(data)})}, [])return <Component data={data} />}return WithDataComponent}export default WithData
在WithDataComponent组件中,传入的title并没有带给Component,最终导致在Alist.jsx中,title属性丢失了。
那么,我们需要将父组件传进来的props全部都传给Component,修改如下:
const WithDataComponent = (props) => {const [data, setData] = useState([])useEffect(() => {fetch(url).then(res => res.json()).then(({ data }) => {setData(data)})}, [])return <Component {...props} data={data} />}return WithDataComponent
然后我们重新运行项目,观察Alist.jsx下的打印信息,结果如下:
结论
至此,一个简单的高阶组件就写完了。当然,复杂业务情况下,高阶组件远不止这么简单,需要对业务的高度理解,以及项目结构的统筹规划,将很多相似、类似的结构一一提取出来,封装成高阶组件。
二次封装 Ant Design 弹窗组件
利用高阶组件的特性,我们来封装一个Ant Design的弹窗以方法调用的形式。
需求分析
我们在业务开发中,经常会使用到Ant Design为我们提供的Modal弹窗组件。正常情况下使用它,需要通过visible属性来控制显示或隐藏,如果在一个页面中存在多个弹窗需求,则会遇到下面这样的情况:
const [visible1, setVisible1] = useState(false)const [visible2, setVisible2] = useState(false)const [visible3, setVisible3] = useState(false)const [visible4, setVisible4] = useState(false)const [visible5, setVisible5] = useState(false)...
N 多个visible状态在同一个页面中进行管理,眼花缭乱。
于是,为了解决这个困境,利用高阶组件优化一波,将弹窗的visible状态封装到高阶组件中控制。
实现逻辑
首先,在上述项目的基础上,通过指令安装Ant Design:
yarn add antd
修改main.jsx全局引入antd样式如下所示:
...import 'antd/dist/antd.css'
正常使用visible控制弹窗,在src目录下添加组件DialogRename.jsx组件,如下所示:
import { Input, Modal } from 'antd'const DialogRename = ({ visible, onCancel }) => {return <Modaltitle="我是弹窗"visible={visible}onOk={onCancel}onCancel={onCancel}><><label>名称:</label><Input style={{ width: 400 }} onChange={(e) => {setValue(e.target.value)}} placeholder='请输入名称' /></></Modal>}export default DialogRename
上述代码中,从父组件接受visible状态,和控制弹窗隐藏的方法onCancel。然后在App.jsx入口页引入弹窗组件,代码如下:
import { useState } from 'react'import { Button } from 'antd'import DialogRename from './DialogRename'import './App.css'function App() {const [visible, setVisible] = useState(false) // 控制 DialogRename 组件显示或隐藏return (<div className="App"><Button onClick={() => setVisible(true)}>打开它</Button><DialogRename visible={visible} onCancel={() => setVisible(false)} /></div>)}export default App
效果如下所示:
接下来,我们来完成一个高阶组件,新建WithDialog.jsx,代码如下所示:
// WithDialog.jsximport React from 'react'import { render, unmountComponentAtNode } from 'react-dom'export default class WithDialog {constructor(Component) {this._ele = nullthis._dom = <Component onCancel={this.close} />this.show()}show = () => {this._ele = document.createElement('div')render(this._dom, document.body.appendChild(this._ele))}close = () => {unmountComponentAtNode(this._ele)this._ele.remove()}}
声明一个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组件如下:
import { useState } from 'react'import { Input, Modal } from 'antd'import WithDialog from './WithDialog'const DialogRename = ({ ...props }) => {const [value, setValue] = useState('') // 输入框的值const handleOk = () => {props.onCancel()}return <Modaltitle="我是弹窗"visibleonCancel={props.onCancel}onOk={handleOk}><><label>名称:</label><Input style={{ width: 400 }} onChange={(e) => {setValue(e.target.value)}} placeholder='请输入名称' /></></Modal>}export default () => new WithDialog(DialogRename)
首先明确一点,在父组件App.jsx中,是需要通过调用方法的形式,发起弹窗组件的。所以我们在DialogRename.jsx中,抛出一个函数,export default () => new WithDialog(DialogRename)。经过WithDialog包裹之后,DialogRename组件可以接收到在WithDialog.jsx中传入的onCancel方法,这里通过解构的形式获取,赋值给Modal的onCancel属性。
之后,我们在App.jsx中通过方法的形式调用组件,如下所示:
import { Button } from 'antd'import DialogRename from './DialogRename'import './App.css'function App() {const handleOpen = () => {DialogRename()}return (<div className="App"><Button onClick={handleOpen}>打开它</Button></div>)}export default App
重启项目,效果如下所示:
当点击弹窗的确认组件时,需要执行一些方法,所以我们可以在DialogRename方法中传入onOk方法,如下所示:
import { Button } from 'antd'import DialogRename from './DialogRename'import './App.css'function App() {const handleOpen = () => {DialogRename({onOk: (val) => {console.log('onOk:', val)}})}return (<div className="App"><Button onClick={handleOpen}>打开它</Button></div>)}export default App
然后在DialogRename.jsx中接受onOk赋值给Modal组件的onOk,修改DialogRename.jsx如下所示:
import { useState } from 'react'import { Input, Modal } from 'antd'import WithDialog from './WithDialog'const DialogRename = ({ onOk, ...props }) => {const [value, setValue] = useState('')const handleOk = () => {onOk(value)props.onCancel()}return <Modaltitle="我是弹窗"visibleonOk={handleOk}{...props}><><label>名称:</label><Input style={{ width: 400 }} onChange={(e) => {setValue(e.target.value)}} placeholder='请输入名称' /></></Modal>}export default () => new WithDialog(DialogRename)
执行onOk(value)方法,将输入框的值回调给App.jsx中的onOk方法,如下所示:
点击OK按钮之后,出现如上图所示的报错。原因很简单,当DialogRename被WithDialog组件包裹的时候,传给它的onOk方法丢失了,此时需要在构造方法中传入父组件传递给DialogRename的props,如下所示:
// DialogRename.jsx...export default (props) => new WithDialog(DialogRename, props)
然后前往WithDialog.jsx,将props透传给Component,如下所示:
// WithDialog.jsx...constructor(Component, props) {this._ele = nullthis._dom = <Component onCancel={this.close} {...props} />this.show()}
此时你才能真正的拿到传递进来的onOk方法,如下所示:
你可以尝试在App.jsx中再传入一个属性,覆盖掉Modal组件的title属性,如下所示:
// App.jsx// ...const handleOpen = () => {DialogRename({onOk: (val) => {console.log('onOk:', val)},title: '我是App传入的title'})}
弹窗的标题就会被覆盖掉,效果如下:
最后,英文看起来怪怪的,我们引入全局中文包,修改WithDialog.jsx如下:
// WithDialog.jsx...import zhCN from 'antd/lib/locale/zh_CN'import { ConfigProvider } from 'antd'...constructor(Component, props) {this._ele = nullthis._dom = <ConfigProvider locale={zhCN}><Component onCancel={this.close} {...props} /></ConfigProvider>this.show()}
总结
掌握好高阶组件,对业务开发会有更深刻的理解。这是初中级前端进阶为高级前端的必经之路,专注业务的优化,也是衡量一个前端开发者技术好坏的评判标准。
