Nuxt3とGoogleMapApiを使用したアプリで本番デプロイする~NetlifyやFirebase利用~

おはようございます。そうすけです!

運営者:そうすけ

愛媛のエンジニア兼ブロガー。
工場勤務→社内SE→自社開発にキャリアチェンジ。
主にバックエンド開発を行っています。

【ブログ運営歴】2021.6~
【プログラミング歴】2022.3~

本業:Java,MySQL、個人:Typescript

本記事ではNetlify・FIREBASEにnuxt3を使ってGoogleMapApiを使用して本番デプロイしたときのエラーをまとめています。

個人開発でに数日つまづいたので、記載しておきます。

アプリ開発ポートフォリオ作成でGoogle関係のAPIを使う方参考になれば幸いです。

ローカル環境では起動しますが、FIREBASE・Netlifyの本番環境でも起こりますので、本番共通のエラーのようです。

今回はNuxtプロジェクトを簡単にデプロイできるNetlifyを使用しました。

目次

デプロイしたコード

詳細検索すればサウナ一覧がでるサウナアプリです。

ざっくりいうと、NUXT3のcomporsablesディレクトリでインスタンス化したloaderを、コンポーネント側で実行して検索した値を取得しています。

<template>


    <div class="container  mx-auto w-full"> 

        <div class="grid 2xl:grid-cols-6 xl:grid-cols-5 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-3 grid-cols-2 gap-x-3 gap-y-6 ">

            <div v-for="(spa,index) in result">
                <NuxtLink :to="`/map/${spa.place_id}`">
                <div class="border-2 border-orange-200 w-50  sm:w-52 rounded-md shadow-lg">
                    <div>
                        <img :src="spa.photos[0].getUrl()" 
                        class="h-44 w-full object-cover rounded-t-md">
                    </div>
                    <div class="flex flex-col items-center py-1">
                        <div class="w-40  px-2 overflow-hidden whitespace-nowrap text-ellipsis">
                            {{ spa.name }}
                        </div>
                        <div>
                       </div>
  
                    </div>
  
                </div>
                </NuxtLink>
            </div>
        </div>
        
    </div>
  


<script lang="ts" setup>

//googlemap API関数
//インスタンスを取得
  const loader = useGoogle()

   const getData = ():void =>{
        

        //検索結果宣言
        const searchResult = ref([])

        //検索実行関数
        loader.load().then((google) => {
        
            const geocoder = new google.maps.Geocoder()
            //位置情報のインスタンス化
            let latLng: google.maps.LatLng = new google.maps.LatLng(0.0)
            geocoder.geocode({
            //入力した値をリクエスト
            //クエリパラメータにある指定都市の名称をリクエスト
                address:searchPoint.value

            },
            //callback関数
            //結果オブジェクトとステータスオブジェクトが帰ってくる
            (result:any,status) => {
                //レスポンスとステータスを引数に取れる
                if(status === google.maps.GeocoderStatus.OK){
                    //周辺検索用に緯度と経度を取得する
                    const center = result[0]
                    latLng = center.geometry.location
                }
                }
            )

            //html要素を持たせたマッププレイスサービスを定義
            const map = new google.maps.Map(document.createElement('div'))
          const service = new google.maps.places.PlacesService(map)
        //テキスト検索用のリクエスト
        service.textSearch(
        {
            //緯度経度
            location:latLng,
            //検索する半径
            radius:2000,
            //検索ワード
            query:
            'サウナ' + ' ' + searchPoint.value + ' ' + searchCondition.value,
            //レスポンス言語
            language:'ja'
        },
        //第二引数で処理
        //callback関数
        //結果オブジェクトとステータスオブジェクトが帰ってくる
        (result,status) =>{
        //レスポンスとステータスを引数にとれる
        if(
            status === google.maps.places.PlacesServiceStatus.OK ||
            status === google.maps.places.PlacesServiceStatus.ZERO_RESULTS
        ){
            result!.forEach((element) => {
                const format =element.formatted_address!.slice(12)
                element.formatted_address = format
            })
            searchResult.value =result!
            emits('getdatas',searchResult.value)

            }        
        }
        
    )
        })
        .catch(
            () =>{

            })
    
        //検索値を表示
        searchPoint_w.value=searchPoint.value
        searchCondition_w.value=searchCondition.value
        //textを空に
        // searchPoint.value=""
        // searchCondition.value=""
    }



</script>
import {Loader} from '@googlemaps/js-api-loader'

export const useGoogle = () => {
        //const runtimeConfig = useRuntimeConfig();
        const loader = new Loader({
            apiKey:'取得したAPIキー',
            version:'weekly',
            libraries:['places','drawing','geometry']
        })

        return (loader)
  
    
        
}

この方をベースに開発してます。開発方法は本筋からそれるので、こちらを覗いてみて下さい。

Netlifyで発生した3つのエラー

Netlifyに沿って本番デプロイ後のエラーです。

デプロイ方法は別途記事にします。

404エラーが発生する

これはNuxt3共通に起こる事象です。

Netlifyのデフォルトコンパイル先(publish directory)のファイルを.nuxt/distではなくdistに変更します。

デフォルトだと.nuxt/distになっていていますので変更してください。

あるあるエラーのようです。

GoogleAPIのエラー:CommonJS

エミュレーターや本番環境で以下のエラーがでます。

500
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'
//netlifyのエラーにつき、記述方法を変更
import * as pkg from '@googlemaps/js-api-loader';
const { Loader } = pkg;

export const useGoogle = () => {
        //const runtimeConfig = useRuntimeConfig();
        const loader = new Loader({
            apiKey:'APIキー',
            version:'weekly',
            libraries:['places','drawing','geometry']
        })

        return (loader)
  
    
        
}

loaderのインスタンス化のタイミング

起動時に変数宣言してインスタンス化するようになっている、次にこのエラーがでてきます。

500 Loader is not a constructor

外部APIなのにコンストラクタとして起動時に実行されると、レンダリング(描画)時に起動するなとエラーが起こるようです。

解決方法

  • Onmountedを使用する
  • 使用する関数内でインスタンス化する
<template>


    <div class="container  mx-auto w-full"> 
        
        <!-- <div class="flex flex-wrap justify-start gap-4"> -->
        <div class="grid 2xl:grid-cols-6 xl:grid-cols-5 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-3 grid-cols-2 gap-x-3 gap-y-6 ">
            
        
            <div v-for="(spa,index) in result">
                <NuxtLink :to="`/map/${spa.place_id}`">
                <div class="border-2 border-orange-200 w-50  sm:w-52 rounded-md shadow-lg">
                    <div>
                        <img :src="spa.photos[0].getUrl()" 
                        class="h-44 w-full object-cover rounded-t-md">
                    </div>
                    <div class="flex flex-col items-center py-1">
                        <div class="w-40  px-2 overflow-hidden whitespace-nowrap text-ellipsis">
                            {{ spa.name }}
                        </div>
                        <div>
                       </div>
  
                    </div>
  
                </div>
                </NuxtLink>
            </div>
        </div>
        
    </div>
  
</template>

<script lang="ts" setup>

//googlemap API関数


   const getData = ():void =>{
        
        //インスタンスを取得
   const loader = useGoogle()


        //検索結果宣言
        const searchResult = ref([])

        //検索実行関数
        loader.load().then((google) => {


        
            const geocoder = new google.maps.Geocoder()
            //位置情報のインスタンス化
            let latLng: google.maps.LatLng = new google.maps.LatLng(0.0)
            geocoder.geocode({
            //入力した値をリクエスト
            //クエリパラメータにある指定都市の名称をリクエスト
                address:searchPoint.value

            },
            //callback関数
            //結果オブジェクトとステータスオブジェクトが帰ってくる
            (result:any,status) => {
                //レスポンスとステータスを引数に取れる
                if(status === google.maps.GeocoderStatus.OK){
                    //周辺検索用に緯度と経度を取得する
                    const center = result[0]
                    latLng = center.geometry.location
                }
                }
            )

            //html要素を持たせたマッププレイスサービスを定義
            const map = new google.maps.Map(document.createElement('div'))
          const service = new google.maps.places.PlacesService(map)
        //テキスト検索用のリクエスト
        service.textSearch(
        {
            //緯度経度
            location:latLng,
            //検索する半径
            radius:2000,
            //検索ワード
            query:
            'サウナ' + ' ' + searchPoint.value + ' ' + searchCondition.value,
            //レスポンス言語
            language:'ja'
        },
        //第二引数で処理
        //callback関数
        //結果オブジェクトとステータスオブジェクトが帰ってくる
        (result,status) =>{
        //レスポンスとステータスを引数にとれる
        if(
            status === google.maps.places.PlacesServiceStatus.OK ||
            status === google.maps.places.PlacesServiceStatus.ZERO_RESULTS
        ){
            result!.forEach((element) => {
                const format =element.formatted_address!.slice(12)
                element.formatted_address = format
            })
            searchResult.value =result!
            emits('getdatas',searchResult.value)

            }        
        }
        
    )
        })
        .catch(
            () =>{

            })
    
        //検索値を表示
        searchPoint_w.value=searchPoint.value
        searchCondition_w.value=searchCondition.value
        //textを空に
        // searchPoint.value=""
        // searchCondition.value=""
    }



</script>

const loader = useGoogle()をget関数内に入れました。

インスタンスを取得する部分を関数内にいれることで、マウント時(templateがHTMLにコンパイルされるとき)にインスタンス化されないようにしました。

あとページ読み込み時にGoogleMapを表示したい場合はすべてにOnmountedをつけましょう。

<template>
<div>

    <div class="container mx-auto mb-10">
        <swiper-container
        navigation="true" 
        pagination="true" 
        scrollbar="true"        
        >
            <swiper-slide
            
            v-for="(photo, i) in spa.photos" :key="i" eager>
                <div>
                <a :href="spa.url" target="_blank"
            ><img class="object-cover h-80 lg:h-96 mx-auto w-full xl:max-w-screen-lg" :src="photo.getUrl()" /></a>
                </div>
            </swiper-slide>
       </swiper-container>
        
     <div class= "flex justify-center my-4" >
        <tbody class="border rounded-md border-gray-300 grid grid-cols-2 divide-y-2 divide-gray-300  [&>td]:p-2">
            
            <th class="border-t-2 border-gray-300">項目</th>
            <th>詳細</th> 
            
            <td>店名</td>
            <td>{{ spa.name }}</td>             
        
        
            <td>評価</td>
            <td>{{ spa.rating }}</td>  
        
            <td>住所</td>
            <td>{{ spa.vicinity }}</td>  
        
            <td>営業ステータス</td>
            <!-- //営業中か判定する -->
            <td v-if="spa.opening_hours?.isOpen()" class="text-green-600">
                〇営業中
            </td>  
            <td v-else class="text-red-500">
                ✖営業時間外
            </td>  
        
            <td>営業時間</td>
            <td>
                <div v-for="(opentime,index) in spa.opening_hours?.weekday_text"
                :key="opentime">
                {{ opentime }}
                </div>
            </td>             
        
        
            <td>WEbサイト</td>
            <td><a :href="spa.website" target="_blank">{{ spa.website }}</a></td>  
        
        </tbody>
        </div>

    </div>
        <swiper-container
        navigation="true" 
        pagination="true" 
        scrollbar="true"  
        >        
        <!-- <div class="flex justify-center "> -->
            <swiper-slider v-for="(review) in spa.reviews" class="mx-4 ">
                <div class="review_card rounded-md">
                    <img :src="review.profile_photo_url"  class="review_card_pic">
                    <p>{{ review.author_name }}</p>
                    <p>{{ review.text }}</p>
        
                </div>
            </swiper-slider>    
       </swiper-container>
  

        <h2 class="bg-slate-400">周辺マップ</h2>
        <div id='map' class="map_size">
            google map
        </div>
</div>       
</template>



<script lang="ts" setup>
//スライダー実装
import { register } from 'swiper/element/bundle';
register();




//ルートIDをURLより取得
const route = useRoute()
const queryPlaceID =route.params.id 
const{reviewer,setDialogData} = useDialog()

//spaで空のオブジェクトを宣言
const spa = ref<google.maps.places.PlaceResult>({})
console.log(spa.value)

onMounted(()=>{
    const loader = useGoogle()

    loader
    .load()
    .then((google)=>{

                //mapインスタンス作成 初期描画用
                const map = new google.maps.Map(document.getElementById('map') ,{
                    //初期表示設定
                    //適当な値で表示
                    zoom:17,
                    center: new google.maps.LatLng(0,0),
                    fullscreenControl:false,
                    mapTypeControl:false,
                    streetViewControl:true,
                    streetViewControlOptions:{
                        position:google.maps.ControlPosition.LEFT_BOTTOM
                    },
                    scaleControl:true
                })
                const service = new google.maps.places.PlacesService(map)

                //受け取ったパラメータを元にmap情報の詳細を検索
                //getDetails(A,B)
                //Aでリクエスト送る
                //Bで戻ってきた関数の実行
                service.getDetails({
                    placeId:queryPlaceID
                },
                
                    (place,status)=>{
                        if(status === google.maps.places.PlacesServiceStatus.OK){
                            spa.value = place
                            useHead({title: spa.value.name})
                            //位置情報を取得
                            map.setCenter(
                                new google.maps.LatLng(
                                place.geometry.location.lat(),
                                place.geometry.location.lng()
                                )
                            )
                            //マーカーオブジェクトをつける
                            new google.maps.Marker({
                                map,
                                position: new google.maps.LatLng(
                                    place.geometry.location.lat(),
                                    place.geometry.location.lng()
                                )
                            })
                        }
                    }
                )

            }
        )
        .catch(()=>{})
})
console.log(spa)
//レビューオブジェクトの型宣言関数
//実際は
function useDialog(){
    //
    const reviewer = reactive({name:'',src:'',content:''})
    const setDialogData = (name:string,src:string,content:string) => {
        reviewer.name = name
        reviewer.src = src
        reviewer.content = content
        activateDialog.value = true
    }
    return{reviewer,setDialogData}

}



</script>
<style scoped>
 略

</style>

参考記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

目次