腾讯地图实现地图找房功能

链家实现的效果

最近接到一个需求,需要使用鹅厂地图实现类似链家网的地图找房功能,然后我去网上看了一下,基本上使用的都是百度地图。于是我打算自己稍微封装一下,可以在使用的时候更加的方便。

01. 分析

在拿到这个需求的时候,我也是一脸懵逼,不知如何下手,在网上看一下,有说使用点聚合来实现的。官网给出的示例如下http://lbs.qq.com/javascript_v2/sample/overlay-markercluster.html

点聚合效果

看起来这个就是我要实现功能,我尝试地写了一下,发现这个样式比较难改,而且需要一次将所有的数据都请求过来,如果数据量非常大的时候请求需要花费的时间将会非常多,对用户体验也不够友好,所以这个方法并没有继续下去。

后来我独自打开了链家的官网,抓了一下链家的数据,研究出了链家的套路。链家的地图找房主要分为三层。第一层为市区层,比如南山、罗湖等;第二层为片区,比如南头、科技园等;第三层则为小区。

因为第一层,第二层的数据没有那么多,这两个接口都是把所有的数据一次返回给前端。但是第三层的数据量就非常的巨大了,链家采取的是返回部分数据,将前端页面上显示的最大经纬度以及最小经纬度传给后台,后台再将筛选后的数据返回给前端。(接口地址大家可以使用 Chrome 的开发工具进行抓包,这里需要注意的是链家的接口采用 jsonp 的形式,所以需要抓取 JS)

开发者工具

PS: Windos 平台可以按 F12 调出开发者工具,Mac 平台则是 Command + Option + I

02. 实现

理论分析完了,接下来就是实现的问题了。看着腾讯地图的 API,我觉得只有自定义覆盖物比较适合这个需求了。因为自定义覆盖物更加灵活,我们可以像写 HTML 一样,绘制出我们需要的样式。

首先需要添加腾讯地图的API,这里我推荐使用异步加载的方式。因为项目使用 Vue 进行开发的单页应用,有可能用户并没有进入地图找房的页面,所以这里建议在打开地图找房的页面时添加腾讯地图的API。

异步加载需要避免一个重复加载的问题,即不管用户是第几次打开地图找房,地图的 API 都是同一个。 这里为了降低代码复杂度,没有使用单例模式,具体的代码如下:

const TXMap = {
  map: undefined, // 地图实例
  // 异步加载获取api
  getApi (funName) {
    let script = document.createElement('script')
    script.type = 'text/javascript'
    script.src = `http://map.qq.com/api/js?v=2.exp&callback=${funName}`
    document.body.appendChild(script)
  }
}

可以看到异步加载就是动态加入 script 标签,src 为腾讯地图 api 的地址,src 包含一个 callback 参数,表示 js 加载完毕后会调用 funName 这个函数。添加了地图 api 之后,window 对象会有一个 qq.maps 对象,我们可以用来判断是否已经添加了 api,来避免重复添加 api。

接下来就是实现自定义覆盖物这个方法了。还是参照官方文档(http://lbs.qq.com/javascript_v2/doc/overlay.html),照葫芦画瓢。

const TXMap = {
  map: undefined,
  overlays: [], // 存放所有覆盖物
  sourceData: [], // 原始数据
  listener: undefined, // 地图缩放或平移的事件监听器

  getApi () {}, /* 前面已经声明,此处省略 */
  
  // 实现自定义覆盖物
  drawOverlay (options) {
    let _this = this // 下面有多个 window 对象的方法,避免 this 的指向问题
    this.sourceData = options.data // 存放原始数据
    // 绘制覆盖物之前,清理之前绘制的覆盖物
    this.clearOverlays()

    // 如果 initMap 方法已经实现,那么我们可以直接调用,否则需要进行定义
    if (window.initMap === undefined) {
      window.initMap = function () {} // 绘制覆盖物的具体实现 

      // 地图 api 如果没有引入则调用 getApi 方法,否则直接调用 initMap ()
      window.qq === undefined ? this.getApi('initMap') : window.initMap()
    } else {
      window.initMap()
    }
  },
  // 清除自定义覆盖物
  clearOverlays () {
    let overlay
    while (overlay = this.overlays.pop()) {
      overlay.onclick = null // 移除点击事件
      overlay.parentNode.removeChild(overlay) // 移除 dom 元素
    }
  },
  // 在 Vue 组件的 beforeDestroy 调用,重置地图,移除时间为监听,避免内存泄漏
  clearMap () {
    this.map = undefined
    if (this.listener) {
      window.qq.maps.event.removeListener(this.listener)
    }
  }
}

这个地图找房的架子到此就搭得差不多了,接下来就看看绘制覆盖物的具体实现了,也就是 initMap 这个方法。

window.initMap = function () {
  if (_this.map === undefined) {
    // 地图对象为undefined时, 需要进行地图的绘制
    _this.map = new window.qq.maps.Map(document.getElementById(options.containerId), {
      // 初始化地图中心
      center: new window.qq.maps.LatLng(options.lat || 22.702, options.lng || 114.09),
      // 初始化缩放级别
      zoom: options.zoom || 10,
      // 地图最小缩放级别
      minZoom: 10,
      // 停用缩放控件
      zoomControl: false,
      // 停用地图类型控件
      mapTypeControl: false
    })
    // idle 事件, 地图缩放或平移之后触发该事件
    _this.listener = window.qq.maps.event.addListener(_this.map, 'idle', () => {
      // 获取当前地图可视范围的最大最小经纬度
      let bounds = _this.map.getBounds()
      // 获取当前地图的缩放级别
      let zoom = _this.map.getZoom()
      // 调用 Vue 组件对 idle 事件的处理函数
      options.callback && options.callback(bounds, zoom)
    })
  }

  // 自定义覆盖物
  if (window.CustomOverlay === undefined) {
    window.CustomOverlay = function (lat, lng, name, houseCount) {
      // 调用地图 api 计算出覆盖物的位置
      this.position = new window.qq.maps.LatLng(lat, lng)
      this.name = name // 区域名
      this.houseCount = houseCount // 房源数量
    }
    // 继承 Overlay
    window.CustomOverlay.prototype = new window.qq.maps.Overlay()
    // 自定义覆盖物构造函数,定义覆盖为的 DOM 结构,DOM 结构,样式大家可以根据需求自己绘制
    window.CustomOverlay.prototype.construct = function () {
      let div = this.div = document.createElement('div')
      div.className = 'my-overlay' // 覆盖物类名
      // 覆盖物 html 结构
      this.div.innerHTML = `<p class="count" >${this.houseCount}<span>套</span></p><p class="name">${this.name}</p>`
      //将dom添加到覆盖物层,overlayMouseTarget的顺序容器 5,此容器包含透明的鼠标相应元素,用于接收Marker的鼠标事件
      this.getPanes().overlayMouseTarget.appendChild(div)
      // 将 div 添加到 overlays,可以用以后续处理
      _this.overlays.push(div)
      // 定义覆盖物的点击事件
      let center = this.position
      this.div.onclick = function () {
        // 点击之后对地图进行缩放以及平移
        let zoom = _this.map.getZoom()
        if (zoom < 13) {
          _this.map.setCenter(center)
          _this.map.setZoom(13)
        } else if (zoom >= 13 && zoom < 15) {
          _this.map.setCenter(center)
          _this.map.setZoom(15)
        }
      }
    }

    // 实现 draw 接口来绘制 DOM 元素
    window.CustomOverlay.prototype.draw = function () {
      let overlayProjection = this.getProjection()
      // 获取覆盖物容器的相对像素坐标
      let pixel = overlayProjection.fromLatLngToDivPixel(this.position)
      let divStyle = this.div.style
      // 根据 DOM 元素调整定位的位置
      divStyle.top = pixel.y - 53 + 'px'
      divStyle.left = pixel.x - 30 + 'px'
    }
  }

  // 根据接口数据绘制覆盖物
  if (_this.sourceData.length > 0) {
    _this.sourceData.map(item => {
      let customOverlay = new window.CustomOverlay(item.latitude, item.longitude, item.name, item.house_count)
      customOverlay.setMap(_this.map)
    })
  }
}

至此,地图找房对绘制覆盖物方法的封装就完成了,接下来只需要将 TXMap 暴露出去,然后在 Vue 组件中进行引入,之后再向下面的方法使用即可

TXMap.drawOverlay({
  containerId: 'map-box',
  data: res.data
})

03.实现效果

这个例子我用了链家的数据做了两层,大家可以根据自己的需要进行修改。

实现效果

项目地址放在 GitHub

如果文章对你有所帮助,那么请您点一下❤
由于本人水平有限,如有错误,欢迎大家指正。如果你在操作过程中发现一些没有讲到的错误或者问题,欢迎在评论留言,一起探讨,共同学习进步!

文章会在我的公众号第一时间发布,如果你有兴趣,欢迎关注我的公众号-前端develop

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,132评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,510评论 18 139
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,952评论 4 60
  • 第一次读这本书,是因为群里很多人推荐。不抱怨的世界是一个怎么样的世界呢?我很好奇地开始这本书的阅读之旅。 我就是一...
    爱发呆的猫小姐阅读 232评论 0 1
  • 虽然明知会有这一刻,但是这一刻真的到来时,心中的不舍瞬间爆炸,泪点也降到了最低! 半小时前还在学姐家嬉戏打闹,半小...
    哆啦A没了梦阅读 393评论 0 2