Appearance
优化篇-公共弹窗拆解优化,让职能更加单一
本章节主要是对公共弹窗的拆解优化,如果你对本章节内容兴趣不大,可以快速阅读或跳过。
公共弹窗拆解—流程梳理及职责划分
想必小伙伴们还记得我们在点击导出PDF
按钮时,会出现一个确认弹窗。如下图所示
这是一个常见的功能,我们在第八章添加了 <MyModal.Comfirm />
组件实现了上图的弹窗效果。但这仅仅只是一个最简单的场景,如果面对更为复杂多样的弹窗效果,就不适用了。让我们来预测一下,之后我们可能会出现什么弹窗场景呢?
- 场景一
当点击 Button 按钮,弹窗打开,弹窗的位置是水平垂直居中,存在蒙层,点击弹窗区域之外(即蒙层),弹窗关闭
- 场景二
当点击 Button 按钮,弹窗打开,弹窗的位置是水平垂直居中,存在蒙层
- 场景三
当点击 Button 按钮,弹窗打开,弹窗的位置是水平垂直居中,没有蒙层
- 场景四
当点击 Button 按钮,弹窗打开,弹窗的位置是垂直居中,位于顶部,没有蒙层,点击弹窗之外区域,弹窗关闭
当然还有其他场景,以目前我们封装的 renderer/common/components/MyModal/MyConfirm
组件仅能满足场景二,其他场景均不满足。所以我们需要进行优化处理,我们必须捋一捋我们的流程。
流程梳理
通过下面流程图,我们来捋一捋这个流程
下面是一段伪代码:
ts
import React, { useState, useEffect } from 'react';
function DownloadPdf() {
// 控制弹窗显隐
const [showModal, setShowModal] = useState(false);
// 监听 click 事件,在组件卸载时移除
useEffect(() => {
window.addEventListener('click', handleClickBody, false);
return () => {
window.removeEventListener('click', handleClickBody, false);
};
}, []);
// 在点击弹窗之外区域,关闭弹窗
const handleClickBody = () => setShowModal(false);
// 点击按钮,显示弹窗
const handleClickButton = () => setShowModal(true);
return (
<div>
<button onClick={handleClickButton}>导出简历</button>
{showModal && (
<div
styleName="mask"
onClick={(e) => {
// 需要阻止冒泡,不然点击弹窗区域会导致弹窗关闭
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}}
>
<DownLoadModal onCancel={() => setShowModal(false)} />
</div>
)}
</div>
);
}
整体来讲,我们弹窗的主要流程大致如上所示。
抽丝剥茧
动动我们的小奶袋瓜,结合四种场景,想一下点击弹窗区域之外,关闭弹窗这个交互效果,是否能做成通用呢?换个说法,我们能否能抽象成:给你一个 elementRef
,点击 elementRef
区域之外,触发回调事件。
举几个例子:
- 点击弹窗区域之外,关闭弹窗(这里的 elementRef 元素就是弹窗组件,回调事件就是关闭弹窗)
- 点击侧边栏区域之外,给个提示框(这里的 elementRef 元素就是侧边栏组件,回调事件就是显示提示框)
- 点击简历头像,给个夸奖(这里的 elementRef 元素就是简历头像,回调事件就是一句夸奖提示)
我们肯定不想在每个实现此交互效果的组件中,内部实现一套 监听事件
,所以我们能否将其抽离出来呢?思考思考如何实现?
还有一个问题,我们想一想,这个位置是否可选?比如我期望该显示的弹窗水平垂直居中,或者头部居中、底部居中,再或者我期望该蒙层的背景色能进行改变?我们肯定不期望每次都自己 Ctrl+C
拷贝一份蒙层相关的代码吧?所以我们能否将其抽离出来呢?思考思考如何实现?
useClickAwayHook
受 react-use 的 useClickAway 影响,自己动手实现了一个 useClickAwayHook,即 triggers a callback when user clicks outside the target element.
具体实现是怎样的呢?我们先前往 renderer/common
目录下,新增一个名为 hook
的文件夹,顾名思义,不再解释,我们新增一个名为 useClickAway.ts
文件,让我们来写一下实现的代码
ts
// renderer/common/hook/useClickAway.ts
import { useEffect, useRef, useState } from 'react';
/**
* @description 点击元素之外区域关闭
*/
function useClickAway(initIsVisible: boolean) {
const ref = useRef() as React.MutableRefObject<HTMLDivElement>;
const [componentVisible, setComponentVisible] = useState(initIsVisible);
const onClickOutSide = (event: any) => {
if (ref.current && !ref.current.contains(event.target)) {
setComponentVisible(false);
}
};
useEffect(() => {
document.addEventListener('click', onClickOutSide, true);
return () => {
document.removeEventListener('click', onClickOutSide, true);
};
});
return { ref, componentVisible, setComponentVisible };
}
export default useClickAway;
使用上非常简单,下面以导出PDF
弹窗确定为例,看看如何使用
ts
// renderer/container/resume/ResumeAction/index.tsx
import useClickAway from '@common/hook/useClickAway';
function ResumeAction() {
const { ref, componentVisible, setComponentVisible } = useClickAway(false);
return (
<div styleName="actions">
<div styleName="back" onClick={onBack}>
返回
</div>
<MyButton
size="middle"
className="export-btn"
onClick={() => setComponentVisible(true)}
>
导出PDF
</MyButton>
{componentVisible && (
<MyModal.Confirm
eleRef={ref}
title="确定要打印简历吗?"
description="请确保信息的正确,目前仅支持单页打印哦~"
config={{
cancelBtn: {
isShow: true,
callback: () => setComponentVisible(false),
},
submitBtn: {
isShow: true,
callback: exportPdf,
},
}}
/>
)}
</div>
);
}
👉 具体相关代码可看此 commit
MyMaskHoc
前往 renderer/common
目录下,新增一个名为 hoc
的文件夹,顾名思义,不再解释,我们新增一个名为 MyMaskHoc
文件夹,让我们来写一下实现的代码
ts
// renderer/common/hoc/MyMaskHoc/index.tsx
/**
* @description 为目标组件添加一层蒙层
*/
import React from 'react';
import './index.less';
import classnames from 'classnames';
export type Position = 'top' | 'bottom' | 'center';
const MyMaskHoc =
(WrappedComponent: React.ComponentType) =>
(hocProps: { position?: Position; backgroundColor?: string }) => {
return class extends React.Component {
getProps = () => ({
...this.props,
});
render() {
const position = hocProps ? hocProps?.position : 'center';
const backgroundColor = hocProps
? hocProps?.backgroundColor
: 'rgba(0, 0, 0, 0.78)';
return (
<div
styleName="vis-mask"
style={{ backgroundColor: backgroundColor }}
>
<div
styleName={classnames({
top: position === 'top',
center: position === 'center',
bottom: position === 'bottom',
})}
>
<WrappedComponent {...this.getProps()} />
</div>
</div>
);
}
};
};
export default MyMaskHoc;
👉 具体相关代码可看此 commit
使用就比较简单了,我们只给需要添加蒙层的组件,包裹一下 MyMaskHoc
即可。这边就不一一改造了,感兴趣的小伙伴可以私下对项目中的弹窗蒙层进行改造一波,下面给出一个简单的使用例子
ts
// 测试例子
import React from 'react';
import MyMaskHoc from '@src/common/hoc/MyMaskHoc';
function TestModal() {
return <div>我是测试带有蒙层的代码</div>;
}
export default MyMaskHoc(TestModal)({
position: 'top',
backgroundColor: 'red',
});
小测试:上面的 TestModal
实现了蒙层效果,那么我想将 useClickAway
添加进来,实现交互级的效果,这代码该如何写呢?
总结
上面我们对公共弹窗的交互进行了分析,从而实现了 useClickAwayHook
和 MyMaskHoc
,至于两者如何使用,我在这里当成是一个小测试。
本章节的重点在于从一个常见的场景出发,去分析一整套流程下来,哪部分可以进行抽离分割,从而达到高内聚、低耦合的结果。关于此章节,如果有疑问,可以在评论区指出。