vue.jsでgoogleMapをPWAっぽく表示するオレオレプラクティス

こんばんは。switchを買ったけど放置してるmorifujiです。
ハマったら全てを放り出しそうで怖いです。

今日はGoogleMapとvuejsの組み合わせでごにょごにょしたのでその知見を共有します

# とりあえず成果物▼
- オリジナルのマーカー
- マーカークリックするとwindow表示
- マーカーアニメーションを設定
- GeolocationAPIを使って現在地を取得しマーカーに追加
    - この部分の実装はこの記事には書いてません。

![blog.gif](https://qiita-image-store.s3.amazonaws.com/0/158393/f538485f-3f03-5f4e-796b-b0ab6d1e799b.gif)

# 経緯
- pwaでgoogleMapをごりごり使いたい案件が発生！？
  - 軽くしか触ったことない
  - しかも生js
- いっちょオレオレGooglemapVueコンポーネント作るか

# 最低要件ライン
- マーカーの追加ができること
- マーカーのクリックイベントを簡単にlistenできること
- マーカーの画像をカスタマイズできること
- 最近のアップデートで現れた`地図を移動させるには指 2 本で操作します`仕様。今回はアプリ風pwaなのでこれを無効化すること
  - **これがあるのとないのでPWAっぽさが出てくると考えてる**
  - <img width="320" alt="IMG_1452.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/158393/4ae2871d-6f2c-b500-7409-cad8582df8dd.png">

# 調べた結果

vuejs + GoogleMapApiで色々調べてみたが、それなりに方法が出てきた。

## 1. [vue2-google-maps](https://www.npmjs.com/package/vue2-google-maps)

vue と googleMapApiで調べるとトップに出てくるライブラリ。

WeeklyDownloadsが1000超えてるので期待していた

```vue
<GmapMap
  :center="{lat:10, lng:10}"
  :zoom="7"
  map-type-id="terrain"
  style="width: 500px; height: 300px"
>
  <GmapMarker
    :key="index"
    v-for="(m, index) in markers"
    :position="m.position"
    :clickable="true"
    :draggable="true"
    @click="center=m.position"
  />
</GmapMap>
```

こんな感じでGmapコンポーネントのslotにマーカー を設定するようである。v-forで回してるからめちゃ簡単。

| 基準                           | vue2-google-maps |
| ------------------------------ | ---------------- |
| マーカーの追加                 | OK               |
| マーカーのクリックイベント取得 | OK               |
| 画像カスタマイズ               | OK               |
| `地図を移動させるには..`問題   | NG               |

`地図を移動させるには...`問題は解決しなかった。ちなみにいうとこの問題の解決方法は、
`gestureHandling: 'greedy'`を設定するだけだ。だがこれができない。GmapMapコンポーネントのpropsにつけてもうんともすんとも言わない。デモサイトには微塵もそんな設定がないし行き詰まった。

```js
     var map = new google.maps.Map(document.getElementById('map'), {
          zoom: 13,
          center: locationRio,
          gestureHandling: 'greedy'
        });
```

> 出典：https://developers.google.com/maps/documentation/javascript/interaction?hl=ja

よくよく見ると、propsの項目が公式GoogleMapsJavascriptApi(以下、公式API)と大きく乖離しているような気がする。
**propsに設定値を渡している時点で、公式apiとライブラリが密結合していて、このライブラリが今後もapiに柔軟に対応できるのか非常に疑問である。よって却下。**

## 2. 生api + [scriptjs](https://www.npmjs.com/package/scriptjs)

コンポーネントにすると、密結合になるから、やっぱり公式のapi普通のjsと同じように読み込むのがいいよね！(ﾃﾉﾋﾗｸﾙｰ)

ですがgoogleさんからはモジュールが提供されていないので、vuejsの中でurlからjsを読込まないといけない。そうなると**index.html**に記載する必要がでてきて、関係ないページにも公式APIが読み込まれてパフォーマンスが下がる。たしかサイズが200kbぐらいあったしそれは避けたい。

ここで[scriptjs](https://www.npmjs.com/package/scriptjs)を使うことにした。これを使うと、要するに、非常に簡単に非同期で外部jsが読み込めるというものらしい。

サンプルコード▼

```js
// load jquery and plugin at the same time. name it 'bundle'
$script(['jquery.js', 'my-jquery-plugin.js'], 'bundle')
 
// load your usage
$script('my-app-that-uses-plugin.js')

/*--- in my-jquery-plugin.js ---*/
$script.ready('bundle', function() {
  // jquery & plugin (this file) are both ready
  // plugin code...
})
```

変なクセもなく、非常に使いやすい。以下のファイルは検証に使った筆者のvueのcomponent。
最低限のものだけpropsにしてるけど許してね :kissing_smiling_eyes: 。
あと、**let,const,var警察はあまり僕のコードを見ないでください** :warning:

```vue
<template>
  <div>
    <div
      id="map"
      :style="{width: mapWidth + 'px',height: mapHeight + 'px'}"/>
  </div>
</template>

<script>
var scriptjs = require("scriptjs");

export default {
  name: "Gmap",
  props: {
    mapWidth: {
      type: Number,
      default: 100
    },
    mapHeight: {
      type: Number,
      default: 100
    },
    lat: {
      type: Number,
      default: 34.722677
    },
    lng: {
      type: Number,
      default: 135.492364
    },
    zoom: {
      type: Number,
      default: 8
    },
    markers: {
      type: Array,
      default: () => {
        return [];
      }
    }
  },
  data() {
    return {
      map: null,
      formattedMarkers: []
    };
  },
  watch: {
    markers() {
      // マーカーを全削除
      this.formattedMarkers.forEach(marker => {
        marker.setMap(null);
      });
      // propsからも削除
      this.formattedMarkers.splice(0, this.formattedMarkers.length);

      // 再描画
      this.addMarker();
    }
  },
  created() {
    scriptjs(
      "https://maps.googleapis.com/maps/api/js?key=XXXXXXXXXXXXXXXXXXXXXXX&callback=initMap",
      "loadGoogleMap"
    );
    scriptjs.ready("loadGoogleMap", this.loadMap);
  },
  mounted() {},
  methods: {
    addMarker() {
      this.markers.forEach(markerInfo => {
        var contentString =
          '<div id="content">' +
          '<div id="siteNotice">' +
          "</div>" +
          '<h1 id="firstHeading" class="firstHeading">Uluru</h1>' +
          '<div id="bodyContent">' +
          "<p><b>Uluru</b>, also referred to as <b>Ayers Rock</b>, is a large " +
          "sandstone rock formation in the southern part of the " +
          "Northern Territory, central Australia. It lies 335&#160;km (208&#160;mi) " +
          "south west of the nearest large town, Alice Springs; 450&#160;km " +
          "(280&#160;mi) by road. Kata Tjuta and Uluru are the two major " +
          "features of the Uluru - Kata Tjuta National Park. Uluru is " +
          "sacred to the Pitjantjatjara and Yankunytjatjara, the " +
          "Aboriginal people of the area. It has many springs, waterholes, " +
          "rock caves and ancient paintings. Uluru is listed as a World " +
          "Heritage Site.</p>" +
          '<p>Attribution: Uluru, <a href="https://en.wikipedia.org/w/index.php?title=Uluru&oldid=297882194">' +
          "https://en.wikipedia.org/w/index.php?title=Uluru</a> " +
          "(last visited June 22, 2009).</p>" +
          "</div>" +
          "</div>";

        // マーカー
        let marker = new google.maps.Marker({
          position: markerInfo.position,
          icon:
            "https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png",
          map: this.map,
          // ポップなアニメーションを付与
          animation: google.maps.Animation.DROP
        });

        // マーカーのwindow
        let infowindow = new google.maps.InfoWindow({
          content: contentString
        });

        // マーカークリック時にwindow表示
        marker.addListener("click", function() {
          infowindow.open(this.map, marker);
        });
        this.formattedMarkers.push(marker);
      });
    },
    loadMap() {
      // googleMapを初期化
      this.map = new google.maps.Map(document.getElementById("map"), {
        center: { lat: this.lat, lng: this.lng },
        zoom: this.zoom,
        // スワイプ判定を強めに設定(地図を移動させるには..問題)
        gestureHandling: "greedy"
      });
      this.addMarker();
    }
  }
};
</script>
```

結果、要件最低ラインをクリアした。

| 基準                           | vue2-google-maps | 生api + scriptjs |
| ------------------------------ | ---------------- | ---------------- |
| マーカーの追加                 | OK               | OK               |
| マーカーのクリックイベント取得 | OK               | OK               |
| 画像カスタマイズ               | OK               | OK               |
| `地図を移動させるには..`問題   | NG               | OK               |

## 3. [google-maps](https://www.npmjs.com/package/google-maps)

もういっちょ最後に検証してみた。

生のjsはなんか心がザワザワしたのでvueのコンポーネントにこだわらず、モジュール単位でもうちょいいいのがないか探した結果がこれ。ほんとにただ単にmodule化しただけのもの。

`生api + scriptjs`と違う主な点は以下のとおり

- API_KEYとかLOCALEとか仕様ライブラリを定数として設定できて非常に気持ちよかった([options](https://www.npmjs.com/package/google-maps#options))
- 非同期メソッドに`load`と`onLoad`があるという謎仕様(`load`は分かるが`onLoad`はどういう場面で使えるん？？)
- [release](https://www.npmjs.com/package/google-maps#unload-google-api)というメソッド(API_KEYとかをclearするメソッドかな？)もあるがSPAなのでうまく動かなかった。(ここ要調査)
- やっぱり非公式だからどこまでメンテするか謎い

使ったvueコンポーネントはこちら(:warning: **let,const,var警察はあまり僕のコードを見ないでください** :warning: )▼

```vue
<template>
  <div>
    <div
      id="map"
      :style="{width: mapWidth + 'px',height: mapHeight + 'px'}"/>
    <button
      class="button"
      @click="release">無効化</button>
  </div>
</template>

<script>
let GoogleMapsLoader = require("google-maps");

GoogleMapsLoader.KEY = "XXXXXXXXXXXXXXXXXXXXXx";
GoogleMapsLoader.LANGUAGE = "ja";

export default {
  name: "Gmap",
  props: {
    mapWidth: {
      type: Number,
      default: 100
    },
    mapHeight: {
      type: Number,
      default: 100
    },
    lat: {
      type: Number,
      default: 34.722677
    },
    lng: {
      type: Number,
      default: 135.492364
    },
    zoom: {
      type: Number,
      default: 8
    },
    markers: {
      type: Array,
      default: () => {
        return [];
      }
    }
  },
  data() {
    return {
      map: null,
      formattedMarkers: []
    };
  },
  watch: {
    markers() {
      // マーカーを全削除
      this.formattedMarkers.forEach(marker => {
        marker.setMap(null);
      });
      // propsからも削除
      this.formattedMarkers.splice(0, this.formattedMarkers.length);

      // 再描画
      this.addMarker();
    }
  },
  created() {},
  mounted() {
    // googleMap描画
    GoogleMapsLoader.load(this.loadMap);
  },
  methods: {
    addMarker() {
      this.markers.forEach(markerInfo => {
        var contentString =
          '<div id="content">' +
          '<div id="siteNotice">' +
          "</div>" +
          '<h1 id="firstHeading" class="firstHeading">Uluru</h1>' +
          '<div id="bodyContent">' +
          "<p><b>Uluru</b>, also referred to as <b>Ayers Rock</b>, is a large " +
          "sandstone rock formation in the southern part of the " +
          "Northern Territory, central Australia. It lies 335&#160;km (208&#160;mi) " +
          "south west of the nearest large town, Alice Springs; 450&#160;km " +
          "(280&#160;mi) by road. Kata Tjuta and Uluru are the two major " +
          "features of the Uluru - Kata Tjuta National Park. Uluru is " +
          "sacred to the Pitjantjatjara and Yankunytjatjara, the " +
          "Aboriginal people of the area. It has many springs, waterholes, " +
          "rock caves and ancient paintings. Uluru is listed as a World " +
          "Heritage Site.</p>" +
          '<p>Attribution: Uluru, <a href="https://en.wikipedia.org/w/index.php?title=Uluru&oldid=297882194">' +
          "https://en.wikipedia.org/w/index.php?title=Uluru</a> " +
          "(last visited June 22, 2009).</p>" +
          "</div>" +
          "</div>";

        // マーカー
        let marker = new google.maps.Marker({
          position: markerInfo.position,
          icon:
            "https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png",
          map: this.map,
          // ポップなアニメーションを付与
          animation: google.maps.Animation.DROP
        });

        // マーカーのwindow
        let infowindow = new google.maps.InfoWindow({
          content: contentString
        });

        // マーカークリック時にwindow表示
        marker.addListener("click", function() {
          infowindow.open(this.map, marker);
        });
        this.formattedMarkers.push(marker);
      });
    },
    loadMap(google) {
      // googleMapを初期化
      this.map = new google.maps.Map(document.getElementById("map"), {
        center: { lat: this.lat, lng: this.lng },
        zoom: this.zoom,
        // スワイプ判定を強めに設定(地図を移動させるには..問題)
        gestureHandling: "greedy"
      });
      this.addMarker();
    },
    release() {
      GoogleMapsLoader.release(function() {
        console.log("No google maps api around");
      });
    }
  }
};
</script>

```

こちらも要件最低ラインはクリアした。

| 基準                           | vue2-google-maps | 生api + scriptjs | google-maps |
| ------------------------------ | ---------------- | ---------------- | ----------- |
| マーカーの追加                 | OK               | OK               | OK          |
| マーカーのクリックイベント取得 | OK               | OK               | OK          |
| 画像カスタマイズ               | OK               | OK               | OK          |
| `地図を移動させるには..`問題   | NG               | OK               | OK          |

# まとめ
- `生api+scriptjs`か`google-maps`だと拡張性高い！
- **なんでもかんでもvueコンポーネントにするのは良くない。**
  - 公式への対応性・拡張性がなくなるため(仕様が変わったら、中の人が頑張らないと新しい仕様に対応することが難しい  :cry:) 
  - その点、jsを軽くモジュール化したものは、要所要所はjsなので拡張性高いかな？
- モジュール化されてなくても、頑張ればvueのsfcで非同期に使える。
- npmのWeeklyDownloads数はあまり当てにならない :cry: 
- :warning: **let,const,var警察はあまり僕のコードを見ないでください** :warning: 

# おまけ

## googleMapの幅を画面いっぱいにしたい

それ、vuejsなら解決します :tada: 

1. 画面幅を取得して
2. styleにバインド

```vuejs
<template>
  <div>
  <!-- 2. styleにバインド -->
    <div
      id="map"
      :style="{width: mapWidth + 'px',height: mapHeight + 'px'}"/>
  </div>
</template>

<script>
export default {
  name: "Gmap",
  data() {
    return {
    	mapHeight: 500,
    	mapWidth: 1000
    };
  },
  created() {
    // 1. 画面幅を取得して
    this.mapWidth = window.innerWidth;
  }
};
</script>

```