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

1. Omówienie

W ramach tego ćwiczenia w Codelabs utworzysz frontend internetowy w Google App Engine, który pozwoli użytkownikom przesyłać zdjęcia z aplikacji internetowej, a także przeglądać przesłane obrazy i ich miniatury.

21741cd63b425aeb.png

Ta aplikacja internetowa będzie korzystać ze platformy CSS o nazwie Bulma, aby uzyskać atrakcyjny interfejs, oraz ze platformy frontendu JavaScript Vue.JS do wywoływania tworzonego przez Ciebie interfejsu API aplikacji.

Ta aplikacja 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 (tych wykrytych przez interfejs Cloud Vision API w poprzednim module).
  • Strona kolażu z kolażem utworzonym z 4 ostatnio przesłanych zdjęć.
  • Strona przesyłania, na którą użytkownicy mogą przesyłać nowe zdjęcia.

Otrzymany frontend będzie 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 obrazów i ich etykiety przez wywołanie AJAX w adresie URL /api/pictures. Strona główna korzysta z kodu Vue.js do pobierania tych danych.
  • Strona kolaż (collage.html) wskazuje obraz collage.png, z którego składa się 4 najnowsze obrazy.
  • Na stronie przesyłania (upload.html) znajduje się prosty formularz przesyłania zdjęć za pomocą żądania POST na adres URL usługi /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 wykorzystaj już istniejący. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Nazwa projektu jest wyświetlaną nazwą uczestników tego projektu. Jest to ciąg znaków, który nie jest używany przez interfejsy API Google i w każdej chwili możesz go zaktualizować.
  • Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i nie można go zmienić (nie można go zmienić po ustawieniu). Cloud Console automatycznie wygeneruje unikalny ciąg znaków. zwykle nieważne, co ona jest. W większości ćwiczeń w Codelabs musisz odwoływać się do identyfikatora projektu (który zwykle nazywa się PROJECT_ID), więc jeśli Ci się nie podoba, wygeneruj kolejny losowy kod lub wypróbuj swój własny i sprawdź, czy jest dostępny. Potem urządzenie jest „zawieszone”. po utworzeniu projektu.
  • Występuje trzecia wartość – numer projektu – używany przez niektóre interfejsy API. Więcej informacji o wszystkich 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w konsoli Cloud, aby móc korzystać z zasobów i interfejsów API Cloud. Ukończenie tego ćwiczenia z programowania nie powinno kosztować zbyt wiele. Aby wyłączyć zasoby, aby nie naliczać opłat po zakończeniu tego samouczka, wykonaj czynności „wyczyść” znajdziesz na końcu tego ćwiczenia. Nowi użytkownicy Google Cloud mogą skorzystać z programu bezpłatnego okresu próbnego o wartości 300 USD.

Uruchamianie Cloud Shell

Google Cloud można obsługiwać zdalnie z laptopa, ale w ramach tego ćwiczenia z programowania wykorzystasz Google Cloud Shell – środowisko wiersza poleceń działające w Cloud.

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

55efc1aaa7a4d3ad.png

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

7ffe5cbb04455448.png

Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, znacząco zwiększając wydajność sieci i uwierzytelnianie. Wszystkie zadania w tym module możesz wykonać w przeglądarce.

3. Włącz interfejsy API

App Engine wymaga interfejsu Compute Engine API. Upewnij się, że jest włączona:

gcloud services enable compute.googleapis.com

Operacja powinna zakończyć się pomyślnie:

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

4. Klonowanie kodu

Sprawdź kod, jeśli jeszcze nie został:

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

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

cd serverless-photosharing-workshop/frontend

Frontend będzie mieć następujący układ pliku:

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

Głównym elementem naszego projektu są 3 pliki:

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

Folder public zawiera zasoby statyczne:

  • index.html to strona z wszystkimi miniaturami i etykietami
  • collage.html pokazuje kolaż najnowszych zdjęć
  • upload.html zawiera formularz do przesyłania nowych zdjęć
  • app.js używa Vue.js do wypełniania danymi na stronie index.html
  • script.js obsługuje menu nawigacyjne i „hamburger” na małych ekranach
  • style.css definiuje niektóre dyrektywy CSS

5. Zapoznaj się z kodem

Zależności

Plik package.json określa potrzebne 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"
  }
}

Działanie aplikacji zależy od tych czynników:

  • firestore: aby uzyskać dostęp do Cloud Firestore za pomocą metadanych obrazu,
  • storage: aby uzyskać dostęp do Google Cloud Storage, gdzie są przechowywane zdjęcia,
  • express: platforma internetowa dla Node.js,
  • dayjs: mała biblioteka do wyświetlania dat w sposób zrozumiały dla człowieka.
  • bluebird: biblioteka JavaScriptu do celów,
  • express-fileupload: biblioteka do łatwego przesyłania plików.

Express frontend

Na początku kontrolera index.js musisz wymagać wszystkich zależności zdefiniowanych wcześniej w narzędziu 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 zostanie utworzona instancja aplikacji Express.

Wykorzystano 2 szybkie programy pośredniczące:

  • Wywołanie express.static() wskazuje, że w podkatalogu public będą dostępne zasoby statyczne.
  • fileUpload() konfiguruje przesyłanie plików tak, aby ich rozmiar nie przekraczał 10 MB, co spowoduje przesłanie ich lokalnie w systemie plikó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 są pliki HTML strony głównej, strony z kolażem i strony przesyłania. Strony te będą wywoływać backend interfejsu API. Ten interfejs API będzie mieć następujące punkty końcowe:

  • POST /api/pictures Za pomocą formularza w load.html zdjęcia zostaną przesłane za pomocą żądania POST
  • GET /api/pictures Ten punkt końcowy zwraca dokument JSON zawierający listę obrazów i ich etykiety
  • GET /api/pictures/:name Ten adres URL przekierowuje do lokalizacji w chmurze, w której znajduje się pełnowymiarowy obraz
  • GET /api/thumbnails/:name Ten adres URL przekierowuje do lokalizacji, w której znajduje się obraz miniatury w chmurze.
  • GET /api/collage Ten ostatni adres URL przekierowuje do lokalizacji w chmurze, do której został wygenerowany obraz kolażu

Prześlij zdjęcie

Zanim zapoznasz się ze zdjęciami i przesłaniem kodu Node.js, spójrz na stronę 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 rzeczywiście jakieś pliki zostały przesłane. Następnie pobierasz pliki lokalnie za pomocą metody mv pochodzącej z naszego modułu węzła do przesyłania plików. Gdy pliki są już dostępne w lokalnym systemie plików, możesz przesłać je do zasobnika Cloud Storage. Na koniec przekierowujesz użytkownika z powrotem na ekran główny aplikacji.

Wyświetlanie listy zdjęć

Czas pokazać Twoje piękne zdjęcia.

W module obsługi /api/pictures możesz zajrzeć do kolekcji pictures bazy danych Firestore, aby pobrać wszystkie obrazy (które zostały wygenerowane przez miniaturę) uporządkowane według daty utworzenia w kolejności malejącej.

Każdy obraz jest przekazywany w tablicy JavaScriptu z jego nazwą, opisami etykiet (pochodzącymi z Cloud Vision API), kolorem dominującym i przyjazną datą utworzenia (dayjs określa względne przesunięcia czasu, np. „3 dni od teraz”).

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 o tym kształcie:

[
   {
      "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 tagu div wskazuje Vue.js, że jest to część znacznika, która będzie renderowana dynamicznie. Iteracje są wykonywane zgodnie z dyrektywami v-for.

Znalezione przez Cloud Vision API obramowanie zdjęć ma ładne kolorowe obramowanie zgodne z kolorem dominującym na zdjęciu. W źródłach linków i obrazów wskazujemy miniatury i zdjęcia o pełnej szerokości.

Na koniec podajemy etykiety opisujące obraz.

Oto kod JavaScript fragmentu kodu Vue.js (w pliku public/app.js zaimportowanym u dołu strony index.html):

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

Kod Vue korzysta z biblioteki Axios do wywołania AJAX do naszego punktu końcowego /api/pictures. Zwrócone dane są następnie powiązane z kodem widoku w zaobserwowanych wcześniej znacznikach.

Wyświetlanie zdjęć

Od index.html użytkownicy mogą wyświetlać miniatury zdjęć, klikać je, by zobaczyć obrazy w pełnym rozmiarze, a następnie collage.html wyświetlać obraz collage.png.

W znacznikach HTML tych stron obraz src i link href wskazują te 3 punkty końcowe, które przekierowują do lokalizacji zdjęć, miniatur i kolażu w Cloud Storage. Nie ma potrzeby kodowania ścieżki na stałe 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 węzła

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

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. Przetestuj lokalnie

Przed wdrożeniem kodu w chmurze przetestuj go lokalnie, aby upewnić się, że działa.

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 poszło dobrze, serwer powinien uruchamiać się 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ą wyświetlane prawdziwe nazwy zasobników, co jest pomocne przy debugowaniu.

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

82fa3266d48c0d0a.png

Użyj CTRL-C, aby wyjść.

7. Wdrażanie w App Engine

Aplikacja jest gotowa do wdrożenia.

Skonfiguruj App Engine

Sprawdź plik konfiguracji app.yaml dla App Engine:

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

W pierwszym wierszu widać, że środowisko wykonawcze opiera się na Node.js 10. Zdefiniowano dwie zmienne środowiskowe, które wskazują 2 segmenty – oryginalne obrazy i miniatury.

Aby zastąpić GOOGLE_CLOUD_PROJECT rzeczywistym 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 i użyj tego samego regionu w poprzednich modułach:

gcloud config set compute/region europe-west1

I wdróż:

gcloud app deploy

Po kilku minutach zobaczysz informację, ż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 zobaczyć, czy aplikacja została wdrożona, i poznać funkcje App Engine, takie jak obsługa wersji i dzielenie ruchu:

db0e196b00fceab1.png

8. Testowanie aplikacji

Aby przetestować, otwórz domyślny URL App Engine aplikacji (https://<YOUR_PROJECT_ID>.appspot.com/). Interfejs frontendu powinien działać.

6a4d5e5603ba4b73.png

9. Czyszczenie (opcjonalnie)

Jeśli nie chcesz zachować aplikacji, możesz zwolnić zasoby, aby ograniczyć koszty i zachować zgodność z zasadami dotyczącymi chmury, usuwając cały projekt:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

10. Gratulacje!

Gratulacje! Ta aplikacja internetowa Node.js hostowana w App Engine łączy wszystkie usługi i umożliwia użytkownikom przesyłanie i wizualizowanie obrazów.

Omówione zagadnienia

  • App Engine
  • Cloud Storage
  • Cloud Firestore

Następne kroki