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() は、インメモリ ファイル システムの /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> &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 コンソールの [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

次のステップ