新蜂商城开源仓库(内涵 Vue 2.x 和 Vue 3.x 的 H5 商城开源代码,带服务端 API 接口):github.com/newbee-ltd
Vue 3.x + Vant 3.x + Vue-Router 4.x 高仿微信记账本开源地址(带服务端 API 接口):github.com/Nick930826/…
React + Vite 2.0 + Zarm 高仿微信记账本开源地址(带服务端 API 接口)以及学习文档:github.com/Nick930826/…
文章简介
- 定义:介绍高阶组件的概念。
- 实例:通过小实例巩固高阶组件的概念。
- 二次封装
Antd
弹窗组件,通过函数方法的形式调用。
正文开始
敲业务敲到一定量的时候,就需要发起一个“质变”。而这个“质变”需要一个契机,那就是要明白如何将新获取的高级知识结合到自己的项目中来。大道理少说几句,大家心里应该都有点逼数的。下面就开始扯一扯React
高阶组件这个知识点。
定义
高阶组件英文全称:Higher Order Component。简称:HOC。
用我自己的理解就是:高阶组件是一个函数,它接受组件作为参数,返回值为一个新的组件(函数组件或类组件)。
React
组件是将props
转换为UI
,而高阶组件(HOC)是将组件转换为另一个组件。
实例
需求分析
编写两个数据类型相似的列表组件,并且展示在父组件中。
首先,通过yarn create vite
新建一个React
项目,在项目src
目录下新建两个文件,Alist.jsx
、Blist.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 <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 Alist
// Blist.jsx
import { 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.jsx
const 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.jsx
import 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.jsx
import 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 <Modal
title="我是弹窗"
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.jsx
import React from 'react'
import { render, unmountComponentAtNode } from 'react-dom'
export default class WithDialog {
constructor(Component) {
this._ele = null
this._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 <Modal
title="我是弹窗"
visible
onCancel={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 <Modal
title="我是弹窗"
visible
onOk={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 = null
this._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 = null
this._dom = <ConfigProvider locale={zhCN}>
<Component onCancel={this.close} {...props} />
</ConfigProvider>
this.show()
}
效果如下所示:
总结
掌握好高阶组件,对业务开发会有更深刻的理解。这是初中级前端进阶为高级前端的必经之路,专注业务的优化,也是衡量一个前端开发者技术好坏的评判标准。