1. 概要
この Codelab では、Google App Engine でウェブ フロントエンドを作成します。ウェブ フロントエンドを使用して、ウェブ アプリケーションから画像をアップロードしたり、アップロードされた写真とそのサムネイルを閲覧したりできます。
このウェブ アプリケーションでは、見栄えの良いユーザー インターフェースを実現する Bulma という CSS フレームワークと、構築するアプリケーションの API を呼び出す Vue.JS JavaScript フロントエンド フレームワークを使用します。
このアプリケーションは 3 つのタブで構成されます。
- アップロードされたすべての画像のサムネイルと、画像を説明するラベル(前のラボで Cloud Vision API によって検出されたラベル)のリストが表示されるホームページ。
- アップロードされた最新の 4 枚の写真から作成されたコラージュを表示するコラージュ ページ。
- ユーザーが新しい写真をアップロードできるアップロード ページ。
表示されるフロントエンドは次のようになります。
これら 3 つのページはシンプルな HTML ページです。
- ホームページ(
index.html
)は、Node の App Engine バックエンド コードを呼び出し、/api/pictures
URL への AJAX 呼び出しでサムネイル画像とそのラベルのリストを取得します。ホームページは、このデータを取得するために Vue.js を使用しています。 - コラージュ ページ(
collage.html
)は、最新の 4 枚の写真を合成したcollage.png
画像をポイントします。 - アップロード ページ(
upload.html
)には、/api/pictures
URL に POST リクエストを介して写真をアップロードするためのシンプルなフォームが用意されています。
学習内容
- App Engine
- Cloud Storage
- Cloud Firestore
2. 設定と要件
セルフペース型の環境設定
- Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。
- プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列で、いつでも更新できます。
- プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud Console により一意の文字列が自動生成されます(通常は内容を意識する必要はありません)。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常、プロジェクト ID は「
PROJECT_ID
」の形式です)。好みの文字列でない場合は、別のランダムな ID を生成するか、独自の ID を試用して利用可能であるかどうかを確認することができます。プロジェクトの作成後、ID は「フリーズ」されます。 - 3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
- 次に、Cloud のリソースや API を使用するために、Cloud Console で課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルを終了した後に課金が発生しないようにリソースをシャットダウンするには、Codelab の最後にある「クリーンアップ」の手順を行います。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。
Cloud Shell の起動
Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。
Google Cloud Console で、右上のツールバーにある Cloud Shell アイコンをクリックします。
プロビジョニングと環境への接続にはそれほど時間はかかりません。完了すると、次のように表示されます。
この仮想マシンには、必要な開発ツールがすべて用意されています。永続的なホーム ディレクトリが 5 GB 用意されており、Google Cloud で稼働します。そのため、ネットワークのパフォーマンスと認証機能が大幅に向上しています。このラボでの作業はすべて、ブラウザから実行できます。
3. API を有効にする
App Engine には Compute Engine API が必要です。有効になっていることを確認します。
gcloud services enable compute.googleapis.com
オペレーションが正常に完了したことがわかります。
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. コードのクローンを作成する
コードをチェックアウトします(まだ行っていない場合)。
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
次に、フロントエンドを含むディレクトリに移動します。
cd serverless-photosharing-workshop/frontend
フロントエンド用のファイル レイアウトは次のようになります。
frontend | ├── index.js ├── package.json ├── app.yaml | ├── public | ├── index.html ├── collage.html ├── upload.html | ├── app.js ├── script.js ├── style.css
プロジェクトのルートには、次の 3 つのファイルがあります。
index.js
には Node.js コードが含まれています。package.json
はライブラリの依存関係を定義します。app.yaml
は、Google App Engine の構成ファイルです。
public
フォルダには、静的リソースが含まれています。
index.html
は、すべてのサムネイル画像とラベルを表示するページです。collage.html
- 最近の写真のコラージュを表示しますupload.html
には、新しい写真をアップロードするためのフォームが含まれていますapp.js
が Vue.js を使用してindex.html
ページにデータを入力するscript.js
はナビゲーション メニューとその「ハンバーガー」を処理します。アイコン(小画面)style.css
はいくつかの CSS ディレクティブを定義します。
5. コードを探索する
依存関係
package.json
ファイルは、必要なライブラリ依存関係を定義します。
{
"name": "frontend",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@google-cloud/firestore": "^3.4.1",
"@google-cloud/storage": "^4.0.0",
"express": "^4.16.4",
"dayjs": "^1.8.22",
"bluebird": "^3.5.0",
"express-fileupload": "^1.1.6"
}
}
応募は以下に依存します。
- firestore: 画像メタデータを使用して Cloud Firestore にアクセスします。
- storage: 画像が保存されている Google Cloud Storage にアクセスするため。
- express: Node.js 用のウェブ フレームワーク。
- dayjs: 日付をわかりやすく表示する小さなライブラリです。
- bluebird: JavaScript Promise ライブラリ
- express-fileupload: ファイルのアップロードを簡単に処理するためのライブラリ。
Express フロントエンド
index.js
コントローラの先頭で、package.json
で定義したすべての依存関係が必要です。
const express = require('express');
const fileUpload = require('express-fileupload');
const Firestore = require('@google-cloud/firestore');
const Promise = require("bluebird");
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const path = require('path');
const dayjs = require('dayjs');
const relativeTime = require('dayjs/plugin/relativeTime')
dayjs.extend(relativeTime)
次に、Express アプリケーション インスタンスを作成します。
2 つの Express ミドルウェアが使用されます。
express.static()
呼び出しは、静的リソースがpublic
サブディレクトリで使用できることを示します。fileUpload()
は、インメモリ ファイル システムの/tmp
ディレクトリにファイルをローカルにアップロードするために、ファイルサイズを 10 MB に制限するようにファイルのアップロードを構成します。
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
limits: { fileSize: 10 * 1024 * 1024 },
useTempFiles : true,
tempFileDir : '/tmp/'
}))
静的リソースの中には、ホームページ、コラージュ ページ、アップロード ページの HTML ファイルがあります。これらのページで API バックエンドが呼び出されます。この API には次のエンドポイントがあります。
POST /api/pictures
画像は、upload.html のフォームから POST リクエストでアップロードされますGET /api/pictures
このエンドポイントは、画像とそのラベルのリストを含む JSON ドキュメントを返します。GET /api/pictures/:name
この URL は、フルサイズの画像のクラウド ストレージの場所にリダイレクトされますGET /api/thumbnails/:name
この URL はサムネイル画像のクラウド ストレージにリダイレクトされますGET /api/collage
この最後の URL は、生成されたコラージュ画像のクラウド ストレージの場所にリダイレクトされます
画像アップロード
画像アップロード用の Node.js コードを確認する前に、public/upload.html
を簡単に見てみましょう。
...
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
...
<input type="file" name="pictures">
<button>Submit</button>
...
</form>
...
フォーム要素は、HTTP POST メソッドとマルチパート形式で /api/pictures
エンドポイントを指定します。index.js
はそのエンドポイントとメソッドに応答し、ファイルを抽出する必要があります。
app.post('/api/pictures', async (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
console.log("No file uploaded");
return res.status(400).send('No file was uploaded.');
}
console.log(`Receiving files ${JSON.stringify(req.files.pictures)}`);
const pics = Array.isArray(req.files.pictures) ? req.files.pictures : [req.files.pictures];
pics.forEach(async (pic) => {
console.log('Storing file', pic.name);
const newPicture = path.resolve('/tmp', pic.name);
await pic.mv(newPicture);
const pictureBucket = storage.bucket(process.env.BUCKET_PICTURES);
await pictureBucket.upload(newPicture, { resumable: false });
});
res.redirect('/');
});
まず、アップロードされているファイルが実際にあることを確認します。次に、ファイル アップロード ノード モジュールから mv
メソッドを使用して、ファイルをローカルにダウンロードします。ファイルがローカル ファイル システムで利用可能になったので、画像を Cloud Storage バケットにアップロードします。最後に、ユーザーをアプリケーションのメイン画面にリダイレクトします。
画像を一覧表示する
美しい写真を飾ろう!
/api/pictures
ハンドラで、Firestore データベースの pictures
コレクションを調べて、(サムネイルが生成された)すべての画像を、作成日の降順で並べ替えて取得します。
各画像を JavaScript 配列に push します。名前、画像を説明するラベル(Cloud Vision API から取得)、ドミナント カラー、わかりやすい作成日(dayjs
を使用すると、「3 日後から 3 日後」のような相対的な時間オフセットを指定します)。
app.get('/api/pictures', async (req, res) => {
console.log('Retrieving list of pictures');
const thumbnails = [];
const pictureStore = new Firestore().collection('pictures');
const snapshot = await pictureStore
.where('thumbnail', '==', true)
.orderBy('created', 'desc').get();
if (snapshot.empty) {
console.log('No pictures found');
} else {
snapshot.forEach(doc => {
const pic = doc.data();
thumbnails.push({
name: doc.id,
labels: pic.labels,
color: pic.color,
created: dayjs(pic.created.toDate()).fromNow()
});
});
}
console.table(thumbnails);
res.send(thumbnails);
});
このコントローラは、次のシェイプの結果を返します。
[
{
"name": "IMG_20180423_163745.jpg",
"labels": [
"Dish",
"Food",
"Cuisine",
"Ingredient",
"Orange chicken",
"Produce",
"Meat",
"Staple food"
],
"color": "#e78012",
"created": "a day ago"
},
...
]
このデータ構造は、index.html
ページの小さな Vue.js スニペットによって使用されます。このページのマークアップを簡略化すると、次のようになります。
<div id="app">
<div class="container" id="app">
<div id="picture-grid">
<div class="card" v-for="pic in pictures">
<div class="card-content">
<div class="content">
<div class="image-border" :style="{ 'border-color': pic.color }">
<a :href="'/api/pictures/' + pic.name">
<img :src="'/api/thumbnails/' + pic.name">
</a>
</div>
<a class="panel-block" v-for="label in pic.labels" :href="'/?q=' + label">
<span class="panel-icon">
<i class="fas fa-bookmark"></i>
</span>
{{ label }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
div の ID は、それが動的にレンダリングされるマークアップの一部であることを Vue.js に示すものです。反復処理は、v-for
ディレクティブにより行われます。
画像には、Cloud Vision API によって検出されるとおり、画像のドミナント カラーに対応する適切な色の枠線が付けられます。また、リンクと画像ソースのサムネイルと全幅サイズの画像をポイントします。
最後に、画像を説明するラベルをリストアップします。
以下は、Vue.js スニペットの JavaScript コードです(index.html
ページの最後にインポートされた public/app.js
ファイル内)。
var app = new Vue({
el: '#app',
data() {
return { pictures: [] }
},
mounted() {
axios
.get('/api/pictures')
.then(response => { this.pictures = response.data })
}
})
Vue コードは Axios ライブラリを使用して、/api/pictures
エンドポイントに対して AJAX 呼び出しを行います。返されたデータは、先ほどマークアップしたビューコードにバインドされます。
画像の表示
index.html
から写真のサムネイルを表示し、クリックするとフルサイズの画像を表示できます。collage.html
では collage.png
画像が表示されます。
これらのページの HTML マークアップでは、画像 src
とリンク href
がこの 3 つのエンドポイントを指し、画像、サムネイル、コラージュの Cloud Storage の場所にリダイレクトされます。HTML マークアップにパスをハードコードする必要はありません。
app.get('/api/pictures/:name', async (req, res) => {
res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_PICTURES}/${req.params.name}`);
});
app.get('/api/thumbnails/:name', async (req, res) => {
res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/${req.params.name}`);
});
app.get('/api/collage', async (req, res) => {
res.redirect(`https://storage.cloud.google.com/${process.env.BUCKET_THUMBNAILS}/collage.png`);
});
Node アプリケーションの実行
すべてのエンドポイントが定義され、Node.js アプリケーションを起動する準備が整いました。Express アプリケーションはデフォルトでポート 8080 をリッスンし、受信リクエストを処理する準備が整っています。
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Started web frontend service on port ${PORT}`);
console.log(`- Pictures bucket = ${process.env.BUCKET_PICTURES}`);
console.log(`- Thumbnails bucket = ${process.env.BUCKET_THUMBNAILS}`);
});
6. ローカルでテストする
コードをローカルでテストして、クラウドにデプロイする前に機能することを確認する。
2 つの Cloud Storage バケットに対応する 2 つの環境変数をエクスポートする必要があります。
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT} export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
frontend
フォルダ内で npm の依存関係をインストールし、サーバーを起動します。
npm install; npm start
問題がなければ、サーバーがポート 8080 で起動します。
Started web frontend service on port 8080 - Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT} - Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}
これらのログにはバケットの本名が表示されます。これはデバッグに役立ちます。
Cloud Shell からウェブ プレビュー機能を使用して、ローカルで実行されているアプリケーションをブラウザできます。
CTRL-C
を使用して終了します。
7. App Engine にデプロイする
アプリケーションをデプロイする準備が整いました。
App Engine を構成する
App Engine の app.yaml
構成ファイルを調べます。
runtime: nodejs16 env_variables: BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT
最初の行は、ランタイムが Node.js 10 に基づくことを宣言しています。元の画像とサムネイルの 2 つのバケットを指すように、2 つの環境変数が定義されています。
GOOGLE_CLOUD_PROJECT
を実際のプロジェクト ID に置き換えるには、次のコマンドを実行します。
sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml
導入
App Engine の優先リージョンを設定します。必ず前回のラボと同じリージョンを使用してください。
gcloud config set compute/region europe-west1
デプロイ:
gcloud app deploy
1 ~ 2 分後、アプリケーションがトラフィックを処理していることが通知されます。
Beginning deployment of service [default]... ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 8 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...done. Setting traffic split for service [default]...done. Deployed service [default] to [https://GOOGLE_CLOUD_PROJECT.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s default To view your application in the web browser run: $ gcloud app browse
また、Cloud コンソールの [App Engine] セクションに移動して、アプリがデプロイされていることを確認したり、バージョニングやトラフィック分割などの App Engine の機能を確認したりすることもできます。
8. アプリをテストする
テストするには、アプリ(https://<YOUR_PROJECT_ID>.appspot.com/
)アプリのデフォルトの App Engine URL に移動すると、フロントエンド UI が稼働しているのを確認できます。
9. クリーンアップ(省略可)
アプリを残すつもりがない場合は、プロジェクト全体を削除することで、リソースをクリーンアップすることで費用を節約し、クラウド全般的に正しい行動を取ることができます。
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
10. 完了
これで、App Engine でホストされているこの Node.js ウェブ アプリケーションは、すべてのサービスをバインドします。ユーザーは、画像をアップロードして可視化できます。
学習した内容
- App Engine
- Cloud Storage
- Cloud Firestore