前言
对话框是日常开发很常用的组件(反正我是基本每个页面都用到,嗯。。。不少情况下一个页面还不只一个),为了能更爽地使用它,一般我们都会进行二次封装,今我就给哥几个讲讲我是这么个封装法
未封装的使用方法
本文使用的是antd的Modal
组件来举例,当然思想都是相通的,我之前用vue3
的时候也封装过类似的组件,下面我们来看看antd官方的基本使用例子:
import React, { useState } from 'react';
import { Modal, Button } from 'antd';
const App = () => {
const [isModalVisible, setIsModalVisible] = useState(false);
const showModal = () => {
setIsModalVisible(true);
};
const handleOk = () => {
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
};
return (
<>
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Modal title="Basic Modal" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</>
);
};
ReactDOM.render(<App />, mountNode);
这里是直接复制的官方的代码👻(别着急,好戏在后头呢)
我个人的一些看法
对于未定制封装的对话框组件,我们每使用一个通常都会为之创建一个相应的hook
用来管理它的显示状态。对此我个人觉得可能hook
会定的太多,有点太繁琐了。基于能省一行就省一行的代码理念,我认为把项目内所有Modal
组件的显示控制权用redux
来管理也许会是个不错的方案👌
具体我是怎么做的?
要实现这一点,我们首先要定义好全局的redux
状态,这里我使用的是reduxjs/toolkit
,下面上代码:
store/index.ts
import { useDispatch, TypedUseSelectorHook, useSelector } from 'react-redux'
import { AnyAction, combineReducers, configureStore, ThunkAction } from '@reduxjs/toolkit'
// redux-persist是用来给redux数据持久化的插件
import {
FLUSH,
PAUSE,
PERSIST,
PersistConfig,
persistReducer,
PURGE,
REGISTER,
REHYDRATE,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import modalReducer from './reducers/modalReducer'
import userReducer from './reducers/userReducer'
const reducers = combineReducers({
modal: modalReducer,
user: userReducer,
})
const persistConfig: PersistConfig<ReturnType<typeof reducers>> = {
key: 'root',
storage,
// 数据持久化白名单,需要持久化就搁这写上
whitelist: [
'user',
],
}
const persistedReducer = persistReducer(persistConfig, reducers)
const store = configureStore({
reducer: persistedReducer,
devTools: !import.meta.env.PROD,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
export type IRootState = ReturnType<typeof store.getState>
export type IAppDispatch = typeof store.dispatch
export type IAppThunk<ReturnType = void> = ThunkAction<
ReturnType,
IRootState,
unknown,
AnyAction
>
export default store
// 添加类型提示方便使用
export const useAppDispatch = () => useDispatch<IAppDispatch>()
export const useAppSelector: TypedUseSelectorHook<IRootState> = useSelector
main.tsx
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { persistStore } from 'redux-persist'
import { PersistGate } from 'redux-persist/integration/react'
import RouterView from './routers/RouterView'
import store from '@store/.'
const persistor = persistStore(store)
ReactDOM.render(
// <React.StrictMode> // antd部分组件严格模式报红
(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ConfigProvider locale={zhCN}>
<RouterView />
</ConfigProvider>
</PersistGate>
</Provider>
),
// </React.StrictMode>,
document.getElementById('root'),
)
store.ts
和main.tsx
这两个文件我本来是不想放上来的,但考虑到有的伙计可能没上手过react
,这里简单地放一下,不做过多解析了,就提一下redux-persist
和react
的严格模式吧
redux-persist
:是一个为redux
数据持久化的插件,有需要可以使用一下,可通过配置黑或白名单来控制要持久化的Reducer
react
严格模式:可以看到我把严格模式给注释掉了,因为antd
的部分组件使用了旧的react
api导致控制台报红,在GitHub
上有不少人提了bug,不知什么时候解决 关于reduxjs/toolkit
其他的一些基本使用可以看官方文档或在站内搜索相关内容,下面来看一下真正与对话框组件有关的Slice
:
modalReducer.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import store, { IRootState } from '..'
export interface IModalState {
visible: boolean
}
interface IModalPayload {
isVisible: boolean
}
const initialState: IModalState = {
visible: false,
}
export const slice = createSlice({
name: 'modal',
initialState,
reducers: {
setModal: (state, { payload }: PayloadAction<IModalPayload>) => {
const {
isVisible,
} = payload
state.visible = isVisible
},
},
})
export const {
setModal,
} = slice.actions
export function showModal () {
store.dispatch(setModal({
isVisible: true,
}))
}
export function hideModal () {
store.dispatch(setModal({
isVisible: false,
}))
}
export const selectPage = (state: IRootState) => state.page
export default (function modalReducer () {
return slice.reducer
}())
- 首先我们将
modal slice
的状态结构定义为IModalState
,其中visible
这个全局状态就是用来控制我们封装后对话框的显隐的,给它默认为false
隐藏 - 接着我们要为
visible
定义专门的Action
状态更新函数setModal
- 最后,为了更方便的调用(我个人觉得🤤)我们再把显示隐藏的控制拆分为两个函数
showModal
和hideModal
- 为什么默认导出是一个闭包?不为什么,就是好玩🤪 到这里我们要为对话框二次封装组件的专属状态就定好了,非常的简单👌,下面就到组件的实现代码了:
import { useAppSelector } from '@store/hooks'
import { hideModal } from '@store/reducers/pageReducer'
import { ColProps, Modal } from 'antd'
import { ReactNode } from 'react'
interface IProps {
title: string
width?: number
children?: ReactNode
labelCol?: ColProps
}
const defaultLabelCol = { span: 6 }
export default function DiyModal (props: IProps) {
const {
title,
children,
width = 600,
labelCol = defaultLabelCol,
} = props
const isModalVisible = useAppSelector(state => state.modal).visible
return (
<Modal
width={width}
title={title}
visible={isModalVisible}
onOk={() => console.log('ojbk')}
onCancel={() => hideModal()}
>
{ children }
</Modal>
)
}
到这里一个由全局状态控制的对话框组件封装就完成了,整个实现也灰常简单,就三行主要的代码:
const isModalVisible = useAppSelector(state => state.page).visible
visible={isModalVisible}
onCancel={() => hideModal()}
就是通过拿到全局的visible
赋予Modal
和关闭时调用hideModal
将visible
设置为false
,使用也是三岁小孩都会方式:
import { Button } from 'antd'
import DiyModal from '@com/DiyModal'
export default function MyPage () {
return <div>
<Button type="primary" onClick={() => showModal()}>打开</Button>
<DiyModal title="我是标题">我是内容</DiyModal>
</div>
}
可想而知的结果
还不够!得上加强版
当然我前面说过一个页面有时是不只一个对话框,并且我司一般对话框都会包含着表单,所以我们还需要进一步改造加强它的功能性
- 既然一个页面不是固定的一个对话框,那么我们将显示控制状态换成
boolean
数组就好了:
export interface IModalState {
visibleList: boolean[] // modal可见列表
}
- 用于对话框内表单数据回显的初始化状态也应该可以放到全局管理,所以加上表单的状态数组:
import { Store } from 'antd/lib/form/interface'
export interface IModalState {
visibleList: boolean[] // modal可见列表
formStateList: (Store | undefined)[] // 对话框表单默认值列表
}
// 也许这样写更优雅点,但我的例子已经用上面的形式写了,就不改了
export interface IModalState {
list: {
visible: boolean
formState?: Store
}[]
}
- 让
setModal
给力点:
interface IModalPayload {
isVisible: boolean
index: number
formState?: Store
}
const initialState: IModalState = {
visibleList: [],
formStateList: [],
}
const slice = createSlice({
name: 'modal',
initialState,
reducers: {
setModal: (state, { payload }: PayloadAction<IModalPayload>) => {
const {
isVisible,
index,
formState,
} = payload
state.formStateList[index] = formState
state.visibleList[index] = isVisible
},
},
})
现在,我们可以通过index
控制指定的对话框组件
- 当然,
showModal
和hideModal
函数不能落下:
export function showModal (
index: number,
formState?: IModalPayload['formState'],
) {
store.dispatch(setModal({
index,
formState,
isVisible: true,
}))
}
export function hideModal (
index: number,
) {
store.dispatch(setModal({
index,
isVisible: false,
}))
}
- 最后,
DiyModal
长这样:
import { useAppSelector } from '@store/hooks'
import { hideModal } from '@store/reducers/pageReducer'
import { ColProps, Form, Modal } from 'antd'
import { Store } from 'antd/lib/form/interface'
import { ReactNode, useEffect } from 'react'
interface IProps {
index: number
title: string
width?: number
children?: ReactNode
labelCol?: ColProps
}
/* eslint-disable no-template-curly-in-string */
const validateMessages = {
required: '${label}是必填项!',
}
/* eslint-disable no-template-curly-in-string */
const defaultLabelCol = { span: 6 }
export default function DiyModal (props: IProps) {
const {
index,
title,
children,
width = 600,
labelCol = defaultLabelCol,
} = props
const modalState = useAppSelector(state => state.modal)
const isModalVisible = modalState.visibleList[index]
const formState = modalState.formStateList[index]
const [form] = Form.useForm()
const onCheck = async () => {
try {
const values = await form.validateFields()
console.log('成功:', values)
} catch (errorInfo) {
console.log('失败:', errorInfo)
}
}
return (
<Modal
destroyOnClose
width={width}
title={title}
visible={isModalVisible}
onOk={onCheck}
onCancel={() => hideModal(index)}
>
<Form
form={form}
preserve={false}
labelCol={labelCol}
validateMessages={validateMessages}
initialValues={formState}
>
{ children }
</Form>
</Modal>
)
}
也还是简简单单的使用方式
- 为页面编写你的对话框:
import DiyModal from '@com/DiyModal'
import { Form, Input } from 'antd'
export default function Modal0 () {
return (
<DiyModal
index={0}
title="我是标题"
>
<Form.Item
name="userName"
label="用户名"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
</DiyModal>
)
}
- 引用
Modal0
:
import { Button } from 'antd'
import Modal0 from './Modal0'
export default function MyPage () {
return <div>
<Button type="primary" onClick={() => showModal(0, { userName: '23' })}>
打开
</Button>
<Modal0/>
</div>
}
数据回显莫得问题🤪,接下来你可以根据自己需要继续扩展组件的属性像onCancel
和onOk
之类的
formState的类型提示
你也许会需要根据index
获得对应formState
的类型提示,关于这一点你可以阅读我另外一篇文章,看完后你应该可以尝试自己去实现它,我就不演示了,文章链接:都快2022年了你连个API都写不好吗?来!这份ts+proxy的API编写风格指南你收好了
结语
因为对话框的状态并不是持久化保存的,所以在基于BrowserRouter
路由跳转页面和刷新的时候会被清掉,这样你就只需要根据单个页面对话框数量来定义index
就好,而不是整个项目🙄,另外在开发环境下有可能出现刷新或路由跳转,状态不被清除的情况,会导致两个页面能互相控制对方对话框的情况,在打包后生产环境暂时没遇到过,个人觉得可以接受。如果你不能接受,可以尝试给路由添加守卫来手动清除它。什么?你不会给React Router v6
添加守卫。那还不点赞关注一波等我更文教你实现React Router v6
的守卫,懒加载和类似vue
的meta
元信息等等功能