1. Обзор
В этом практическом задании вы создадите веб-интерфейс на Google App Engine, который позволит пользователям загружать изображения из веб-приложения, а также просматривать загруженные изображения и их миниатюры.

В этом веб-приложении будет использоваться CSS-фреймворк Bulma для создания привлекательного пользовательского интерфейса, а также JavaScript-фреймворк Vue.JS для вызова API приложения, который вы будете создавать.
Это приложение будет состоять из трех вкладок:
- На главной странице будут отображаться миниатюры всех загруженных изображений, а также список подписей, описывающих изображение (те, которые были обнаружены API Cloud Vision в предыдущей лабораторной работе).
- Страница -коллаж , на которой будет представлено изображение, составленное из 4 последних загруженных фотографий.
- Страница загрузки , где пользователи могут загружать новые фотографии.
В результате получился следующий интерфейс пользователя:

Эти 3 страницы — простые HTML-страницы:
- На главной странице (
index.html) для получения списка миниатюр изображений и их подписей используется AJAX-запрос к URL-адресу/api/pictures, который обращается к бэкэнду Node App Engine. Для получения этих данных на главной странице используется Vue.js. - Страница с коллажем (
collage.html) ссылается на изображениеcollage.png, которое объединяет 4 последних снимка. - Страница загрузки (
upload.html) предлагает простую форму для загрузки изображения посредством POST-запроса к URL-адресу/api/pictures.
Что вы узнаете
- App Engine
- Облачное хранилище
- Облачный Firestore
2. Настройка и требования
Настройка среды для самостоятельного обучения
- Войдите в консоль Google Cloud и создайте новый проект или используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .



- Название проекта — это отображаемое имя участников данного проекта. Это строка символов, не используемая API Google, и вы можете изменить её в любое время.
- Идентификатор проекта должен быть уникальным для всех проектов Google Cloud и неизменяемым (его нельзя изменить после установки). Консоль Cloud автоматически генерирует уникальную строку; обычно вам неважно, какая она. В большинстве практических заданий вам потребуется указать идентификатор проекта (обычно он обозначается как
PROJECT_ID), поэтому, если он вам не нравится, сгенерируйте другой случайный идентификатор или попробуйте свой собственный и посмотрите, доступен ли он. Затем он "замораживается" после создания проекта. - Существует третье значение — номер проекта , который используется некоторыми API. Подробнее обо всех трех значениях можно узнать в документации .
- Далее вам потребуется включить оплату в консоли Cloud, чтобы использовать ресурсы/API Cloud. Выполнение этого практического задания не должно стоить дорого, если вообще что-либо. Чтобы отключить ресурсы и избежать дополнительных расходов после завершения этого урока, следуйте инструкциям по «очистке», приведенным в конце практического задания. Новые пользователи Google Cloud имеют право на бесплатную пробную версию стоимостью 300 долларов США .
Запустить Cloud Shell
Хотя Google Cloud можно управлять удаленно с ноутбука, в этом практическом занятии вы будете использовать Google Cloud Shell — среду командной строки, работающую в облаке.
В консоли Google Cloud нажмите на значок Cloud Shell на панели инструментов в правом верхнем углу:

Подготовка и подключение к среде займут всего несколько минут. После завершения вы должны увидеть что-то подобное:

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предоставляет постоянный домашний каталог размером 5 ГБ и работает в облаке Google, что значительно повышает производительность сети и аутентификацию. Всю работу в этой лаборатории можно выполнять с помощью обычного браузера.
3. Включите API.
Для работы App Engine требуется API Compute Engine. Убедитесь, что он включен:
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, где хранятся фотографии.
- Express : веб-фреймворк для Node.js.
- dayjs : небольшая библиотека для отображения дат в удобном для человека формате.
- bluebird : библиотека для работы с промисами на JavaScript.
- express-fileupload : библиотека для упрощения загрузки файлов.
Экспресс-фронтенд
В начале контроллера 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 МБ, и загружает файлы локально в файловую систему в оперативной памяти в каталог/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Этот URL перенаправляет на облачное хранилище, где находится полноразмерное изображение. -
GET /api/thumbnails/:nameЭтот URL перенаправляет на облачное хранилище, где находится изображение миниатюры. -
GET /api/collageЭтот последний URL-адрес перенаправляет на местоположение сгенерированного изображения коллажа в облачном хранилище.
Загрузка изображения
Прежде чем изучать код загрузки изображений в 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('/');
});
Сначала вы проверяете, действительно ли загружаются файлы. Затем вы скачиваете файлы локально с помощью метода mv из нашего модуля Node для загрузки файлов. Теперь, когда файлы доступны в локальной файловой системе, вы загружаете изображения в хранилище Cloud Storage. Наконец, вы перенаправляете пользователя обратно на главный экран приложения.
Список фотографий
Пришло время выставить ваши прекрасные фотографии!
В обработчике /api/pictures вы обращаетесь к коллекции pictures в базе данных Firestore, чтобы получить все изображения (с созданными миниатюрами), отсортированные по дате создания в порядке убывания.
Каждое изображение помещается в массив JavaScript, содержащий его имя, метки (полученные из API Cloud Vision), преобладающий цвет и удобную дату создания (в 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"
},
...
]
Эта структура данных используется небольшим фрагментом кода Vue.js со страницы index.html . Вот упрощенная версия разметки с этой страницы:
<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` укажет Vue.js, что именно эта часть разметки будет динамически отображаться. Итерации выполняются благодаря директиве v-for .
Изображения получают приятную цветную рамку, соответствующую преобладающему цвету на изображении, определяемому API Cloud Vision, и мы указываем на миниатюры и полноэкранные изображения в ссылках и исходных файлах изображений.
В заключение мы приводим подписи, описывающие изображение.
Вот JavaScript-код для фрагмента кода Vue.js (в файле public/app.js , импортированном внизу страницы index.html ):
var app = new Vue({
el: '#app',
data() {
return { pictures: [] }
},
mounted() {
axios
.get('/api/pictures')
.then(response => { this.pictures = response.data })
}
})
В коде Vue используется библиотека Axios для выполнения AJAX-запроса к нашей конечной точке /api/pictures . Полученные данные затем привязываются к коду представления в разметке, которую вы видели ранее.
Просмотр фотографий
В файле index.html пользователи могут просмотреть миниатюры изображений, щелкнуть по ним, чтобы просмотреть изображения в полном размере, а в файле collage.html — просмотреть изображение collage.png .
В HTML-разметке этих страниц атрибуты src изображения и href ссылки указывают на эти 3 конечные точки, которые перенаправляют на адреса облачного хранилища, где хранятся изображения, миниатюры и коллажи. Нет необходимости жестко прописывать путь в 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
После определения всех конечных точек ваше приложение 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.yaml для App Engine:
runtime: nodejs16 env_variables: BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT
Первая строка указывает, что среда выполнения основана на Node.js 10. Определены две переменные среды, указывающие на два хранилища: для исходных изображений и для миниатюр.
Чтобы заменить GOOGLE_CLOUD_PROJECT на фактический идентификатор вашего проекта, выполните следующую команду:
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
Вы также можете перейти в раздел App Engine в Cloud Console, чтобы убедиться, что приложение развернуто, и изучить такие функции App Engine, как версионирование и разделение трафика:

8. Протестируйте приложение.
Для проверки перейдите по стандартному URL-адресу приложения в App Engine ( https://<YOUR_PROJECT_ID>.appspot.com/ ), и вы должны увидеть работающий пользовательский интерфейс!

9. Уборка (необязательно)
Если вы не планируете сохранять приложение, вы можете освободить ресурсы, чтобы сэкономить средства и в целом ответственно относиться к облачным сервисам, удалив весь проект:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
10. Поздравляем!
Поздравляем! Это веб-приложение на Node.js, размещенное на App Engine, объединяет все ваши сервисы и позволяет пользователям загружать и просматривать изображения.
Что мы рассмотрели
- App Engine
- Облачное хранилище
- Облачный Firestore