前言
之前做不过不少关于地图交互的产品系统,目前国内主流的地图应用 SDK 只有几家:高德、百度和腾讯。所以个人觉得在 PC 应用上高德地图开发相对好一些,至少体验起来没有很明显的坑。这篇文章算是总结下开发地图应用总结吧。
异步加载
因为使用 js sdk 应用,脚本文件本身体积很大,所以要注意下加载的白屏时间,解决用户体验问题,目前绝大部分产品应用都是 SPA
单页面应用系统,所以我封装一个异步加载的方法:
const loadScripts = async scripts => {
const get = src => {
return new Promise(function(resolve, reject) {
const el = document.createElement('script')
el.addEventListener('load', function() {
resolve(src)
}, false)
el.addEventListener('error', function() {
reject(src)
}, false)
el.id = src.id
el.src = src.url
document.getElementsByTagName('body')[0].appendChild(el) || document.getElementsByTagName('head')[0].appendChild(el)
})
}
const myPromises = scripts.map(async script => {
if (document.getElementById(script.id) === null) {
return await get(script)
}
})
return await Promise.all(myPromises)
}
export default loadScripts
这个方法在加载脚本的时候先去判断页面是否存在该脚本,如果存在就不会加载第二次,然后再利用加载完毕回调执行相关方法。
封装组件
如果系统中有多个页面需要地图应用业务,那么需要封装一个通用型的地图组件,提高项目可维护性,我这边就简单的封装下地图应用:
<template>
<div
:style="{
width: width,
height: height
}"
class="amap-container"
>
<div ref="amap" class="amap">
<slot />
</div>
</div>
</template>
<style lang="scss" scoped>
.amap-container {
.amap {
width: 100%;
height: 100%;
}
}
</style>
指定一个地图应用容器,外面包裹一层指定高宽,高宽作为外部变量传入,业务逻辑如下:
import loadScripts from '@/loadScripts'
export default {
name: 'AMapContainer',
props: {
width: {
require: false,
type: String,
default: '100%'
},
height: {
require: false,
type: String,
default: '600px'
},
options: {
require: false,
type: Object,
default: () => {}
}
},
data: () => ({
amap: null,
amapInfo: {
key: 'xxxxxxxxxxxxxx'
}
}),
created() {
this.initAMap()
},
beforeDestroy() {
// 销毁地图
if (!this.amap) {
return
}
this.amap.destroy()
this.amap = null
},
methods: {
initAMap() {
loadScripts([{
id: 'ampa',
url: `https://webapi.amap.com/maps?v=2.0&key=${this.amapInfo.key}&plugin=AMap.PolygonEditor`
}]).then(() => {
this.amap = new window.AMap.Map(this.$refs['amap'], this.options)
this.$emit('map', this.amap, window.AMap)
})
}
}
}
应用加载的时候初始化地图容器:异步加载高德地图 js sdk
然后回调方法里进行实例化地图应用,并且把地图实例化的对象传入 $emit
事件里,方便父类组件需要。另外注意要在销毁生命周期里对地图应用进行销毁,否则会占用大量的系统内存。
使用组件
封装好组件后就可以在对应的页面进行引入组件使用即可:
<template>
<amap-container height="100%" :options="amapOptions" @map="getAMapData" />
</template>
<script>
import AMap from '@/components/AMap'
export default {
name: 'AMapDemo',
components: {
'amap-container': AMap
},
data: () => ({
amapOptions: {
zoom: 14,
resizeEnable: true,
viewMode: '3D',
mapStyle: 'amap://styles/normal'
},
AMap: null, // 地图对象
amap: null // 当前地图实例
}),
methods: {
/**
* 地图加载完毕回调
* @param amap
* @param AMap
*/
getAMapData(amap, AMap) {
// 从组件获取地图 amap 对象
this.amap = amap
// 从组件获取地图 AMap 静态对象
this.AMap = AMap
}
}
}
</script>
然后在上面基础上展开相关业务。对于地图应用来说,最核心的数据就是地图应用中的坐标,无论是地图的标记元素,折线元素(轨迹等),绘制图元素等,只需要获取对应的经纬度数据存到数据库即可,至于怎么获取这边不再详述。
自定义界面最佳实践
之前制作的地图应用,在标记的详细界面(选择某个标记左键打开界面),这个界面是需要传入原生 document
对象,但是在 vue
对象里面不符合这种写法,所以导致之前很多系统都是花大量的时间去编写 dom
结构,甚是头疼,后续为了解决这个问题,vue
是否有相关方法挂载组件获取真实的 document
对象,查阅相关文档后,确实有这个 api
: Vue.extend,利用这个 api
挂载组件对象即可得到实例化组件的对象。
import ContextCard from './components/ContextCard'
// 创建标记
const marker = new this.AMap.Marker({
map: this.amap,
position: [119.058904, 33.537069]
})
// 绑定点击事件
marker.on('click', this.markerInfoWindow)
// 点击打开弹窗
const markerInfoWindow = () => {
// 引入 Vue 组件构造器实例化
const ContextCardContent = Vue.extend(ContextCard)
// 挂载组件
const contextCardContent = new ContextCardContent().$mount()
// 实例化窗口对象
this.amapInfoWindow = new this.AMap.InfoWindow({
isCustom: true,
content: contextCardContent.$el,
offset: new this.AMap.Pixel(0, -40)
})
// 打开窗口
this.amapInfoWindow.open(this.amap, marker.getPosition())
// 监听组件事件关闭窗口
contextCardContent.$on('closeWindow', () => this.amapInfoWindow.close())
}
ContextCard.vue 组件:
<template>
<el-card class="context-box-card box-card">
<div slot="header" class="header">
<span>卡片名称</span>
<el-button type="text" class="close-btn" @click="closeWindow">关闭</el-button>
</div>
<div v-for="o in 4" :key="o" class="text item">
{{ '列表内容 ' + o }}
</div>
</el-card>
</template>
<script>
export default {
name: 'AMapContextCard',
methods: {
closeWindow() {
this.$emit('closeWindow')
}
}
}
</script>
<style lang="scss" scoped>
.context-box-card {
width: 320px;
height: 200px;
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
::v-deep .el-card__header {
padding: 5px 20px;
}
}
</style>
上面就是一个标点点击打开标记弹窗的详细信息,利用 Vue.extend
构造器进行实例化组件。这样很大程度上提高项目健壮性。