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.

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:

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 obrazcollage.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
- 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ć.



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

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:

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.jszawiera kod Node.js.package.jsonokreśla zależności biblioteki.app.yamlto plik konfiguracyjny Google App Engine.
Folder public zawiera zasoby statyczne:
index.htmlto strona ze wszystkimi miniaturami i etykietami.collage.html– wyświetla kolaż ostatnich zdjęć.upload.htmlzawiera formularz do przesyłania nowych zdjęć.app.jsużywa Vue.js do wypełniania stronyindex.htmldanymi.script.jsobsługuje menu nawigacyjne i jego ikonę „hamburgera” na małych ekranach.style.cssdefiniuje 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 podkatalogupublic. - 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/picturesZa pomocą formularza w pliku upload.html zdjęcia będą przesyłane za pomocą żądania POST.GET /api/picturesTen punkt końcowy zwraca dokument JSON zawierający listę zdjęć i ich etykiet.GET /api/pictures/:nameTen adres URL przekierowuje do miejsca w chmurze, w którym znajduje się obraz w pełnym rozmiarze.GET /api/thumbnails/:nameTen adres URL przekierowuje do lokalizacji miniatury obrazu w miejscu w chmurze.GET /api/collageTen 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>
</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:

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:

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.

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