Vue2 高德地图地址选择器完整实战(组件抽离+高并发优化+@amap/amap-jsapi-loader最佳实践)

Vue2 高德地图JSAPI2.0零基础实战(含注意事项+面试题+可直接上线代码)

本文专为零基础开发者打造,从环境准备、代码实现到上线部署,全程手把手教学,基于 Vue2 + vue-property-decorator + 高德官方 @amap/amap-jsapi-loader 实现企业级地图地址选择组件,同时补充零基础必看注意事项、高频面试题,代码可直接复制上线,兼顾入门学习与面试备考。

核心亮点:零基础友好、注释详尽、代码可复用、避坑指南齐全、面试考点全覆盖,彻底放弃停止维护的第三方库 vue-amap,采用高德官方推荐方案,适合前端新手入门实践。

一、零基础前置准备(环境+工具)

零基础无需担心,先准备好以下环境和工具,跟着步骤来即可,全程无复杂操作:

  1. 开发环境:Node.js(建议14.x版本,下载地址:https://nodejs.org/zh-cn/,安装后默认自带npm)

  2. 开发工具:VS Code(免费,下载后安装Vue相关插件:Vetur、ESLint,方便代码提示和格式化)

  3. 高德开发者账号 :注册地址:https://lbs.amap.com/,注册后创建「Web端应用」,获取 keysecurityJsCode(后续配置地图必须用)

  4. 基础项目 :已创建好的 Vue2 项目(若未创建,执行命令vue init webpack my-amap-project,按提示一步步操作即可,无需额外配置)

二、技术选型说明(为什么选这些技术)

零基础无需纠结技术选型,本文选型均为企业常用、官方推荐,难度低、易上手:

  1. Vue2 + vue-property-decorator :Vue2 是前端主流框架,语法简单,vue-property-decorator 采用Class类写法,比传统Options写法更清晰,适合零基础入门。

  2. @amap/amap-jsapi-loader :高德官方原生异步加载器,用于加载高德地图JSAPI2.0,替代传统<script>标签引入,异步加载不阻塞页面,是官方唯一推荐方案。

  3. 放弃 vue-amap:第三方开源库,已停止维护,不支持高德JSAPI2.0,存在兼容性问题,零基础不建议使用,避免踩坑。

实现功能:关键字搜索、输入联想、POI分页、地图点击选点、当前位置定位、地图工具栏、标点浮窗,满足企业级地址选择场景。

三、项目整体分层架构(零基础易懂)

采用三层分层架构,职责清晰,零基础也能看懂,后续修改、复用都很方便,避免代码混乱:

plain 复制代码
AmapDialog.vue    // 外层弹窗容器(只负责弹窗的显示/隐藏,不处理地图逻辑)
Amap.vue          // 地图UI内容面板(只负责页面渲染,比如搜索框、结果列表)
AmapManager.js    // 地图核心逻辑管理类(所有地图相关的业务、插件、优化都在这里)

核心逻辑:视图(Amap.vue)和业务(AmapManager.js)完全分离,弹窗(AmapDialog.vue)和地图解耦,后续想单独复用地图面板,直接引用Amap.vue即可。

四、@amap/amap-jsapi-loader 零基础使用说明

这是加载高德地图的核心工具,零基础只需记住3步:安装 → 配置 → 使用,全程复制命令和代码即可。

1. 安装(复制命令执行)

打开VS Code终端,进入Vue2项目根目录,执行以下命令安装:

bash 复制代码
npm install @amap/amap-jsapi-loader --save
# 若用yarn,执行:yarn add @amap/amap-jsapi-loader

2. 核心配置(零基础直接复制,替换自己的key和securityJsCode)

加载地图时需要配置核心参数,其中 keysecurityJsCode 需从高德开发者账号获取(步骤:高德开放平台 → 应用管理 → Web端应用 → 复制对应信息),相关核心URL补充如下:

关键URL补充

  1. 高德开发者平台(获取key/securityJsCode):https://lbs.amap.com/dev/key/app

  2. 高德JSAPI2.0 官方CDN基础URL(传统script标签引入用,本文主用loader加载,备用):https://webapi.amap.com/maps?v=2.0&key=你的key&securityJsCode=你的安全密钥

  3. 常用插件官方文档URL(可查询插件详细用法):https://lbs.amap.com/api/javascript-api-v2/documentation#plugins

核心配置模板,直接复制到AmapManager.js中使用,替换自己的key和securityJsCode即可:

javascript 复制代码
// 核心配置模板,直接复制到AmapManager.js中使用
AMAP_CONFIG = {
  key: '你的Web端key',          // 必须替换,零基础注意:不要填错
  securityJsCode: '你的安全密钥',// 线上必须替换,本地开发可暂时省略
  version: '2.0',               // 固定2.0,不要修改
  plugins: []                   // 预加载的地图插件,后续会补充
}

3. 基础使用(零基础必懂)

通过AMapLoader.load() 异步加载地图,加载成功后即可创建地图实例,代码模板如下(注释详尽,零基础能看懂):

javascript 复制代码
import AMapLoader from '@amap/amap-jsapi-loader'

// 异步加载地图SDK
async initMap() {
  try {
    // 加载SDK,传入配置
    const AMap = await AMapLoader.load({
      key: '你的key',
      securityJsCode: '你的安全密钥',
      version: '2.0',
      plugins: ['AMap.Map'] // 加载地图核心插件
    })
    // 创建地图实例,容器ID为"amap-container"(对应页面中的div)
    const map = new AMap.Map('amap-container', {
      zoom: 13, // 地图缩放级别,1-18,越大越清晰
      center: [120.15, 30.28] // 默认中心点(可修改)
    })
  } catch (err) {
    // 加载失败提示,零基础可直接复制
    console.error('地图加载失败,请检查key和安全密钥是否正确', err)
  }
}

五、高德JSAPI2.0 常用插件详解(零基础必懂)

插件是扩展地图功能的核心,零基础无需记所有插件,重点掌握本文用到的6个,其他插件了解即可,后续用到再查文档。

插件使用规则:先在 plugins 数组中预加载,加载完成后通过 new AMap.插件名() 创建实例,再使用。各插件对应官方详情URL补充如下(方便零基础查询扩展):

插件名称 核心作用 零基础使用场景 是否必用 官方详情URL
AMap.ToolBar 地图工具栏,包含缩放、定位、全屏、复位功能 让用户可以手动缩放地图、复位到初始位置 推荐 https://lbs.amap.com/api/javascript-api-v2/documentation#toolbar
AMap.Scale 地图比例尺,显示当前地图缩放的实际距离(如1km) 让用户了解地图上的距离大小 推荐 https://lbs.amap.com/api/javascript-api-v2/documentation#scale
AMap.AutoComplete 输入框智能联想,输入关键词自动提示匹配的地址 搜索框下拉提示,提升用户体验 必用 https://lbs.amap.com/api/javascript-api-v2/documentation#autocomplete
AMap.PlaceSearch POI地点搜索,根据关键词查询地址列表、分页 用户输入关键词后,显示匹配的地址列表 必用 https://lbs.amap.com/api/javascript-api-v2/documentation#placesearch
AMap.Geocoder 地理/逆地理编码,实现「经纬度→地址」「地址→经纬度」转换 用户点击地图,获取点击位置的详细地址 必用 https://lbs.amap.com/api/javascript-api-v2/documentation#geocoder
AMap.Geolocation 浏览器GPS定位,获取用户当前所在位置 一键定位到用户当前位置,提升体验 推荐 https://lbs.amap.com/api/javascript-api-v2/documentation#geolocation

六、全套带注释代码实现(3个核心文件)

所有代码都加了详细注释,零基础可直接复制到项目中,替换 keysecurityJsCode 即可运行,无需修改其他内容。

1. AmapManager.js(核心地图管理类,所有地图逻辑都在这里)

javascript 复制代码
import AMapLoader from '@amap/amap-jsapi-loader'

// 全局唯一的高德SDK实例,整个项目只加载一次,避免重复请求(零基础重点:不要删除,否则会重复加载地图)
let globalAMap = null

// 地图核心管理类,封装所有地图相关的业务、插件、优化
export default class AmapManager {
  constructor() {
    // ==================== 地图核心实例(零基础无需修改) ====================
    this.AMap = null // 高德SDK根对象(全局单例,加载一次即可)
    this.map = null // 地图实例(整个地图的核心载体)
    this.marker = null // 地图标点(复用,避免频繁创建销毁,提升性能)
    this.infoWindow = null // 信息浮窗(点击标点显示的地址详情)

    // ==================== 高德插件实例(对应上面讲解的常用插件) ====================
    this.geocoder = null // 逆地理编码插件(坐标→地址)
    this.placeSearch = null // POI搜索插件(关键词搜地址)
    this.autocomplete = null // 输入联想插件(搜索框下拉提示)
    this.toolBar = null // 地图工具栏插件
    this.scale = null // 比例尺插件
    this.geoLocation = null // 定位插件(获取当前位置)

    // ==================== 并发/节流控制(零基础了解即可,防止地图崩溃) ====================
    this.isSearching = false // POI搜索锁,防止用户频繁点击搜索,导致重复请求
    this.mapClickThrottle = false // 地图点击节流,防止用户狂点地图,频繁调用逆地理编码
    this.autoTimer = null // 输入联想防抖定时器,300ms内连续输入不触发请求,减少并发
  }

  // 高德地图基础配置(零基础重点:替换成自己的key和securityJsCode)
  AMAP_CONFIG = {
    key: '7ab1f4b8ec1d035ebe7f23ec27b5c485', // 替换成你的Web端key
    securityJsCode: '你的安全密钥', // 替换成你的安全密钥,线上必须填
    version: '2.0', // 固定2.0,不要修改
    plugins: [
      'AMap.Geocoder',
      'AMap.PlaceSearch',
      'AMap.AutoComplete',
      'AMap.ToolBar',
      'AMap.Scale',
      'AMap.Geolocation'
    ] // 预加载常用插件,无需额外引入
  }

  /**
   * 初始化地图(零基础重点:核心方法,弹窗打开时调用)
   * @param {string} containerId 地图容器ID(对应Amap.vue中的div id)
   * @param {object} defaultAddress 默认回显地址(如编辑时的已有地址)
   * @param {Function} onMapClickCallback 地图点击选点回调(点击地图后,返回选中的地址)
   */
  async init(containerId, defaultAddress = {}, onMapClickCallback) {
    // 先销毁旧地图实例,防止内存泄漏(零基础重点:必须有,否则多次打开弹窗会报错)
    this.destroy()

    try {
      // 全局只加载一次高德SDK,提升性能,避免重复请求
      if (!globalAMap) {
        globalAMap = await AMapLoader.load(this.AMAP_CONFIG)
      }
      this.AMap = globalAMap // 赋值给当前实例,方便后续使用

      // 地图基础配置(零基础可修改zoom和默认center)
      const mapOptions = {
        viewMode: '2D', // 2D视图,零基础不建议用3D,复杂度高
        zoom: 13, // 地图缩放级别,1-18,越大越清晰
        resizeEnable: true // 地图容器大小自适应,防止窗口缩放后地图错乱
      }

      // 如果有默认地址(如编辑页面的已有地址),设置地图中心点为默认地址
      if (defaultAddress.longitude && defaultAddress.latitude) {
        mapOptions.center = [
          Number(defaultAddress.longitude), // 转数字,防止字符串格式报错
          Number(defaultAddress.latitude)
        ]
      }

      // 创建地图实例,绑定到页面中的容器(containerId对应Amap.vue中的"amap-container")
      this.map = new this.AMap.Map(containerId, mapOptions)

      // ==================== 添加地图控件(工具栏+比例尺) ====================
      this.toolBar = new this.AMap.ToolBar({ position: 'RB' }) // 位置:右下角(RB=Right Bottom)
      this.map.addControl(this.toolBar) // 将工具栏添加到地图
      this.scale = new this.AMap.Scale() // 创建比例尺
      this.map.addControl(this.scale) // 将比例尺添加到地图

      // ==================== 初始化业务插件(零基础无需修改,直接复用) ====================
      this.geocoder = new this.AMap.Geocoder() // 逆地理编码插件
      this.placeSearch = new this.AMap.PlaceSearch({
        map: this.map, // 绑定到当前地图实例
        autoFitView: true, // 搜索结果显示后,自动调整地图视角,让结果可见
        extensions: 'all' // 显示完整的POI信息(如地址、经纬度)
      })
      this.autocomplete = new this.AMap.AutoComplete() // 输入联想插件
      this.geoLocation = new this.AMap.Geolocation({
        enableHighAccuracy: true, // 开启高精度定位
        timeout: 10000 // 定位超时时间(10秒),超时则定位失败
      })

      // 初始化信息浮窗(点击标点显示的详情)
      this.infoWindow = new this.AMap.InfoWindow({
        retainWhenClose: true, // 关闭浮窗后,保留浮窗内容,下次打开无需重新渲染
        offset: new this.AMap.Pixel(0, -30) // 浮窗偏移量,避免遮挡标点
      })

      // ==================== 地图点击事件(节流优化,防止频繁请求) ====================
      this.map.on('click', (e) => {
        if (this.mapClickThrottle) return // 如果节流锁开启,直接返回,不执行后续逻辑
        this.mapClickThrottle = true // 开启节流锁,1次点击后,防止短时间内再次点击

        const lng = e.lnglat.getLng() // 获取点击位置的经度
        const lat = e.lnglat.getLat() // 获取点击位置的纬度

        // 调用逆地理编码,将经纬度转换为详细地址
        this.regeo(lng, lat, (address) => {
          onMapClickCallback?.(address) // 回调返回选中的地址
          this.mapClickThrottle = false // 关闭节流锁,允许下次点击
        })
      })

      // 如果有默认地址,初始化时显示标点和浮窗
      if (defaultAddress.longitude && defaultAddress.latitude) {
        this.setMarker(defaultAddress)
      }
    } catch (err) {
      // 地图加载失败提示,零基础可根据错误信息排查问题(常见:key错误、安全密钥缺失)
      console.error('地图初始化失败,请检查配置:', err)
    }
  }

  /**
   * 获取用户当前位置(定位功能,零基础可直接复用)
   * @param {Function} callback 定位成功后的回调,返回当前位置地址
   */
  getCurrentLocation(callback) {
    if (!this.geoLocation) return // 如果定位插件未初始化,直接返回
    // 调用定位插件,获取当前位置
    this.geoLocation.getCurrentPosition((status, result) => {
      if (status === 'complete') { // 定位成功
        const addr = {
          longitude: result.position.lng, // 当前位置经度
          latitude: result.position.lat, // 当前位置纬度
          province: result.addressComponent.province || '', // 省
          city: result.addressComponent.city || '', // 市
          area: result.addressComponent.district || '', // 区
          detailAddress: result.formattedAddress || '' // 详细地址
        }
        this.setMarker(addr) // 定位成功后,在地图上显示标点
        callback?.(addr) // 回调返回当前位置地址
      } else {
        console.error('定位失败,请检查浏览器权限(需开启GPS)')
      }
    })
  }

  /**
   * 逆地理编码(核心方法:经纬度 → 详细地址,零基础无需修改)
   * @param {number} lng 经度
   * @param {number} lat 纬度
   * @param {Function} callback 编码成功后的回调,返回详细地址
   */
  regeo(lng, lat, callback) {
    if (!this.geocoder) return // 插件未初始化,直接返回
    // 调用逆地理编码插件,传入经纬度
    this.geocoder.getAddress([lng, lat], (status, result) => {
      if (status === 'complete' && result?.regeocode) { // 编码成功
        const { addressComponent, formattedAddress } = result.regeocode
        const address = {
          longitude: lng,
          latitude: lat,
          province: addressComponent.province || '', // 省(兼容部分地址无省的情况)
          city: addressComponent.city || '', // 市
          area: addressComponent.district || '', // 区
          detailAddress: formattedAddress || '' // 完整详细地址
        }
        this.setMarker(address) // 编码成功后,显示标点和浮窗
        callback?.(address) // 回调返回详细地址
      } else {
        console.error('逆地理编码失败,无法获取地址')
      }
    })
  }

  /**
   * 设置地图标点和信息浮窗(复用标点,提升性能,零基础无需修改)
   * @param {object} address 地址对象(包含经度、纬度、详细地址)
   */
  setMarker(address) {
    // 空安全判断,防止地址为空或经纬度缺失导致报错
    if (!this.map || !this.AMap || !address.longitude || !address.latitude) return

    const position = [address.longitude, address.latitude] // 标点位置(经纬度)
    // 复用标点实例,避免频繁创建、删除标点,提升性能
    if (this.marker) {
      this.marker.setPosition(position) // 已有标点,直接修改位置
    } else {
      this.marker = new this.AMap.Marker({ position }) // 没有标点,创建新标点
      this.map.add(this.marker) // 将标点添加到地图
    }

    this.map.setCenter(position) // 地图中心点定位到标点位置
    // 设置浮窗内容(显示详细地址)
    this.infoWindow.setContent(`${address.detailAddress}`)
    this.infoWindow.open(this.map, this.marker.getPosition()) // 打开浮窗,显示在标点上方
  }

  /**
   * 输入联想搜索(防抖优化,300ms防抖,零基础无需修改)
   * @param {string} keywords 用户输入的关键词
   * @param {Function} callback 联想结果回调,返回匹配的地址列表
   */
  autoSearch(keywords, callback) {
    clearTimeout(this.autoTimer) // 清除上一次的定时器,实现防抖
    // 300ms内连续输入,不会触发联想请求,减少接口并发
    this.autoTimer = setTimeout(() => {
      if (!this.autocomplete) return callback([]) // 插件未初始化,返回空列表
      // 调用联想插件,搜索关键词匹配的地址
      this.autocomplete.search(keywords, (status, result) => {
        // 处理联想结果,格式化为前端可用的列表
        const tips = status === 'complete' && result.tips
          ? result.tips.map(item => ({ ...item, value: item.name }))
          : []
        callback(tips) // 回调返回联想列表
      })
    }, 300)
  }

  /**
   * POI关键字搜索(防并发,分页,零基础无需修改)
   * @param {string} keywords 用户输入的搜索关键词
   * @param {number} pageIndex 页码(默认第1页)
   * @returns {object} 搜索结果(list:地址列表,total:总条数)
   */
  async search(keywords, pageIndex = 1) {
    // 防并发:如果正在搜索,或关键词为空,直接返回空结果
    if (!this.placeSearch || this.isSearching || !keywords) {
      return { list: [], total: 0 }
    }
    this.isSearching = true // 开启搜索锁,防止重复请求

    return new Promise((resolve) => {
      this.placeSearch.setPageIndex(pageIndex) // 设置当前页码
      // 调用POI搜索插件,搜索关键词
      this.placeSearch.search(keywords, (status, result) => {
        this.isSearching = false // 关闭搜索锁,允许下次搜索
        if (status === 'complete' && result?.poiList) { // 搜索成功
          resolve({
            list: result.poiList.pois || [], // 地址列表
            total: Number(result.poiList.count) || 0 // 总条数
          })
        } else {
          // 搜索失败,返回空结果
          resolve({ list: [], total: 0 })
        }
      })
    })
  }

  /**
   * 选中POI搜索结果,定位到该地址(零基础无需修改)
   * @param {object} poi 选中的POI地址对象
   * @returns {object} 格式化后的地址对象
   */
  pickPoi(poi) {
    if (!poi?.location) return null // 如果POI对象为空,返回null
    // 格式化POI地址,转换为前端可用的格式
    const address = {
      longitude: poi.location.lng,
      latitude: poi.location.lat,
      province: poi.pname || '',
      city: poi.cityname || '',
      area: poi.adname || '',
      detailAddress: `${poi.pname}${poi.cityname !== poi.pname ? poi.cityname : ''}${poi.adname}${poi.name}`
    }
    this.setMarker(address) // 定位到选中的地址,显示标点和浮窗
    return address // 返回格式化后的地址
  }

  /**
   * 彻底销毁地图资源(零基础重点:防止内存泄漏,弹窗关闭时必须调用)
   */
  destroy() {
    clearTimeout(this.autoTimer) // 清除防抖定时器
    this.isSearching = false // 重置搜索锁
    this.mapClickThrottle = false // 重置点击节流锁

    // 销毁地图实例,移除事件监听(核心:防止内存泄漏)
    if (this.map) {
      this.map.off('click') // 移除地图点击事件监听
      this.map.destroy() // 销毁地图实例
    }

    // 重置所有实例,释放内存
    this.map = null
    this.marker = null
    this.infoWindow = null
    this.geocoder = null
    this.placeSearch = null
    this.autocomplete = null
    this.toolBar = null
    this.scale = null
    this.geoLocation = null
  }
}

2. Amap.vue(地图UI面板,负责页面渲染,零基础可修改样式)

js 复制代码
<template>
  <div class="amap-box">
    <!-- 搜索面板(编辑模式显示,查看模式隐藏) -->
    <el-card class="search-card" v-if="isEdit">
      <div slot="header">
        <!-- 搜索模式:输入框 + 搜索按钮 -->
        <template v-if="mode === 'search'">
          <el-autocomplete
            v-model="keywords"
            size="small"
            :fetch-suggestions="autoSearch"
            placeholder="输入关键词搜索地址"
            @select="onSelect"
          >
            <el-button
              @click="doSearch(true)"
              :disabled="!keywords"
              slot="append"
              type="primary"
            >搜索</el-button>
          </el-autocomplete>
        </template>

        <!-- 搜索结果模式:返回按钮 + 结果统计 -->
        <template v-if="mode === 'result'">
          <div class="flex">
            <el-button icon="el-icon-arrow-left" size="mini" @click="back" />
            <span>搜索 {{ keywords }} 共 {{ loading ? '...' : total }} 条结果</span>
          </div>
        </template>
      </div>

      <!-- 搜索结果列表 + 分页 -->
      <div class="list" v-if="mode === 'result'">
        <el-pagination
          small
          layout="prev, pager, next"
          :page-size="10"
          :current-page="page"
          :total="total"
          @current-change="pageChange"
        />
        <div class="item" v-for="item in list" :key="item.id" @click="pick(item)">
          <div class="name">{{ item.name }}</div>
          <div class="addr">{{ item.address }}</div>
        </div>
      </div>
    </el-card>

    <!-- 地图容器(零基础重点:id必须和AmapManager.js中的containerId一致) -->
    <div id="amap-container"></div>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import AmapManager from './AmapManager' // 引入地图管理类

// 地图UI组件,只负责渲染,不处理核心逻辑
@Component({ name: 'Amap' })
export default class extends Vue {
  // 父组件传入的参数(零基础了解:从AmapDialog.vue传入)
  @Prop({ default: null }) location // 默认回显地址
  @Prop({ default: false }) isEdit // 是否为编辑模式(编辑模式显示搜索面板)

  // 初始化地图管理实例(核心:所有地图逻辑都通过这个实例调用)
  amap = new AmapManager()
  // 当前选中的地址(用于回显和提交)
  address = { longitude: null, latitude: null, province: '', city: '', area: '', detailAddress: '' }

  // ==================== 页面状态(零基础可修改默认值) ====================
  mode = 'search' // 页面模式:search(搜索)、result(搜索结果)
  keywords = '' // 用户输入的搜索关键词
  loading = false // 搜索加载状态(加载中显示...)
  list = [] // 搜索结果列表
  total = 0 // 搜索结果总条数
  page = 1 // 当前页码(默认第1页)

  /**
   * 初始化地图(弹窗打开时调用,零基础无需修改)
   */
  async init() {
    this.address = { ...this.location } // 赋值默认回显地址
    // 调用地图管理类的init方法,初始化地图
    await this.amap.init('amap-container', this.address, (addr) => {
      this.address = addr // 地图点击选点后,更新当前选中地址
    })
  }

  /**
   * 输入联想搜索(调用地图管理类的方法,零基础无需修改)
   */
  autoSearch(kw, cb) { this.amap.autoSearch(kw, cb) }

  /**
   * 执行搜索(零基础无需修改)
   * @param {boolean} clear 是否清空原有结果(首次搜索清空,分页不清空)
   */
  async doSearch(clear = false) {
    if (clear) { // 首次搜索,清空原有结果和页码
      this.list = []
      this.total = 0
      this.page = 1
    }
    this.mode = 'result' // 切换到搜索结果模式
    this.loading = true // 开启加载状态
    // 调用地图管理类的search方法,获取搜索结果
    const res = await this.amap.search(this.keywords, this.page)
    this.loading = false // 关闭加载状态
    this.list = res.list // 更新结果列表
    this.total = res.total // 更新总条数
  }

  /**
   * 分页切换(零基础无需修改)
   */
  pageChange(p) { this.page = p; this.doSearch() }
  /**
   * 选中联想结果,执行搜索(零基础无需修改)
   */
  onSelect() { this.doSearch(true) }
  /**
   * 选中搜索结果,定位到该地址(零基础无需修改)
   */
  pick(item) { this.address = this.amap.pickPoi(item) }
  /**
   * 返回搜索模式(零基础无需修改)
   */
  back() { this.mode = 'search'; this.keywords = '' }
  /**
   * 提交选中的地址(向父组件传递地址,零基础无需修改)
   */
  submit() { this.$emit('change', this.address) }
  /**
   * 销毁地图(弹窗关闭时调用,零基础无需修改)
   */
  destroy() { this.amap.destroy() }
  /**
   * 定位到当前位置(调用地图管理类的定位方法,零基础无需修改)
   */
  locate() { this.amap.getCurrentLocation((addr) => this.address = addr) }
}
</script>

<style lang="scss" scoped>
/* 零基础可修改样式,调整布局和外观 */
.amap-box {
  display: flex;
  height: 600px; // 地图容器高度,必须设置,否则地图不显示
}
.search-card {
  width: 320px;
  margin-right: 12px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
#amap-container {
  flex: 1;
  height: 100%; // 地图容器高度100%,和父容器一致
}
.list {
  max-height: 480px;
  overflow-y: auto;
  padding: 8px;
}
.item {
  padding: 8px 12px;
  cursor: pointer;
  border-bottom: 1px solid #eee;
  &:hover {
    background-color: #f5f7fa;
  }
}
.name {
  font-weight: 500;
  margin-bottom: 4px;
}
.addr {
  font-size: 12px;
  color: #666;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.flex {
  display: flex;
  align-items: center;
  gap: 8px;
}
</style>

3. AmapDialog.vue(外层弹窗容器,零基础无需修改)

js 复制代码
<template>
  <el-dialog
    v-draggable
    destroy-on-close
    :title="title"
    :visible="visible"
    width="1100px"
    :close-on-click-modal="!isEdit"
    @opened="onOpen"
    @close="onClose"
    :append-to-body="true"
  >
    <!-- 引入地图UI组件 -->
    <Amap ref="amap" :is-edit="isEdit" :location="location" @change="submit"/>
    
    <!-- 弹窗底部按钮 -->
    <template #footer>
      <el-button @click="onClose">关闭</el-button>
      <el-button type="primary" @click="submit" v-if="isEdit">确定</el-button>
    </template>
  </el-dialog>
</template>

<script lang="ts">
import { Component, Prop, Vue, Ref } from 'vue-property-decorator'
import Amap from './Amap.vue' // 引入地图UI组件

// 弹窗容器组件,只负责弹窗的显示/隐藏,不处理地图逻辑
@Component({ name: 'AmapDialog', components: { Amap } })
export default class extends Vue {
  // 父组件传入的参数(零基础了解:从使用弹窗的页面传入)
  @Prop({ default: null }) location // 默认回显地址
  @Prop({ default: false }) visible // 弹窗显示/隐藏状态
  @Prop({ default: '地图选择' }) title // 弹窗标题
  @Prop({ default: false }) isEdit // 是否为编辑模式
  @Ref('amap') amap // 引用地图UI组件,用于调用其方法

  /**
   * 弹窗打开时,初始化地图(零基础无需修改)
   */
  onOpen() { this.amap.init() }
  /**
   * 提交选中的地址(向父组件传递地址,零基础无需修改)
   */
  submit() { this.amap.submit() }
  /**
   * 弹窗关闭时,销毁地图(零基础重点:必须调用,防止内存泄漏)
   */
  onClose() {
    this.amap.destroy() // 销毁地图
    this.$emit('close') // 向父组件传递关闭事件
  }
}
</script>

七、零基础开发注意事项(避坑关键,必看)

零基础最容易踩坑,以下注意事项全部是实战中总结的,每条都要牢记,避免浪费时间排查问题:

  1. key和securityJsCode必须正确:这是最常见的坑,一定要从高德开放平台「Web端应用」中复制,不要填错、漏填;线上部署必须同时填写key和securityJsCode,本地开发可暂时省略securityJsCode。

  2. 地图容器必须设置宽高 :Amap.vue中,#amap-container.amap-box必须设置明确的高度(本文设置为600px),否则地图会空白不显示。

  3. 全局AMap只能加载一次 :AmapManager.js中的 globalAMap 不要删除,也不要在其他地方重复调用 AMapLoader.load(),否则会导致地图重复加载、报错。

  4. 弹窗关闭必须销毁地图 :AmapDialog.vue的 onClose 方法中,必须调用 this.amap.destroy(),否则会导致内存泄漏,多次打开弹窗后页面卡顿、报错。

  5. 定位失败的排查方向:定位失败大概率是3个原因:① 浏览器未开启GPS权限;② 本地开发未开启HTTPS(浏览器定位需要HTTPS环境,本地可用localhost测试);③ 定位插件未正确加载。

  6. 不要使用vue-amap:该第三方库已停止维护,不支持高德JSAPI2.0,会出现兼容性问题,零基础直接用本文的官方方案即可。

  7. 代码复制后不要乱改:零基础建议先复制本文代码,替换key和securityJsCode,运行成功后再逐步修改样式或功能,避免一开始就修改核心逻辑导致报错。

  8. Node.js版本不要太高:建议使用14.x版本,过高版本(如18.x)可能和Vue2不兼容,导致项目运行失败。

  9. 插件预加载不要遗漏 :用到的插件必须在 AMAP_CONFIG.plugins 中预加载,否则会出现 AMap.XXX is not a constructor 报错。

  10. 经纬度必须是数字类型:传递经纬度时,要确保是数字类型,避免字符串类型导致地图中心点定位错误。

八、高频面试题(含标准答案,零基础也能背)

本文相关的高频面试题,适合零基础备战面试,答案简洁易懂,直接背诵即可:

1. 你用高德地图JSAPI2.0时,用的是什么加载方式?为什么不用传统script标签?

标准答案 :用的是高德官方提供的 @amap/amap-jsapi-loader 异步加载器。传统script标签是同步加载,会阻塞页面渲染,影响页面加载速度;而该加载器是异步加载,不阻塞页面,同时支持按需加载插件,还能避免重复加载SDK,提升性能,是官方唯一推荐的方案。传统script标签引入URL(备用):https://webapi.amap.com/maps?v=2.0&amp;key=你的key&amp;securityJsCode=你的安全密钥,需手动拼接key和安全密钥。

2. 你是如何优化地图组件的?(核心面试题)

标准答案:主要做了5点优化:① 全局单例加载SDK,避免重复请求;② 输入联想添加300ms防抖,减少接口并发;③ 地图点击添加节流,防止频繁调用逆地理编码;④ 复用marker实例,避免频繁创建销毁,提升性能;⑤ 弹窗关闭时彻底销毁地图资源(移除事件监听、销毁实例),防止内存泄漏。

3. 高德地图中,逆地理编码和地理编码的区别是什么?

标准答案 :逆地理编码是将「经纬度」转换为「详细地址」(比如用户点击地图,获取点击位置的地址);地理编码是将「地址文字」转换为「经纬度」(比如用户输入地址,定位到地图对应的位置),两者是互逆的操作,核心插件是 AMap.Geocoder

4. 为什么放弃vue-amap,选择@amap/amap-jsapi-loader?

标准答案:因为vue-amap是第三方开源库,已停止维护,不支持高德JSAPI2.0,存在兼容性问题,扩展能力有限;而@amap/amap-jsapi-loader是高德官方原生加载器,支持JSAPI2.0,异步加载性能好,可扩展所有高德原生API,维护性强,是新项目的首选。

5. 地图组件中,如何防止内存泄漏?

标准答案:主要做3点:① 弹窗关闭时,调用地图的destroy()方法,销毁地图实例;② 移除地图的事件监听(比如click事件),避免事件残留;③ 重置所有地图相关的实例(marker、插件等),释放内存,不残留未销毁的对象。

6. 高德地图常用的插件有哪些?分别作用是什么?

标准答案:常用5个插件:① AMap.AutoComplete:输入联想提示;② AMap.PlaceSearch:POI地点搜索;③ AMap.Geocoder:地理/逆地理编码;④ AMap.ToolBar:地图工具栏(缩放、定位);⑤ AMap.Geolocation:浏览器定位,获取用户当前位置。

7. 输入联想的防抖是如何实现的?为什么要做防抖?

标准答案:用setTimeout定时器实现,设置300ms延迟,用户连续输入时,清除上一次的定时器,只有在用户停止输入300ms后,才触发联想请求。做防抖是为了减少接口并发请求,避免用户快速输入时,多次调用联想接口,减轻服务器压力,同时提升用户体验。

九、上线部署注意事项(零基础必看)

  1. 配置域名白名单:在高德开放平台,找到自己的Web端应用,添加上线域名(比如www.xxx.com),否则地图会报「key未授权」错误,无法正常显示。

  2. 必须配置securityJsCode:线上部署时,AMAP_CONFIG中必须填写securityJsCode,否则地图会空白、跨域,本地开发可省略,但线上必须配置。

  3. 压缩打包优化 :上线前执行 npm run build 打包项目,打包后的文件体积更小,加载速度更快,避免直接部署开发环境代码。

  4. HTTPS环境:定位功能需要HTTPS环境,上线时必须部署到HTTPS服务器,否则定位会失败。

总结

本文从零基础视角,完整实现了Vue2 + 高德地图JSAPI2.0的地址选择组件,包含环境准备、代码实现、注意事项、面试题,所有代码带详细注释,可直接复制上线。零基础开发者只需跟着步骤,替换自己的key和securityJsCode,就能快速实现企业级地图功能,同时掌握地图组件的优化技巧和面试考点,兼顾实战与面试。

如果运行中遇到问题,可对照「注意事项」排查,大部分问题都是key错误、容器宽高未设置、地图未销毁导致的,按步骤排查即可解决。

相关推荐
深海鱼在掘金1 小时前
Next.js从入门到实战保姆级教程(第八章):图像、字体与媒体优化
前端·typescript·next.js
深海鱼在掘金1 小时前
Next.js从入门到实战保姆级教程(第七章):样式方案与 UI 优化
前端·typescript·next.js
晴天丨1 小时前
🛡️ Vue 3 错误处理完全指南:全局异常捕获、前端监控、用户反馈
前端·vue.js
孙凯亮1 小时前
Electron 接口请求全解析:从疑问到落地(真实开发对话整理)
前端·electron
闲坐含香咀翠1 小时前
Electron 桌面端多语言优化实战:从静态全量加载到懒加载与用户自定义
前端·electron·客户端
Wect2 小时前
HTML5 原生拖拽 API 实战案例与拓展避坑
前端·面试·浏览器
河阿里2 小时前
Vue3:全流程开发
vue.js
threelab2 小时前
从工厂模式到简化封装:三维引擎架构演进之路 threejs设计
javascript·3d·架构·webgl
踩着两条虫2 小时前
VTJ:项目模型系统
前端·低代码·ai编程