【2023年】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年11月 書き方を更新
- 2023/3/18 nuxt3の正式リリース版に合わせて、pluginsの記述方法を変更しました。
@googlemaps/js-api-loaderを用いたローカル開発
インストール
$ npm i @googlemaps/js-api-loader
$ npm i -D @types/google.maps
ページ作成
ドキュメント通りですが、vue3に合わせて少し書き方を変えています。
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 pkg from '@googlemaps/js-api-loader';
+ const {Loader} = pkg
//以下略
再度、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 pkg from '@googlemaps/js-api-loader';
const {Loader} = pkg
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(async()=>{
// インスタンス化はonMountedで実行する
const loader = new Loader({
apiKey: "********************",
version: "weekly",
libraries: ["places"]
});
const {Map} = await loader.importLibrary("maps")
new Map(
gmap.value,
{
center:new google.maps.LatLng(35,135),
zoom:12
}
)
})
</script>
<template>
<h1>マップ</h1>
<div ref="gmap" class="h-[500px] w-[800px]"></div>
</template>
実装
loaderをpluginsに移行
上記の書き方ではapiキーをページ内にベタ書きしているのでセキュリティ上良くありませんし、見た目も乱雑です。
apiキーを用いた認証はページ表示時に一度実行すれば良いので、loaderはpluginsで実行するようにしましょう。
注意点として、google maps apiはクライアントサイドのみで実行する必要があるので、pluginsのファイル名には「client」をつけます。
import * as pkg from '@googlemaps/js-api-loader';
const { Loader } = pkg;
export default defineNuxtPlugin((nuxtApp) => {
return {
provide:{
loader: new l.Loader({
apiKey: "****************",
version: "weekly",
libraries: ["places"]
})
}
}
});
※ Nuxt3の正式リリース版では、plugins内ファイルにimport { defineNuxtPlugin } from "#app";
の記述は必要なくなりました
runtimeConfigを使って、apiKeyを隠す
runtimeConfigの使い方は今回の趣旨ではありませんので詳細は割愛しますが、以下のような感じで書いてください
NUXT_PUBLIC_GOOGLE_MAPS_API_KEY="**************"
export default defineNuxtConfig({
・・・,
runtimeConfig:{
public:{
googleMapsApiKey:process.env.NUXT_PUBLIC_GOOGLE_MAPS_API_KEY,
・・・
}
}
・・・,
})
import * as pkg from '@googlemaps/js-api-loader';
const { Loader } = pkg;
export default defineNuxtPlugin((nuxtApp) => {
return {
provide:{
loader: new l.Loader({
apiKey: config.public.googleMapsApiKey,
version: "weekly",
libraries: ["places"]
})
}
}
});
ちなみにnuxt.config.tsのgoogleMapsApiKeyを書かなくても、開発環境上は地図が使えますが、エミュレーター環境(本番環境)では使えなくなるので注意。
コンポーネントでの使い方
js-api-loaderをplugins内に書いたことにより、コンポーネントは非常にシンプルに書き直せます。
<script setup lang="ts">
const {$loader} = useNuxtApp()
const map = ref<HTMLElement>()
onMounted(async()=>{
const {Map} = await $loader.importLibrary("maps")
map.value = new Map(
document.getElementById('map') as HTMLDivElement,
{
center:new google.maps.LatLng(35,135),
zoom:12
}
)
})
</script>
<template>
<div>
<div class=" font-bold">テスト</div>
<div ref="gmap" class="h-[500px] w-[800px]"></div>
</div>
</template>
マーカーと情報ウィンドウを表示する
マーカーをクリックしたら情報ウィンドウを表示するようにします。
ポイントは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())
});
})
})
})
Discussion