Vue2 高德地图JSAPI2.0零基础实战(含注意事项+面试题+可直接上线代码)
本文专为零基础开发者打造,从环境准备、代码实现到上线部署,全程手把手教学,基于 Vue2 + vue-property-decorator + 高德官方 @amap/amap-jsapi-loader 实现企业级地图地址选择组件,同时补充零基础必看注意事项、高频面试题,代码可直接复制上线,兼顾入门学习与面试备考。
核心亮点:零基础友好、注释详尽、代码可复用、避坑指南齐全、面试考点全覆盖,彻底放弃停止维护的第三方库 vue-amap,采用高德官方推荐方案,适合前端新手入门实践。
一、零基础前置准备(环境+工具)
零基础无需担心,先准备好以下环境和工具,跟着步骤来即可,全程无复杂操作:
-
开发环境:Node.js(建议14.x版本,下载地址:https://nodejs.org/zh-cn/,安装后默认自带npm)
-
开发工具:VS Code(免费,下载后安装Vue相关插件:Vetur、ESLint,方便代码提示和格式化)
-
高德开发者账号 :注册地址:https://lbs.amap.com/,注册后创建「Web端应用」,获取
key和securityJsCode(后续配置地图必须用) -
基础项目 :已创建好的 Vue2 项目(若未创建,执行命令
vue init webpack my-amap-project,按提示一步步操作即可,无需额外配置)
二、技术选型说明(为什么选这些技术)
零基础无需纠结技术选型,本文选型均为企业常用、官方推荐,难度低、易上手:
-
Vue2 + vue-property-decorator :Vue2 是前端主流框架,语法简单,
vue-property-decorator采用Class类写法,比传统Options写法更清晰,适合零基础入门。 -
@amap/amap-jsapi-loader :高德官方原生异步加载器,用于加载高德地图JSAPI2.0,替代传统
<script>标签引入,异步加载不阻塞页面,是官方唯一推荐方案。 -
放弃 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)
加载地图时需要配置核心参数,其中 key 和 securityJsCode 需从高德开发者账号获取(步骤:高德开放平台 → 应用管理 → Web端应用 → 复制对应信息),相关核心URL补充如下:
关键URL补充:
-
高德开发者平台(获取key/securityJsCode):
https://lbs.amap.com/dev/key/app -
高德JSAPI2.0 官方CDN基础URL(传统script标签引入用,本文主用loader加载,备用):
https://webapi.amap.com/maps?v=2.0&key=你的key&securityJsCode=你的安全密钥 -
常用插件官方文档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个核心文件)
所有代码都加了详细注释,零基础可直接复制到项目中,替换 key 和 securityJsCode 即可运行,无需修改其他内容。
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>
七、零基础开发注意事项(避坑关键,必看)
零基础最容易踩坑,以下注意事项全部是实战中总结的,每条都要牢记,避免浪费时间排查问题:
-
key和securityJsCode必须正确:这是最常见的坑,一定要从高德开放平台「Web端应用」中复制,不要填错、漏填;线上部署必须同时填写key和securityJsCode,本地开发可暂时省略securityJsCode。
-
地图容器必须设置宽高 :Amap.vue中,
#amap-container和.amap-box必须设置明确的高度(本文设置为600px),否则地图会空白不显示。 -
全局AMap只能加载一次 :AmapManager.js中的
globalAMap不要删除,也不要在其他地方重复调用AMapLoader.load(),否则会导致地图重复加载、报错。 -
弹窗关闭必须销毁地图 :AmapDialog.vue的
onClose方法中,必须调用this.amap.destroy(),否则会导致内存泄漏,多次打开弹窗后页面卡顿、报错。 -
定位失败的排查方向:定位失败大概率是3个原因:① 浏览器未开启GPS权限;② 本地开发未开启HTTPS(浏览器定位需要HTTPS环境,本地可用localhost测试);③ 定位插件未正确加载。
-
不要使用vue-amap:该第三方库已停止维护,不支持高德JSAPI2.0,会出现兼容性问题,零基础直接用本文的官方方案即可。
-
代码复制后不要乱改:零基础建议先复制本文代码,替换key和securityJsCode,运行成功后再逐步修改样式或功能,避免一开始就修改核心逻辑导致报错。
-
Node.js版本不要太高:建议使用14.x版本,过高版本(如18.x)可能和Vue2不兼容,导致项目运行失败。
-
插件预加载不要遗漏 :用到的插件必须在
AMAP_CONFIG.plugins中预加载,否则会出现AMap.XXX is not a constructor报错。 -
经纬度必须是数字类型:传递经纬度时,要确保是数字类型,避免字符串类型导致地图中心点定位错误。
八、高频面试题(含标准答案,零基础也能背)
本文相关的高频面试题,适合零基础备战面试,答案简洁易懂,直接背诵即可:
1. 你用高德地图JSAPI2.0时,用的是什么加载方式?为什么不用传统script标签?
标准答案 :用的是高德官方提供的 @amap/amap-jsapi-loader 异步加载器。传统script标签是同步加载,会阻塞页面渲染,影响页面加载速度;而该加载器是异步加载,不阻塞页面,同时支持按需加载插件,还能避免重复加载SDK,提升性能,是官方唯一推荐的方案。传统script标签引入URL(备用):https://webapi.amap.com/maps?v=2.0&key=你的key&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后,才触发联想请求。做防抖是为了减少接口并发请求,避免用户快速输入时,多次调用联想接口,减轻服务器压力,同时提升用户体验。
九、上线部署注意事项(零基础必看)
-
配置域名白名单:在高德开放平台,找到自己的Web端应用,添加上线域名(比如www.xxx.com),否则地图会报「key未授权」错误,无法正常显示。
-
必须配置securityJsCode:线上部署时,AMAP_CONFIG中必须填写securityJsCode,否则地图会空白、跨域,本地开发可省略,但线上必须配置。
-
压缩打包优化 :上线前执行
npm run build打包项目,打包后的文件体积更小,加载速度更快,避免直接部署开发环境代码。 -
HTTPS环境:定位功能需要HTTPS环境,上线时必须部署到HTTPS服务器,否则定位会失败。
总结
本文从零基础视角,完整实现了Vue2 + 高德地图JSAPI2.0的地址选择组件,包含环境准备、代码实现、注意事项、面试题,所有代码带详细注释,可直接复制上线。零基础开发者只需跟着步骤,替换自己的key和securityJsCode,就能快速实现企业级地图功能,同时掌握地图组件的优化技巧和面试考点,兼顾实战与面试。
如果运行中遇到问题,可对照「注意事项」排查,大部分问题都是key错误、容器宽高未设置、地图未销毁导致的,按步骤排查即可解决。