Posted at

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

More than 1 year has passed since last update.

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>