feat: 增加KeepAliveOverlay组件用于缓存iframe

pull/64/head
moonrailgun 2 years ago
parent fcc2684a34
commit 373e424e6a

@ -11,9 +11,7 @@ const GroupWebPanelRender: React.FC<{ panelInfo: any }> = (props) => {
const url = panelInfo?.meta?.url;
return (
<Webview key={String(url)} className="w-full h-full bg-white" url={url} />
);
return <Webview key={String(url)} className="w-full h-full" url={url} />;
};
GroupWebPanelRender.displayName = 'GroupWebPanelRender';

@ -0,0 +1,33 @@
import React, { FC } from 'react';
import { useKeepAliveStore } from './store';
export const KeepAliveOverlayHost: FC<React.PropsWithChildren> = (props) => {
const cachedComponents = useKeepAliveStore((state) => state.cachedComponents);
return (
<>
{props.children}
<div className="keep-alive-overlay-host">
{Object.entries(cachedComponents).map(([cacheId, cacheItem]) => {
const { visible, element, rect } = cacheItem;
return (
<div
id={`cache-${cacheId}`}
style={{
display: visible ? 'block' : 'none',
...rect,
position: 'fixed',
}}
key={cacheId}
>
{element}
</div>
);
})}
</div>
</>
);
};
KeepAliveOverlayHost.displayName = 'KeepAliveOverlayHost';

@ -0,0 +1,3 @@
组件状态缓存, 使用覆盖层的方式来保持存活(类似小程序叠加原生程序的感觉)
支持`iframe`的渲染缓存.

@ -0,0 +1,2 @@
export { KeepAliveOverlayHost } from './KeepAliveOverlayHost';
export { withKeepAliveOverlay } from './withKeepAliveOverlay';

@ -0,0 +1,79 @@
import type React from 'react';
import create from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface ElementDisplayRect {
left: number;
top: number;
width: number;
height: number;
}
interface KeepAliveState {
cachedComponents: Record<
string,
{
visible: boolean;
element: React.ReactElement;
rect: ElementDisplayRect;
}
>;
mount: (cacheId: string, element: React.ReactElement) => void;
show: (cacheId: string) => void;
hide: (cacheId: string) => void;
updateRect: (cacheId: string, rect: ElementDisplayRect) => void;
}
export const useKeepAliveStore = create<KeepAliveState>()(
immer((set) => ({
cachedComponents: {},
mount: (cacheId, element) => {
set((state) => {
const cachedComponents = state.cachedComponents;
if (cachedComponents[cacheId]) {
// 已经挂载过
state.cachedComponents[cacheId].visible = true;
return;
}
cachedComponents[cacheId] = {
visible: true,
element,
rect: {
left: 0,
top: 0,
width: 0,
height: 0,
},
};
});
},
show: (cacheId) => {
set((state) => {
if (!state.cachedComponents[cacheId]) {
return;
}
state.cachedComponents[cacheId].visible = true;
});
},
hide: (cacheId) => {
set((state) => {
if (!state.cachedComponents[cacheId]) {
return;
}
state.cachedComponents[cacheId].visible = false;
});
},
updateRect: (cacheId: string, rect: ElementDisplayRect) => {
set((state) => {
if (!state.cachedComponents[cacheId]) {
return;
}
state.cachedComponents[cacheId].rect = rect;
});
},
}))
);

@ -0,0 +1,86 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { useKeepAliveStore } from './store';
import _omit from 'lodash/omit';
/**
*
*/
interface StyleProps {
className?: string;
style?: React.CSSProperties;
}
/**
* : (className, style),
*
*/
export function withKeepAliveOverlay<
P extends StyleProps = StyleProps,
OP extends Omit<P, 'className' | 'style'> = Omit<P, 'className' | 'style'>
>(
OriginComponent: React.ComponentType<OP>,
config: { cacheId: string | ((props: OP) => string) }
) {
// eslint-disable-next-line react/display-name
return (props: P) => {
const containerRef = useRef<HTMLDivElement>(null);
const originProps = _omit(props, ['className', 'style']) as OP;
const { mount, hide, updateRect } = useKeepAliveStore();
const cacheId = useMemo(() => {
if (typeof config.cacheId === 'function') {
return config.cacheId(originProps);
}
return config.cacheId;
}, []);
useEffect(() => {
mount(cacheId, <OriginComponent key={cacheId} {...originProps} />);
return () => {
hide(cacheId);
};
}, []);
useEffect(() => {
if (!containerRef.current) {
return;
}
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { target } = entry;
if (!target.parentElement) {
return;
}
const rect = target.getBoundingClientRect();
updateRect(cacheId, {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
});
});
});
resizeObserver.observe(containerRef.current);
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, []);
return (
<div
id={`withKeepAlive${cacheId}`}
ref={containerRef}
className={props.className}
style={props.style}
/>
);
};
}

@ -1,17 +1,20 @@
import React from 'react';
import { withKeepAliveOverlay } from './KeepAliveOverlay';
interface WebviewProps {
url: string;
className?: string;
style?: React.CSSProperties;
url: string;
}
/**
*
*/
export const Webview: React.FC<WebviewProps> = React.memo((props) => {
return (
<iframe className={props.className} style={props.style} src={props.url} />
export const Webview: React.FC<WebviewProps> =
withKeepAliveOverlay<WebviewProps>(
(props) => {
return <iframe className="w-full h-full bg-white" src={props.url} />;
},
{ cacheId: (props) => props.url }
);
});
Webview.displayName = 'Webview';

@ -18,6 +18,7 @@ import { PortalHost } from '@/components/Portal';
import { setGlobalSocket, setGlobalStore } from '@/utils/global-state-helper';
import { SocketContextProvider } from '@/context/SocketContext';
import { Problem } from '@/components/Problem';
import { KeepAliveOverlayHost } from '@/components/KeepAliveOverlay';
/**
* hooks
@ -92,7 +93,9 @@ export const MainProvider: React.FC<PropsWithChildren> = React.memo((props) => {
<ReduxProvider store={store}>
<SocketContextProvider socket={socket}>
<SidebarContextProvider>
<KeepAliveOverlayHost>
<PortalHost>{props.children}</PortalHost>
</KeepAliveOverlayHost>
</SidebarContextProvider>
</SocketContextProvider>
</ReduxProvider>

Loading…
Cancel
Save