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