这里只是hooks useMap的封装使用,没有对地图组件进行封装,地图组件封装
地图组件封装
useMap.ts
import { Map } from 'maplibre-gl';
import { markRaw } from 'vue';
/**
* 地图实例接口
*/
interface MapInstance {
id: string;
map: Map;
isDestroyed: boolean;
createdAt: number;
}
/**
* 等待地图的回调函数类型
*/
interface MapWaitCallback {
resolve: (map: Map) => void;
reject: (error: Error) => void;
timeout?: NodeJS.Timeout;
}
/**
* 使用闭包创建多地图实例管理器
* 提供安全的地图实例存储、获取和销毁功能
*/
const createMapManager = (() => {
// 私有变量:存储多个地图实例,使用markRaw避免Vue响应式处理
const mapInstances: Record<string, MapInstance> = {};
// 私有变量:当前活跃的地图ID
let activeMapId: string | null = null;
// 私有变量:地图ID计数器
let mapIdCounter = 0;
// 私有变量:等待地图初始化的回调队列
const waitingCallbacks: Record<string, MapWaitCallback[]> = {};
/**
* 生成唯一的地图ID
*/
const generateMapId = (): string => {
return `map_${++mapIdCounter}_${Date.now()}`;
};
/**
* 触发等待回调
* @param mapId - 地图ID
* @param map - 地图实例
*/
const triggerWaitingCallbacks = (mapId: string, map: Map): void => {
const callbacks = waitingCallbacks[mapId];
if (callbacks && callbacks.length > 0) {
callbacks.forEach(callback => {
if (callback.timeout) {
clearTimeout(callback.timeout);
}
callback.resolve(map);
});
delete waitingCallbacks[mapId];
}
};
/**
* 触发等待回调的错误
* @param mapId - 地图ID
* @param error - 错误信息
*/
const triggerWaitingCallbacksError = (mapId: string, error: Error): void => {
const callbacks = waitingCallbacks[mapId];
if (callbacks && callbacks.length > 0) {
callbacks.forEach(callback => {
if (callback.timeout) {
clearTimeout(callback.timeout);
}
callback.reject(error);
});
delete waitingCallbacks[mapId];
}
};
return {
/**
* 添加地图实例
* @param map - MapLibre地图实例
* @param customId - 自定义地图ID(可选)
* @returns 返回地图ID
* @throws 如果传入的不是有效的Map实例则抛出错误
*/
addMap(map: Map, customId?: string): string {
if (!(map instanceof Map)) {
throw new Error('Invalid map instance provided. Expected MapLibre Map instance.');
}
const mapId = customId || generateMapId();
// 如果已经存在相同ID的地图实例,先销毁旧实例
if (mapInstances[mapId]) {
console.warn(`Replacing existing map instance with ID: ${mapId}`);
this.destroyMap(mapId);
}
// 创建新的地图实例记录
const mapInstance: MapInstance = {
id: mapId,
map: markRaw(map),
isDestroyed: false,
createdAt: Date.now()
};
mapInstances[mapId] = mapInstance;
// 如果是第一个地图实例,设置为活跃地图
if (!activeMapId) {
activeMapId = mapId;
}
// 触发等待该地图的回调
triggerWaitingCallbacks(mapId, map);
// 如果有等待活跃地图的回调,也要触发
if (mapId === activeMapId) {
triggerWaitingCallbacks('active', map);
}
return mapId;
},
/**
* 获取地图实例(同步版本)
* @param mapId - 地图ID,如果不提供则返回活跃的地图
* @returns 返回地图实例
* @throws 如果地图不存在或已销毁则抛出错误
*/
getMap(mapId?: string): Map {
const targetId = mapId || activeMapId;
if (!targetId) {
throw new Error('No active map found. Please add a map instance first.');
}
const mapInstance = mapInstances[targetId];
if (!mapInstance) {
throw new Error(`Map with ID "${targetId}" not found.`);
}
if (mapInstance.isDestroyed) {
throw new Error(`Map with ID "${targetId}" has been destroyed.`);
}
return mapInstance.map;
},
/**
* 异步获取地图实例
* @param mapId - 地图ID,如果不提供则等待活跃地图
* @param timeout - 超时时间(毫秒),默认30秒
* @returns 返回地图实例的Promise
*/
async getMapAsync(mapId?: string, timeout: number = 30000): Promise<Map> {
const targetId = mapId || activeMapId || 'active';
// 如果地图已经存在,直接返回
if (targetId !== 'active' && mapInstances[targetId] && !mapInstances[targetId].isDestroyed) {
return mapInstances[targetId].map;
}
// 如果是活跃地图且已存在,直接返回
if (targetId === 'active' && activeMapId && mapInstances[activeMapId] && !mapInstances[activeMapId].isDestroyed) {
return mapInstances[activeMapId].map;
}
// 创建Promise等待地图初始化
return new Promise<Map>((resolve, reject) => {
const waitKey = targetId === 'active' ? 'active' : targetId;
if (!waitingCallbacks[waitKey]) {
waitingCallbacks[waitKey] = [];
}
// 设置超时
const timeoutId = setTimeout(() => {
// 从等待队列中移除这个回调
const callbacks = waitingCallbacks[waitKey];
if (callbacks) {
const index = callbacks.findIndex(cb => cb.timeout === timeoutId);
if (index !== -1) {
callbacks.splice(index, 1);
}
if (callbacks.length === 0) {
delete waitingCallbacks[waitKey];
}
}
reject(new Error(`Timeout waiting for map${targetId !== 'active' ? ` with ID "${targetId}"` : ''} to be initialized after ${timeout}ms`));
}, timeout);
waitingCallbacks[waitKey].push({
resolve,
reject,
timeout: timeoutId
});
});
},
/**
* 获取地图实例信息
* @param mapId - 地图ID
* @returns 返回地图实例信息
*/
getMapInfo(mapId: string): MapInstance | null {
return mapInstances[mapId] || null;
},
/**
* 获取所有地图实例
* @returns 返回所有地图实例的数组
*/
getAllMaps(): MapInstance[] {
return Object.values(mapInstances).filter(instance => !instance.isDestroyed);
},
/**
* 根据索引获取地图实例
* @param index - 地图索引
* @returns 返回地图实例
*/
getMapByIndex(index: number): Map {
const maps = this.getAllMaps();
if (index < 0 || index >= maps.length) {
throw new Error(`Map index ${index} is out of range. Available maps: ${maps.length}`);
}
return maps[index].map;
},
/**
* 异步根据索引获取地图实例
* @param index - 地图索引
* @param timeout - 超时时间(毫秒),默认30秒
* @returns 返回地图实例的Promise
*/
async getMapByIndexAsync(index: number, timeout: number = 30000): Promise<Map> {
return new Promise<Map>((resolve, reject) => {
const checkMaps = () => {
const maps = this.getAllMaps();
if (index >= 0 && index < maps.length) {
resolve(maps[index].map);
return true;
}
return false;
};
// 立即检查一次
if (checkMaps()) {
return;
}
// 设置超时
const timeoutId = setTimeout(() => {
reject(new Error(`Timeout waiting for map at index ${index} to be available after ${timeout}ms`));
}, timeout);
// 定期检查地图是否可用
const interval = setInterval(() => {
if (checkMaps()) {
clearTimeout(timeoutId);
clearInterval(interval);
}
}, 100);
});
},
/**
* 设置活跃地图
* @param mapId - 地图ID
* @throws 如果地图不存在或已销毁则抛出错误
*/
setActiveMap(mapId: string): void {
const mapInstance = mapInstances[mapId];
if (!mapInstance) {
throw new Error(`Map with ID "${mapId}" not found.`);
}
if (mapInstance.isDestroyed) {
throw new Error(`Map with ID "${mapId}" has been destroyed.`);
}
activeMapId = mapId;
// 触发等待活跃地图的回调
triggerWaitingCallbacks('active', mapInstance.map);
},
/**
* 获取活跃地图ID
* @returns 返回活跃地图ID
*/
getActiveMapId(): string | null {
return activeMapId;
},
/**
* 检查地图是否已初始化
* @param mapId - 地图ID,如果不提供则检查活跃地图
* @returns 返回地图初始化状态
*/
isMapInitialized(mapId?: string): boolean {
const targetId = mapId || activeMapId;
if (!targetId) {
return false;
}
const mapInstance = mapInstances[targetId];
return mapInstance ? !mapInstance.isDestroyed : false;
},
/**
* 销毁地图实例
* @param mapId - 地图ID,如果不提供则销毁活跃地图
*/
destroyMap(mapId?: string): void {
const targetId = mapId || activeMapId;
if (!targetId) {
return;
}
const mapInstance = mapInstances[targetId];
if (mapInstance && !mapInstance.isDestroyed) {
// 触发等待该地图的错误回调
triggerWaitingCallbacksError(targetId, new Error(`Map with ID "${targetId}" has been destroyed`));
// 如果地图实例有remove方法,调用它进行清理
if (typeof mapInstance.map.remove === 'function') {
try {
mapInstance.map.remove();
} catch (error) {
console.warn(`Error while removing map instance ${targetId}:`, error);
}
}
mapInstance.isDestroyed = true;
// 如果销毁的是活跃地图,重新设置活跃地图
if (activeMapId === targetId) {
const activeMaps = this.getAllMaps();
activeMapId = activeMaps.length > 0 ? activeMaps[0].id : null;
// 触发等待活跃地图的错误回调
triggerWaitingCallbacksError('active', new Error('Active map has been destroyed'));
}
}
},
/**
* 销毁所有地图实例
*/
destroyAllMaps(): void {
// 触发所有等待回调的错误
Object.keys(waitingCallbacks).forEach(key => {
triggerWaitingCallbacksError(key, new Error('All maps have been destroyed'));
});
for (const mapId in mapInstances) {
this.destroyMap(mapId);
}
Object.keys(mapInstances).forEach(key => delete mapInstances[key]);
activeMapId = null;
},
/**
* 清理已销毁的地图实例
*/
cleanup(): void {
for (const mapId in mapInstances) {
if (mapInstances[mapId].isDestroyed) {
delete mapInstances[mapId];
}
}
},
/**
* 重置地图管理器状态
* 用于测试或特殊情况下的状态重置
*/
reset(): void {
// 清理所有等待回调
Object.keys(waitingCallbacks).forEach(key => {
triggerWaitingCallbacksError(key, new Error('Map manager has been reset'));
});
this.destroyAllMaps();
mapIdCounter = 0;
}
};
})();
/**
* 提供地图实例给其他组件使用
* @param map - MapLibre地图实例
* @param customId - 自定义地图ID(可选)
* @returns 返回地图ID
*/
export function useProvideMap(map: Map, customId?: string): string {
return createMapManager.addMap(map, customId);
}
/**
* 注入并获取地图实例(同步版本)
* @param mapId - 地图ID,如果不提供则返回活跃地图
* @returns 返回地图实例
*/
export function useInjectMap(mapId?: string): Map {
return createMapManager.getMap(mapId);
}
/**
* 异步注入并获取地图实例
* @param mapId - 地图ID,如果不提供则等待活跃地图
* @param timeout - 超时时间(毫秒),默认30秒
* @returns 返回地图实例的Promise
*/
export function useInjectMapAsync(mapId?: string, timeout?: number): Promise<Map> {
return createMapManager.getMapAsync(mapId, timeout);
}
/**
* 获取所有地图实例
* @returns 返回所有地图实例的数组
*/
export function useGetAllMaps(): Array<{ id: string; map: Map; createdAt: number }> {
return createMapManager.getAllMaps().map(instance => ({
id: instance.id,
map: instance.map,
createdAt: instance.createdAt
}));
}
/**
* 根据索引获取地图实例(同步版本)
* @param index - 地图索引
* @returns 返回地图实例
*/
export function useGetMapByIndex(index: number): Map {
return createMapManager.getMapByIndex(index);
}
/**
* 异步根据索引获取地图实例
* @param index - 地图索引
* @param timeout - 超时时间(毫秒),默认30秒
* @returns 返回地图实例的Promise
*/
export function useGetMapByIndexAsync(index: number, timeout?: number): Promise<Map> {
return createMapManager.getMapByIndexAsync(index, timeout);
}
/**
* 设置活跃地图
* @param mapId - 地图ID
*/
export function useSetActiveMap(mapId: string): void {
createMapManager.setActiveMap(mapId);
}
/**
* 获取活跃地图ID
* @returns 返回活跃地图ID
*/
export function useGetActiveMapId(): string | null {
return createMapManager.getActiveMapId();
}
/**
* 检查地图是否已初始化
* @param mapId - 地图ID,如果不提供则检查活跃地图
* @returns 返回地图初始化状态
*/
export function useMapStatus(mapId?: string): boolean {
return createMapManager.isMapInitialized(mapId);
}
/**
* 销毁地图实例
* @param mapId - 地图ID,如果不提供则销毁活跃地图
*/
export function useDestroyMap(mapId?: string): void {
createMapManager.destroyMap(mapId);
}
/**
* 销毁所有地图实例
*/
export function useDestroyAllMaps(): void {
createMapManager.destroyAllMaps();
}
/**
* 重置地图管理器
* 主要用于测试或开发环境
*/
export function useResetMap(): void {
createMapManager.reset();
}
useMap
是一个专为 Vue3 + MapLibre 项目设计的地图实例管理工具,提供了完整的地图生命周期管理功能。该工具采用闭包设计模式,确保地图实例的安全存储和访问,支持多地图实例管理、异步获取、自动清理等功能。
markRaw
避免不必要的响应式处理// 导入所需的函数
import {
useProvideMap, // 提供地图实例
useInjectMap, // 同步获取地图实例
useInjectMapAsync, // 异步获取地图实例
useGetAllMaps, // 获取所有地图实例
useSetActiveMap, // 设置活跃地图
useDestroyMap, // 销毁地图实例
useMapStatus // 检查地图状态
} from 'shared-utils/hooks/web/useMap'
<template>
<div ref="mapContainer" class="map-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Map } from 'maplibre-gl'
import { useProvideMap, useDestroyMap } from 'shared-utils/hooks/web/useMap'
const mapContainer = ref<HTMLElement>()
let mapId: string
onMounted(() => {
// 创建地图实例
const map = new Map({
container: mapContainer.value!,
style: 'https://demotiles.maplibre.org/style.json',
center: [116.404, 39.915],
zoom: 10
})
// 注册地图实例到管理器
mapId = useProvideMap(map, 'main-map')
console.log('地图已注册,ID:', mapId)
})
onUnmounted(() => {
// 组件销毁时清理地图
useDestroyMap(mapId)
})
</script>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useInjectMap, useInjectMapAsync } from 'shared-utils/hooks/web/useMap'
// 方式1:同步获取(地图必须已经初始化)
onMounted(() => {
try {
const map = useInjectMap('main-map')
console.log('获取到地图实例:', map)
// 使用地图实例
map.flyTo({
center: [116.404, 39.915],
zoom: 12
})
} catch (error) {
console.error('获取地图失败:', error)
}
})
// 方式2:异步获取(推荐)
onMounted(async () => {
try {
const map = await useInjectMapAsync('main-map', 10000) // 10秒超时
console.log('异步获取到地图实例:', map)
// 使用地图实例
map.addLayer({
id: 'my-layer',
type: 'circle',
source: 'my-source',
paint: {
'circle-radius': 5,
'circle-color': '#007cbf'
}
})
} catch (error) {
console.error('异步获取地图失败:', error)
}
})
</script>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Map } from 'maplibre-gl'
import {
useProvideMap,
useGetAllMaps,
useSetActiveMap,
useGetMapByIndex
} from 'shared-utils/hooks/web/useMap'
const mapContainer1 = ref<HTMLElement>()
const mapContainer2 = ref<HTMLElement>()
onMounted(() => {
// 创建第一个地图
const map1 = new Map({
container: mapContainer1.value!,
style: 'https://demotiles.maplibre.org/style.json',
center: [116.404, 39.915],
zoom: 10
})
// 创建第二个地图
const map2 = new Map({
container: mapContainer2.value!,
style: 'https://demotiles.maplibre.org/style.json',
center: [121.473, 31.230],
zoom: 10
})
// 注册多个地图实例
const mapId1 = useProvideMap(map1, 'beijing-map')
const mapId2 = useProvideMap(map2, 'shanghai-map')
// 获取所有地图实例
const allMaps = useGetAllMaps()
console.log('所有地图实例:', allMaps)
// 设置活跃地图
useSetActiveMap('shanghai-map')
// 根据索引获取地图
const firstMap = useGetMapByIndex(0)
console.log('第一个地图:', firstMap)
})
</script>
<script setup lang="ts">
import {
useMapStatus,
useInjectMapAsync,
useGetActiveMapId
} from 'shared-utils/hooks/web/useMap'
// 检查地图状态
const checkMapStatus = () => {
const isInitialized = useMapStatus('main-map')
console.log('地图是否已初始化:', isInitialized)
const activeMapId = useGetActiveMapId()
console.log('当前活跃地图ID:', activeMapId)
}
// 安全的地图获取
const safeGetMap = async () => {
try {
// 设置较短的超时时间
const map = await useInjectMapAsync('main-map', 5000)
// 检查地图是否可用
if (map && !map.isDestroyed) {
console.log('地图可用,执行操作...')
// 执行地图操作
}
} catch (error) {
if (error.message.includes('Timeout')) {
console.error('地图获取超时,请检查地图是否正确初始化')
} else {
console.error('获取地图失败:', error.message)
}
}
}
</script>
// MapProvider.vue - 地图提供者组件
<template>
<div class="map-provider">
<div ref="mapContainer" class="map-container"></div>
<slot :map-id="mapId" :map-ready="mapReady"></slot>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Map } from 'maplibre-gl'
import { useProvideMap, useDestroyMap } from 'shared-utils/hooks/web/useMap'
interface Props {
mapId?: string
mapOptions?: any
}
const props = withDefaults(defineProps<Props>(), {
mapId: 'default-map',
mapOptions: () => ({
style: 'https://demotiles.maplibre.org/style.json',
center: [116.404, 39.915],
zoom: 10
})
})
const mapContainer = ref<HTMLElement>()
const mapId = ref<string>('')
const mapReady = ref(false)
onMounted(() => {
const map = new Map({
container: mapContainer.value!,
...props.mapOptions
})
map.on('load', () => {
mapReady.value = true
})
mapId.value = useProvideMap(map, props.mapId)
})
onUnmounted(() => {
if (mapId.value) {
useDestroyMap(mapId.value)
}
})
</script>
// MapConsumer.vue - 地图消费者组件
<template>
<div class="map-consumer">
<button @click="addMarker">添加标记</button>
<button @click="flyToLocation">飞行到位置</button>
</div>
</template>
<script setup lang="ts">
import { useInjectMapAsync } from 'shared-utils/hooks/web/useMap'
interface Props {
mapId: string
}
const props = defineProps<Props>()
const addMarker = async () => {
try {
const map = await useInjectMapAsync(props.mapId)
// 添加标记逻辑
map.addSource('marker-source', {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [116.404, 39.915]
}
}
})
map.addLayer({
id: 'marker-layer',
type: 'circle',
source: 'marker-source',
paint: {
'circle-radius': 8,
'circle-color': '#ff0000'
}
})
} catch (error) {
console.error('添加标记失败:', error)
}
}
const flyToLocation = async () => {
try {
const map = await useInjectMapAsync(props.mapId)
map.flyTo({
center: [121.473, 31.230],
zoom: 12,
duration: 2000
})
} catch (error) {
console.error('飞行到位置失败:', error)
}
}
</script>
// 创建一个地图操作的包装函数
const safeMapOperation = async (
mapId: string,
operation: (map: Map) => void | Promise<void>
) => {
try {
const map = await useInjectMapAsync(mapId, 10000)
await operation(map)
} catch (error) {
console.error(`地图操作失败 (${mapId}):`, error)
// 可以添加用户友好的错误提示
ElMessage.error('地图操作失败,请稍后重试')
}
}
// 使用示例
safeMapOperation('main-map', (map) => {
map.addLayer({
id: 'my-layer',
type: 'fill',
source: 'my-source'
})
})
// 使用 computed 缓存地图状态
import { computed } from 'vue'
const mapStatus = computed(() => {
return useMapStatus('main-map')
})
// 避免频繁的地图获取
let cachedMap: Map | null = null
const getCachedMap = async (mapId: string) => {
if (!cachedMap || cachedMap.isDestroyed) {
cachedMap = await useInjectMapAsync(mapId)
}
return cachedMap
}
// 在路由切换时清理地图
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave(() => {
// 清理当前页面的地图实例
useDestroyMap('current-page-map')
})
// 在应用关闭时清理所有地图
import { onBeforeUnmount } from 'vue'
import { useDestroyAllMaps } from 'shared-utils/hooks/web/useMap'
onBeforeUnmount(() => {
useDestroyAllMaps()
})
// 问题:地图实例未找到
// 解决:使用异步获取并设置合理的超时时间
try {
const map = await useInjectMapAsync('my-map', 15000)
} catch (error) {
if (error.message.includes('not found')) {
console.log('地图实例未注册,请检查地图ID')
} else if (error.message.includes('Timeout')) {
console.log('地图初始化超时,请检查网络连接')
}
}
// 问题:尝试使用已销毁的地图实例
// 解决:在使用前检查地图状态
const useMapSafely = async (mapId: string) => {
if (!useMapStatus(mapId)) {
throw new Error('地图实例不可用')
}
return await useInjectMapAsync(mapId)
}
// 问题:多个地图实例使用相同ID
// 解决:使用唯一的地图ID
const generateUniqueMapId = () => {
return `map_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
const mapId = generateUniqueMapId()
useProvideMap(map, mapId)
useMap
地图管理工具为 Vue3 + MapLibre 项目提供了完整的地图实例管理解决方案。通过合理使用其提供的 API,可以:
建议在实际项目中结合具体业务需求,选择合适的 API 组合使用,并遵循最佳实践以确保代码的稳定性和可维护性。
提示:本文档基于 Vue3 + MapLibre + TypeScript 技术栈,如果您使用的是其他技术栈,请根据实际情况调整代码示例。
相关资源:
- MapLibre GL JS 官方文档
- Vue3 Composition API 文档
- TypeScript 官方文档