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にデプロイしたら動かなくなるトラブルで数時間苦しんだので、メモしときます。
更新
- 2023/3/18 nuxt3の正式リリース版に合わせて、pluginsの記述方法を変更しました。
@googlemaps/js-api-loaderを用いたローカル開発
インストール
$ npm i @googlemaps/js-api-loader
$ npm i -D @types/google.maps
ページ作成
ドキュメント通りですが、vue3に合わせて少し書き方を変えています。
vue3なので、HTMLエレメントはrefで取得できます。
(document.getElementByIdでも良いですが、カスタムコンポーネントだとidがバッティングしてしまいます)
HTMLエレメントにgoogleマップを描画するので、loaderの実行はDOM生成後つまりonMountedで実行しましょう。(DOM生成前に実行しようとすると、window is not finedエラーとなります)
<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>
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>
loaderをpluginsに移行
上記の書き方ではapiキーをページ内にベタ書きしているのでセキュリティ上良くありませんし、見た目も乱雑です。
apiキーを用いた認証はページ表示時に一度実行すれば良いので、loaderはpluginsで実行するようにしましょう。
pluginのprovideで定義したプロパティは、カスタムコンポーネントでは$~~を使って使えます。
import * as l from '@googlemaps/js-api-loader';
export default defineNuxtPlugin((nuxtApp) => {
return {
provide:{
loader:()=>{
return new l.Loader({
apiKey: "****************",
version: "weekly",
libraries: ["places"]
})
}
}
}
});
※ Nuxt3の正式リリース版では、plugins内ファイルにimport { defineNuxtPlugin } from "#app";
の記述は必要なくなりました
※ apiKeyはruntimeConfigを使って隠してください
変数loaderが、Loaderのインスタンスではなく関数を返すようにしているようにしている理由は、上記で述べたようにLoaderのインスタンス化はonMountedで行う必要があるからです。
上述のonMounted内は以下のように非常にシンプルに書き直せます。
・・・
onMounted(async()=>{
const {$loader} = useNuxtApp()
$loader().load().then(google=>{
new google.maps.Map(gmap.value, mapOptions)
})
})
・・・
マーカーと情報ウィンドウを表示する
マーカーをクリックしたら情報ウィンドウを表示するようにします。
ポイントはinfoWindowインスタンスの作成をマーカーインスタンスと別に行うことです。
これにより、infoWindowインスタンスはただ一つとなりますので、あるマーカーをクリックした後で別のマーカーをクリックすると、infoWindowは切り替わったように見えます。
onMounted(()=>{
・・・省略
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(()=>{
・・・省略
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制作をやっている人が、エンジニアハウスに来てから電子工作にハマるようになるのが多いです)
土日に活動していますので、興味ある方は覗きにきてください。
管理者メールアドレス zhitian_xiongai@yahoo.co.jp
Discussion