【每日一拳】打在 vue+高德地图api 封装选址组件实现上的

8,904 阅读4分钟

起因

之前公司有个调用地图选址组件来实现快速定位的需求。

原本是一个很简单的需求,之前调用的是腾讯地图的选点组件,通过 iframe 内嵌调用,具体可以查看 腾讯地图 的官网,代码如下。一开始都是可以正常使用的,但是最近发现腾讯地图 api 会疯狂报错,提示 “您已关闭GPS,请在设置>隐私>位置里打开”,目前还没有找到解决方案。

<iframe id="mapView" width="100%" height="100%" frameborder=0
    src="https://apis.map.qq.com/tools/locpicker?search=1&type=1&key=your key&referer=myapp">
</iframe>
 
<script>
    window.addEventListener('message', function(event) {
        // 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
        var loc = event.data;
        if (loc && loc.module == 'locationPicker') {//防止其他应用也会向该页面post信息,需判断module是否为'locationPicker'
          console.log('location', loc);
        }
    }, false);
</script>

于是转战高德地图的选址组件,高德同样封装了一个组件可以供我们使用 iframe 内嵌调用,具体可以查看 高德地图 的官网,代码如下。但是这个选址组件功能过于简易,定位不会根据选址列表的选中而移动,由于是通过 iframe 内嵌使用,还不易修改代码功能,还是不满足既定需求。

<iframe
  id="mapView"
  width="100%"
  height="100%"
  src="https://m.amap.com/picker/?key=your key"
></iframe>
(function(){
    var iframe = document.getElementById('test').contentWindow;
    document.getElementById('test').onload = function(){
      iframe.postMessage('hello','https://m.amap.com/picker/');
    };
    window.addEventListener("message", function(e){
      alert('您选择了:' + e.data.name + ',' + e.data.location)
    }, false);
}())

最终决定,使用零碎的高德地图 api 来自己封装一个简易的选址组件。就目前而言,对于高德地图 api 的使用看到过三种方式:

  1. 引入 @amap/amap-jsapi-loader
  2. 引入原生的高德地图 api
  3. 引入 vue-amap

由于 vue-amap 是一套基于 Vue 2.0 和高德地图的地图组件且已经很久不维护了,所以不予考虑。

对于高德地图 api 三种方式的基本使用,我之前有一片文章 《vue+高德地图api基础实践》 已经非常详细讲解了,大家有兴趣可以参考一下,接下来就直接开始封装。


@amap/amap-jsapi-loader

@amap/amap-jsapi-loader 这个依赖是目前高德地图主流的一种使用方式。

1. 安装依赖并引入

pnpm install @amap/amap-jsapi-loader

然后在 components 下创建 Amap 组件并且引入之

import AMapLoader from "@amap/amap-jsapi-loader";

2. 初始化地图

使用 AMapLoader.load 来初始化渲染地图组件,使用 AMap.Map 类创建和展示地图对象。

let map;
const mapConfigure = {
  amapKey: "", // 申请好的Web端开发者Key
  options: {
    resizeEnable: true,  // 是否监控地图容器尺寸变化
    center: [121.553958, 29.869472],  // 初始地图中心点
    zoom: 14, // 初始地图级别
  }
};

onBeforeMount(() => {
  if (!instance) return;
  let { options } = MapConfigure;

  AMapLoader.load({
    key: mapConfigure.amapKey,
    version: "2.0",
    plugins: [],
    AMapUI: {
      version: "1.1",
      plugins: []
    }
  })
    .then(AMap => {
      // 创建地图实例
      map = new AMap.Map(instance.refs.mapView, options);
    })
    .catch(() => {
      throw "地图加载失败,请重新加载";
    });
});
<template>
  <div class="map-container">
    <div ref="mapView" class="map-view"></div>
  </div>
</template>

<style lang="scss" scoped>
.map-container {
  background-color: #fff;
  width: 100%;
  height: 100vh;

  .map-view {
    position: relative;
    width: 100%;
    height: 50vh;
    position: fixed !important;
    top: 0;
  }
}
</style>

选址初始化地图.jpg

3. 使用 Geolocation 实现定位

初始化地图以后,可以使用高德地图提供的 Geolocation 插件来实现定位功能。

AMapLoader.load({
  key: mapConfigure.amapKey,
  version: "2.0",
  plugins: [],
  AMapUI: {
    version: "1.1",
    plugins: []
  }
})
  .then(AMap => {
    // 创建地图实例
    map = new AMap.Map(instance.refs.mapView, options);

    map.plugin(["AMap.Geolocation"], () => {
      let geolocation = new AMap.Geolocation({
	  // 是否使用高精度定位,默认:true
          enableHighAccuracy: true,
          // 设置定位超时时间,默认:无穷大
          timeout: 10000,
          // 定位按钮的停靠位置的偏移量,默认:Pixel(10, 20)
          buttonOffset: new AMap.Pixel(10, 20),
          //  定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
          zoomToAccuracy: true,
          //  定位按钮的排放位置,  RB表示右下
          buttonPosition: "RB"
      });

      map.addControl(geolocation);
      geolocation.getCurrentPosition(function (status, result) {
        if (status == "complete") {
          onComplete(result);
        } else {
          onError(result);
        }
      });
    });

    function onComplete(data) {
      map.setCenter(data.position);
    }
    function onError(error) {
      console.log("error", error);
    }
  })
  .catch(() => {
    throw "地图加载失败,请重新加载";
  });

geolocation.jpg

4. 使用 PositionPicker 拖拽选址创建列表

PositionPicker(拖拽选址),用于在地图上选取位置,并获取所选位置的地址信息,以及周边POI、周边道路、周边路口等信息。

  1. 加载 PositionPicker(模块名:ui/misc/PositionPicker

    AMapLoader.load({
      key: mapConfigure.amapKey,
      version: "2.0",
      AMapUI: {
        version: "1.1",
        plugins: ["misc/PositionPicker"]  // 需要加载的 AMapUI ui插件
      }
    })
    
  2. 创建 PositionPicker 实例

    let positionPicker = new AMapUI.PositionPicker({
      mode: "dragMap", // 设定为拖拽地图模式,可选'dragMap'、'dragMarker',默认为'dragMap'
      map: map // 依赖地图对象
    });
    
  3. 绑定事件处理函数,获取选址列表(默认展示30条数据)

    positionPicker.on("success", function (positionResult: any) {
      console.log("success", positionResult);
      addressList.value = positionResult.regeocode.pois;
    });
    positionPicker.on('fail', function(positionResult) {
      console.log("fail", positionResult);
    });
    
  4. 开启拖拽选址

    positionPicker.start();
    
  5. 渲染选址列表

    <template>
      <div class="map-container">
        <div ref="mapView" class="map-view"></div>
        <div class="address-wrapper">
          <div class="address-list">
            <div
              class="address-list-item"
              v-for="item in addressList"
              :key="item.id"
              @click="sureAddress(item)"
            >
              <p>{{ item.name }}</p>
              <p>{{ item.address }}</p>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <style lang="scss" scoped>
    .map-container {
      background-color: #fff;
      width: 100%;
      height: 100vh;
    
      .map-view {
        position: relative;
        width: 100%;
        height: 50vh;
        position: fixed !important;
        top: 0;
      }
    
      .address-wrapper {
        padding-top: 50vh;
    
        .address-list {
          position: relative;
          height: 50vh;
          overflow: auto;
    
          &-item {
            font-size: 12px;
            padding: 6px 12px;
            border-bottom: 1px solid #e8e8e8;
    
            p:first-child {
              color: #333;
              font-size: 13px;
            }
    
            p:last-child {
              color: #666;
            }
          }
        }
      }
    }
    </style>
    
  6. 注意:如果控制台报错:INVALID_USER_SCODE,只需要添加一下高德安全密钥,安全密钥是和 key 一起申请的

    window._AMapSecurityConfig = {
      securityJsCode: "你的安全密钥"
    };
    

positionPicker.jpg

5. 使用 PoiPicker 实现搜索功能

PoiPicker(POI选点)在给定的输入框上集成输入提示和关键字搜索功能,方便用户选取特定地点(即POI)。

  1. 加载 PoiPicker(模块名:ui/misc/PoiPicker

    AMapLoader.load({
      key: mapConfigure.amapKey,
      version: "2.0",
      AMapUI: {
        version: "1.1",
        plugins: ["misc/PoiPicker"]  // 需要加载的 AMapUI ui插件
      }
    })
    
  2. 创建 PoiPicker 实例,参考官方文档:developer.amap.com/api/amap-ui…

    AMapLoader.load({
      key: mapConfigure.amapKey,
      version: "2.0",
      AMapUI: {
        version: "1.1",
        plugins: ["misc/PoiPicker"]
      }
    }).then(AMap => {
      …………
      
      let poiPicker = new AMapUI.PoiPicker({
        input: instance.refs.pickerInput  // 输入框id
      });
      // 监听poi选中信息
      poiPicker.on("poiPicked", function (poiResult) {
        // 用户选中的poi点信息
        map.setCenter(poiResult.item.location);
      });
    })
    .catch(() => {
      throw "地图加载失败,请重新加载";
    });
    
  3. 渲染搜索框组件

    <div class="search">
      <input type="text" ref="pickerInput" placeholder="搜索地点" />
    </div>
    
    input {
      width: 100%;
      height: 47px;
      padding: 8px 15px;
      position: fixed;
      z-index: 10;
      top: 0;
      outline: none;
      border: 1px solid #d3d3d3;
      border-radius: 5px;
      background-color: #fff;
    }
    

PoiPicker1.jpg

PoiPicker2.jpg

7. 完整代码

<script setup lang="ts">
import AMapLoader from "@amap/amap-jsapi-loader";
import { getCurrentInstance, onBeforeMount, onUnmounted, ref } from "vue";

interface MapConfigOption {
  resizeEnable?: boolean;
  center?: number[];
  zoom?: number;
}

interface MapConfigure {
  amapKey: string;
  options: MapConfigOption;
}

interface MapConfigureInter {
  on?: Fn;
  destroy?: Fn;
  clearEvents?: Fn;
  addControl?: Fn;
  getCenter?: Fn;
  setCenter?: Fn;
  setZoom?: Fn;
  plugin?: Fn;
}

let map;

const mapConfigure = {
  amapKey: "key",
  options: {
    resizeEnable: true,
    // center: [121.553958, 29.869472],
    zoom: 16
  }
};

window._AMapSecurityConfig = {
  securityJsCode: "安全密钥"
};

const instance = getCurrentInstance();

const addressList = ref<any[]>([]);

const sureAddress = (data: any) => {
  map.setCenter(data.location);

  addressList.value = [];
};

onBeforeMount(() => {
  if (!instance) return;
  let { options } = mapConfigure;

  AMapLoader.load({
    key: mapConfigure.amapKey,
    version: "2.0",
    plugins: [],
    AMapUI: {
      version: "1.1",
      plugins: ["misc/PositionPicker", "misc/PoiPicker"]
    }
  })
    .then(AMap => {
      map = new AMap.Map(instance.refs.mapView, options);

      map.plugin(["AMap.Geolocation"], () => {
        let geolocation = new AMap.Geolocation({
          enableHighAccuracy: true,
          timeout: 10000,
          buttonOffset: new AMap.Pixel(10, 20),
          zoomToAccuracy: true,
          buttonPosition: "RB"
        });

        map.addControl(geolocation);
        geolocation.getCurrentPosition(function (status: any, result: unknown) {
          if (status == "complete") {
            onComplete(result);
          } else {
            onError(result);
          }
        });
      });

      function onComplete(data: any) {
        map.setCenter(data.position);
      }
      function onError(error: unknown) {
        console.log("error", error);
      }

      let positionPicker = new AMapUI.PositionPicker({
        mode: "dragMap",
        map: map
      });

      positionPicker.on("success", function (positionResult: any) {
        addressList.value = positionResult.regeocode.pois;
      });

      positionPicker.on("fail", function (positionResult: any) {
        console.log("positionResult", positionResult);
      });

      positionPicker.start();

      let poiPicker = new AMapUI.PoiPicker({
        input: instance.refs.pickerInput
      });
      poiPicker.on("poiPicked", function (poiResult: any) {
        map.setCenter(poiResult.item.location);
      });
    })
    .catch(() => {
      throw "地图加载失败,请重新加载";
    });
});

onUnmounted(() => {
  if (map) {
    // 销毁地图实例
    map.destroy() && map.clearEvents("click");
  }
});
</script>

<template>
  <div class="map-container">
    <div ref="mapView" class="map-view"></div>
    <div class="search">
      <input type="text" ref="pickerInput" placeholder="搜索地点" />
    </div>
    <div class="address-wrapper">
      <div class="address-list">
        <div
          class="address-list-item"
          v-for="item in addressList"
          :key="item.id"
          @click="sureAddress(item)"
        >
          <p>{{ item.name }}</p>
          <p>{{ item.address }}</p>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.map-container {
  background-color: #fff;
  width: 100%;
  height: 100vh;

  .map-view {
    position: relative;
    width: 100%;
    height: 50vh;
    position: fixed !important;
    top: 0;
  }

  input {
    width: 100%;
    height: 47px;
    padding: 8px 15px;
    position: fixed;
    z-index: 10;
    top: 0;
    outline: none;
    border: 1px solid #d3d3d3;
    border-radius: 5px;
    background-color: #fff;
  }

  .address-wrapper {
    padding-top: 50vh;

    .address-list {
      position: relative;
      height: 50vh;
      overflow: auto;

      &-item {
        font-size: 12px;
        padding: 6px 12px;
        border-bottom: 1px solid #e8e8e8;

        p:first-child {
          color: #333;
          font-size: 13px;
        }

        p:last-child {
          color: #666;
        }
      }
    }
  }
}
</style>

引入原生的高德 api

接下来使用原生的高德地图 api + vue2 也来浅试一下封装一个选址组件,代码其实也大同小异。

1. 初始化地图

在 components 下创建 Amap 组件,使用 <script> 标签导入高德地图 api。

<template>
  <div class="map-container">
    <div ref="mapView" class="map-view"></div>
  </div>
</template>

<script>
let map = null;

export default {
  name: "map-view",
  mounted() {
    const amap_key = '', cb = "amap_callback";
    const scriptUrl = `https://webapi.amap.com/maps?v=1.4.18&key=${amap_key}&callback=${cb}`;
    const mapuiUrl = "https://webapi.amap.com/ui/1.0/main.js?v=1.0.11";

    // 导入script
    importScript(scriptUrl);
    importScript(mapuiUrl);

    window[cb] = () => {
      // 初始化地图
      this.initMap();
    };
  },
  methods: {
    initMap() {
      map = new AMap.Map(this.$refs.mapView, {
        resizeEnable: true,
        zoom: 15,
        center: [121.553958, 29.869472],
      });
    },
  }
};

function importScript(sSrc, success) {
  function loadError(err) {
    throw new URIError("The script " + err.target.src + " is not accessible.");
  }

  var oScript = document.createElement("script");
  oScript.type = "text\/javascript";
  oScript.onerror = loadError;
  if (success) oScript.onload = success;
  document.body.appendChild(oScript);
  oScript.src = sSrc;
}
</script>

<style lang="scss" scoped>
.map-container {
  background-color: #fff;
  width: 100%;
  height: 100vh;

  .map-view {
    position: relative;
    width: 100%;
    height: 50vh;
    position: fixed !important;
    top: 0;
  }
}
</style>

2. 使用 Geolocation 实现定位

initMap() {
  map = new AMap.Map(this.$refs.mapView, {
    resizeEnable: true,
    zoom: 15,
    // center: [121.553958, 29.869472],
  });

  this.getLocation();
},
getLocation() {
  map.plugin("AMap.Geolocation", function () {
    let geolocation = new AMap.Geolocation({
      enableHighAccuracy: true,
      timeout: 10000,
      buttonOffset: new AMap.Pixel(10, 20),
      zoomToAccuracy: true,
      buttonPosition: "RB",
    });

    map.addControl(geolocation);

    geolocation.getCurrentPosition(function (status, result) {
      if (status == "complete") {
        onComplete(result);
      } else {
        onError(result);
      }
    });
  });
  function onComplete(data) {
    map.setCenter(data.position);
  }
  function onError(error) {
    console.log("error", error);
  }
},

3. 使用 PoiPicker 和 PositionPicker

initMap() {
  map = new AMap.Map(this.$refs.mapView, {
    resizeEnable: true,
    zoom: 16,
  });

  this.loadPositionPicker();
  this.loadPoiPicker();
  this.getLocation();
},
loadPositionPicker() {
  let that = this;
  AMapUI.loadUI(["misc/PositionPicker"], function (PositionPicker) {
    let positionPicker = new PositionPicker({
      mode: "dragMap",
      map: map,
    });

    positionPicker.on("success", function (positionResult) {
      console.log("success", positionResult);
      that.addressList = positionResult.regeocode.pois;
    });

    positionPicker.on("fail", function (positionResult) {
      console.log("fail", positionResult);
    });

    positionPicker.start();
  });
},
loadPoiPicker() {
  let that = this;
  AMapUI.loadUI(["misc/PoiPicker"], function (PoiPicker) {
    let poiPicker = new PoiPicker({
      input: that.$refs.pickerInput,
    });

    poiPicker.on("poiPicked", function (poiResult) {
      map.setCenter(poiResult.item.location);
    });
  });
},
sureAddress(data) {
  map.setCenter(data.location);
  this.addressList = [];
},
<template>
  <div class="map-container">
    <div ref="mapView" class="map-view"></div>
    <div class="search">
      <input type="text" ref="pickerInput" placeholder="搜索地点" />
    </div>
    <div class="address-wrapper">
      <div class="address-list">
        <div
          class="address-list-item"
          v-for="item in addressList"
          :key="item.id"
          @click="sureAddress(item)"
        >
          <p>{{ item.name }}</p>
          <p>{{ item.address }}</p>
        </div>
      </div>
    </div>
  </div>
</template>

4. 完整代码

<template>
  <div class="map-container">
    <div ref="mapView" class="map-view"></div>
    <div class="search">
      <input type="text" ref="pickerInput" placeholder="搜索地点" />
    </div>
    <div class="address-wrapper">
      <div class="address-list">
        <div
          class="address-list-item"
          v-for="item in addressList"
          :key="item.id"
          @click="sureAddress(item)"
        >
          <p>{{ item.name }}</p>
          <p>{{ item.address }}</p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
let map = null;

export default {
  name: "map-view",
  data() {
    return {
      addressList: [],
    };
  },
  mounted() {
    const amap_key = '', cb = "amap_callback";
    const scriptUrl = `https://webapi.amap.com/maps?v=1.4.18&key=${amap_key}&callback=${cb}`;
    const mapuiUrl = "https://webapi.amap.com/ui/1.0/main.js?v=1.0.11";

    // 导入script
    importScript(scriptUrl);
    importScript(mapuiUrl);

    window[cb] = () => {
      this.initMap();
    };
  },
  methods: {
    initMap() {
      map = new AMap.Map(this.$refs.mapView, {
        resizeEnable: true,
        zoom: 15,
        // center: [121.553958, 29.869472],
      });

      this.loadPositionPicker();
      this.loadPoiPicker();
      this.getLocation();
    },
    loadPositionPicker() {
      let that = this;
      AMapUI.loadUI(["misc/PositionPicker"], function (PositionPicker) {
        let positionPicker = new PositionPicker({
          mode: "dragMap",
          map: map,
        });

        positionPicker.on("success", function (positionResult) {
          console.log("success", positionResult);
          that.addressList = positionResult.regeocode.pois;
        });

        positionPicker.on("fail", function (positionResult) {
          console.log("fail", positionResult);
        });

        positionPicker.start();
      });
    },
    loadPoiPicker() {
      let that = this;
      AMapUI.loadUI(["misc/PoiPicker"], function (PoiPicker) {
        let poiPicker = new PoiPicker({
          input: that.$refs.pickerInput,
        });

        poiPicker.on("poiPicked", function (poiResult) {
          map.setCenter(poiResult.item.location);
        });
      });
    },
    getLocation() {
      map.plugin("AMap.Geolocation", function () {
        let geolocation = new AMap.Geolocation({
          enableHighAccuracy: true,
          timeout: 10000,
          buttonOffset: new AMap.Pixel(10, 20),
          zoomToAccuracy: true,
          buttonPosition: "RB",
        });

        map.addControl(geolocation);

        geolocation.getCurrentPosition(function (status, result) {
          if (status == "complete") {
            onComplete(result);
          } else {
            onError(result);
          }
        });
      });
      function onComplete(data) {
        map.setCenter(data.position);
      }
      function onError(error) {
        console.log("error", error);
      }
    },
    sureAddress(data) {
      map.setCenter(data.location);
      this.addressList = [];
    },
  },
};

function importScript(sSrc, success) {
  function loadError(err) {
    throw new URIError("The script " + err.target.src + " is not accessible.");
  }

  var oScript = document.createElement("script");
  oScript.type = "text\/javascript";
  oScript.onerror = loadError;
  if (success) oScript.onload = success;
  document.body.appendChild(oScript);
  oScript.src = sSrc;
}
</script>

<style lang="scss" scoped>
.map-container {
  background-color: #fff;
  width: 100%;
  height: 100vh;

  .map-view {
    position: relative;
    width: 100%;
    height: 50vh;
    position: fixed !important;
    top: 0;
  }

  input {
    width: 100%;
    height: 47px;
    padding: 8px 15px;
    position: fixed;
    z-index: 10;
    top: 0;
    outline: none;
    border: 1px solid #d3d3d3;
    border-radius: 5px;
    background-color: #fff;
  }

  .address-wrapper {
    padding-top: 50vh;

    .address-list {
      position: relative;
      height: 50vh;
      overflow: auto;

      &-item {
        font-size: 12px;
        padding: 6px 12px;
        border-bottom: 1px solid #e8e8e8;

        p:first-child {
          color: #333;
          font-size: 13px;
        }

        p:last-child {
          color: #666;
        }
      }
    }
  }
}
</style>

选址引入原生的高德api.jpg


写在最后

其实地图组件的坑实在是太多了,还是需要继续学习不断的探索。

如果你也对地图组件有兴趣的话,欢迎大家一起来交流学习。