Zdjęcia dzienne: moduł 4 – tworzenie frontendu internetowego

1. Przegląd

W tym ćwiczeniu utworzysz frontend internetowy w Google App Engine, który umożliwi użytkownikom przesyłanie zdjęć z aplikacji internetowej, a także przeglądanie przesłanych zdjęć i ich miniatur.

21741cd63b425aeb.png

Ta aplikacja internetowa będzie korzystać z architektury CSS o nazwie Bulma, która zapewnia atrakcyjny interfejs użytkownika, a także z architektury frontendu JavaScript Vue.JS, która będzie wywoływać interfejs API aplikacji, którą utworzysz.

Aplikacja będzie składać się z 3 kart:

  • Strona główna, na której będą wyświetlane miniatury wszystkich przesłanych obrazów wraz z listą etykiet opisujących zdjęcie (wykrytych przez Cloud Vision API w poprzednim ćwiczeniu).
  • Strona kolażu, na której będzie wyświetlany kolaż utworzony z 4 ostatnio przesłanych zdjęć.
  • Strona przesyłania, na której użytkownicy mogą przesyłać nowe zdjęcia.

Wynikowy interfejs wygląda tak:

6a4d5e5603ba4b73.png

Te 3 strony to proste strony HTML:

  • Strona główna (index.html) wywołuje kod backendu Node App Engine, aby pobrać listę miniatur i ich etykiet za pomocą wywołania AJAX do adresu URL /api/pictures. Strona główna używa Vue.js do pobierania tych danych.
  • Strona kolażu (collage.html) wskazuje obraz collage.png, który zawiera 4 najnowsze zdjęcia.
  • Strona upload (upload.html) zawiera prosty formularz do przesyłania obrazu za pomocą żądania POST do adresu URL /api/pictures.

Czego się nauczysz

  • App Engine
  • Cloud Storage
  • Cloud Firestore

2. Konfiguracja i wymagania

Samodzielne konfigurowanie środowiska

  1. Zaloguj się w konsoli Google Cloud i utwórz nowy projekt lub użyj istniejącego. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Nazwa projektu to wyświetlana nazwa uczestników tego projektu. Jest to ciąg znaków, który nie jest używany przez interfejsy API Google. Możesz go w dowolnym momencie zaktualizować.
  • Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i jest niezmienny (nie można go zmienić po ustawieniu). Konsola Cloud automatycznie generuje unikalny ciąg znaków. Zwykle nie musisz się nim przejmować. W większości modułów z kodem musisz odwoływać się do identyfikatora projektu (zwykle oznaczanego jako PROJECT_ID). Jeśli Ci się nie podoba, wygeneruj inny losowy identyfikator lub spróbuj użyć własnego i sprawdź, czy jest dostępny. Po utworzeniu projektu jest on „zamrażany”.
  • Istnieje też trzecia wartość, czyli numer projektu, którego używają niektóre interfejsy API. Więcej informacji o tych 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w konsoli Cloud, aby korzystać z zasobów i interfejsów API Google Cloud. Ukończenie tego laboratorium nie powinno wiązać się z dużymi kosztami, a nawet z żadnymi. Aby wyłączyć zasoby i uniknąć naliczenia opłat po zakończeniu tego samouczka, postępuj zgodnie z instrukcjami „czyszczenia” na końcu ćwiczenia. Nowi użytkownicy Google Cloud mogą skorzystać z bezpłatnego okresu próbnego, w którym mają do dyspozycji środki w wysokości 300 USD.

Uruchamianie Cloud Shell

Z Google Cloud można korzystać zdalnie na laptopie, ale w tym module praktycznym będziesz używać Google Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.

W konsoli Google Cloud kliknij ikonę Cloud Shell na pasku narzędzi w prawym górnym rogu:

55efc1aaa7a4d3ad.png

Uzyskanie dostępu do środowiska i połączenie się z nim powinno zająć tylko kilka chwil. Po zakończeniu powinno wyświetlić się coś takiego:

7ffe5cbb04455448.png

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Wszystkie zadania w tym module możesz wykonać w przeglądarce.

3. Włącz interfejsy API

App Engine wymaga interfejsu Compute Engine API. Sprawdź, czy jest włączona:

gcloud services enable compute.googleapis.com

Operacja powinna zostać ukończona:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. Klonowanie kodu

Sprawdź kod, jeśli jeszcze tego nie zrobiono:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop

Następnie możesz przejść do katalogu zawierającego interfejs:

cd serverless-photosharing-workshop/frontend

Interfejs będzie miał następujący układ plików:

frontend
 |
 ├── index.js
 ├── package.json
 ├── app.yaml
 |
 ├── public
      |
      ├── index.html
      ├── collage.html
      ├── upload.html
      |
      ├── app.js
      ├── script.js
      ├── style.css

W katalogu głównym projektu znajdują się 3 pliki:

  • index.js zawiera kod Node.js.
  • package.json określa zależności biblioteki.
  • app.yaml to plik konfiguracyjny Google App Engine.

Folder public zawiera zasoby statyczne:

  • index.html to strona ze wszystkimi miniaturami i etykietami.
  • collage.html – wyświetla kolaż ostatnich zdjęć.
  • upload.html zawiera formularz do przesyłania nowych zdjęć.
  • app.js używa Vue.js do wypełniania strony index.html danymi.
  • script.js obsługuje menu nawigacyjne i jego ikonę „hamburgera” na małych ekranach.
  • style.css definiuje niektóre dyrektywy CSS.

5. Poznaj kod

Zależności

Plik package.json określa wymagane zależności biblioteki:

{
  "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"
  }
}

Nasza aplikacja zależy od:

  • firestore: aby uzyskać dostęp do Cloud Firestore z metadanymi obrazów,
  • storage: dostęp do Google Cloud Storage, w którym są przechowywane zdjęcia;
  • express: platforma internetowa dla Node.js,
  • dayjs: mała biblioteka do wyświetlania dat w przystępny sposób,
  • bluebird: biblioteka JavaScriptu do obsługi obietnic;
  • express-fileupload: biblioteka do łatwego przesyłania plików.

Ekspresowe – frontend

Na początku kontrolera index.js musisz zdefiniować wszystkie zależności zdefiniowane wcześniej w 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)

Następnie tworzona jest instancja aplikacji Express.

Używane są 2 elementy pośredniczące Express:

  • Wywołanie express.static() oznacza, że zasoby statyczne będą dostępne w podkatalogu public.
  • Aplikacja fileUpload() konfiguruje przesyłanie plików, aby ograniczyć ich rozmiar do 10 MB, i przesyła pliki lokalnie w systemie plików w pamięci w katalogu /tmp.
const app = express();
app.use(express.static('public'));
app.use(fileUpload({
    limits: { fileSize: 10 * 1024 * 1024 },
    useTempFiles : true,
    tempFileDir : '/tmp/'
}))

Wśród zasobów statycznych znajdują się pliki HTML strony głównej, strony kolażu i strony przesyłania. Te strony będą wywoływać backend interfejsu API. Ten interfejs API będzie miał te punkty końcowe:

  • POST /api/pictures Za pomocą formularza w pliku upload.html zdjęcia będą przesyłane za pomocą żądania POST.
  • GET /api/pictures Ten punkt końcowy zwraca dokument JSON zawierający listę zdjęć i ich etykiet.
  • GET /api/pictures/:name Ten adres URL przekierowuje do miejsca w chmurze, w którym znajduje się obraz w pełnym rozmiarze.
  • GET /api/thumbnails/:name Ten adres URL przekierowuje do lokalizacji miniatury obrazu w miejscu w chmurze.
  • GET /api/collage Ten ostatni adres URL przekierowuje do miejsca w chmurze, w którym znajduje się wygenerowany kolaż.

Przesyłanie zdjęć

Zanim zapoznasz się z kodem Node.js do przesyłania zdjęć, rzuć okiem na public/upload.html.

... 
<form method="POST" action="/api/pictures" enctype="multipart/form-data">
    ... 
    <input type="file" name="pictures">
    <button>Submit</button>
    ... 
</form>
... 

Element formularza wskazuje punkt końcowy /api/pictures z metodą HTTP POST i formatem wieloczęściowym. index.js musi teraz odpowiedzieć na ten punkt końcowy i metodę oraz wyodrębnić pliki:

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('/');
});

Najpierw sprawdź, czy pliki są przesyłane. Następnie pobierasz pliki lokalnie za pomocą metody mv pochodzącej z naszego modułu Node do przesyłania plików. Teraz, gdy pliki są dostępne w lokalnym systemie plików, możesz przesłać zdjęcia do zasobnika Cloud Storage. Na koniec przekieruj użytkownika z powrotem na ekran główny aplikacji.

Wyświetlanie zdjęć

Czas wyświetlić piękne zdjęcia!

W funkcji obsługi /api/pictures sprawdzasz kolekcję pictures w bazie danych Firestore, aby pobrać wszystkie zdjęcia (których miniatury zostały wygenerowane) posortowane według daty utworzenia w kolejności malejącej.

Każde zdjęcie umieszczasz w tablicy JavaScript wraz z jego nazwą, etykietami opisującymi zdjęcie (pochodzącymi z Cloud Vision API), dominującym kolorem i przyjazną datą utworzenia (z dayjs, czyli względnymi przesunięciami czasu, np. „za 3 dni”).

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

Ten kontroler zwraca wyniki w tym formacie:

[
   {
      "name": "IMG_20180423_163745.jpg",
      "labels": [
         "Dish",
         "Food",
         "Cuisine",
         "Ingredient",
         "Orange chicken",
         "Produce",
         "Meat",
         "Staple food"
      ],
      "color": "#e78012",
      "created": "a day ago"
   },
   ...
]

Ta struktura danych jest wykorzystywana przez mały fragment kodu Vue.js ze strony index.html. Oto uproszczona wersja znaczników z tej strony:

<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>

Identyfikator elementu div poinformuje Vue.js, że jest to część kodu, która będzie renderowana dynamicznie. Iteracje są wykonywane dzięki v-for dyrektywom.

Zdjęcia mają ładną kolorową ramkę odpowiadającą dominującemu kolorowi na zdjęciu, który został znaleziony przez interfejs Cloud Vision API. Wskazujemy miniatury i zdjęcia o pełnej szerokości w źródłach linków i obrazów.

Na koniec wyświetlamy etykiety opisujące obraz.

Oto kod JavaScript dla fragmentu Vue.js (w public/app.jspliku zaimportowanym u dołu stronyindex.html):

var app = new Vue({
  el: '#app',
  data() {
    return { pictures: [] }
  },
  mounted() {
    axios
      .get('/api/pictures')
      .then(response => { this.pictures = response.data })
  }
})

Kod Vue używa biblioteki Axios do wysyłania wywołania AJAX do naszego punktu końcowego /api/pictures. Zwrócone dane są następnie powiązane z kodem widoku w znacznikach, które widzieliśmy wcześniej.

Wyświetlanie zdjęć

Z poziomu index.html użytkownicy mogą wyświetlać miniatury zdjęć, klikać je, aby zobaczyć obrazy w pełnym rozmiarze, a z poziomu collage.html mogą wyświetlać obraz collage.png.

W kodzie HTML tych stron obraz src i link href wskazują te 3 punkty końcowe, które przekierowują do lokalizacji w Cloud Storage, gdzie znajdują się zdjęcia, miniatury i kolaż. Nie musisz zakodowywać na stałe ścieżki w znacznikach 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`);
});

Uruchamianie aplikacji Node

Po zdefiniowaniu wszystkich punktów końcowych aplikacja Node.js jest gotowa do uruchomienia. Aplikacja Express domyślnie nasłuchuje na porcie 8080 i jest gotowa do obsługi przychodzących żądań.

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. Testowanie lokalne

Przetestuj kod lokalnie, aby sprawdzić, czy działa, zanim wdrożysz go w chmurze.

Musisz wyeksportować 2 zmienne środowiskowe odpowiadające 2 zasobnikom Cloud Storage:

export BUCKET_THUMBNAILS=thumbnails-${GOOGLE_CLOUD_PROJECT}
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

W folderze frontend zainstaluj zależności npm i uruchom serwer:

npm install; npm start

Jeśli wszystko przebiegło pomyślnie, serwer powinien zostać uruchomiony na porcie 8080:

Started web frontend service on port 8080
- Pictures bucket = uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
- Thumbnails bucket = thumbnails-${GOOGLE_CLOUD_PROJECT}

W tych logach będą widoczne prawdziwe nazwy zasobników, co jest przydatne do debugowania.

W Cloud Shell możesz użyć funkcji podglądu w przeglądarce, aby przeglądać aplikację działającą lokalnie:

82fa3266d48c0d0a.png

Aby wyjść, użyj CTRL-C.

7. Wdrażanie w App Engine

Aplikacja jest gotowa do wdrożenia.

Konfigurowanie App Engine

Sprawdź plik konfiguracji app.yaml App Engine:

runtime: nodejs16
env_variables:
  BUCKET_PICTURES: uploaded-pictures-GOOGLE_CLOUD_PROJECT
  BUCKET_THUMBNAILS: thumbnails-GOOGLE_CLOUD_PROJECT

Pierwszy wiersz deklaruje, że środowisko wykonawcze jest oparte na Node.js 10. Zdefiniowane są 2 zmienne środowiskowe wskazujące 2 koszy: jeden na oryginalne obrazy, a drugi na miniatury.

Aby zastąpić GOOGLE_CLOUD_PROJECT identyfikatorem projektu, możesz uruchomić to polecenie:

sed -i -e "s/GOOGLE_CLOUD_PROJECT/${GOOGLE_CLOUD_PROJECT}/" app.yaml

Wdróż

Ustaw preferowany region dla App Engine. Pamiętaj, aby używać tego samego regionu co w poprzednich ćwiczeniach:

gcloud config set compute/region europe-west1

Wdróż:

gcloud app deploy

Po minucie lub dwóch zobaczysz komunikat informujący, że aplikacja obsługuje ruch:

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

Możesz też otworzyć sekcję App Engine w konsoli Cloud, aby sprawdzić, czy aplikacja została wdrożona, i zapoznać się z funkcjami App Engine, takimi jak obsługa wersji i podział ruchu:

db0e196b00fceab1.png

8. Testowanie aplikacji

Aby przetestować aplikację, otwórz jej domyślny adres URL App Engine (https://<YOUR_PROJECT_ID>.appspot.com/). Powinien się wyświetlić interfejs użytkownika.

6a4d5e5603ba4b73.png

9. Zwalnianie miejsca (opcjonalnie)

Jeśli nie zamierzasz zachować aplikacji, możesz usunąć zasoby, aby zaoszczędzić koszty i być dobrym użytkownikiem chmury. W tym celu usuń cały projekt:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. Gratulacje!

Gratulacje! Ta aplikacja internetowa Node.js hostowana w App Engine łączy wszystkie Twoje usługi i umożliwia użytkownikom przesyłanie i wyświetlanie zdjęć.

Omówione zagadnienia

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Następne kroki