前言
我们在写公共组件的时候,有时候希望整个项目只暴露一个组件实例出来,以节约内存。
react实现
封装一个instance.ts用于暴露组件实例。
import React from 'react'
import { createRoot, Root } from 'react-dom/client'
// 容器
type Container = Element | DocumentFragment
type ExtendedRoot = Root & { unmountPromise?: Promise<boolean> }
// 容器对应 root 的 map, 因为 Container 类型问题所以使用 weakMap
const containerMap = new WeakMap<Container, ExtendedRoot>()
export async function modernRender(node: React.ReactElement, container: Container) {
let root = containerMap.get(container)
root = containerMap.has(container) ? (containerMap.get(container) as ExtendedRoot) : createRoot(container)
containerMap.set(container, root)
root.render(node)
}
export function modernUnmount(container: Container) {
// Delay to unmount to avoid React 18 sync warning
if (containerMap.has(container)) {
const root = containerMap.get(container) as ExtendedRoot
root?.unmount()
containerMap.delete(container)
}
}
/**
* 创建全局公共组件(单例)
*/
export default components => {
let instance
return () => {
if (instance) {
return instance
}
const div = document.createElement('div')
document.body.appendChild(div)
return (instance = {
show: (option = {}) => {
const props = {
...(typeof option === 'object' ? option : { text: option }),
visible: true,
manual: true,
key: Date.now()
}
return modernRender(React.createElement(components, props), div)
},
hide: (option = {}) => {
const props = {
...(typeof option === 'object' ? option : { text: option }),
visible: false,
manual: true,
key: Date.now()
}
return modernRender(React.createElement(components, props), div)
}
})
}
}
假设我们现在有一个公共loading组件,比如:
import React, { useState, memo, useEffect } from 'react'
import getInstance from '../instance'
import styles from './index.module.scss'
interface ILoadingProps {
hasMask?: boolean // 是否有蒙层
duration?: number // 展示时长
onClose?: () => void // 关闭回调
visible?: boolean // 是否展示
manual?: boolean // 是否是手动调用,true表示来自手动调用
}
const defaultOptions: ILoadingProps = {
hasMask: true,
duration: 0,
visible: true,
onClose: () => {
// do nothing!
},
manual: false
}
let timer: NodeJS.Timeout | undefined
const clearTimer = () => {
if (timer) {
clearTimeout(timer)
timer = undefined
}
}
let loadingCallTimes = 0
const _Loading: React.FC<ILoadingProps> = (props: ILoadingProps) => {
const { hasMask, duration, visible, onClose, manual } = props
const [show, setShow] = useState(true)
useEffect(() => {
setShow(visible || false)
if (manual) {
if (visible) {
loadingCallTimes++
if (duration) {
clearTimer()
timer = setTimeout(() => {
hide()
clearTimer()
}, duration)
}
} else {
hide()
}
}
}, [visible])
const hide = () => {
loadingCallTimes--
if (loadingCallTimes <= 0) {
if (loadingCallTimes === 0) {
setShow(false)
onClose && onClose()
} else {
loadingCallTimes = 0
console.warn('【loading】请勿多次调用 loading.hide ')
}
}
}
return (
<>
{show && (
<>
{hasMask && <div className={'full'} onClick={e => e.stopPropagation()}></div>}
<div className={`center-middle ${styles.loadingWrap} flex-center`}>
<div className={`${styles.loading}`}></div>
</div>
</>
)}
</>
)
}
_Loading.defaultProps = defaultOptions
const Loading = getInstance(_Loading)()
export default Loading
// 输出通用 中间查询状态对应的路由页面
const RouteLoading = memo(_Loading)
const RouteLoadingComp = (props: ILoadingProps) => <RouteLoading {...props} />
export { RouteLoadingComp }
loading组件样式文件index.module.scss:
.loadingWrap {
position: fixed;
width: 80px;
height: 80px;
background: transparent;
border-radius: 8px;
z-index: 900;
.loading {
width: 80px;
height: 80px;
background: url('~src/assets/images/loading.png') center no-repeat;
background-size: contain;
animation: rotate 0.8s linear infinite;
}
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}