Pic-a-daily: ラボ 4 - ウェブ フロントエンドを作成する

1. 概要

この Codelab では、ユーザーがウェブ アプリケーションからアップロードした写真とサムネイルを参照できるようにするウェブ フロントエンドを Google App Engine 上に作成します。

21741cd63b425aeb.png

このウェブ アプリケーションでは、見栄えの良いユーザー インターフェースを実現するために Bulma という CSS フレームワークを使用します。また、構築するアプリケーションの API を呼び出すために Vue.JS という JavaScript フロントエンド フレームワークも使用します。

このアプリケーションは次の 3 つのタブで構成されます。

  • アップロードされたすべての画像のサムネイルと、画像の説明ラベルのリスト(前のラボで Cloud Vision API によって検出されたもの)を表示するホームページ
  • アップロードされた最新の 4 枚の写真で作成されたコラージュを表示するコラージュ ページ。
  • ユーザーが新しい写真をアップロードできるアップロード ページ。

結果のフロントエンドは次のようになります。

6a4d5e5603ba4b73.png

これらの 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. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列で、いつでも更新できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud Console により一意の文字列が自動生成されます(通常は内容を意識する必要はありません)。ほとんどの Codelab では、プロジェクト ID を参照する必要があります(通常、プロジェクト ID は「PROJECT_ID」の形式です)。好みの文字列でない場合は、別のランダムな ID を生成するか、独自の ID を試用して利用可能であるかどうかを確認することができます。プロジェクトの作成後、ID は「フリーズ」されます。
  • 3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud Console で課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルを終了した後に課金が発生しないようにリソースをシャットダウンするには、Codelab の最後にある「クリーンアップ」の手順を行います。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell の起動

Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。

Google Cloud Console で、右上のツールバーにある Cloud Shell アイコンをクリックします。

55efc1aaa7a4d3ad.png

プロビジョニングと環境への接続にはそれほど時間はかかりません。完了すると、次のように表示されます。

7ffe5cbb04455448.png

この仮想マシンには、必要な開発ツールがすべて用意されています。永続的なホーム ディレクトリが 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() は、ファイルサイズを 10 MB に制限し、/tmp ディレクトリのインメモリ ファイル システムにファイルをローカルでアップロードするようにファイル アップロードを構成します。
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('/');
});

まず、ファイルが実際にアップロードされていることを確認します。次に、ファイル アップロード Node モジュールから提供される mv メソッドを使用して、ファイルをローカルにダウンロードします。ファイルがローカル ファイル システムで使用可能になったので、写真を Cloud Storage バケットにアップロードします。最後に、ユーザーをアプリケーションのメイン画面に戻します。

写真の一覧表示

美しい写真を飾る時間です。

/api/pictures ハンドラでは、Firestore データベースの pictures コレクションを調べて、作成日の降順で並べられたすべての写真(サムネイルが生成されたもの)を取得します。

各写真は、その名前、説明ラベル(Cloud Vision API から取得)、主な色、作成日(dayjs を使用して「3 日後」などの相対的な時間オフセット)とともに JavaScript 配列にプッシュされます。

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> &nbsp;
                                                        </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 からウェブ プレビュー機能を使用して、ローカルで実行されているアプリケーションをブラウジングできます。

82fa3266d48c0d0a.png

終了するには 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 Console の App Engine セクションにアクセスして、アプリがデプロイされていることを確認し、バージョニングやトラフィック分割などの App Engine の機能を試すこともできます。

db0e196b00fceab1.png

8. アプリをテストする

テストするには、アプリ(https://<YOUR_PROJECT_ID>.appspot.com/)のデフォルトの App Engine URL にアクセスします。フロントエンド UI が起動して実行されていることを確認できます。

6a4d5e5603ba4b73.png

9. クリーンアップ(省略可)

アプリを保持しない場合は、プロジェクト全体を削除してリソースをクリーンアップし、費用を節約してクラウドのマナーを守りましょう。

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. 完了

おめでとうございます!App Engine でホストされているこの Node.js ウェブ アプリケーションは、すべてのサービスをバインドし、ユーザーが写真をアップロードして視覚化できるようにします。

学習した内容

  • App Engine
  • Cloud Storage
  • Cloud Firestore

次のステップ