1. 總覽
在本程式碼研究室中,您將在 Google App Engine 上建立網頁前端,讓使用者從網頁應用程式上傳圖片,以及瀏覽上傳的圖片和縮圖。

這個網頁應用程式會使用名為 Bulma 的 CSS 架構,提供美觀的使用者介面,並使用 Vue.JS JavaScript 前端架構呼叫您建構的應用程式 API。
這個應用程式包含三個分頁:
- 首頁:顯示所有上傳圖片的縮圖,以及描述圖片的標籤清單 (先前實驗室中由 Cloud Vision API 偵測到的標籤)。
- 系統會顯示拼貼頁面,其中包含最近上傳的 4 張相片。
- 上傳頁面,使用者可以在這裡上傳新圖片。
產生的前端如下所示:

這 3 個網頁都是簡單的 HTML 網頁:
- 首頁 (
index.html) 會呼叫 Node App Engine 後端程式碼,透過對/api/pictures網址的 AJAX 呼叫,取得縮圖清單和標籤。首頁會使用 Vue.js 擷取這項資料。 - 「美術拼貼」頁面 (
collage.html) 會指向collage.png圖片,該圖片會組合 4 張最新相片。 - 「上傳」頁面 (
upload.html) 提供簡單的表單,可透過對/api/picturesURL 的 POST 要求上傳圖片。
課程內容
- App Engine
- Cloud Storage
- Cloud Firestore
2. 設定和需求
自修實驗室環境設定
- 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶。



- 專案名稱是這個專案參與者的顯示名稱。這是 Google API 未使用的字元字串,您隨時可以更新。
- 專案 ID 在所有 Google Cloud 專案中不得重複,且設定後即無法變更。Cloud 控制台會自動產生專屬字串,通常您不需要在意該字串為何。在大多數程式碼研究室中,您需要參照專案 ID (通常會標示為
PROJECT_ID),因此如果您不喜歡該字串,可以產生另一個隨機字串,或是嘗試使用自己的字串,看看是否可用。專案建立後,系統就會「凍結」該值。 - 還有第三個值,也就是部分 API 使用的「專案編號」。如要進一步瞭解這三種值,請參閱說明文件。
- 接著,您需要在 Cloud 控制台中啟用帳單,才能使用 Cloud 資源/API。完成本程式碼研究室的費用應該不高,甚至完全免費。如要停用資源,避免在本教學課程結束後繼續產生帳單費用,請按照程式碼研究室結尾的「清除」操作說明操作。Google Cloud 新使用者可參加價值$300 美元的免費試用計畫。
啟動 Cloud Shell
雖然可以透過筆電遠端操作 Google Cloud,但在本程式碼研究室中,您將使用 Google Cloud Shell,這是可在雲端執行的指令列環境。
在 Google Cloud 控制台中,點選右上工具列的 Cloud Shell 圖示:

佈建並連線至環境的作業需要一些時間才能完成。完成後,您應該會看到如下的內容:

這部虛擬機器搭載各種您需要的開發工具,並提供永久的 5GB 主目錄,而且可在 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 中的圖片中繼資料。
- 儲存空間:存取儲存圖片的 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>
...
表單元素指向 /api/pictures 端點,使用 HTTP POST 方法和多部分格式。現在,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 bucket。最後,將使用者重新導向回應用程式主畫面。
列出圖片
現在就來展示美麗的相片吧!
在 /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>
</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. 在本機測試
請先在本機測試程式碼,確認運作正常後再部署至雲端。
您需要匯出與兩個 Cloud Storage bucket 對應的兩個環境變數:
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 為基礎。定義兩個環境變數,分別指向原始圖片和縮圖的兩個值區。
如要將 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 Console 的 App Engine 專區,查看應用程式是否已部署,並探索 App Engine 的各項功能,例如版本控管和流量分配:

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

9. 清除 (選用)
如果您不打算保留應用程式,可以刪除整個專案來清除資源,節省費用,並當個優質的雲端使用者:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
10. 恭喜!
恭喜!這個 Node.js 網路應用程式託管於 App Engine,可將所有服務繫結在一起,並讓使用者上傳及顯示圖片。
涵蓋內容
- App Engine
- Cloud Storage
- Cloud Firestore