1. Tổng quan
Trong lớp học lập trình này, bạn sẽ tạo một giao diện người dùng web trên Google App Engine. Giao diện này sẽ cho phép người dùng tải ảnh lên từ ứng dụng web, cũng như duyệt xem ảnh đã tải lên và hình thu nhỏ của ảnh.

Ứng dụng web này sẽ sử dụng một khung CSS có tên là Bulma để có giao diện người dùng đẹp mắt, cũng như khung giao diện người dùng JavaScript Vue.JS để gọi API của ứng dụng mà bạn sẽ tạo.
Ứng dụng này sẽ có 3 thẻ:
- Một trang chủ sẽ hiển thị hình thu nhỏ của tất cả hình ảnh đã tải lên, cùng với danh sách nhãn mô tả hình ảnh (những nhãn do Cloud Vision API phát hiện trong một phòng thí nghiệm trước đó).
- Trang ảnh ghép sẽ hiển thị ảnh ghép được tạo từ 4 bức ảnh gần đây nhất mà bạn đã tải lên.
- Trang tải lên, nơi người dùng có thể tải ảnh mới lên.
Giao diện người dùng kết quả có dạng như sau:

3 trang đó là các trang HTML đơn giản:
- Trang chủ (
index.html) gọi mã phụ trợ Node App Engine để lấy danh sách hình thu nhỏ và nhãn của chúng thông qua lệnh gọi AJAX đến URL/api/pictures. Trang chủ đang sử dụng Vue.js để tìm nạp dữ liệu này. - Trang ảnh ghép (
collage.html) trỏ đến hình ảnhcollage.pngtập hợp 4 bức ảnh mới nhất. - Trang tải lên (
upload.html) cung cấp một biểu mẫu đơn giản để tải ảnh lên thông qua yêu cầu POST đến URL/api/pictures.
Kiến thức bạn sẽ học được
- App Engine
- Cloud Storage
- Cloud Firestore
2. Thiết lập và yêu cầu
Thiết lập môi trường theo tốc độ của riêng bạn
- Đăng nhập vào Google Cloud Console rồi tạo một dự án mới hoặc sử dụng lại một dự án hiện có. Nếu chưa có tài khoản Gmail hoặc Google Workspace, bạn phải tạo một tài khoản.



- Tên dự án là tên hiển thị của những người tham gia dự án này. Đây là một chuỗi ký tự mà các API của Google không dùng và bạn có thể cập nhật chuỗi này bất cứ lúc nào.
- Mã dự án phải là duy nhất trên tất cả các dự án trên Google Cloud và không thể thay đổi (bạn không thể thay đổi sau khi đã đặt). Cloud Console sẽ tự động tạo một chuỗi duy nhất; thường thì bạn không cần quan tâm đến chuỗi này. Trong hầu hết các lớp học lập trình, bạn sẽ cần tham chiếu đến Mã dự án (thường được xác định là
PROJECT_ID). Vì vậy, nếu không thích mã này, bạn có thể tạo một mã ngẫu nhiên khác hoặc thử mã của riêng mình để xem mã đó có dùng được hay không. Sau đó, mã này sẽ "đóng băng" sau khi dự án được tạo. - Có một giá trị thứ ba là Số dự án mà một số API sử dụng. Tìm hiểu thêm về cả 3 giá trị này trong tài liệu.
- Tiếp theo, bạn cần bật tính năng thanh toán trong Cloud Console để sử dụng các tài nguyên/API trên Cloud. Việc thực hiện lớp học lập trình này sẽ không tốn nhiều chi phí, nếu có. Để tắt các tài nguyên nhằm tránh bị tính phí ngoài phạm vi hướng dẫn này, hãy làm theo mọi hướng dẫn "dọn dẹp" ở cuối lớp học lập trình. Người dùng mới của Google Cloud đủ điều kiện tham gia chương trình Dùng thử miễn phí trị giá 300 USD.
Khởi động Cloud Shell
Mặc dù có thể vận hành Google Cloud từ xa trên máy tính xách tay, nhưng trong lớp học lập trình này, bạn sẽ sử dụng Google Cloud Shell, một môi trường dòng lệnh chạy trên Cloud.
Trên Bảng điều khiển Google Cloud, hãy nhấp vào biểu tượng Cloud Shell trên thanh công cụ ở trên cùng bên phải:

Quá trình này chỉ mất vài phút để cung cấp và kết nối với môi trường. Khi quá trình này kết thúc, bạn sẽ thấy như sau:

Máy ảo này được trang bị tất cả các công cụ phát triển mà bạn cần. Nó cung cấp một thư mục chính có dung lượng 5 GB và chạy trên Google Cloud, giúp tăng cường đáng kể hiệu suất mạng và hoạt động xác thực. Bạn chỉ cần một trình duyệt là có thể thực hiện mọi thao tác trong phòng thí nghiệm này.
3. Bật API
App Engine yêu cầu Compute Engine API. Đảm bảo bạn đã bật tính năng này:
gcloud services enable compute.googleapis.com
Bạn sẽ thấy thao tác hoàn tất thành công:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. Sao chép mã
Kiểm tra mã nếu bạn chưa thực hiện:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
Sau đó, bạn có thể chuyển đến thư mục chứa giao diện người dùng:
cd serverless-photosharing-workshop/frontend
Bạn sẽ có bố cục tệp sau đây cho giao diện người dùng:
frontend
|
├── index.js
├── package.json
├── app.yaml
|
├── public
|
├── index.html
├── collage.html
├── upload.html
|
├── app.js
├── script.js
├── style.css
Ở thư mục gốc của dự án, bạn có 3 tệp:
index.jschứa mã Node.jspackage.jsonxác định các phần phụ thuộc của thư việnapp.yamllà tệp cấu hình cho Google App Engine
Thư mục public chứa các tài nguyên tĩnh:
index.htmllà trang hiển thị tất cả hình thu nhỏ và nhãncollage.htmlcho thấy ảnh ghép của những bức ảnh gần đâyupload.htmlcó biểu mẫu để tải ảnh mới lênapp.jsđang sử dụng Vue.js để điền dữ liệu vào trangindex.htmlscript.jsxử lý trình đơn điều hướng và biểu tượng "hamburger" trên màn hình nhỏstyle.cssxác định một số chỉ thị CSS
5. Khám phá mã
Phần phụ thuộc
Tệp package.json xác định các phần phụ thuộc cần thiết của thư viện:
{
"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"
}
}
Ứng dụng của chúng tôi phụ thuộc vào:
- firestore: để truy cập Cloud Firestore bằng siêu dữ liệu hình ảnh của chúng ta,
- bộ nhớ: để truy cập vào Google Cloud Storage nơi lưu trữ hình ảnh,
- express: khung web cho Node.js,
- dayjs: một thư viện nhỏ để hiển thị ngày theo cách thân thiện với người dùng,
- bluebird: một thư viện promise của JavaScript,
- express-fileupload: một thư viện để dễ dàng xử lý việc tải tệp lên.
Giao diện người dùng Express
Khi bắt đầu bộ điều khiển index.js, bạn sẽ cần tất cả các phần phụ thuộc được xác định trong package.json trước đó:
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)
Tiếp theo, phiên bản ứng dụng Express sẽ được tạo.
Hai phần mềm trung gian Express được sử dụng:
- Lệnh gọi
express.static()cho biết các tài nguyên tĩnh sẽ có trong thư mục conpublic. - Và
fileUpload()định cấu hình tính năng tải tệp lên để giới hạn kích thước tệp ở mức 10 MB, nhằm tải tệp lên cục bộ trong hệ thống tệp trong bộ nhớ trong thư mục/tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
limits: { fileSize: 10 * 1024 * 1024 },
useTempFiles : true,
tempFileDir : '/tmp/'
}))
Trong số các tài nguyên tĩnh, bạn có các tệp HTML cho trang chủ, trang ảnh ghép và trang tải lên. Những trang đó sẽ gọi phần phụ trợ API. API này sẽ có các điểm cuối sau:
POST /api/picturesThông qua biểu mẫu trong upload.html, hình ảnh sẽ được tải lên thông qua yêu cầu POSTGET /api/picturesĐiểm cuối này trả về một tài liệu JSON chứa danh sách hình ảnh và nhãn của hình ảnhGET /api/pictures/:nameURL này chuyển hướng đến vị trí bộ nhớ trên đám mây của hình ảnh có kích thước đầy đủGET /api/thumbnails/:nameURL này chuyển hướng đến vị trí bộ nhớ trên đám mây của hình ảnh thu nhỏGET /api/collageURL cuối cùng này sẽ chuyển hướng đến vị trí bộ nhớ trên đám mây của hình ảnh ghép được tạo
Tải ảnh lên
Trước khi khám phá mã Node.js tải ảnh lên, hãy xem nhanh public/upload.html.
...
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
...
<input type="file" name="pictures">
<button>Submit</button>
...
</form>
...
Phần tử biểu mẫu trỏ đến điểm cuối /api/pictures, bằng phương thức POST qua HTTP và định dạng nhiều phần. Giờ đây, index.js phải phản hồi điểm cuối và phương thức đó, đồng thời trích xuất các tệp:
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('/');
});
Trước tiên, bạn kiểm tra để đảm bảo rằng có tệp đang được tải lên. Sau đó, bạn tải các tệp xuống cục bộ thông qua phương thức mv có trong mô-đun Node tải tệp lên của chúng tôi. Giờ đây, khi các tệp có sẵn trên hệ thống tệp cục bộ, bạn có thể tải hình ảnh lên vùng lưu trữ Cloud Storage. Cuối cùng, bạn chuyển hướng người dùng quay lại màn hình chính của ứng dụng.
Liệt kê các bức ảnh
Đã đến lúc bạn khoe những bức ảnh đẹp của mình!
Trong trình xử lý /api/pictures, bạn xem xét tập hợp pictures của cơ sở dữ liệu Firestore để truy xuất tất cả hình ảnh (đã tạo hình thu nhỏ), được sắp xếp theo ngày tạo giảm dần.
Bạn đẩy từng bức ảnh vào một mảng JavaScript, cùng với tên, nhãn mô tả (lấy từ Cloud Vision API), màu chủ đạo và ngày tạo thân thiện (với dayjs, chúng ta có các độ lệch thời gian tương đối như "3 ngày nữa").
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);
});
Bộ điều khiển này trả về kết quả có dạng như sau:
[
{
"name": "IMG_20180423_163745.jpg",
"labels": [
"Dish",
"Food",
"Cuisine",
"Ingredient",
"Orange chicken",
"Produce",
"Meat",
"Staple food"
],
"color": "#e78012",
"created": "a day ago"
},
...
]
Cấu trúc dữ liệu này được sử dụng bởi một đoạn mã Vue.js nhỏ trên trang index.html. Dưới đây là phiên bản đơn giản của mã đánh dấu trên trang đó:
<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>
Mã nhận dạng của div sẽ cho Vue.js biết rằng đó là một phần của mã đánh dấu sẽ được kết xuất linh động. Các lần lặp lại được thực hiện nhờ các chỉ thị v-for.
Các bức ảnh có đường viền màu đẹp tương ứng với màu chủ đạo trong ảnh, do Cloud Vision API tìm thấy. Chúng tôi chỉ vào hình thu nhỏ và ảnh toàn chiều rộng trong đường liên kết và nguồn hình ảnh.
Cuối cùng, chúng ta liệt kê các nhãn mô tả bức ảnh.
Sau đây là mã JavaScript cho đoạn mã Vue.js (trong tệp public/app.js được nhập ở cuối trang index.html):
var app = new Vue({
el: '#app',
data() {
return { pictures: [] }
},
mounted() {
axios
.get('/api/pictures')
.then(response => { this.pictures = response.data })
}
})
Mã Vue đang sử dụng thư viện Axios để thực hiện lệnh gọi AJAX đến điểm cuối /api/pictures của chúng tôi. Sau đó, dữ liệu được trả về sẽ được liên kết với mã khung hiển thị trong mã đánh dấu mà bạn đã thấy trước đó.
Xem ảnh
Từ index.html, người dùng có thể xem hình thu nhỏ của các bức ảnh, nhấp vào hình thu nhỏ để xem ảnh có kích thước đầy đủ và từ collage.html, người dùng có thể xem ảnh collage.png.
Trong mã đánh dấu HTML của những trang đó, hình ảnh src và đường liên kết href trỏ đến 3 điểm cuối đó, các điểm cuối này sẽ chuyển hướng đến vị trí của hình ảnh, hình thu nhỏ và ảnh ghép trên Cloud Storage. Bạn không cần phải mã hoá cứng đường dẫn trong mã đánh dấu 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`);
});
Chạy ứng dụng Node
Sau khi xác định tất cả các điểm cuối, ứng dụng Node.js của bạn đã sẵn sàng để ra mắt. Ứng dụng Express mặc định sẽ lắng nghe trên cổng 8080 và sẵn sàng xử lý các yêu cầu đến.
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. Kiểm thử cục bộ
Kiểm thử mã cục bộ để đảm bảo mã hoạt động trước khi triển khai lên đám mây.
Bạn cần xuất 2 biến môi trường tương ứng với 2 bộ chứa Cloud Storage:
export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
Trong thư mục frontend, hãy cài đặt các phần phụ thuộc npm và khởi động máy chủ:
npm install; npm start
Nếu mọi thứ diễn ra suôn sẻ, thì máy chủ sẽ khởi động trên cổng 8080:
Started web frontend service on port 8080
- Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
- Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}
Tên thực của các vùng chứa sẽ xuất hiện trong những nhật ký đó, điều này rất hữu ích cho mục đích gỡ lỗi.
Trong Cloud Shell, bạn có thể sử dụng tính năng xem trước trên web để duyệt xem ứng dụng đang chạy cục bộ:

Sử dụng CTRL-C để thoát.
7. Triển khai lên App Engine
Ứng dụng của bạn đã sẵn sàng để triển khai.
Định cấu hình App Engine
Kiểm tra tệp cấu hình app.yaml cho App Engine:
runtime: nodejs16 env_variables: BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT
Dòng đầu tiên khai báo rằng thời gian chạy dựa trên Node.js 10. Hai biến môi trường được xác định để trỏ đến hai nhóm, cho hình ảnh gốc và cho hình thu nhỏ.
Để thay thế GOOGLE_CLOUD_PROJECT bằng mã dự án thực tế, bạn có thể chạy lệnh sau:
sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml
Triển khai
Đặt khu vực ưu tiên cho App Engine, nhớ sử dụng cùng một khu vực trong các phòng thí nghiệm trước đây:
gcloud config set compute/region europe-west1
Và triển khai:
gcloud app deploy
Sau một hoặc hai phút, bạn sẽ nhận được thông báo rằng ứng dụng đang phân phát lưu lượng truy cập:
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
Bạn cũng có thể truy cập vào phần App Engine của Cloud Console để xem ứng dụng đã được triển khai hay chưa và khám phá các tính năng của App Engine như phân chia lưu lượng truy cập và quản lý phiên bản:

8. Kiểm thử ứng dụng
Để kiểm thử, hãy chuyển đến URL App Engine mặc định cho ứng dụng (https://<YOUR_PROJECT_ID>.appspot.com/) và bạn sẽ thấy giao diện người dùng hoạt động!

9. Dọn dẹp (Không bắt buộc)
Nếu không có ý định giữ lại ứng dụng này, bạn có thể dọn dẹp tài nguyên để tiết kiệm chi phí và trở thành một người dùng đám mây có trách nhiệm bằng cách xoá toàn bộ dự án:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
10. Xin chúc mừng!
Xin chúc mừng! Ứng dụng web Node.js này được lưu trữ trên App Engine, liên kết tất cả các dịch vụ của bạn với nhau và cho phép người dùng tải lên cũng như hình dung hình ảnh.
Nội dung đã đề cập
- App Engine
- Cloud Storage
- Cloud Firestore