この記事では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 で書かれたアプリケーションサーバーに送るためのコードを書いていく。
現在地と希望金額を送る
今回だと送信ボタンを押したらアプリケーションサーバーに情報が送られる仕組みになっている。
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(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の型に直したデータ型はこのような形になる
{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 が引き渡される
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など)を決め、もし特に異常がなければのルートを描く
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 を設定している。
これにて、フロント側の全てのコードの説明を終える
最後に
大事なこととしてはまずは設計をきちんとやることだと感じた。技術構成や、どのようなデータ型を扱うかなどもきちんと決めるべきだと思う。画面上での動き、などは後からでも割とどうにでもなるので、拡張性の高いデータ型だったりを最初に考えると良いと思う。