Goでwebアプリケーションを作成してみよう！　第3回　~フロントエンドの実装~

 この記事では5回に分けてwebアプリケーションの作り方を説明していく
第1回
　[webアプリケーションの構成、仕組み](https://qiita.com/Yutosaki/items/0c6f9349b427d4aee08e "第３回")
 第2回
 　[サーバー立ち上げ](https://qiita.com/Yutosaki/items/fcd773a278e9b8663f36 "第２回")
 第3回
 　フロントエンドの実装(この記事)
 第4回
　 [バックエンドの実装](https://qiita.com/Yutosaki/items/da564b74d6fac87aa601 "第4回")
 第5回
 メニュー画像の読み取り

 実際に作ったアプリケーション

 https://github.com/Yutosaki/WiFiRader

 # やること
以下のような画面構成にするための JavaScript を書く。また、Go で作成するアプリケーションサーバーに HTTP リクエストを送る。

![今回作成した画像](https://github.com/Yutosaki/WiFiRader/assets/129819607/ace28799-3184-4718-8f1d-dc611beeeb6e "今回作成した画像")

まずは見た目の部分を行っていく。正確に言うと、サイドバーの処理とマップの処理とエラーハンドリングだ。その後、現在地と希望金額を送るための処理を書いていく。

## 地図の初期化と現在地の表示
画面に地図と現在地を表示するコードを書いていく。
```js:現在地とエラーハンドリング
document.addEventListener('DOMContentLoaded', function() {
    initMap();
});

function initMap() {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function(position) {
            var pos = {
                lat: position.coords.latitude,
                lng: position.coords.longitude
            };

            var map = new google.maps.Map(document.getElementById('map'), {
                center: pos,
                zoom: 15
            });

            new google.maps.Marker({
                position: pos,
                map: map,
                title: '現在地'
            });
        }, function() {
            handleLocationError(true, { lat:35.681236, lng: 139.767125 });//東京駅
        });
    } else {
        handleLocationError(false, { lat:35.681236, lng: 139.767125 });
    }
}

function handleLocationError(browserHasGeolocation, pos) {
    console.log(browserHasGeolocation ?
        'Error: The Geolocation service failed.' :
        'Error: Your browser does not support geolocation.');
    var map = new google.maps.Map(document.getElementById('map'), {
        center: pos,
        zoom: 15
    });
    new google.maps.Marker({
        position: pos,
        map: map,
        title: 'Default Location'
    });
}
```
簡単に上から順に見ていく
```js
document.addEventListener('DOMContentLoaded', function() {
    initMap();
});
```
これにより、[DOM](https://udemy.benesse.co.jp/development/dom.html "DOM")が完全に読み込まれた後に関数を実行するようにしてある。
DOM とは、簡単に言うとHTMLやXMLドキュメントの構造をオブジェクトとして表現したもの。Webページを構成する各要素（例えば、テキスト、画像、リンクなど）は、DOMにおいてオブジェクトとして扱われ、これにより、**JavaScript**を使用して動的にページの内容を変更したり、ユーザーの操作に応じてページの挙動を制御したりすることが可能になる

```js
if (navigator.geolocation)
```
ここで、ブラウザがジオロケーションライブラリをサポートしているかを確かめる。これを有効にするために Google Maps JavaScript API が必要になる。そのため、前回の server.js で API キーを環境変数から読み込んで有効にしている。

その下の var 部分で現在地の緯度、経度のを pos に代入したりマークしたりしている。

```js:エラーハンドリング
function() {
            handleLocationError(true, { lat:35.681236, lng: 139.767125 });//東京駅
        }
```
ここでは、ブラウザはジオロケーションライブラリをサポートしているが、なんらかの理由で現在地を取得できなかったときに行われるエラー処理だ。

一つ目の引数は、ジオロケーションをサポートしているかどうかを示す
二つ目の引数は、デフォルトの位置を示すオブジェクト。このオブジェクトは、latとlngの2つのプロパティを持ち、それぞれ緯度と経度を示す

```js:エラーハンドリング
else {
        handleLocationError(false, { lat:35.681236, lng: 139.767125 });
    }
```
これはそもそもジオロケーションライブラリをサポートしていなかった場合の処理だ。これもデフォルトの位置を東京駅にしてある。

```js:エラーハンドリングの関数
function handleLocationError(browserHasGeolocation, pos) {
    console.log(browserHasGeolocation ?
        'Error: The Geolocation service failed.' :
        'Error: Your browser does not support geolocation.');
    var map = new google.maps.Map(document.getElementById('map'), {
        center: pos,
        zoom: 15
    });
    new google.maps.Marker({
        position: pos,
        map: map,
        title: 'Default Location'
    });
}
```

エラーが起こった場合はこの関数で処理する。ジオロケーションをサポートしているかどうかでメッセージの内容が変わる。その後の処理は同じで、現在地にマッピングをしている。

これでマップを初期化して現在地を表示できた。

## サイドバーの表示

ここは簡潔に済ます。
```js:サイドバー
document.addEventListener('DOMContentLoaded', function() {
    const toggleButton = document.getElementById('toggleButton');
    const sidebar = document.getElementById('sidebar');
    let isSidebarOpen = true;

    toggleButton.addEventListener('click', function() {
        if (isSidebarOpen) {
            sidebar.style.transform = 'translateX(-100%)'; // サイドバーを左に隠す
        } else {
            sidebar.style.transform = 'translateX(0)'; // サイドバーを表示
        }
        isSidebarOpen = !isSidebarOpen;
    });

    initMap();//現在地の読み込み
});
```
toggleButtonとは、サイドバーを左右に出し引きするためのボタンである。それを押すと左右にサイドバーが移動する設定をしている。

次に現在地と希望金額を Go で書かれたアプリケーションサーバーに送るためのコードを書いていく。

## 現在地と希望金額を送る
今回だと送信ボタンを押したらアプリケーションサーバーに情報が送られる仕組みになっている。
```js:server.js(前回記載の通り)
require('dotenv').config();
const express = require('express');
const app = express();
const port = 3000;
const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY;

const cors = require('cors');
app.use(express.static('.'));
app.use(cors());

app.get('/', (_, res) => {
    res.send(`
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>Wifi Radar</title>
        <link rel="stylesheet" href="/app.css">
        <script async src="https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&language=ja&libraries=geometry"></script>
        <script src="/app.js" defer></script>
    </head>
    <body>
        <div id="container">
            <div id="sidebar">
                <button id="toggleButton">⇄</button>
                <input type="number" id="desiredAmount" placeholder="希望金額を入力" />
                <button onclick="submitLocationAndAmount()">送信</button>
                <iframe id="placeIframe"></iframe>
            </div>
            <div id="map"></div>
        </div>
    </body>
    </html>
    `);
});

const startServer = async () => {
    app.listen(port, async () => {
        console.log(`Server running on http://localhost:${port}`);
        try {
            const open = (await import('open')).default;
            open(`http://localhost:${port}`);
        } catch (error) {
            console.error("Failed to open browser:", error);
        }
    });
};

startServer();
```

よって今回は
```js
submitLocationAndAmount()
```
という関数を書いていく。

まずはブラウザが geolocation をサポートしているかを確認する。
```js
if (!navigator.geolocation) {
    console.error("Geolocation is not supported by your browser.");
    return;
}
```

次に入力された金額が正しいか確認していく。今回は "desiredAmount" というタグのIDを参照してそこから希望金額を取得する
```js
const amountInput = document.getElementById('desiredAmount').value;
const desiredAmount = parseInt(amountInput, 10);
if (isNaN(desiredAmount)) {  // 数値変換が正しく行われたかチェック
    alert("金額を数値で入力してください。");
    return;
}
```
この時、数字が10進数にして正しく変換できているかを確かめる。もし数字じゃなかった場合(全角数字を含む)は警告を出すようにしている。


その後、現在地の取得とアプリケーションサーバーへのリクエストを行っていく。この時送るのは JSON 形式にしたposオブジェクト（ユーザーの位置情報）とdesiredAmount（ユーザーが入力した金額）である。ここがメインどころだ。返ってきたデータの処理も含めて書く。

先にコードを見せてそれを解説していく。
```js:現在地の取得とサーバーへのリクエストとレスポンスの処理
navigator.geolocation.getCurrentPosition(position => {
        const pos = {
            latitude: position.coords.latitude,
            longitude: position.coords.longitude
        };
        console.log("Position data:", pos);

        fetch('http://localhost:8080/submit-location', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ pos: pos, desiredAmount: desiredAmount})
        })
        .then(response => response.json())
        .then(data => {
            if (data !== null) {
                console.log('Number of places received:', data.length);
                console.log("Position data:", pos);
                if (data.length > 0) {
                    addMarkerAndUrl(data, pos);
                } else {
                    console.error('No valid locations received:', data);
                    alert('カフェの情報が見つかりませんでした。');
                }
            } else {
                console.error('No valid locations received:', data);
                alert('カフェの情報が見つかりませんでした。');
            }
        })
        .catch((error) => {
            console.error('Error:', error);
        });
    }, () => {
        alert("現在地の取得に失敗しました。");
    });
```
今回も非同期処理を用いて書く。現在地の取得等は時間のかかる処理であり、それらの処理をしている間にブラウザがフリーズしたり、応答不能にならないようにするため(ブラウザの主要なフローをブロックしないようにするため)に非同期処理で、コールバック関数を用いて書いた。

まずは pos というオブジェクト変数に現在地の緯度と軽度を代入していく。取得した位置情報を確認のためにコンソール画面に出力する。

次にアプリケーションサーバーへのリクエストをする。
```js:リクエスト
fetch('http://localhost:8080/submit-location', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ pos: pos, desiredAmount: desiredAmount})
        })
```

ここでは fetch API を使用して Go で書かれたアプリケーションサーバーへリクエストを送る。
fetch API とそれぞれについて説明していく。

```js:fetch API
fetch(url, options)
  .then(response => response.json()) // レスポンスをJSONとして解析
  .then(data => console.log(data)) // データを扱う処理
  .catch(error => console.error('Error:', error)); // エラーハンドリング
```
url : リクエストを送る先のURL。
options : リクエストのタイプ、ヘッダー、ボディなどを設定するオブジェクト。

次に URL についての詳しい設定と options オブジェクトの中身の設定について見ていく

**URL** : http://localhost:8080/submit-location はリクエストを送る先のサーバーアドレスとエンドポイント。ここでは8080ポートを使用している。
エンドポイントとは、ネットワーク上でサービスが提供される特定のURLのこと。このURLの中でも二つの部品に分かれており、
ベースURL : API の基本アドレスで、すべてのエンドポイントで共通。今回だと、 http://localhost:8080 がここにあたる。
パス　：　ベースURLに続く部分で、アクセスするリソースを指定する。例えば、users、products、articlesなどが一般的なパス。今回だと submit-location がそれにあたる。ここに以下の method を指定してアクセスする。
**method** : HTTPメソッドは、クライアントがWebサーバーに対して何をしたいかを指定するために使用されるもの。それぞれのメソッドは異なる種類のアクションを示し、APIやWebサービスにおいてリソースの操作に用いられる。以下は主なメソッド。
* GET : リソースを取得する際に使用される。ページの表示、クエリに基づく情報の検索などに使用
* POST : 新しいリソースを作成するために使用される。新しいユーザーの追加やコメントの投稿などで使用
* PUT : 既存のリソースを更新するために使用される。ユーザープロファイルの更新、設定の変更などで使用
* DELETE :指定されたリソースを削除するために使用される。アカウントの削除、記事の削除などで使用

今回は現在地や希望金額を送るために使用するので POST に設定

**headers** : HTTPヘッダーは、HTTPリクエストやレスポンスのメタデータを含むキーとバリューのペア。これによって、クライアントとサーバー間で追加情報が伝達される。fetch APIでは、リクエストを送信する際に必要なヘッダーを指定することができる。

今回は、Content-Type を JSON（JavaScript Object Notation） に指定した。これによってリクエストのボディのタイプを決めることができる。JSON 形式はデータ交換のための軽量なデータ形式であり、人間にとって読み書きが容易で、マシンにとっても簡単に解析・生成できる。多くのプログラミング言語でサポートされているためよく使われる

つまり今回は headers で JSON 形式でやりとりすることを宣言して、サーバーに対して送信する形式が JSON 形式であることを伝える。その後、body を実際に JSON.stringify 関数を使用してJavaScriptのオブジェクト（{ pos: pos, desiredAmount: desiredAmount}）をJSON形式の文字列に変換している。ここで : の左側にある pos と　deseiredAmount はキー（プロパティ名）であり、右側の pos や desiredAmount が実際の値である

これにてリクエストの処理は完了したので、次は返された結果をうまく処理していく

## レスポンスの処理
バックエンドの処理は次回以降で書いていくが、先にレスポンスが返ってきた後の処理も全て書いてしまう。
返却される値は JOSN 形式のデータで、中には店名と緯度、経度そして、その店のホームページの URL の情報が入ったデータが返却されるとする。

```js:レスポンス処理
        fetch('http://localhost:8080/submit-location', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ pos: pos, desiredAmount: desiredAmount})
        })
        .then(response => response.json())
        .then(data => {
            if (data !== null) {
                console.log('Number of places received:', data.length);
                console.log("Position data:", pos);
                if (data.length > 0) {
                    addMarkerAndUrl(data, pos);
                } else {
                    console.error('No valid locations received:', data);
                    alert('カフェの情報が見つかりませんでした。');
                }
            } else {
                console.error('No valid locations received:', data);
                alert('カフェの情報が見つかりませんでした。');
            }
        })
        .catch((error) => {
            console.error('Error:', error);
        });
    }, () => {
        alert("現在地の取得に失敗しました。");
    });
}
```

それを .then 以降で受け取って処理している。
```js:デコード
.then(response => response.json())
```
ここで受け取った JSON 形式のデータを　JavaScript のデータ型にデコードしている。
JavaScriptの型に直したデータ型はこのような形になる
```js:data
{name: 'Working cafe halo', 
url: 'http://www.cafehalo.net/',
Latitude: 35.6519254,
Longitude: 139.6463515}
```

そして
```js
.then(data => {
    ...
```
以下でエラーハンドリングをしている。
エラーハンドリングの内容としては data が null じゃないかどうかや、返ってきたデータにちゃんと中身があるかどうかなどを調べている。また、.catch((error) => {...}): このブロックで、リクエストの送信やレスポンスの処理中に発生したエラーを捕捉している。

そして、もしきちんとデータが返ってきていた場合は addMarkerAndUrlという関数にデータを飛ばして、実際の画面に表示させる

## 店舗に印を付け、クリックしたら URL が表示されるようにする

```js
function addMarkerAndUrl(places, pos) 
```
このような関数を作っていく。places には data が引き渡される

```js:mapの初期化
const map = new google.maps.Map(document.getElementById('map'), {
        center: { lat: pos.latitude, lng: pos.longitude },
        zoom: 10
    });

    // 現在地のマーカー
    new google.maps.Marker({
        position: { lat: pos.latitude, lng: pos.longitude },
        map: map,
        icon: {
            path: google.maps.SymbolPath.CIRCLE,
            scale: 8,
            fillColor: 'blue',
            fillOpacity: 0.6,
            strokeColor: 'white',
            strokeWeight: 2
        },
        title: '現在地'
    });
```

ここでは、現在地の設定やマーカーを青丸に設定している

```js:最寄りの店を検索
    let nearestPlace = null;
    let shortestDistance = Infinity;

    places.forEach(place => {
        if (!place.url) {
            return;
        }
        const placePos = { lat: place.Latitude, lng: place.Longitude };
            const distance = google.maps.geometry.spherical.computeDistanceBetween(
            { lat: pos.latitude, lng: pos.longitude },
            placePos
        );

        if (distance < shortestDistance) {
            shortestDistance = distance;
            nearestPlace = place;
        }
    });
```

各店の緯度経度から、現在地から最も近い場所を探している。最初に Infinity として最短距離を設定し、それ以下の距離の場所があれば更新していく。

```js:最寄り店舗までのルート表示
 var directionsService = new google.maps.DirectionsService();
     var directionsRenderer = new google.maps.DirectionsRenderer({
        suppressMarkers: true  // マーカーを非表示にする
    });
     directionsRenderer.setMap(map);
 
     var request = {
         origin: new google.maps.LatLng(pos.latitude, pos.longitude),
         destination: new google.maps.LatLng(nearestPlace.Latitude, nearestPlace.Longitude),
         travelMode: 'WALKING'
     };
     directionsService.route(request, function(result, status) {
         if (status == 'OK') {
             directionsRenderer.setDirections(result);
         }
     }); 
```

ここでは最寄り店舗までのルート表示するオプションなどの設定などをしている。
```go
var directionsRenderer = new google.maps.DirectionsRenderer({
        suppressMarkers: true  // マーカーを非表示にする
    });
```
ここではデフォルトのマーカーを非表示にしている。この後のコードで、一番近い場所にはデフォルトの赤ピンを立て、それ以外の場所には小さな赤ピンを立てているが、ここのデフォルトのマーカーはが、赤い円をルートの行き先に勝手に立ててしまうのでそれを非表示にしている

その後リクエスト（どのようにその場所へ行くのか、WALk,CARなど）を決め、もし特に異常がなければのルートを描く


```js:各店舗にピンを立て、URL情報を表示する
places.forEach(place => {
        if (!place.url) {
            return;
        }
        const placePos = { lat: place.Latitude, lng: place.Longitude };
        const marker = new google.maps.Marker({
            position: placePos,
            map: map,
            icon: nearestPlace === place ? '' : 'http://maps.google.com/mapfiles/ms/icons/red-dot.png',
            title: place.name
        });

        const infowindow = new google.maps.InfoWindow({
            content: `<a href="${place.url}" target="_blank">${place.name}</a>`
        });

        marker.addListener('click', function() {
            infowindow.open(map, marker);
        });

        // サイドバーに最も近い場所の情報を表示
        if (nearestPlace === place) {
            console.log('Nearest place:', place);
            const placeIframe = document.getElementById('placeIframe');
            placeIframe.src = place.url ? place.url : "URLを取得できませんでした"; 
        }
    });
```

ここでは再度全ての店舗について見ていき、もし最寄りの店舗ならばデフォルトのマーカーを、そうでなければ別のマーカーを立てるようにしている。また、そのマーカーをクリックしたら店舗のURLが出てくるように infowindow を設定している。

これにて、フロント側の全てのコードの説明を終える

## 最後に
大事なこととしてはまずは設計をきちんとやることだと感じた。技術構成や、どのようなデータ型を扱うかなどもきちんと決めるべきだと思う。画面上での動き、などは後からでも割とどうにでもなるので、拡張性の高いデータ型だったりを最初に考えると良いと思う。