1. 概览
在此 Codelab 中,您将在 Google App Engine 上创建一个 Web 前端,它可让用户从 Web 应用上传图片,以及浏览上传的图片及其缩略图。
此 Web 应用将使用一个名为 Bulma 的 CSS 框架(具有一些美观的界面),并使用 Vue.JS JavaScript 前端框架来调用您将构建的应用 API。
此应用将由三个标签页组成:
- 首页,将显示所有已上传图片的缩略图以及描述图片的标签列表(Cloud Vision API 在之前的实验中检测到的标签)。
- 拼图页面,可显示由最近上传的 4 张照片构成的拼图。
- 上传页面,用户可以上传新照片。
生成的前端如下所示:
这 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. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。
- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串,您可以随时对其进行更新。
- 项目 ID 在所有 Google Cloud 项目中必须是唯一的,并且不可变(一经设置便无法更改)。Cloud Console 会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为
PROJECT_ID
),因此如果您不喜欢某个 ID,请再生成一个随机 ID,还可以尝试自己创建一个,并确认是否可用。然后,项目创建后,ID 会处于“冻结”状态。 - 第三个值是一些 API 使用的项目编号。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud Console 中启用结算功能,才能使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。要关闭资源以避免产生超出本教程范围的费用,请按照此 Codelab 末尾提供的任何“清理”说明操作。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
启动 Cloud Shell
虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 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,
- storage:用于访问存储图片的 Google Cloud Storage;
- express:适用于 Node.js 的 Web 框架,
- 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 存储分区。最后,将用户重定向回应用的主屏幕。
列出图片
是时候展示您的漂亮图片了!
在 /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 存储分区对应的两个环境变量:
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 控制台的 App Engine 部分,查看应用是否已部署,并探索 App Engine 的功能,例如版本控制和流量分配:
8. 测试应用
如需进行测试,请转到应用的默认 App Engine 网址 (https://<YOUR_PROJECT_ID>.appspot.com/
),您应该会看到前端界面已启动并运行!
9. 清理(可选)
如果您不打算保留该应用,可以通过删除整个项目来清理资源,从而节省成本并成为一个整体优秀的云公民:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
10. 恭喜!
恭喜!此托管在 App Engine 上的 Node.js Web 应用可将您的所有服务绑定在一起,并让您的用户上传并直观呈现图片。
所学内容
- App Engine
- Cloud Storage
- Cloud Firestore