【Vue.js】Google Maps APIと連携して地図を表示する方法

【Vue.js】Google Maps APIと連携して地図を表示する方法 Vue.js

Google Maps JavaScript APIを使うと、Vueアプリに地図を埋め込み、店舗や拠点をマーカーで示せます。ただし、古い解説のコードをそのまま使うと、非推奨のAPIを使うことになったり、課金設定やAPIキーの制限でつまずいたりします。この記事では、現在の推奨に沿った実装を、Vue 3の動くコードで解説します。

この記事の構成は、①APIキーの取得と制限、②スクリプトの動的読み込み、③地図コンポーネント(propsでマーカーを受け取る)の3つです。マーカーには、非推奨のgoogle.maps.Markerではなく、AdvancedMarkerElementを使います。

スポンサーリンク

事前準備:APIキーと課金設定

Google Cloud Consoleで、次の準備をします。

  1. プロジェクトを作成する
  2. 請求先アカウントを設定する(Google Maps Platformの利用に必須です。未設定だと地図が正常に表示されません)
  3. 「Maps JavaScript API」を有効にする
  4. APIキーを発行し、制限をかける

料金や無料枠は、改定されることがあります。利用前に、公式の料金ページで最新の内容を確認してください。

APIキーは公開される前提で制限する

「.envに入れれば漏洩を防げる」と説明されることがありますが、これは正確ではありません。ViteなどのフロントエンドのビルドでAPIキーを読み込むと、完成したJavaScriptにキーが含まれ、ブラウザから見えてしまいます。.envに入れる目的は、キーをGitに載せないことです。

.env.local
# .env.local(Gitに入れない)
VITE_GOOGLE_MAPS_API_KEY=取得したAPIキー

実際の防御は、Cloud Consoleでの制限です。

  • アプリケーションの制限:「ウェブサイト(HTTPリファラー)」を選び、自分のドメイン(例:https://example.com/*)だけを許可します。開発用にhttp://localhost:*も追加します。
  • APIの制限:使うAPI(Maps JavaScript API)だけに絞ります。

環境ごとにキーを分ける方法は、環境ごとにAPIのURLを切り替える方法をご覧ください。

スクリプトを動的に読み込む

旧来の解説は、index.htmlに<script>タグを書きます。この方法では、コンポーネントが動くときにまだ読み込みが終わっておらず、google is not definedになることがあります。また、地図を使わないページでも読み込みが走ります。

そこで、地図が必要になった時点で読み込み、完了を待つ関数を用意します。loading=asyncを付けて読み込むと、google.maps.importLibrary()で必要なライブラリだけを取り出せます。

JavaScript:loadGoogleMaps.js
// utils/loadGoogleMaps.js
let promise = null

export function loadGoogleMaps(apiKey) {
  // すでに読み込み済みなら、何もしない
  if (window.google?.maps?.importLibrary) return Promise.resolve()
  if (promise) return promise

  promise = new Promise((resolve, reject) => {
    window.__onGoogleMapsLoaded = resolve

    const script = document.createElement('script')
    script.src =
      'https://maps.googleapis.com/maps/api/js' +
      `?key=${apiKey}&v=weekly&loading=async&callback=__onGoogleMapsLoaded`
    script.async = true
    script.onerror = () => {
      promise = null // 失敗したら、次回やり直せるようにする
      reject(new Error('Google Mapsの読み込みに失敗しました'))
    }
    document.head.append(script)
  })
  return promise
}

地図コンポーネントを作る

緯度・経度・マーカーをpropsで受け取り、マーカーが変わったら描き直します。

Vue:GoogleMap.vue
<script setup>
import { ref, shallowRef, onMounted, onBeforeUnmount, watch } from 'vue'
import { loadGoogleMaps } from '@/utils/loadGoogleMaps'

const props = defineProps({
  center: { type: Object, default: () => ({ lat: 35.681236, lng: 139.767125 }) }, // 東京駅
  zoom: { type: Number, default: 14 },
  locations: { type: Array, default: () => [] } // [{ name, lat, lng }]
})

const container = ref(null)
const error = ref('')
// 地図オブジェクトはVueのリアクティブにしない(shallowRef)
const map = shallowRef(null)
let markers = []
let AdvancedMarkerElement = null
let InfoWindow = null

function renderMarkers() {
  markers.forEach((m) => (m.map = null)) // 古いマーカーを地図から外す
  markers = props.locations.map((loc) => {
    const marker = new AdvancedMarkerElement({
      map: map.value,
      position: { lat: loc.lat, lng: loc.lng },
      title: loc.name
    })
    const info = new InfoWindow({ content: loc.name })
    marker.addListener('click', () => info.open({ anchor: marker, map: map.value }))
    return marker
  })
}

onMounted(async () => {
  try {
    await loadGoogleMaps(import.meta.env.VITE_GOOGLE_MAPS_API_KEY)
    const { Map, InfoWindow: IW } = await google.maps.importLibrary('maps')
    const { AdvancedMarkerElement: AME } = await google.maps.importLibrary('marker')
    AdvancedMarkerElement = AME
    InfoWindow = IW

    map.value = new Map(container.value, {
      center: props.center,
      zoom: props.zoom,
      mapId: 'DEMO_MAP_ID' // AdvancedMarkerElementに必須。本番ではCloud Consoleで作成したIDに
    })
    renderMarkers()
  } catch (e) {
    error.value = '地図を読み込めませんでした。'
  }
})

watch(() => props.locations, () => map.value && renderMarkers(), { deep: true })

onBeforeUnmount(() => {
  markers.forEach((m) => (m.map = null))
  markers = []
})
</script>

<template>
  <p v-if="error" role="alert">{{ error }}</p>
  <div ref="container" class="map"></div>
</template>

<style scoped>
/* 高さを指定しないと0pxになり、地図が表示されない */
.map { width: 100%; height: 400px; }
</style>

ポイントは次のとおりです。

  • mapIdが必須:AdvancedMarkerElementを使うには、地図の作成時にmapIdが必要です。動作確認ならDEMO_MAP_IDが使えます。本番では、Cloud Consoleの「地図の管理」で作成したIDを指定します。
  • shallowRefを使う:地図のオブジェクトをrefで包むと、Vueが深くProxy化してしまい、不具合の原因になります。shallowRefか、通常の変数で持ちます。
  • 緯度と経度の順番:{ lat, lng }です。逆にすると、まったく別の場所が表示されます。
  • 後片付け:コンポーネントが消えるときは、マーカーをmap = nullで地図から外します。
  • 高さの指定:地図を入れる要素に高さがないと、0pxになって何も表示されません。
Vue:使う側
<script setup>
import GoogleMap from '@/components/GoogleMap.vue'

const shops = [
  { name: '東京駅', lat: 35.681236, lng: 139.767125 },
  { name: '上野駅', lat: 35.713768, lng: 139.777254 }
]
</script>

<template>
  <GoogleMap :locations="shops" :zoom="12" />
</template>

使う側では、マーカーの配列を渡すだけです。共通の処理を切り出す考え方はComposable関数でロジックを共通化する方法でも解説しています。

Markerは非推奨になっています

古い解説で使われているgoogle.maps.Markerは、2024年2月から非推奨になり、AdvancedMarkerElementへの移行が案内されています。現時点でも動きますが、コンソールに警告が出ます。新しく作るなら、最初からAdvancedMarkerElementを使いましょう。

JavaScript:Markerからの移行
// NG: 非推奨。コンソールに警告が出る
new google.maps.Marker({ position, map, title })

// OK: AdvancedMarkerElement(mapIdが必要)
const { AdvancedMarkerElement } = await google.maps.importLibrary('marker')
new AdvancedMarkerElement({ position, map, title })

よくあるエラーと原因

  • google is not defined:スクリプトの読み込みが終わる前に、googleを使っています。上のloadGoogleMapsで完了を待ちます。
  • BillingNotEnabledMapError:請求先アカウントが設定されていません。
  • ApiNotActivatedMapError:Maps JavaScript APIが有効になっていません。
  • RefererNotAllowedMapError:HTTPリファラーの制限に、今のURLが含まれていません。開発中はlocalhost、本番は実際のドメインを許可してください。
  • 地図が真っ白、または見えない:地図を入れる要素の高さが0です。

エラーの詳細は、ブラウザの開発者ツールのコンソールに出ます。

表示を軽くするコツ

地図は、ページの初期表示とは関係のない、大きなスクリプトを読み込みます。画面の下のほうにある地図は、見える位置に来てから読み込むと、初期表示が軽くなります。仕組みは画像の遅延読み込みと同じで、コードスプリッティングと遅延読み込みとも相性がよい使い方です。

よくある質問(FAQ)

Q無料で使えますか?
A請求先アカウントの設定が必要で、一定の無料利用枠を超えると課金されます。枠や料金は改定されるため、公式の料金ページで確認してください。APIキーに制限をかけ、予算アラートも設定しておくと安心です。
Q「この地図は開発目的でのみ使用できます」と表示されます。
A多くの場合、請求先アカウントが有効になっていないか、APIキーの設定に問題があります。Cloud Consoleで、請求の設定とAPIキーの制限を確認してください。
Q公式のVue向けライブラリはありますか?
A公式にはありません。Googleが提供するのはJavaScript APIで、Vue向けにはコミュニティのライブラリがあります。ライブラリの更新状況を確認したうえで、自分で薄くラップする今回の方法も、選択肢の1つです。
Qマーカーをクリックしたときの処理はどう書きますか?
A上のコードのように、marker.addListener('click', ...)で処理を登録し、InfoWindowを開く方法が基本です。

まとめ

Vue 3でGoogle Mapsを使うときは、①請求先の設定とAPIキーの制限、②スクリプトの動的読み込み、③AdvancedMarkerElementとmapId、の3点が要になります。APIキーはブラウザから見える前提で、リファラーとAPIの制限で守ってください。地図のオブジェクトはshallowRefで持ち、不要になったマーカーは外す、という点も押さえておくと、安定して動きます。