Pic-a-daily: Лабораторная работа 4. Создание веб-интерфейса

1. Обзор

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

21741cd63b425aeb.png

В этом веб-приложении будет использоваться CSS-фреймворк Bulma для создания привлекательного пользовательского интерфейса, а также JavaScript-фреймворк Vue.JS для вызова API приложения, который вы будете создавать.

Это приложение будет состоять из трех вкладок:

  • На главной странице будут отображаться миниатюры всех загруженных изображений, а также список подписей, описывающих изображение (те, которые были обнаружены API Cloud Vision в предыдущей лабораторной работе).
  • Страница -коллаж , на которой будет представлено изображение, составленное из 4 последних загруженных фотографий.
  • Страница загрузки , где пользователи могут загружать новые фотографии.

В результате получился следующий интерфейс пользователя:

6a4d5e5603ba4b73.png

Эти 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. Настройка и требования

Настройка среды для самостоятельного обучения

  1. Войдите в консоль Google Cloud и создайте новый проект или используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

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

Запустить Cloud Shell

Хотя Google Cloud можно управлять удаленно с ноутбука, в этом практическом занятии вы будете использовать Google Cloud Shell — среду командной строки, работающую в облаке.

В консоли Google Cloud нажмите на значок Cloud Shell на панели инструментов в правом верхнем углу:

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предоставляет постоянный домашний каталог размером 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> &nbsp;
                                                        </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 вы можете использовать функцию веб-просмотра, чтобы просмотреть приложение, работающее локально:

82fa3266d48c0d0a.png

Для выхода используйте сочетание 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, как версионирование и разделение трафика:

db0e196b00fceab1.png

8. Протестируйте приложение.

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

6a4d5e5603ba4b73.png

9. Уборка (необязательно)

Если вы не планируете сохранять приложение, вы можете освободить ресурсы, чтобы сэкономить средства и в целом ответственно относиться к облачным сервисам, удалив весь проект:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. Поздравляем!

Поздравляем! Это веб-приложение на Node.js, размещенное на App Engine, объединяет все ваши сервисы и позволяет пользователям загружать и просматривать изображения.

Что мы рассмотрели

  • App Engine
  • Облачное хранилище
  • Облачный Firestore

Следующие шаги