React 组件:Loading 弹窗
# 0. 前言
实现效果:当发起网络请求后,在数据获取前,展示全屏 Loading 弹窗;数据获取后,展示实际的内容页面。
# 1. 代数分离逻辑
const SamplePreview = () => {
// 将 Loading 的状态,通过 useState 进行副作用分离操作
const [condition, setCondition] = useState({});
async function init() {
setCondition({ xxx: xxxx });
}
if (condition.xxx) {
init();
return <Loading fullScreen />;
}
return <div>真实的组件显示</div>;
};
ReactDOM.render(<SamplePreview />, document.getElementById("root"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
参考:https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/preview.tsx
# 2. 函数方案
上面的方案中,需借助 React 中抽离 state 副作用的特性,对 Loading 组件进行渲染拦截,当 condition 满足条件后,才允许打开弹窗。
此方案的缺点是复用性比较差,需要改变原有组件逻辑,可通过 HOC 实现。如果抽离成函数,则能大大提供复用性。
const open = (target: string | HTMLElement = "body", props?: any) => {
/* 1. 获取绑定父元素 */
let divEle: HTMLElement;
if (typeof target === "string") {
divEle = document.querySelector(target) as HTMLElement;
} else {
divEle = target;
}
/* 2. 创建 loading 子元素 */
const loadingEle = document.createElement("div");
loadingEle.className = "loading-box";
/* 3. 两者产生联系 */
divEle.append(loadingEle);
/* 使用 ReactDOM.render 函数对组件进行渲染 */
ReactDOM.render(<Loading {...props} />, loadingEle);
/* 提供销毁方法 */
return {
destroy: () => {
/* 1.删除 dom 元素 */
loadingEle.remove();
if (loadingEle) {
ReactDOM.unmountComponentAtNode(loadingEle);
}
},
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
open 函数内部的逻辑
- 找到
loading挂载的父元素,构建loading自身的子元素,通过append产生联系。 - 提供
target入参,手动选择loading挂载的父元素,默认body。并提供destroy销毁函数。
# 3. 进一步优化:
以上函数还是有缺点,当同一时间发生多次网络请求,会出现 loading 弹窗提前关闭的问题。因此需要对 loading 进行计数操作。
计数操作:
interface LoadingCountObj {
count: number;
instance: {
destroy: () => void;
};
}
/* key 值为绑定的父元素, value 为弹窗打开的计数 */
const loadingCounts = new Map<string | HTMLElement, LoadingCountObj>();
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
每次执行 openLoading 操作时,第一次打开弹窗时,open 调用后 count = 1,当已打开弹窗时,不会调用 open ,只是 count += 1。
/* 用户可传入 target */
interface LoadingConfig {
target?: string | HTMLElement;
}
const openLoading = (options: LoadingConfig) => {
/* 当打开 loading 后,初始化 Map */
const target = options.target || "body";
if (!loadingCounts.has(target)) {
loadingCounts.set(target, {
count: 0,
instance: undefined,
});
}
/* 如果 Map 中存在 target,则计数*/
const countObj = loadingCounts.get(target);
if (countObj.count <= 0) {
countObj.count = 1;
countObj.instance = open(target);
} else {
countObj.count += 1;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
关闭弹窗逻辑类似,关闭弹窗时,弹窗计数重置 count = 0
const closeLoading = (options: LoadingConfig) => {
const target = options.target || "body";
const countObj = loadingCounts.get(target);
if (!countObj) {
return;
}
countObj.count = countObj.count - 1;
if (countObj.count <= 0) {
countObj.count = 0;
if (countObj.instance) {
countObj.instance.destroy();
countObj.instance = undefined;
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4. 细节
Loading 弹窗组件有可能存在 zIndex 问题,即,子元素的 zIndex 受到父元素的 zIndex 的影响,因此需通过 React.createProtal 渲染到 .loading-box 节点下。
const Loading = () => {
return (
React.createProtal(
<div>
<Spin spining={true}></Spin>
</div>,
),
document.querySelector(".loading-box")
);
};
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
编辑 (opens new window)
上次更新: 2023/02/21, 10:02:00