LoginSignup
117
87

More than 5 years have passed since last update.

Vue.jsでライブラリ使わずにgoogle mapを利用する

Posted at

vuejsではvue2-google-mapなどgoogle-mapを利用するライブラリがあるが、色々小回りを効かせたいくて直接APIを触れるようにしたかったのでやってみたらvueのあらゆる機能を触ることになったので実装をまとめてみる。

出来たもの

Demo: https://vue-google-map-provider-sample.netlify.com/
Source: https://github.com/inuscript/example-vue-inject-provide-google-map

実装

登場人物は下記のようになる

  • index.html (起動するファイル)
  • MyMap.vue (マップの大本の実装。コンポーネントを組み合わせる親)
  • MapLoader.vue (Google Mapを呼び出すだけ)
  • MapProvider.vue (vueのprovideを提供するだけ)
  • ChildMarker.vue (例として子要素でマーカーを表示する)

google-map以外でもcallbackを利用するような場合に扱える知見を得ることが出来たので、ここから実装について書いてみる。

index.html

    <!-- index.html -->
    <div id="app">
      <my-map :markers='[
        {"lat":35.6432027,"lng":139.6729435},
        {"lat":35.5279833,"lng":139.6989209},
        {"lat":35.6563623,"lng":139.7215211},
        {"lat":35.6167531,"lng":139.5469376},
        {"lat":35.6950961,"lng":139.5037899}
      ]'>
      </my-map>
    </div>

index.htmlは呼び出すだけなので何も無し

MyMap.vue

呼び出しの中核はこんな感じにしてみた。
ここは汎用的に使えないものとして書いている。

<!-- MyMap.vue -->
<template>
  <div>
    <h1>Map</h1>
    <map-loader 
      :map-config="mapConfig"
      apiKey="YOUR API KEY"
    >
      <template v-for="marker in markers">
        <child-marker :position="marker" />
      </template>
    </map-loader>
  </div>
</template>

<script>
import MapLoader from "./MapLoader.vue"
import ChildMarker from './ChildMarker'

export default {
  props: {
    markers: Array
  },
  data(){
    return {
      mapConfig: {
        zoom: 12,
        center: this.markers[0]
      }
    }
  },
  components: {
    MapLoader,
    ChildMarker
  }
}
</script>

ここからmap-loaderを呼び出し、内部でchild-markerを利用している

MapLoader

肝の部分その1。google mapをロードを担う

<!-- MapLoader.vue -->
<template>
  <div>
    <div id="map"></div> <!-- point 1 -->
    <template v-if="!!this.google && !!this.map"> <!-- point 2 -->
      <map-provider
        :google="google"
        :map="map"
      >
        <slot/>
      </map-provider>
    </template>
  </div>
</template>

<script>
import GoogleMapsApiLoader from 'google-maps-api-loader'
import MapProvider from './MapProvider'

export default {
  props:{
    mapConfig: Object,
    apiKey: String
  },
  components: {
    MapProvider
  },
  data(){
    return {
      google: null,
      map: null
    }
  },
  mounted () { // point 3
    GoogleMapsApiLoader({
      apiKey: this.apiKey
    }).then((google) => {
      this.google = google
      this.initializeMap()
    })
  },
  methods: {
    initializeMap (){
      const mapContainer = this.$el.querySelector('#map') // point 1
      const { Map } = this.google.maps
      this.map = new Map(mapContainer, this.mapConfig)
    }
  }
}
</script>

<style scoped>
#map {
  height: 100vh;
  width: 100%;
}
</style>
  • point 1: #map google mapを吐き出す部分としてdomを使っている。それを後半でthis.$elを利用してマウントしている。
  • point 2: 一番のキモ。googlemapがロード完了するまで子を レンダリングさせない。この制御をすることで、provide/injectをうまく利用出来る。
  • point 3: ロードの処理どのタイミングでやるの?というのはmountedで行う。ここでGoogleMapApiLoader

MapProvider

キモその2。
vueのprovide/injectを利用するために、loaderから分離した。
provideはコンポーネントがロードされた初期のタイミングでのみ評価されるらしく、loaderで値の算出が完了した後に実行されるようにしないといけない。
loaderがgooglemapが揃うまで子要素をレンダリングしないようにすることで、MapProviderは確実にgooglemapの値を取得出来る状態になっている。

<!-- MapProvider.vue -->
<template>
  <div>
    <slot />
  </div>
</template>

<script>
export default {
  props: {
    google: Object,
    map: Object,
  },
  provide() {
    return {
      google: this.google,
      map: this.map
    }
  },
}
</script>

ChildMarker

<!-- ChildMarker.vue -->
<template></template>
<script>
export default {
  inject: ["google", "map"],
  props: {
    position: Object
  },
  data(){
    return { marker: null}
  },
  mounted(){
    const { Marker } = this.google.maps
    this.marker = new Marker({
      position: this.position,
      map: this.map,
      title: "Child marker!"
    })
  }
}
</script>

子要素の実装側。
injectを利用してprovideされた値を貰ってくる。
MapLoaderでやったのと同じく、mountedで処理をする。表示は何もしなくて良いので空にしている。

provide/injectを使わないパターン(slot-scopeを使う)

provide / injectは公式ドキュメントによれば「ライブラリ向けで、一般ユーザーは使うべきでない」という話もある。
これらを使わない場合は、値をバケツリレーして子要素に渡していくことになる。
ここでvueの場合はslot-socpeを利用する必要が出てくる

MapLoaderはMapProvierを利用せず、slotに直接googlemapの値を渡す

<!-- MapLoader.vue -->
<template>
  <div>
    <div id="map"></div>
    <template v-if="!!this.google && !!this.map">
      <slot
        :google="google"
        :map="map"
      />
    </template>
  </div>
</template>

<script>
 :
</script>

そしてMyMap側でslot-scopeを利用して、map-loaderから受け取ったgooglemapの変数をchild-markerへ渡す

<!-- MyMap -->
<template>
  <div>
    <h1>Map</h1>
    <map-loader 
      :map-config="mapConfig"
      apiKey="YOUR API KEY">
      <template slot-scope="scopeProps"> <!-- slot-scope -->
        <child-marker 
          v-for="(marker,i) in markers"
          :key="i"
          :position="marker" 
          :google="scopeProps.google"
          :map="scopeProps.map"
        />
      </template>
    </map-loader>
  </div>
</template>

ChildMarkerはinjectがなくなりpropsで値を受けるようになる

<!-- ChildMarker -->
<template></template>
<script>
export default {
  props: {
    google: Object,
    map: Object,
    position: Object
  },
  data(){
    return { marker: null }
  },
  mounted(){
    const { Marker } = this.google.maps
    this.marker = new Marker({
      position: this.position,
      map: this.map,
      title: "Child marker!"
    })
  }
}
</script>
117
87
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
117
87