perf(web): release monitor resources on unmount
This commit is contained in:
@@ -13,6 +13,9 @@ const markerSetMap = vi.fn();
|
||||
const markerSetPosition = vi.fn();
|
||||
const setZoomAndCenter = vi.fn();
|
||||
const panTo = vi.fn();
|
||||
const mapOff = vi.fn();
|
||||
const mapDestroy = vi.fn();
|
||||
const massOff = vi.fn();
|
||||
const getZoom = vi.fn(() => 5);
|
||||
const getBounds = vi.fn((): ReturnType<NonNullable<AMapMap['getBounds']>> => ({}));
|
||||
const mapHandlers = new Map<string, (event: unknown) => void>();
|
||||
@@ -26,8 +29,9 @@ class TestMap {
|
||||
}
|
||||
add = vi.fn();
|
||||
addControl = vi.fn();
|
||||
destroy = vi.fn();
|
||||
destroy = mapDestroy;
|
||||
on = vi.fn((event: string, handler: (value: unknown) => void) => mapHandlers.set(event, handler));
|
||||
off = mapOff;
|
||||
getZoom = getZoom;
|
||||
getBounds = getBounds;
|
||||
setZoomAndCenter = setZoomAndCenter;
|
||||
@@ -36,6 +40,7 @@ class TestMap {
|
||||
|
||||
class TestMassMarks {
|
||||
on = vi.fn();
|
||||
off = massOff;
|
||||
setMap = vi.fn();
|
||||
setData = setData;
|
||||
setStyle = setStyle;
|
||||
@@ -145,6 +150,9 @@ afterEach(() => {
|
||||
markerSetPosition.mockReset();
|
||||
setZoomAndCenter.mockReset();
|
||||
panTo.mockReset();
|
||||
mapOff.mockReset();
|
||||
mapDestroy.mockReset();
|
||||
massOff.mockReset();
|
||||
getZoom.mockReset();
|
||||
getZoom.mockReturnValue(5);
|
||||
getBounds.mockReset();
|
||||
@@ -187,6 +195,21 @@ test('renders data that arrives before the delayed AMap SDK is ready', async ()
|
||||
expect(decodeURIComponent(clusterStyles[5].url)).not.toContain('10+');
|
||||
});
|
||||
|
||||
test('detaches AMap listeners and destroys the map on unmount', async () => {
|
||||
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
|
||||
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
|
||||
|
||||
const view = render(<FleetMap vehicles={[]} monitorMap={pointMap} onSelect={() => undefined} />);
|
||||
await waitFor(() => expect(setData).toHaveBeenCalled());
|
||||
view.unmount();
|
||||
|
||||
expect(massOff).toHaveBeenCalledWith('click', expect.any(Function));
|
||||
expect(mapOff).toHaveBeenCalledWith('moveend', expect.any(Function));
|
||||
expect(mapOff).toHaveBeenCalledWith('zoomend', expect.any(Function));
|
||||
expect(mapOff).toHaveBeenCalledWith('dragstart', expect.any(Function));
|
||||
expect(mapDestroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('converts AMap GCJ-02 bounds back to WGS-84 before requesting monitor data', async () => {
|
||||
const [west, south] = wgs84ToGcj02(113, 22);
|
||||
const [east, north] = wgs84ToGcj02(114, 24);
|
||||
|
||||
@@ -171,6 +171,11 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
let cancelled = false;
|
||||
let resizeTimer: number | undefined;
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
let mapInstance: AMapMap | undefined;
|
||||
let massInstance: AMapMassMarks | undefined;
|
||||
let selectMassPoint: ((event: { data: AMapMassPoint }) => void) | undefined;
|
||||
let notifyViewport: (() => void) | undefined;
|
||||
let stopFollowing: (() => void) | undefined;
|
||||
setState('loading');
|
||||
loadAMap(['AMap.Scale', 'AMap.ToolBar']).then((AMap) => {
|
||||
if (cancelled || !containerRef.current) return;
|
||||
@@ -186,13 +191,15 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
showLabel: true,
|
||||
resizeEnable: true
|
||||
});
|
||||
mapInstance = map;
|
||||
map.addControl(new AMap.Scale());
|
||||
if (AMap.ToolBar) map.addControl(new AMap.ToolBar({ position: { right: '18px', bottom: '76px' } }));
|
||||
const styles = [
|
||||
...COLORS.map((color) => ({ url: dotDataUrl(color), anchor: new AMap.Pixel(9, 9), size: new AMap.Size(18, 18) }))
|
||||
];
|
||||
const mass = new AMap.MassMarks([], { opacity: 0.96, zIndex: 120, cursor: 'pointer', style: styles, zooms: [3, 20] });
|
||||
mass.on('click', (event) => {
|
||||
massInstance = mass;
|
||||
selectMassPoint = (event: { data: AMapMassPoint }) => {
|
||||
const cluster = clustersRef.current.get(event.data.id);
|
||||
if (cluster) {
|
||||
map.setZoomAndCenter?.(Math.min(20, (map.getZoom?.() ?? 5) + 2), wgs84ToGcj02(cluster.longitude, cluster.latitude));
|
||||
@@ -204,12 +211,13 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
}
|
||||
const vehicle = vehiclesRef.current.get(event.data.id);
|
||||
if (vehicle) onSelectRef.current(vehicle);
|
||||
});
|
||||
};
|
||||
mass.on('click', selectMassPoint);
|
||||
mass.setMap(map);
|
||||
const labels = AMap.LabelsLayer ? new AMap.LabelsLayer({ zooms: [11, 18.99], zIndex: 110, collision: true, allowCollision: false }) : null;
|
||||
const denseLabels = AMap.LabelsLayer ? new AMap.LabelsLayer({ zooms: [19, 20], zIndex: 110, collision: false, allowCollision: true }) : null;
|
||||
labels?.setMap(map);
|
||||
const notifyViewport = () => {
|
||||
notifyViewport = () => {
|
||||
setMapZoom(map.getZoom?.() ?? 5);
|
||||
window.clearTimeout(viewportTimerRef.current);
|
||||
viewportTimerRef.current = window.setTimeout(() => {
|
||||
@@ -219,11 +227,12 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
};
|
||||
map.on?.('moveend', notifyViewport);
|
||||
map.on?.('zoomend', notifyViewport);
|
||||
map.on?.('dragstart', () => {
|
||||
stopFollowing = () => {
|
||||
if (!centeredVinRef.current) return;
|
||||
followSelectedRef.current = false;
|
||||
setFollowSelected(false);
|
||||
});
|
||||
};
|
||||
map.on?.('dragstart', stopFollowing);
|
||||
mapRef.current = map;
|
||||
amapRef.current = AMap;
|
||||
massRef.current = mass;
|
||||
@@ -246,6 +255,12 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
|
||||
resizeObserver?.disconnect();
|
||||
window.clearTimeout(resizeTimer);
|
||||
window.clearTimeout(viewportTimerRef.current);
|
||||
if (selectMassPoint) massInstance?.off?.('click', selectMassPoint);
|
||||
if (notifyViewport) {
|
||||
mapInstance?.off?.('moveend', notifyViewport);
|
||||
mapInstance?.off?.('zoomend', notifyViewport);
|
||||
}
|
||||
if (stopFollowing) mapInstance?.off?.('dragstart', stopFollowing);
|
||||
massRef.current?.setMap(null);
|
||||
labelsRef.current?.setMap(null);
|
||||
denseLabelsRef.current?.setMap(null);
|
||||
|
||||
@@ -35,6 +35,8 @@ export function TrackMap({ points, stops, activeIndex, showStops, follow, onSele
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !isAMapConfigured(getAMapConfig())) { setState('fallback'); return; }
|
||||
let cancelled = false;
|
||||
let mapInstance: AMapMap | undefined;
|
||||
let stopFollowing: (() => void) | undefined;
|
||||
loadAMap(['AMap.Scale', 'AMap.ToolBar']).then((AMap) => {
|
||||
if (cancelled || !containerRef.current) return;
|
||||
const first = valid[0]?.point;
|
||||
@@ -44,15 +46,18 @@ export function TrackMap({ points, stops, activeIndex, showStops, follow, onSele
|
||||
viewMode: '2D', mapStyle: 'amap://styles/whitesmoke', showLabel: true, resizeEnable: true,
|
||||
zooms: [3, 20]
|
||||
});
|
||||
mapInstance = map;
|
||||
map.addControl(new AMap.Scale());
|
||||
if (AMap.ToolBar) map.addControl(new AMap.ToolBar({ position: { right: '18px', bottom: '148px' } }));
|
||||
map.on?.('dragstart', () => followChangeRef.current(false));
|
||||
stopFollowing = () => followChangeRef.current(false);
|
||||
map.on?.('dragstart', stopFollowing);
|
||||
mapRef.current = map;
|
||||
amapRef.current = AMap;
|
||||
setState('ready');
|
||||
}).catch(() => { if (!cancelled) setState('error'); });
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (stopFollowing) mapInstance?.off?.('dragstart', stopFollowing);
|
||||
overlaysRef.current.forEach((overlay) => overlay.setMap?.(null));
|
||||
currentMarkerRef.current?.setMap?.(null);
|
||||
mapRef.current?.destroy();
|
||||
|
||||
Reference in New Issue
Block a user