组件封装--单例组件

63 阅读2分钟

前言

我们在写公共组件的时候,有时候希望整个项目只暴露一个组件实例出来,以节约内存。

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);
	}
}

参考:

前端必学-完美组件封装原则