Contents

微信小程序 Map 组件点聚合里的弯弯绕绕

微信小程序 Map 组件点聚合里的弯弯绕绕

总所周知,微信小程序的文档看了等于没看,它总能很巧妙的避开你会遇到的问题,所以很多时候只能你自己慢慢的去摸索(人工适配)。

本文为我在开发时遇到各种问题的总结,也会尝试带你实现 Map 组件的点聚合功能。

解决 Map 组件的类型定义

!在 Typescript 5.1.3 中,此定义无效

<map></map> 组件默认是 html 中用于定义一个图像映射(一个可点击的链接区域)的标准组件,详见 MDN - <map>

这在没有使用 ts 开发的项目中不会产生什么问题, 但在诸如 uni-app-ts-vue3 等 ts 项目中,map组件可能就会导致类型检查错误(一般来说都是由于 vue vscode 插件 volar 产生的,建议在出现问题的时候切换版本为 v1.2.0 发布版本)

那么我们就需要扩展 map 组件为小程序中应有的类型定义。

从微信文档中定义 Map 组件

虽然吐槽了微信小程序的文档,但开发的过程却也逃不出它的魔爪…

我在 npm 上检索了一下,没有发现有现有的类型 package,所以就只能手写加一些 HTML 数据处理(dom.query)了。 最终得到了以下的类型定义,虽然不是全部,但实现点聚合应该是没有遗漏了:

import type { SVGAttributes } from 'vue';

export interface Point {
  latitude: number;
  longitude: number;
}

export interface MarkerCallout {
  /** 文本 */
  content?: string;
  /** 文本颜色 */
  color?: string;
  /** 文字大小 */
  fontSize?: number;
  /** 边框圆角 */
  borderRadius?: number;
  /** 边框宽度 */
  borderWidth?: number;
  /** 边框颜色 */
  borderColor?: string;
  /** 背景色 */
  bgColor?: string;
  /** 文本边缘留白 */
  padding?: number;
  /** 'BYCLICK':点击显示; 'ALWAYS':常显 */
  display?: 'BYCLICK' | 'ALWAYS';
  /** 文本对齐方式。有效值: left, right, center */
  textAlign?: string;
  /** 横向偏移量,向右为正数 */
  anchorX?: number;
  /** 纵向偏移量,向下为正数 */
  anchorY?: number;
}

export interface MarkerCustomCallout {
  /** 'BYCLICK':点击显示; 'ALWAYS':常显 */
  display?: 'BYCLICK' | 'ALWAYS';
  /** 横向偏移量,向右为正数 */
  anchorX?: number;
  /** 纵向偏移量,向下为正数 */
  anchorY?: number;
}

export interface MarkerLabel {
  width?: number;
  height?: number;
  /** 文本 */
  content?: string;
  /** 文本颜色 */
  color?: string;
  /** 文字大小 */
  fontSize?: number;
  /** label的坐标(废弃) */
  x?: number;
  /** label的坐标(废弃) */
  y?: number;
  /** label的坐标,原点是 marker 对应的经纬度 */
  anchorX?: number;
  /** label的坐标,原点是 marker 对应的经纬度 */
  anchorY?: number;
  /** 边框宽度 */
  borderWidth?: number;
  /** 边框颜色 */
  borderColor?: string;
  /** 边框圆角 */
  borderRadius?: number;
  /** 背景色 */
  bgColor?: string;
  /** 文本边缘留白 */
  padding?: number;
  /** 文本对齐方式。有效值: left, right, center */
  textAlign?: string;
}

export interface Marker {
  /** 标记点 id */
  id?: number;
  /** 聚合簇的 id */
  clusterId?: Number;
  /** 是否参与点聚合 */
  joinCluster?: Boolean;
  /** 纬度 */
  latitude: number;
  /** 经度 */
  longitude: number;
  /** 标注点名 */
  title?: string;
  /** 显示层级 */
  zIndex?: number;
  /** 显示的图标 */
  iconPath?: string;
  /** 旋转角度 */
  rotate?: number;
  /** 标注的透明度 */
  alpha?: number;
  /** 标注图标宽度 */
  width?: number | string;
  /** 标注图标高度 */
  height?: number | string;
  /** 标记点上方的气泡窗口 */
  callout?: MarkerCallout;
  /** 自定义气泡窗口 */
  customCallout?: MarkerCustomCallout;
  /** 为标记点旁边增加标签 */
  label?: MarkerLabel;
  /** 经纬度在标注图标的锚点,默认底边中点 */
  anchor?: {
    /** x 表示横向(0-1) */
    x: number;
    /** y 表示纵向(0-1) */
    y: number;
  };
  /** 无障碍访问,(属性)元素的额外描述 */
  ariaLabel?: string;
}

export interface TextStyle {
  /** 文本颜色 */
  textColor?: string;
  /** 描边颜色	 */
  strokeColor?: string;
  /** 文本大小 */
  fontSize?: number;
}

export interface SegmentText {
  /** 名称 */
  name?: string;
  /** 起点 */
  startIndex?: number;
  /** 终点 */
  endIndex?: number;
}

export interface Polyline {
  /** 经纬度数组 */
  points: Point[];
  /** 线的颜色 */
  color?: string;
  /** 彩虹线 */
  colorList?: string[];
  /** 线的宽度 */
  width?: number;
  /** 是否虚线 */
  dottedLine?: boolean;
  /** 带箭头的线 */
  arrowLine?: boolean;
  /** 更换箭头图标 */
  arrowIconPath?: string;
  /** 线的边框颜色 */
  borderColor?: string;
  /** 线的厚度 */
  borderWidth?: number;
  /** 压盖关系 */
  level?: string;
  /** 文字样式 */
  textStyle?: TextStyle;
  /** 分段文本 */
  segmentTexts?: SegmentText[];
}

export interface Polygon {
  /** 边线虚线 */
  dashArray?: number[];
  /** 经纬度数组 */
  points: Point[];
  /** 描边的宽度 */
  strokeWidth?: number;
  /** 描边的颜色 */
  strokeColor?: string;
  /** 填充颜色 */
  fillColor?: string;
  /** 设置多边形 Z 轴数值 */
  zIndex?: number;
  /** 压盖关系 */
  level?: string;
}

export interface Circle {
  /** 纬度 */
  latitude: number;
  /** 经度 */
  longitude: number;
  /** 描边的颜色 */
  color?: string;
  /** 填充颜色 */
  fillColor?: string;
  /** 半径 */
  radius: number;
  /** 描边的宽度 */
  strokeWidth?: number;
  /** 压盖关系 */
  level?: string;
}

export interface Control {
  /** 控件id */
  id?: number;
  /** 控件在地图的位置 */
  position: object;
  /** 显示的图标 */
  iconPath: string;
  /** 是否可点击 */
  clickable?: boolean;
}

export interface Position {
  /** 距离地图的左边界多远 */
  left?: number;
  /** 距离地图的上边界多远 */
  top?: number;
  /** 控件宽度 */
  width?: number;
  /** 控件高度 */
  height?: number;
}
export interface MarkerEvent {
  detail: {
    markerId: number;
  };
}

export interface TapEvent {
  detail: {
    name: string;
    longitude: number;
    latitude: number;
  };
}

export interface ControlEvent {
  detail: {
    controlId: number;
  };
}

export interface RegionChangeBeginEvent {
  type: 'begin';
  causedBy: 'gesture' | 'update';
  detail: {
    rotate: number;
    skew: number;
    scale: number;
    centerLocation: number;
    region: number;
  };
}

export interface RegionChangeEndEvent {
  type: 'end';
  causedBy: 'drag' | 'scale' | 'update';
  detail: {
    rotate: number;
    skew: number;
    scale: number;
    centerLocation: number;
    region: number;
  };
}

export type RegionChangeEvent = RegionChangeBeginEvent | RegionChangeEndEvent;

export interface MapElement extends SVGAttributes {
  /** 中心经度 */
  longitude: number;
  /** 中心纬度 */
  latitude: number;
  /** 缩放级别,取值范围为3-20 */
  scale?: number;
  /** 最小缩放级别 */
  minScale?: number;
  /** 最大缩放级别 */
  maxScale?: number;
  /** 标记点 */
  markers?: Marker[];
  /** 路线 */
  polyline?: Polyline[];
  /** 圆 */
  circles?: Circle[];
  /** 控件(即将废弃,建议使用 cover-view 代替) */
  controls?: Control[];
  /** 缩放视野以包含所有给定的坐标点 */
  includePoints?: Point[];
  /** 显示带有方向的当前定位点 */
  showLocation?: boolean;
  /** 多边形 */
  polygons?: Polygon[];
  /** 个性化地图使用的key */
  subkey?: string;
  /** 个性化地图配置的 style,不支持动态修改 */
  layerStyle?: number;
  /** 旋转角度,范围 0 ~ 360, 地图正北和设备 y 轴角度的夹角 */
  rotate?: number;
  /** 倾斜角度,范围 0 ~ 40 , 关于 z 轴的倾角 */
  skew?: number;
  /** 展示3D楼块 */
  enable3D?: boolean;
  /** 显示指南针 */
  showCompass?: boolean;
  /** 显示比例尺,工具暂不支持 */
  showScale?: boolean;
  /** 开启俯视 */
  enableOverlooking?: boolean;
  /** 开启最大俯视角,俯视角度从 45 度拓展到 75 度 */
  enableAutoMaxOverlooking?: boolean;
  /** 是否支持缩放 */
  enableZoom?: boolean;
  /** 是否支持拖动 */
  enableScroll?: boolean;
  /** 是否支持旋转 */
  enableRotate?: boolean;
  /** 是否开启卫星图 */
  enableSatellite?: boolean;
  /** 是否开启实时路况 */
  enableTraffic?: boolean;
  /** 是否展示 POI 点 */
  enablePoi?: boolean;
  /** 是否展示建筑物 */
  enableBuilding?: boolean;
  /** 配置项 */
  setting?: object;
  /** 点击地图时触发,2.9.0起返回经纬度信息 */
  onTap?: (event: TapEvent) => void;
  /** 点击标记点时触发,e.detail = {markerId} */
  onMarkertap?: (event: MarkerEvent) => void;
  /** 点击label时触发,e.detail = {markerId} */
  onLabeltap?: (event: MarkerEvent) => void;
  /** 点击控件时触发,e.detail = {controlId} */
  onControltap?: (event: ControlEvent) => void;
  /** 点击标记点对应的气泡时触发e.detail = {markerId} */
  onCallouttap?: (event: MarkerEvent) => void;
  /** 在地图渲染更新完成时触发 */
  onUpdated?: (event: RegionChangeEvent) => void;
  /** 视野发生变化时触发, */
  onRegionchange?: (event: RegionChangeEvent) => void;
  /** 点击地图poi点时触发,e.detail = {name, longitude, latitude} */
  onPoitap?: (event: TapEvent) => void;
  /** 点击定位标时触发,e.detail = {longitude, latitude} */
  onAnchorpointtap?: (event: TapEvent) => void;
}

export type MapContext = ReturnType<typeof uni.createMapContext>;

export type MapContextAddMarkersOptions = Parameters<MapContext['addMarkers']>[0];

export interface MarkerCluster {
  clusters: {
    center: Marker;
    clusterId: number;
    markerIds: number[];
  }[];
}

扩展 map 组件为小程序的类型定义

要扩展 map 组件就需要先了解它是怎么被定义的,我们可以通过 Ctrl + 左键查看定义它类型的上下文:

interface IntrinsicElementAttributes {
  // ...
  link: LinkHTMLAttributes;
  main: HTMLAttributes;
  map: MapHTMLAttributes;
  mark: HTMLAttributes;
  menu: MenuHTMLAttributes;
  meta: MetaHTMLAttributes;
  // ...
}

IntrinsicElementAttributes 这里是所有的内部元素标签的定义,可以看到很多熟悉的元素标签。

不过它并没有 export,那我们得再看看它在那儿使用了:

type ReservedProps = {
  key?: string | number | symbol;
  ref?: RuntimeCore.VNodeRef;
  ref_for?: boolean;
  ref_key?: string;
};

type ElementAttrs<T> = T & ReservedProps;

type NativeElements = {
  [K in keyof IntrinsicElementAttributes]: ElementAttrs<IntrinsicElementAttributes[K]>;
};

它在 NativeElements 使用并扩展了 ref 等 Vue Component 扩展类型,这里其实就是 Vue 中所有内部元素的类型实现。

并且它在全局环境 JSX 命名空间中覆盖了默认类型定义:

declare global {
  namespace JSX {
    interface Element extends VNode {}
    interface ElementClass {
      $props: {};
    }
    interface ElementAttributesProperty {
      $props: {};
    }
    // 这里覆盖了默认的 IntrinsicElements
    interface IntrinsicElements extends NativeElements {
      // allow arbitrary elements
      // @ts-ignore suppress ts:2374 = Duplicate string index signature.
      [name: string]: any;
    }
    interface IntrinsicAttributes extends ReservedProps {}
  }
}

那么我们扩展 map 组件也是同样的思路,在全局环境 JSX 命名空间中覆盖 map 的定义。

  1. 创建一个 global.d.ts 文件

    import type { MapElement } from './models/Map/Core';
    
    declare global {
      namespace JSX {
        interface IntrinsicElements {
          // 扩展小程序内置map组件
          map: MapElement;
        }
      }
    }
    
  2. 将其导入至 tsconfig.json 中:

    {
      "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.nvue"]
    }
    

这样我们就可以在 IDE 中获得小程序的 map 组件的类型定义了。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0d94209b86242bd970d63300181e044~tplv-k3u1fbpfcp-zoom-1.image

创建 MapContext

实现点聚合只能通过 MapContext 的方式,我们可以通过 wx.createMapContext 获取,由于我用的是 uniApp,对应的 API 为:uni.createMapContext

MapContext 通过 id 跟一个 map 组件绑定,操作对应的 map 组件。

<script setup lang="ts">
  // createMapContext的第一个参数为对应的 map id,在这里即为 centerMap
  const mapCtx = ref<MapContext>();

  onMounted(() => {
    mapCtx.value = uni.createMapContext('centerMap');
  });
</script>
<template>
  <map id="centerMap" />
</template>

这里有个小坑,当 map 不是在页面直接使用,而是在某个组件中使用时,uni.createMapContext 将返回 null

所以我们需要把它,也只能把它放到页面中来使用,这在封装 hooks 的时候也需要注意。

创建 useMapContext hooks

我们知道 vue3 推出了 组合式 API(Composition API),这让我们很容易拆离部分逻辑代码。

那我们就可以将 uni.createMapContext 相关的逻辑独立出去,让组件内部的代码更加简洁。

lib/hooks/map.ts

/**
 * 创建MapContext
 * @param mapId Map组件的id
 */
export function useMapContext(mapId: string) {
  // MapContext 实例
  const mapCtx = ref<MapContext>();

  // 在 map 组件渲染后创建 Context
  onMounted(() => {
    mapCtx.value = uni.createMapContext(mapId);
    if (!mapCtx.value) {
      console.error('请检查是否为组件内部调用,请将其添加至页面内部');
    }
  });

  return {
    mapCtx,
  };
}

在组件中使用也很方便:

<script setup lang="ts">
  const { mapCtx } = useMapContext('centerMap');
</script>
<template>
  <map id="centerMap" />
</template>

这样我们就完成了 MapContext 的创建,并很好的将其作为 hooks 独立在页面代码之外了。

实现点聚合

点聚合功能是基于图层标记 Markers 的,它是 Markers 过多时的一种优化方式。

在用户进行平移、缩放等操作时,动态的计算可视区域的 Markers 图层,其中的算法实现感兴趣的同学可以看看这篇文章 地图兴趣点聚合算法的探索与实践

封装操作 Markers 工具与点聚合初始化

我们在点聚合初始化之前可以做一些预备工作,封装一些与 Markers 相关的工具方法

lib/hooks/map.ts

export function useMapContext() {
  // ...
  // 用于记录 marker
  const markerMap = ref(new Map<number, Marker>());

  /**
   * 添加多个 marker
   * @param options 选项
   */
  const addMarkers = (options: Omit<MapContextAddMarkersOptions, 'success' | 'fail'>) => {
    return new Promise<{ errMsg: string }>((resolve, reject) => {
      if (!mapCtx.value) reject('mapContext 不存在,请检查函数调用情况!');
      else {
        mapCtx.value.addMarkers({ ...options, success: resolve, fail: reject });
        options.markers.forEach(marker => markerMap.value.set(marker.id as number, marker));
      }
    });
  };

  /**
   * 移除指定的多个 marker
   * @param options 选项
   */
  const removeMarkers = (options: { markerIds: number[] }) => {
    return new Promise<{ errMsg: string }>((resolve, reject) => {
      if (!mapCtx.value) reject('mapContext 不存在,请检查函数调用情况!');
      else if (options.markerIds.some(id => !markerMap.value.has(id))) reject('markerId 不存在!');
      else {
        options.markerIds.forEach(id => markerMap.value.delete(id));
        mapCtx.value.removeMarkers({ ...options, success: resolve, fail: reject });
      }
    });
  };

  /**
   * 修改指定的 marker
   * @param options 选项
   */
  const modifyMarker = async (options: { markerId: number; marker: Partial<Omit<Marker, 'id'>> }) => {
    if (!mapCtx.value) throw new Error('mapContext 不存在,请检查函数调用情况!');
    const mm = unref(markerMap);
    if (!mm.has(options.markerId)) throw new Error('markerId 不存在!');
    const modifiedMarker = { ...mm.get(options.markerId), ...options.marker } as Marker;
    await addMarkers({ markers: [modifiedMarker], clear: false });
    mm.set(options.markerId, modifiedMarker);
    return modifiedMarker;
  };

  /**
   * 查找指定的 marker
   * @param markerId markerId
   */
  const findMarker = (markerId: number) => {
    return markerMap.value.get(markerId);
  };

  // ...
}

有了工具,我们再封装 useMarkerCluster hooks,独立点聚合方法并实现初始化:

lib/hooks/map.ts

/**
 * Map点聚合
 * @param mapId Map组件的id
 */
export function useMarkerCluster(mapId: string) {
  const { mapCtx, addMarkers, ...othersTools } = useMapContext(mapId);

  const addMarkersUseJoin = (markers: Marker[]) => {
    return addMarkers({ markers: markers.map(item => ({ ...item, joinCluster: true })), clear: true });
  };

  onMounted(() => {
    // 初始化聚合
    mapCtx.value?.initMarkerCluster({
      enableDefaultStyle: false,
      zoomOnClick: true,
      gridSize: 60,
      complete: res => {
        console.log('initMarkerCluster', res);
      },
    });
  });

  return {
    mapCtx,
    addMarkers: addMarkersUseJoin,
    ...othersTools,
  };
}

注册聚合点创建事件

当用户的操作触发了聚合事件后,会同步触发 markerClusterCreate 事件,在这里我们需要添加新的 Markers 以实现聚合功能。

MapContext 上注册 markerClusterCreate 事件:

lib/hooks/map.ts

import MapMarkerClusterPNG from '@/static/map-marker-cluster.png';

// ...
export function useMarkerCluster(mapId: string) {
  // ...
  onMounted(() => {
    // ...
    // 注册聚合创建事件
    mapCtx.value?.on('markerClusterCreate', async (res: MarkerCluster) => {
      const clusters = res.clusters;
      if (!clusters || !clusters.length) return;
      // 添加聚合点
      await addMarkers({
        markers: clusters.map(({ center, clusterId, markerIds }) => ({
          ...center,
          // 聚合点大小
          width: 33,
          height: 38,
          clusterId,
          // 你的聚合点 icon
          iconPath: MapMarkerClusterPNG,
        })),
        clear: false,
      });
    });
  });
  // ...
}

在添加聚合点时有个大坑!重要的事情说三遍!

不能修改原有的 marker id,否则会在 android 平台出现无法更新 Markers 的情况。

不能修改原有的 marker id,否则会在 android 平台出现无法更新 Markers 的情况。

不能修改原有的 marker id,否则会在 android 平台出现无法更新 Markers 的情况。

marker label 样式渲染器

为了方便渲染样式,我们可以在使用 useMarkerCluster 的时候传入一个自定义的 label 样式渲染器,这样可以减少编写样式的样板代码。

lib/hooks/map.ts

// 默认聚合点的样式渲染器
const DEFAULT_CLUSTER_RENDERER = (count: number): MarkerLabel => ({
  fontSize: 17,
  textAlign: 'center',
  color: '#fff',
  content: count.toString(),
  anchorY: -33,
});

/**
 * Map点聚合
 * @param mapId Map组件的id
 * @param rendererCluster 聚合点的样式渲染器
 */
export function useMarkerCluster(mapId: string, rendererCluster = DEFAULT_CLUSTER_RENDERER) {
  const rendererClusterRef = ref(rendererCluster);
  // ...
  onMounted(() => {
    // ...
    // 注册聚合创建事件
    mapCtx.value?.on('markerClusterCreate', async (res: MarkerCluster) => {
      // ...
      await addMarkers({
        markers: clusters.map(({ center, clusterId, markerIds }) => ({
          // ...
          // 使用样式渲染器渲染 label
          label: rendererClusterRef.value(markerIds.length),
        })),
      });
    });
  });
  // ...
}

在页面中使用

这里写一个简单的例子给大家参考:

<script setup lang="ts">
  // 地图点聚合与标点
  const { addMarkers, modifyMarker, mapCtx, findMarker } = useMarkerCluster('centerMap');
  // 点击 marker 事件处理
  const handleMarkerTap = ({ detail: { markerId } }: MarkerEvent) => {
    // 查找 Marker
    const marker = findMarker(markerId) as Marker;
    console.log('OldMarker ->', marker);
    // 修改 Marker
    await modifyMarker({ markerId, marker: { width: 46, height: 52 } });

    // 移动视图中心至 marker 的位置
    mapCtx.value?.moveToLocation({
      longitude: marker.longitude,
      latitude: marker.latitude,
    });
  };
  // 获取 Markers
  const getMarkers = async () => {
    const yourMapMarkers = await fetchMarkers();
    console.log('GetMockMarkers', await addMarkers(yourMapMarkers));
  };

  getMarkers();
</script>
<template>
  <map id="centerMap" @markertap="handleMarkerTap" />
</template>

Android 平台的适配

小程序的 map 组件在 Android 平台和 ios 的实现有着极大的差异,就如上文所提到的 修改marker id导致无法更新 Markers 的情况。

还有一些问题也是只在 Android 平台才会出现,我们可以通过 uni.getSystemInfoSync 方法获取平台信息,这里我总结了一些遇到的问题方便各位 debug 。

获取平台信息

const { platform } = uni.getSystemInfoSync();
// Android 平台
const isAndroidPlatform = platform === 'android';

modifyMarker 需要移除原有的 marker

在 Android 平台不能通过 addMarkers 相同 id 的 marker 实现更新,这样会导致存在两个重叠的 marker。

所以我们需要主动移除对应的 marker :

lib/hooks/map.ts

// ...
/**
 * 修改指定的 marker
 * @param options 选项
 */
const modifyMarker = async (options: { markerId: number; marker: Partial<Omit<Marker, 'id'>> }) => {
  if (!mapCtx.value) throw new Error('mapContext 不存在,请检查函数调用情况!');
  const mm = unref(markerMap);
  if (!mm.has(options.markerId)) throw new Error('markerId 不存在!');
  const modifiedMarker = { ...mm.get(options.markerId), ...options.marker } as Marker;
  // Android 平台需要主动移除对应的 marker
  if (isAndroidPlatform) removeMarkers({ markerIds: [options.markerId] });
  await addMarkers({ markers: [modifiedMarker], clear: false });
  mm.set(options.markerId, modifiedMarker);
  return modifiedMarker;
};
// ...

label 的 anchorX 坐标差异

在 Android 平台上,marker 的坐标与 ios 的实现不同,它们的差异大概是宽度的一半。

如果你需要调整 label anchorX 就需要做一些适配的工作:

lib/hooks/map.ts

// ...
// 默认聚合点的样式渲染器
const DEFAULT_CLUSTER_RENDERER = (count: number, isAndroidPlatform): MarkerLabel => ({
  width: 33,
  height: 38,
  fontSize: 17,
  textAlign: 'center',
  color: '#fff',
  content: count.toString(),
  anchorY: -33,
  // 差异为宽度的一半,你需要减去这些才能与 ios 保持一直
  anchorX: isAndroidPlatform ? -17 : 0,
});
// ...

Map 组件中遇到的问题

顺便记录在使用 Map 组件中遇到的一些其他问题。

scale 的计算与显示

如果你的地图需要展示用户缩放的情况,并且需要让它可以动态的调整大小,你写的代码可能是这样的:

<script setup lang="ts">
  // 缩放
  const scale = ref(12);
  // 修改 Scale
  const handleChangeScale = (add = true) => {
    // 设置 scale 的最大、最小值
    scale.value = Math.max(Math.min(scale.value + (add ? 1 : -1), 20), 3);
  };
  // 处理RegionChange事件
  const handleRegionChange = (env: RegionChangeEvent) => {
    if (env.type === 'end' && env.causedBy === 'scale') {
      scale.value = Math.round(env.detail.scale);
    }
  };
</script>
<template>
  <map id="centerMap" :scale="scale" @regionchange="handleRegionChange" />
</template>

这里就出现了一个 bug !在监听 map regionChange 事件时,直接修改 scale 会导致用户的视图重新定位到中心位置

那怎么解决呢?我们可以定义一个只供展示的 showScale 避免这种情况:

<script setup lang="ts">
  // 缩放
  const scale = ref(12);
  const showScale = ref(12);
  // 修改 Scale
  const handleChangeScale = (add = true) => {
    // 设置 scale 的最大、最小值
    // scale.value = Math.max(Math.min(scale.value + (add ? 1 : -1), 20), 3);
    // 修改时使用 showScale 的值
    scale.value = Math.max(Math.min(showScale.value + (add ? 1 : -1), 20), 3);
    // 同步 showScale
    showScale.value = scale.value;
  };
  // 处理RegionChange事件
  const handleRegionChange = (env: RegionChangeEvent) => {
    if (env.type === 'end' && env.causedBy === 'scale') {
      // scale.value = Math.round(env.detail.scale);
      // 仅修改 showScale
      showScale.value = Math.round(env.detail.scale);
    }
  };
</script>
<template>
  <map id="centerMap" :scale="scale" show-scale @regionchange="handleRegionChange" />
</template>

getLocation 的授权检查

如果你需要获取用户的定位信息,直接调用 uni.getLocation 是会提示授权失败的,所以我们需要先检查用户的授权情况。

实现的代码如下:

<script setup lang="ts">
  // 获取用户地理位置
  const latitude = ref(0);
  const longitude = ref(0);

  async function getLocation() {
    try {
      // 请求授权,如果用户未授权则会报错,这时就需要我们提示用户开启授权
      await uni.authorize({ scope: 'scope.userLocation' });
      const res = await uni.getLocation({});
      latitude.value = res.latitude;
      longitude.value = res.longitude;
    } catch {
      const { confirm } = await uni.showModal({
        title: '提示',
        content: '请授权地理位置',
      });
      if (!confirm) {
        console.log('用户取消了授权');
        return;
      }
      const { authSetting } = await uni.openSetting();
      // 当 authSetting 不存在 scope.userLocation 时,代表用户没有同一授权
      if (!('scope.userLocation' in authSetting)) console.log('用户取消了授权');
    }
  }
</script>
<template>
  <!-- show-location是展示定位点的必要参数 -->
  <map id="centerMap" show-location />
</template>

总结

这篇文章主要是记录了我在使用小程序的 map 组件实现点聚合功能时遇到的一些问题。

可以看出写代码还是不能停留在文档,得多实践,才能感受到双重的折磨~~~

这里给大家提供一个附带工程化的 uniApp vue3 的 vite template:vite-uniapp-template

如果你想在 vscode 中支持 uniApp nvue 文件的开发适配,可以参考我的这篇文章 实现在 VSCode 中无痛开发 nvue:语法高亮、代码提示、eslint 配置及插件 Patch

输出文章不易,希望大家能多多收藏、评论与点赞!谢谢大家!

参考

微信官方文档 - 小程序 - map