騰訊地圖實(shí)現(xiàn)地圖找房功能

鏈家實(shí)現(xiàn)的效果

最近接到一個(gè)需求,需要使用鵝廠地圖實(shí)現(xiàn)類似鏈家網(wǎng)的地圖找房功能,然后我去網(wǎng)上看了一下,基本上使用的都是百度地圖。于是我打算自己稍微封裝一下,可以在使用的時(shí)候更加的方便。

01. 分析

在拿到這個(gè)需求的時(shí)候,我也是一臉懵逼,不知如何下手,在網(wǎng)上看一下,有說使用點(diǎn)聚合來實(shí)現(xiàn)的。官網(wǎng)給出的示例如下http://lbs.qq.com/javascript_v2/sample/overlay-markercluster.html

點(diǎn)聚合效果

看起來這個(gè)就是我要實(shí)現(xiàn)功能,我嘗試地寫了一下,發(fā)現(xiàn)這個(gè)樣式比較難改,而且需要一次將所有的數(shù)據(jù)都請求過來,如果數(shù)據(jù)量非常大的時(shí)候請求需要花費(fèi)的時(shí)間將會非常多,對用戶體驗(yàn)也不夠友好,所以這個(gè)方法并沒有繼續(xù)下去。

后來我獨(dú)自打開了鏈家的官網(wǎng),抓了一下鏈家的數(shù)據(jù),研究出了鏈家的套路。鏈家的地圖找房主要分為三層。第一層為市區(qū)層,比如南山、羅湖等;第二層為片區(qū),比如南頭、科技園等;第三層則為小區(qū)。

因?yàn)榈谝粚樱诙拥臄?shù)據(jù)沒有那么多,這兩個(gè)接口都是把所有的數(shù)據(jù)一次返回給前端。但是第三層的數(shù)據(jù)量就非常的巨大了,鏈家采取的是返回部分?jǐn)?shù)據(jù),將前端頁面上顯示的最大經(jīng)緯度以及最小經(jīng)緯度傳給后臺,后臺再將篩選后的數(shù)據(jù)返回給前端。(接口地址大家可以使用 Chrome 的開發(fā)工具進(jìn)行抓包,這里需要注意的是鏈家的接口采用 jsonp 的形式,所以需要抓取 JS)

開發(fā)者工具

PS: Windos 平臺可以按 F12 調(diào)出開發(fā)者工具,Mac 平臺則是 Command + Option + I

02. 實(shí)現(xiàn)

理論分析完了,接下來就是實(shí)現(xiàn)的問題了。看著騰訊地圖的 API,我覺得只有自定義覆蓋物比較適合這個(gè)需求了。因?yàn)樽远x覆蓋物更加靈活,我們可以像寫 HTML 一樣,繪制出我們需要的樣式。

首先需要添加騰訊地圖的API,這里我推薦使用異步加載的方式。因?yàn)轫?xiàng)目使用 Vue 進(jìn)行開發(fā)的單頁應(yīng)用,有可能用戶并沒有進(jìn)入地圖找房的頁面,所以這里建議在打開地圖找房的頁面時(shí)添加騰訊地圖的API。

異步加載需要避免一個(gè)重復(fù)加載的問題,即不管用戶是第幾次打開地圖找房,地圖的 API 都是同一個(gè)。 這里為了降低代碼復(fù)雜度,沒有使用單例模式,具體的代碼如下:

const TXMap = {
  map: undefined, // 地圖實(shí)例
  // 異步加載獲取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)
  }
}

可以看到異步加載就是動(dòng)態(tài)加入 script 標(biāo)簽,src 為騰訊地圖 api 的地址,src 包含一個(gè) callback 參數(shù),表示 js 加載完畢后會調(diào)用 funName 這個(gè)函數(shù)。添加了地圖 api 之后,window 對象會有一個(gè) qq.maps 對象,我們可以用來判斷是否已經(jīng)添加了 api,來避免重復(fù)添加 api。

接下來就是實(shí)現(xiàn)自定義覆蓋物這個(gè)方法了。還是參照官方文檔(http://lbs.qq.com/javascript_v2/doc/overlay.html),照葫蘆畫瓢。

const TXMap = {
  map: undefined,
  overlays: [], // 存放所有覆蓋物
  sourceData: [], // 原始數(shù)據(jù)
  listener: undefined, // 地圖縮放或平移的事件監(jiān)聽器

  getApi () {}, /* 前面已經(jīng)聲明,此處省略 */
  
  // 實(shí)現(xiàn)自定義覆蓋物
  drawOverlay (options) {
    let _this = this // 下面有多個(gè) window 對象的方法,避免 this 的指向問題
    this.sourceData = options.data // 存放原始數(shù)據(jù)
    // 繪制覆蓋物之前,清理之前繪制的覆蓋物
    this.clearOverlays()

    // 如果 initMap 方法已經(jīng)實(shí)現(xiàn),那么我們可以直接調(diào)用,否則需要進(jìn)行定義
    if (window.initMap === undefined) {
      window.initMap = function () {} // 繪制覆蓋物的具體實(shí)現(xiàn) 

      // 地圖 api 如果沒有引入則調(diào)用 getApi 方法,否則直接調(diào)用 initMap ()
      window.qq === undefined ? this.getApi('initMap') : window.initMap()
    } else {
      window.initMap()
    }
  },
  // 清除自定義覆蓋物
  clearOverlays () {
    let overlay
    while (overlay = this.overlays.pop()) {
      overlay.onclick = null // 移除點(diǎn)擊事件
      overlay.parentNode.removeChild(overlay) // 移除 dom 元素
    }
  },
  // 在 Vue 組件的 beforeDestroy 調(diào)用,重置地圖,移除時(shí)間為監(jiān)聽,避免內(nèi)存泄漏
  clearMap () {
    this.map = undefined
    if (this.listener) {
      window.qq.maps.event.removeListener(this.listener)
    }
  }
}

這個(gè)地圖找房的架子到此就搭得差不多了,接下來就看看繪制覆蓋物的具體實(shí)現(xiàn)了,也就是 initMap 這個(gè)方法。

window.initMap = function () {
  if (_this.map === undefined) {
    // 地圖對象為undefined時(shí), 需要進(jìn)行地圖的繪制
    _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 事件, 地圖縮放或平移之后觸發(fā)該事件
    _this.listener = window.qq.maps.event.addListener(_this.map, 'idle', () => {
      // 獲取當(dāng)前地圖可視范圍的最大最小經(jīng)緯度
      let bounds = _this.map.getBounds()
      // 獲取當(dāng)前地圖的縮放級別
      let zoom = _this.map.getZoom()
      // 調(diào)用 Vue 組件對 idle 事件的處理函數(shù)
      options.callback && options.callback(bounds, zoom)
    })
  }

  // 自定義覆蓋物
  if (window.CustomOverlay === undefined) {
    window.CustomOverlay = function (lat, lng, name, houseCount) {
      // 調(diào)用地圖 api 計(jì)算出覆蓋物的位置
      this.position = new window.qq.maps.LatLng(lat, lng)
      this.name = name // 區(qū)域名
      this.houseCount = houseCount // 房源數(shù)量
    }
    // 繼承 Overlay
    window.CustomOverlay.prototype = new window.qq.maps.Overlay()
    // 自定義覆蓋物構(gòu)造函數(shù),定義覆蓋為的 DOM 結(jié)構(gòu),DOM 結(jié)構(gòu),樣式大家可以根據(jù)需求自己繪制
    window.CustomOverlay.prototype.construct = function () {
      let div = this.div = document.createElement('div')
      div.className = 'my-overlay' // 覆蓋物類名
      // 覆蓋物 html 結(jié)構(gòu)
      this.div.innerHTML = `<p class="count" >${this.houseCount}<span>套</span></p><p class="name">${this.name}</p>`
      //將dom添加到覆蓋物層,overlayMouseTarget的順序容器 5,此容器包含透明的鼠標(biāo)相應(yīng)元素,用于接收Marker的鼠標(biāo)事件
      this.getPanes().overlayMouseTarget.appendChild(div)
      // 將 div 添加到 overlays,可以用以后續(xù)處理
      _this.overlays.push(div)
      // 定義覆蓋物的點(diǎn)擊事件
      let center = this.position
      this.div.onclick = function () {
        // 點(diǎn)擊之后對地圖進(jìn)行縮放以及平移
        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)
        }
      }
    }

    // 實(shí)現(xiàn) draw 接口來繪制 DOM 元素
    window.CustomOverlay.prototype.draw = function () {
      let overlayProjection = this.getProjection()
      // 獲取覆蓋物容器的相對像素坐標(biāo)
      let pixel = overlayProjection.fromLatLngToDivPixel(this.position)
      let divStyle = this.div.style
      // 根據(jù) DOM 元素調(diào)整定位的位置
      divStyle.top = pixel.y - 53 + 'px'
      divStyle.left = pixel.x - 30 + 'px'
    }
  }

  // 根據(jù)接口數(shù)據(jù)繪制覆蓋物
  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 組件中進(jìn)行引入,之后再向下面的方法使用即可

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

03.實(shí)現(xiàn)效果

這個(gè)例子我用了鏈家的數(shù)據(jù)做了兩層,大家可以根據(jù)自己的需要進(jìn)行修改。

實(shí)現(xiàn)效果

項(xiàng)目地址放在 GitHub

如果文章對你有所幫助,那么請您點(diǎn)一下?
由于本人水平有限,如有錯(cuò)誤,歡迎大家指正。如果你在操作過程中發(fā)現(xiàn)一些沒有講到的錯(cuò)誤或者問題,歡迎在評論留言,一起探討,共同學(xué)習(xí)進(jìn)步!

文章會在我的公眾號第一時(shí)間發(fā)布,如果你有興趣,歡迎關(guān)注我的公眾號-前端develop

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容