每一天:研究室 4:建立網路前端

1. 總覽

在本程式碼研究室中,您會在 Google App Engine 中建立網路前端,讓使用者從網頁應用程式上傳圖片,並瀏覽上傳的圖片及其縮圖。

21741cd63b425aeb.png

此網頁應用程式將使用名為 Bulma 的 CSS 架構,擁有良好的使用者介面,以及 Vue.JS JavaScript 前端架構,用來呼叫您將建構的應用程式 API。

這個應用程式包含三個分頁:

  • 首頁會顯示所有上傳圖片的縮圖,以及描述圖片的標籤清單 (先前研究室中 Cloud Vision API 偵測到的圖片)。
  • 美術拼貼頁面會顯示最近上傳的 4 張相片組成的美術拼貼。
  • 「上傳」網頁:使用者可以上傳新圖片。

產生的前端如下所示:

6a4d5e5603ba4b73.png

這 3 個頁面是簡易的 HTML 網頁:

  • 首頁 (index.html) 會透過對 /api/pictures 網址的 AJAX 呼叫來呼叫 Node App Engine 後端程式碼,以取得縮圖圖片及其標籤清單。首頁使用 Vue.js 擷取這項資料。
  • 美術拼貼頁面 (collage.html) 指向組成 4 張最新圖片的 collage.png 圖片。
  • 「上傳」頁面 (upload.html) 提供簡單的表單,讓您透過 POST 要求將圖片上傳到 /api/pictures 網址。

課程內容

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. 設定和需求

自修環境設定

  1. 登入 Google Cloud 控制台,建立新專案或重複使用現有專案。如果您還沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 「專案名稱」是這項專案參與者的顯示名稱。這是 Google API 不使用的字元字串,您可以隨時更新。
  • 所有 Google Cloud 專案的專案 ID 均不得重複,且設定後即無法變更。Cloud 控制台會自動產生一個不重複的字串。但通常是在乎它何在在大部分的程式碼研究室中,您必須參照專案 ID (通常稱為 PROJECT_ID),因此如果您不喜歡的話,可以再隨機產生一個,或者,您也可以自行嘗試看看是否可用。是「凍結」建立專案後
  • 還有第三個值,也就是部分 API 使用的專案編號。如要進一步瞭解這三個值,請參閱說明文件
  1. 接下來,您需要在 Cloud 控制台中啟用計費功能,才能使用 Cloud 資源/API。執行這個程式碼研究室並不會產生任何費用,如果有的話。如要關閉資源,以免產生本教學課程結束後產生的費用,請按照任「清除所用資源」操作請參閱本程式碼研究室結尾處的操作說明。Google Cloud 的新使用者符合 $300 美元免費試用計畫的資格。

啟動 Cloud Shell

雖然 Google Cloud 可以從筆記型電腦遠端操作,但在本程式碼研究室中,您將使用 Google Cloud Shell,這是一種在 Cloud 中執行的指令列環境。

Google Cloud 控制台,按一下右上方的工具列上的 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 應用程式執行個體。

您會使用兩個 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 這個網址會重新導向至原尺寸圖片的雲端儲存空間位置
  • GET /api/thumbnails/:name 這個網址會重新導向至縮圖的雲端儲存空間位置
  • GET /api/collage 這個最後網址會重新導向至在系統產生的美術拼貼圖片的雲端儲存空間位置

上傳圖片

在探索圖片上傳 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 陣列中,並加上圖片的名稱、描述標籤 (由 Cloud Vision API 提供)、主要顏色,以及友善的建立日期 (使用 dayjs 時,我們則使用相對時間偏移,例如「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.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. 在本機測試

先在本機測試程式碼,確保程式碼能正常運作再部署至雲端。

您必須匯出兩個 Cloud Storage 值區相對應的兩個環境變數:

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 為基礎。定義兩個環境變數時,指向兩個值區:原始圖片和縮圖。

如要將 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

一到兩分鐘後,您將收到通知,說明應用程式正在處理流量:

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. 測試應用程式

如要測試,請前往應用程式的預設 App Engine 網址 (https://<YOUR_PROJECT_ID>.appspot.com/),您應該會看到前端 UI 已啟動並開始運作!

6a4d5e5603ba4b73.png

9. 清除 (選用)

如果不想保留應用程式,可以刪除整個專案來清理資源,以節省成本並成為良好的雲端公民:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. 恭喜!

恭喜!這個由 App Engine 代管的 Node.js 網頁應用程式能夠將您的所有服務繫結在一起,讓使用者上傳圖片並以視覺化方式呈現。

涵蓋內容

  • App Engine
  • Cloud Storage
  • Cloud Firestore

後續步驟