LoginSignup
0
0

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

Posted at

この記事では5回に分けてwebアプリケーションの作り方を説明していく
第1回
 webアプリケーションの構成、仕組み
第2回
 サーバー立ち上げ
第3回
 フロントエンドの実装(この記事)
第4回
  バックエンドの実装
第5回
メニュー画像の読み取り

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

やること

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

今回作成した画像

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

地図の初期化と現在地の表示

画面に地図と現在地を表示するコードを書いていく。

現在地とエラーハンドリング
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'
    });
}

簡単に上から順に見ていく

document.addEventListener('DOMContentLoaded', function() {
    initMap();
});

これにより、DOMが完全に読み込まれた後に関数を実行するようにしてある。
DOM とは、簡単に言うとHTMLやXMLドキュメントの構造をオブジェクトとして表現したもの。Webページを構成する各要素(例えば、テキスト、画像、リンクなど)は、DOMにおいてオブジェクトとして扱われ、これにより、JavaScriptを使用して動的にページの内容を変更したり、ユーザーの操作に応じてページの挙動を制御したりすることが可能になる

if (navigator.geolocation)

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

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

エラーハンドリング
function() {
            handleLocationError(true, { lat:35.681236, lng: 139.767125 });//東京駅
        }

ここでは、ブラウザはジオロケーションライブラリをサポートしているが、なんらかの理由で現在地を取得できなかったときに行われるエラー処理だ。

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

エラーハンドリング
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'
    });
}

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

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

サイドバーの表示

ここは簡潔に済ます。

サイドバー
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 で書かれたアプリケーションサーバーに送るためのコードを書いていく。

現在地と希望金額を送る

今回だと送信ボタンを押したらアプリケーションサーバーに情報が送られる仕組みになっている。

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();

よって今回は

submitLocationAndAmount()

という関数を書いていく。

まずはブラウザが geolocation をサポートしているかを確認する。

if (!navigator.geolocation) {
    console.error("Geolocation is not supported by your browser.");
    return;
}

次に入力された金額が正しいか確認していく。今回は "desiredAmount" というタグのIDを参照してそこから希望金額を取得する

const amountInput = document.getElementById('desiredAmount').value;
const desiredAmount = parseInt(amountInput, 10);
if (isNaN(desiredAmount)) {  // 数値変換が正しく行われたかチェック
    alert("金額を数値で入力してください。");
    return;
}

この時、数字が10進数にして正しく変換できているかを確かめる。もし数字じゃなかった場合(全角数字を含む)は警告を出すようにしている。

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

先にコードを見せてそれを解説していく。

現在地の取得とサーバーへのリクエストとレスポンスの処理
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 というオブジェクト変数に現在地の緯度と軽度を代入していく。取得した位置情報を確認のためにコンソール画面に出力する。

次にアプリケーションサーバーへのリクエストをする。

リクエスト
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 とそれぞれについて説明していく。

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 の情報が入ったデータが返却されるとする。

レスポンス処理
        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 以降で受け取って処理している。

デコード
.then(response => response.json())

ここで受け取った JSON 形式のデータを JavaScript のデータ型にデコードしている。
JavaScriptの型に直したデータ型はこのような形になる

data
{name: 'Working cafe halo', 
url: 'http://www.cafehalo.net/',
Latitude: 35.6519254,
Longitude: 139.6463515}

そして

.then(data => {
    ...

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

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

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

function addMarkerAndUrl(places, pos) 

このような関数を作っていく。places には data が引き渡される

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: '現在地'
    });

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

最寄りの店を検索
    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 として最短距離を設定し、それ以下の距離の場所があれば更新していく。

最寄り店舗までのルート表示
 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);
         }
     }); 

ここでは最寄り店舗までのルート表示するオプションなどの設定などをしている。

var directionsRenderer = new google.maps.DirectionsRenderer({
        suppressMarkers: true  // マーカーを非表示にする
    });

ここではデフォルトのマーカーを非表示にしている。この後のコードで、一番近い場所にはデフォルトの赤ピンを立て、それ以外の場所には小さな赤ピンを立てているが、ここのデフォルトのマーカーはが、赤い円をルートの行き先に勝手に立ててしまうのでそれを非表示にしている

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

各店舗にピンを立て、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 を設定している。

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

最後に

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0