Nuxt3+firebase hostingでgoogle maps apiを使う


google maps apiのライブラリ

vue3やnuxt3でgoogle maps apiを使うにおいて、@fawmi/vue-google-mapsなどvue3用のライブラリはいくつかありますが、どれもバグが多く2022年4月現在まともに使えるものがありませんでした。

結局一番シンプルなライブラリであるgooglemaps/js-api-loaderを使うのが最も手っ取り早いのですが、firebase hostingにデプロイしたら動かなくなるトラブルで数時間苦しんだので、メモしときます。

@googlemaps/js-api-loaderを用いたローカル開発

インストール

$ npm i @googlemaps/js-api-loader
$ npm i -D @types/google.maps

ページ作成

ドキュメント通りですが、vue3に合わせて少し書き方を変えています。

Nuxt(というかVue)なので、HTMLエレメントはrefで取得できます。
HTMLエレメントにgoogleマップを描画するので、loaderの実行はDOM生成後つまりonMountedになります。

pages/map.vue

<script setup lang="ts">
import { Loader } from '@googlemaps/js-api-loader';

const gmap = ref<HTMLElement>()

const loader = new Loader({
    apiKey: "******************",
    version: "weekly",
    libraries: ["places"]
});

const mapOptions = {
  center: {
    lat: 34.60,
    lng: 135.52
  },
  zoom: 15
};

onMounted(()=>{
    loader
    .load()
    .then((google) => {
        new google.maps.Map(gmap.value, mapOptions);
    })
    .catch(e => {
        // do something
    });
})
</script>

<template>
    <h1>マップ</h1>
    <div ref="gmap" class="h-[500px] w-[800px]"></div>
</template>

この状態でnpm run devを実行し、localhost:3000/mapを開きましょう。
問題なく表示されるはずです。

firebaseエミュレーターでの失敗と対処

エミュレーター実行

NITRO_PRESET=firebase yarn buildを実行し、firebase emulators:startでlocalhost:5001/mapを開いてみてください。

おそらくページ表示は失敗し、以下のようなエラーメッセージが表示されているでしょう。

>  Named export 'Loader' not found. The requested module '@googlemaps/js-api-loader' is a CommonJS module, which may not support all module.exports as named exports.
>  CommonJS modules can always be imported via the default export, for example using:
>  
>  import pkg from '@googlemaps/js-api-loader';
>  const { Loader } = pkg;

対処①

エラーメッセージの通り、以下のように修正します

- import { Loader } from '@googlemaps/js-api-loader';
+ import * as l from '@googlemaps/js-api-loader';
+ const {Loader} = l

//以下略

再度、NITRO_PRESET=firebase yarn buildを実行し、firebase emulators:startでlocalhost:5001/mapを開いてみます。

今度は、「500」と「Loader is not a constructor」という表示がされるはずです。

対処②

Loaderはコンストラクタなのに変だぞ、、、とめちゃくちゃ検証しました。

結論から書きます。
Loaderのインスタンス化はonMountedで実行する必要があります。

<script setup lang="ts">
import * as l from '@googlemaps/js-api-loader';
const {Loader} = l

const gmap = ref<HTMLElement>()

// setupでインスタンス化しようとすると失敗する
// const loader = new Loader({
//     apiKey: "*******************",
//     version: "weekly",
//     libraries: ["places"]
// });

const mapOptions = {
  center: {
    lat: 34.60,
    lng: 135.52
  },
  zoom: 15
};

onMounted(()=>{
    // インスタンス化はonMountedで実行する
    const loader = new Loader({
        apiKey: "********************",
        version: "weekly",
        libraries: ["places"]
    });

    loader
    .load()
    .then((google) => {
        console.log(google)
        new google.maps.Map(gmap.value, mapOptions);
    })
    .catch(e => {
        // do something
    });
})
</script>

<template>
<h1>マップ</h1>
<div ref="gmap" class="h-[500px] w-[800px]"></div>
</template>

マーカーと情報ウィンドウを表示する

マーカーをクリックしたら情報ウィンドウを表示するようにします。

ポイントはinfoWindowインスタンスの作成をマーカーインスタンスと別に行うことです。
これにより、infoWindowインスタンスはただ一つとなりますので、あるマーカーをクリックした後で別のマーカーをクリックすると、infoWindowは切り替わったように見えます。

onMounted(()=>{

    new Loader({
        apiKey: "*****",
        version: "weekly",
        libraries: ["places"]
    }).load()
      .then(google=>{
            const map = new google.maps.Map(mapElem.value,mapOptions) 
            if(!markers.length)return
            const infoWindow = new google.maps.InfoWindow() //ポイント
            markers.forEach(d=>{
                const marker = new google.maps.Marker({
                    position: d.position,
                    map:map,
                    title:d.title,
                    label:d.label,
                });

                marker.addListener("click", (e) => {
                    infoWindow.close();  
                    infoWindow.setContent(`
                        ${d.title.slice(0,20)}
                        ${d.title.slice(62,80)}
                    `);
                    infoWindow.open(marker.getMap(), marker);
                  
                    emits('clicked',marker.getTitle())                    
                });
            })
      })
})

下のようにマーカーごとにinfoWidowインスタンスを作成すると、別のマーカーをクリックしても前に開いていたinfoWindowは自動で閉じません(これはこれで使い道ありそうですが)

onMounted(()=>{

    new Loader({
        apiKey: "*****",
        version: "weekly",
        libraries: ["places"]
    }).load()
      .then(google=>{
            const map = new google.maps.Map(mapElem.value,mapOptions) 
            if(!markers.length)return
            
            markers.forEach(d=>{
                const marker = new google.maps.Marker({
                    position: d.position,
                    map:map,
                    title:d.title,
                    label:d.label,
                });
		const infoWindow = new google.maps.InfoWindow() //ここだと一つのマーカーに一つのウィンドウが紐づく
                marker.addListener("click", (e) => {
                    infoWindow.close();  
                    infoWindow.setContent(`
                        ${d.title.slice(0,20)}
                        ${d.title.slice(62,80)}
                    `);
                    infoWindow.open(marker.getMap(), marker);
                  
                    emits('clicked',marker.getTitle())                    
                });
            })
      })
})

お知らせ

今回地図の中心に示しているのは、大阪の長居公園の近くにあるエンジニアハウスです。

エンジニアハウスは、エンジニアが集まって電子工作やweb制作をしています。
(web制作をやっている人が、エンジニアハウスに来てから電子工作にハマるようになるのが多いです)

https://engineerhouse.org/

土日に活動していますので、興味ある方は覗きにきてください。
管理者メールアドレス  [email protected]