Vue.js & TypeScript で Google Mapの自作コンポーネント作成

# 背景
Vue.jsでGoogle Mapを表示する際に、既存のライブラリである[vue2-google-maps](https://github.com/xkjyeah/vue-google-maps)を使う選択肢もありましたが、今後様々なカスタマイズをしていきたいと考えた場合に使い勝手が少し悪いため自作することにしました。

今回の実装では現状私が必要とする機能のみなので、必要な場合は追加で実装する必要があります。

# 実装

## GoogleMap

Google Map表示用のコンポーネントです。

```GoogleMap.vue
<template>
  <div>
    <div id="googleMap" :style="`height:${height}`"></div>
    <template v-if="google && map">
      <slot :google="google" :map="map" />
    </template>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import qs from 'query-string'

const params = {
  key: 'Google API key',
  libraries: 'geometry,drawing,places',
  callback: 'handleLoadGoogleMapsScript'
}

interface GoogleMapWindow extends Window {
  handleLoadGoogleMapsScript: Function
  google: any
}

declare const window: GoogleMapWindow

@Component
export default class GoogleMap extends Vue {
  @Prop() private zoom!: number
  @Prop() private center!: { lat: number; lng: number }
  @Prop({ default: '240px' }) private height!: string
  google: any = null
  map: any = null

  mounted() {
    this.loadGoogleMapsScript().then(google => {
      this.google = google
      this.initializeMap()
    })
  }

  loadGoogleMapsScript() {
    return new Promise((resolve, reject) => {
      if (window.google) {
        return resolve(window.google)
      }
      const script = document.createElement('script')
      script.src = `https://maps.googleapis.com/maps/api/js?${qs.stringify(
        params
      )}`
      const head = document.querySelector('head')
      if (!head) return reject(new Error('head node is undefined'))
      head.appendChild(script)
      window.handleLoadGoogleMapsScript = () => {
        resolve(window.google)
      }
      setTimeout(() => {
        if (!window.google) {
          reject(new Error('failed load google api'))
        }
      }, 5000)
    })
  }

  initializeMap() {
    const mapContainer = this.$el.querySelector('#googleMap')
    const { Map, MapTypeId } = this.google.maps
    this.map = new Map(mapContainer, {
      zoom: this.zoom,
      center: this.center,
      mapTypeId: MapTypeId.ROADMAP
    })
  }
}
</script>
```

## GoogleMapMarker
Goggle Map上に配置するMarker用のコンポーネントです。
追加機能として、クリックされた際に情報を表示させます。

```GoogleMapMarker.vue
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class GoogleMapMarker extends Vue {
  @Prop() private google!: any
  @Prop() private map!: any
  @Prop() private position!: { lat: number; lng: number }
  @Prop() private title!: string
  marker: any
  infoWindow: any

  mounted() {
    const { Marker } = this.google.maps
    this.marker = new Marker({
      position: this.position,
      map: this.map
    })
    this.infoWindow = new this.google.maps.InfoWindow({
      content: `<div>${this.title}<div>`
    })
    this.google.maps.event.addListener(this.marker, 'click', () => {
      this.infoWindow.open(this.map, this.marker)
    })
  }
}
</script>
```

## 使用例
```vue
<GoogleMap
  :zoom="17"
  :center="center"
  :height="240"
>
  <template slot-scope="scope">
    <GoogleMapMarker
      v-for="(marker, i) in markers"
      :key="i"
      :position="marker"
      :google="scope.google"
      :map="scope.map"
    />
  </template>
</GoogleMap>
```

## 表示例

配置したMarkerクリック後のスクリーンショットです。
![スクリーンショット 2019-10-24 13.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442883/f6bebb98-728f-2eac-2563-7e024651bc63.png)



## 参考
https://github.com/laurencedorman/google-maps-api-loader