Pic-a-daily: Phòng thí nghiệm 4 – Tạo giao diện người dùng web

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 để cho phép người dùng tải ảnh lên từ ứng dụng web, cũng như duyệt qua các ảnh đã tải lên và hình thu nhỏ của họ.

21741cd63b425aeb.png.

Ứng dụng web này sẽ sử dụng khung CSS có tên Bulma để có một số 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ẽ xây dựng.

Ứng dụng này sẽ bao gồm ba 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 (các nhãn được Cloud Vision API phát hiện trong phòng thí nghiệm trước).
  • Trang ảnh ghép sẽ hiển thị ảnh ghép từ 4 ảnh được tải lên gần đây nhất.
  • 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 thu được sẽ có dạng như sau:

6a4d5e5603ba4b73.png.

3 trang đó là các trang HTML đơn giản:

  • Trang chủ (index.html) gọi mã phụ trợ của Node App Engine để lấy danh sách hình thu nhỏ và nhãn của chú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ỏ vào hình ảnh collage.png ghép thành 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 qua yêu cầu POST tới 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 tiến độ riêng

  1. Đăng nhập vào Google Cloud Console rồi tạo dự án mới hoặc sử dụng lại 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.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 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ự không được API của Google sử 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 trong tất cả các dự án Google Cloud và không thể thay đổi (không thể thay đổi sau khi đã đặt). Cloud Console sẽ tự động tạo một chuỗi duy nhất; thường bạn không quan tâm đến sản phẩm đó là gì. 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 (và mã này thường được xác định là PROJECT_ID). Vì vậy, nếu không thích, bạn có thể tạo một mã ngẫu nhiên khác hoặc bạn có thể thử mã của riêng mình để xem có mã này chưa. Sau đó, video sẽ được "đóng băng" sau khi tạo dự án.
  • Có giá trị thứ ba là Project Number (Số dự án) mà một số API sử dụng. Tìm hiểu thêm về cả ba giá trị này trong tài liệu này.
  1. Tiếp theo, bạn sẽ cần bật tính năng thanh toán trong Cloud Console để sử dụng tài nguyên/API trên Cloud. Việc chạy qua lớp học lập trình này sẽ không tốn nhiều chi phí. Để tắt các tài nguyên để bạn không phải chịu thanh toán ngoài hướng dẫn này, hãy làm theo mọi thao tác "dọn dẹp" hướng dẫn ở 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í 300 USD.

Khởi động Cloud Shell

Mặc dù bạn 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 trong Đám mây.

Trong Google Cloud Console, hãy nhấp vào biểu tượng Cloud Shell ở thanh công cụ trên cùng bên phải:

55efc1aaa7a4d3ad.pngS

Sẽ chỉ mất một chút thời gian để cấp phép và kết nối với môi trường. Sau khi hoàn tất, bạn sẽ thấy như sau:

7ffe5cbb04455448.pngS

Máy ảo này chứa tất cả các công cụ phát triển mà bạn cần. Phiên bản này cung cấp thư mục gốc có dung lượng ổn định 5 GB và chạy trên Google Cloud, giúp nâng cao đáng kể hiệu suất và khả năng xác thực của mạng. Bạn có thể thực hiện tất cả công việc trong phòng thí nghiệm này chỉ bằng một trình duyệt.

3. Bật API

App Engine cần có Compute Engine API. Đảm bảo bạn đã bật chế độ 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 việc này:

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

Trong thư mục gốc của dự án, bạn có 3 tệp:

  • index.js chứa mã Node.js
  • package.json xác định các phần phụ thuộc của thư viện
  • app.yaml là 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.html là trang hiển thị tất cả hình thu nhỏ và nhãn
  • collage.html hiển thị ảnh ghép của các bức ảnh gần đây
  • upload.html có chứa một biểu mẫu để tải ảnh mới lên
  • app.js đang sử dụng Vue.js để điền dữ liệu vào trang index.html
  • script.js xử lý trình đơn điều hướng và "biểu tượng ba đường kẻ" biểu tượng trên màn hình nhỏ
  • style.css xác định một số lệnh CSS

5. Khám phá đoạn 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"
  }
}

Đơn đăng ký của chúng tôi phụ thuộc vào:

  • firestore: để truy cập vào Cloud Firestore bằng siêu dữ liệu hình ảnh,
  • storage: để 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 tháng theo cách thân thiện với con người,
  • bluebird: thư viện lời hứa JavaScript,
  • express-fileupload: thư viện để xử lý tải tệp lên một cách dễ dàng.

Giao diện người dùng Express

Ở đầu bộ điều khiển index.js, bạn sẽ yêu cầu tất 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 của ứng dụng Express sẽ được tạo.

Hai phần mềm trung gian của Express được sử dụng:

  • Lệnh gọi express.static() cho biết rằng các tài nguyên tĩnh sẽ có trong thư mục con public.
  • Đồng thời, 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 các tệp đó lên cục bộ trong hệ thống tệp trong bộ nhớ ở 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/pictures Thô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 POST
  • GET /api/pictures Điểm cuối này trả về tài liệu JSON chứa danh sách ảnh và nhãn của các ảnh đó
  • GET /api/pictures/:name URL 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/:name URL này chuyển hướng đến vị trí bộ nhớ trên đám mây của hình thu nhỏ
  • GET /api/collage URL cuối cùng này chuyển hướng đến vị trí bộ nhớ trên đám mây của hình ảnh ghép đã tạo

Tải ảnh lên

Trước khi tìm hiểu mã Node.js tải lên hình ảnh, 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ỏ vào điểm cuối /api/pictures, với phương thức POST qua HTTP và định dạng nhiều phần. index.js hiện 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 thực sự có các tệp đang được tải lên. Sau đó, bạn tải tệp xuống cục bộ thông qua phương thức mv từ mô-đun Nút tải tệp lên của chúng tôi. Giờ đây, các tệp đã có trong hệ thống tệp cục bộ, bạn tải hình ảnh lên bộ chứa 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ê hình ảnh

Đã đến lúc hiển thị những bức ảnh đẹp của bạn!

Trong trình xử lý /api/pictures, bạn xem xét bộ sưu tập pictures của cơ sở dữ liệu Firestore để truy xuất tất cả các hình ảnh (có hình thu nhỏ đã được tạo), được sắp xếp theo ngày tạo giảm dần.

Bạn đẩy mỗi ảnh vào một mảng JavaScript, có tên, nhãn mô tả ảnh (lấy từ Cloud Vision API), màu chủ đạo và ngày tạo dễ dàng (với dayjs, chúng ta sẽ chênh lệch thời gian tương đối như "3 ngày từ bây giờ").

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);
});

Trình điều khiển này trả về kết quả có hình 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 một đoạn mã Vue.js nhỏ sử dụng trên trang index.html. Dưới đây là phiên bản đơn giản của mục đánh dấu từ 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> &nbsp;
                                                        </span>
                                                        {{ label }}
                                                </a>
                                        </div>
                                </div>
                        </div>
            </div>
        </div>
</div>

Mã của div sẽ cho Vue.js biết rằng đó là một phần của mã đánh dấu sẽ được hiển thị động. Việc lặp lại được thực hiện nhờ các lệnh v-for.

Những hình ảnh này sẽ có đường viền màu đẹp tương ứng với màu chủ đạo trong ảnh, như được tìm thấy bởi Cloud Vision API và chúng tôi trỏ vào hình thu nhỏ cũng như ảnh có chiều rộng đầy đủ trong nguồn của đường liên kết và nguồn hình ảnh.

Cuối cùng, chúng tôi liệt kê các nhãn mô tả hình ảnh.

Dưới đâ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. Sau đó, dữ liệu trả về đượ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ủa chúng tôi có thể xem hình thu nhỏ của các ảnh, nhấp vào hình thu nhỏ để xem hình ảnh có kích thước đầy đủ và từ collage.html, người dùng có thể xem hình ảnh collage.png.

Trong mã đánh dấu HTML của các trang đó, hình ảnh src và đường liên kết href trỏ đến 3 điểm cuối đó. Những điểm cuối đó sẽ chuyển hướng đến vị trí của ảnh, hình thu nhỏ và ảnh ghép trong Cloud Storage. 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 Nút

Sau khi xác định tất cả các điểm cuối, ứng dụng Node.js của bạn sẽ sẵn sàng để khởi chạy. Theo mặc định, ứng dụng Express sẽ nghe trên cổng 8080 và sẵn sàng đáp ứng 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ã trên thiết 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}

Bên 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ẻ, 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 bộ chứa của bạn sẽ xuất hiện trong các nhật ký đó, điều này rất hữu ích cho mục đích gỡ lỗi.

Từ Cloud Shell, bạn có thể sử dụng tính năng xem trước trên web để trình duyệt ứng dụng đang chạy trên máy:

82fa3266d48c0d0a.png.

Sử dụng CTRL-C để thoát.

7. Triển khai cho 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ỏ vào 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 của bạn cho App Engine, nhớ sử dụng chính khu vực đó trong các phòng thí nghiệm trước:

gcloud config set compute/region europe-west1

Và triển khai:

gcloud app deploy

Sau 1 hoặc 2 phút, bạn sẽ đượ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 và khám phá các tính năng của App Engine như tạo phiên bản và phân tách lưu lượng truy cập:

db0e196b00fceab1.png

8. Kiểm thử ứng dụng

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

6a4d5e5603ba4b73.png.

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 công dân tốt nói chung về đám mây 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 sẽ liên kết tất cả các dịch vụ của bạn lại với nhau, đồng thời cho phép người dùng của bạn tải lên và trực quan hóa hình ảnh.

Nội dung đã đề cập

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Các bước tiếp theo