1. Omówienie
Celem tego ćwiczenia w Codelabs jest zdobycie doświadczenia w zakresie technologii „bezserwerowych” usługi oferowane przez Google Cloud Platform:
- Cloud Functions – do wdrażania niewielkich jednostek logiki biznesowej w formie funkcji, które reagują na różne zdarzenia (wiadomości Pub/Sub, nowe pliki w Cloud Storage, żądania HTTP itp.)
- App Engine – do wdrażania i udostępniania aplikacji internetowych, internetowych interfejsów API, backendów mobilnych, zasobów statycznych z możliwością szybkiego skalowania w górę i w dół
- Cloud Run – do wdrażania i skalowania kontenerów zawierających dowolny język, środowisko wykonawcze lub bibliotekę.
Poznanie sposobów wykorzystania tych usług bezserwerowych do wdrażania i skalowania interfejsów API typu REST oraz interfejsów API typu REST.
W tym warsztacie utworzymy moduł „Eksplorator półki na książki”:
- za pomocą funkcji w Cloud Functions: aby zaimportować początkowy zbiór danych z książkami dostępnymi w naszej bibliotece, w bazie danych dokumentów Cloud Firestore;
- kontener Cloud Run, który będzie udostępniać interfejs API REST nad zawartością naszej bazy danych;
- Interfejs internetowy App Engine: do przeglądania listy książek przez wywołanie interfejsu API REST.
Tak będzie wyglądać internetowy frontend. Na koniec tego ćwiczenia z programowania:
Czego się nauczysz
- Cloud Functions
- Cloud Firestore
- Cloud Run
- App Engine
2. Konfiguracja i wymagania
Samodzielne konfigurowanie środowiska
- 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ć.
- Nazwa projektu jest wyświetlaną nazwą uczestników tego projektu. To ciąg znaków, który nie jest używany przez interfejsy API Google. W każdej chwili możesz ją zaktualizować.
- Identyfikator projektu jest unikalny we wszystkich projektach Google Cloud i nie można go zmienić (po jego ustawieniu nie można go zmienić). Cloud Console automatycznie wygeneruje unikalny ciąg znaków. zwykle nieważne, co ona jest. W większości ćwiczeń w Codelabs musisz podać swój identyfikator projektu (zwykle identyfikowany jako
PROJECT_ID
). Jeśli nie podoba Ci się wygenerowany identyfikator, możesz wygenerować kolejny losowy. Możesz też spróbować własnych sił i sprawdzić, czy jest dostępna. Po wykonaniu tej czynności nie można jej już zmienić. Pozostanie ona przez cały czas trwania projektu. - Jest jeszcze trzecia wartość, numer projektu, z którego korzystają niektóre interfejsy API. Więcej informacji o wszystkich 3 wartościach znajdziesz w dokumentacji.
- Następnie musisz włączyć płatności w Cloud Console, aby korzystać z zasobów Cloud/interfejsów API. Ukończenie tego ćwiczenia z programowania nic nie kosztuje. Aby wyłączyć zasoby w celu uniknięcia naliczania opłat po zakończeniu tego samouczka, możesz usunąć utworzone zasoby lub projekt. 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 chmurze.
W konsoli Google Cloud kliknij ikonę Cloud Shell na górnym pasku narzędzi:
Uzyskanie dostępu do środowiska i połączenie się z nim powinno zająć tylko kilka chwil. Po zakończeniu powinno pojawić się coś takiego:
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 ramach tego ćwiczenia z programowania można wykonywać w przeglądarce. Nie musisz niczego instalować.
3. Przygotowywanie środowiska i włączanie interfejsów API w chmurze
Aby umożliwić korzystanie z usług, które będą nam potrzebne w trakcie tego projektu, włączymy kilka interfejsów API. Zrobimy to, uruchamiając w Cloud Shell to polecenie:
$ gcloud services enable \ appengine.googleapis.com \ cloudbuild.googleapis.com \ cloudfunctions.googleapis.com \ compute.googleapis.com \ firestore.googleapis.com \ run.googleapis.com
Po pewnym czasie operacja powinna się zakończyć:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
Skonfigurujemy też zmienną środowiskową, która będzie nam przy tym potrzebna: region chmury, w którym wdrożymy naszą funkcję, aplikację i kontener:
$ export REGION=europe-west3
Dane będą przechowywane w bazie danych Cloud Firestore, więc trzeba ją utworzyć:
$ gcloud app create --region=${REGION} $ gcloud firestore databases create --location=${REGION}
W dalszej części tego ćwiczenia z programowania, podczas implementacji interfejsu API REST, będziemy musieli sortować i filtrować dane. W tym celu utworzymy 3 indeksy:
$ gcloud firestore indexes composite create --collection-group=books \ --field-config field-path=language,order=ascending \ --field-config field-path=updated,order=descending $ gcloud firestore indexes composite create --collection-group=books \ --field-config field-path=author,order=ascending \ --field-config field-path=updated,order=descending
Te 3 indeksy odpowiadają zapytaniom wg autora lub języka, zachowując kolejność w kolekcji za pomocą zaktualizowanego pola.
4. Pobierz kod
Pobierz kod z tego repozytorium GitHub:
$ git clone https://github.com/glaforge/serverless-web-apis
Kod aplikacji jest napisany w Node.JS.
W tym module dostępna będzie taka struktura folderów:
serverless-web-apis | ├── data | ├── books.json | ├── function-import | ├── index.js | ├── package.json | ├── run-crud | ├── index.js | ├── package.json | ├── Dockerfile | ├── appengine-frontend ├── public | ├── css/style.css | ├── html/index.html | ├── js/app.js ├── index.js ├── package.json ├── app.yaml
Odpowiednie foldery:
data
– ten folder zawiera przykładowe dane listy 100 książek.function-import
– ta funkcja udostępnia punkt końcowy umożliwiający importowanie przykładowych danych.run-crud
– ten kontener udostępni interfejs Web API dostęp do danych książek przechowywanych w Cloud Firestore.appengine-frontend
– ta aplikacja internetowa App Engine wyświetli prosty frontend tylko do odczytu do przeglądania listy książek.
5. Przykładowe dane biblioteki książek
W folderze danych znajduje się plik books.json
zawierający listę 100 książek, które zapewne warto przeczytać. Ten dokument JSON jest tablicą zawierającą obiekty JSON. Przyjrzyjmy się kształtowi danych, które pozyskamy za pomocą funkcji w Cloud Functions:
[
{
"isbn": "9780435272463",
"author": "Chinua Achebe",
"language": "English",
"pages": 209,
"title": "Things Fall Apart",
"year": 1958
},
{
"isbn": "9781414251196",
"author": "Hans Christian Andersen",
"language": "Danish",
"pages": 784,
"title": "Fairy tales",
"year": 1836
},
...
]
Wszystkie wpisy naszych książek w tej tablicy zawierają następujące informacje:
isbn
– kod ISBN-13 identyfikujący książkę.author
– imię i nazwisko autora książki.language
– język mówiony, w którym napisana jest książka.pages
– liczba stron w książce.title
– tytuł książki.year
– rok publikacji książki.
6. Punkt końcowy funkcji do importowania przykładowych danych książki
W pierwszej sekcji wdrożymy punkt końcowy, który posłuży do importowania danych przykładowych książek. Do tego celu użyjemy Cloud Functions.
Zapoznaj się z kodem
Przyjrzyjmy się najpierw plikowi package.json
:
{
"name": "function-import",
"description": "Import sample book data",
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/firestore": "^4.9.9"
},
"devDependencies": {
"@google-cloud/functions-framework": "^3.1.0"
},
"scripts": {
"start": "npx @google-cloud/functions-framework --target=parseBooks"
}
}
W zależnościach środowiska wykonawczego potrzebny jest tylko moduł NPM @google-cloud/firestore
, aby uzyskać dostęp do bazy danych i przechowywać dane książki. Środowisko wykonawcze Cloud Functions udostępnia też platformę Express w środowisku internetowym, więc nie trzeba deklarować jej jako zależności.
W zależnościach programistycznych deklarujemy platformę funkcji (@google-cloud/functions-framework
), czyli platformę środowiska wykonawczego używaną do wywoływania funkcji. Jest to platforma open source, której możesz też używać lokalnie na swoim komputerze (w naszym przypadku w Cloud Shell) do uruchamiania funkcji bez wdrażania przy każdej zmianie, co usprawnia pętlę informacji zwrotnych podczas programowania.
Aby zainstalować zależności, użyj polecenia install
:
$ npm install
Skrypt start
wykorzystuje platformę funkcji do utworzenia polecenia, którego można użyć do lokalnego uruchomienia funkcji z tą instrukcją:
$ npm start
Aby wejść w interakcję z funkcją, możesz użyć narzędzia curl lub podglądu internetowego Cloud Shell.
Spójrzmy teraz na plik index.js
, który zawiera funkcje logiczne naszej funkcji importu danych książek:
const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');
Tworzymy instancję modułu Firestore i wskazujemy kolekcję książek (podobnie jak w przypadku tabeli w relacyjnych bazach danych).
functions.http('parseBooks', async (req, resp) => {
if (req.method !== "POST") {
resp.status(405).send({error: "Only method POST allowed"});
return;
}
if (req.headers['content-type'] !== "application/json") {
resp.status(406).send({error: "Only application/json accepted"});
return;
}
...
})
Eksportujemy funkcję JavaScript parseBooks
. Tę funkcję zadeklarujemy przy jej późniejszym wdrożeniu.
Oto kilka kolejnych instrukcji, które pozwolą Ci to sprawdzić:
- Akceptujemy tylko żądania HTTP
POST
. W przeciwnym razie zwracamy kod stanu405
wskazujący, że inne metody HTTP są niedozwolone. - Akceptujemy tylko ładunki typu
application/json
. W przeciwnym razie wysyłamy kod stanu406
, który wskazuje, że nie jest to dopuszczalny format ładunku.
const books = req.body;
const writeBatch = firestore.batch();
for (const book of books) {
const doc = bookStore.doc(book.isbn);
writeBatch.set(doc, {
title: book.title,
author: book.author,
language: book.language,
pages: book.pages,
year: book.year,
updated: Firestore.Timestamp.now()
});
}
Następnie możemy pobrać ładunek JSON za pomocą żądania body
. Przygotowujemy operację wsadową Firestore, która ma na celu zbiorcze przechowywanie wszystkich książek. Wykonujemy iterację tablicy JSON zawierającej szczegóły książki, przechodząc przez pola isbn
, title
, author
, language
, pages
i year
. Kod ISBN książki będzie służyć jako jej klucz podstawowy lub identyfikator.
try {
await writeBatch.commit();
console.log("Saved books in Firestore");
} catch (e) {
console.error("Error saving books:", e);
resp.status(400).send({error: "Error saving books"});
return;
};
resp.status(202).send({status: "OK"});
Teraz gdy spora ilość danych jest gotowa, możemy zatwierdzić operację. Jeśli operacja związana z pamięcią się nie powiedzie, zwracamy kod stanu 400
informujący o niepowodzeniu. W przeciwnym razie możemy zwrócić odpowiedź OK z kodem stanu 202
wskazującym, że żądanie zapisu zbiorczego zostało zaakceptowane.
Uruchamianie i testowanie funkcji importowania
Przed uruchomieniem kodu zainstalujemy zależności z:
$ npm install
Aby uruchomić funkcję lokalnie za pomocą platformy Functions, użyjemy polecenia skryptu start
zdefiniowanego w package.json
:
$ npm start > start > npx @google-cloud/functions-framework --target=parseBooks Serving function... Function: parseBooks URL: http://localhost:8080/
Aby wysłać żądanie HTTP POST
do funkcji lokalnej, możesz uruchomić:
$ curl -d "@../data/books.json" \ -H "Content-Type: application/json" \ http://localhost:8080/
Po uruchomieniu tego polecenia wyświetlą się następujące dane wyjściowe, które będą potwierdzać, że funkcja działa lokalnie:
{"status":"OK"}
Możesz też sprawdzić w interfejsie Cloud Console, czy dane są rzeczywiście przechowywane w Firestore:
Na powyższym zrzucie ekranu widać utworzoną kolekcję books
, listę dokumentów książki oznaczonych kodem ISBN oraz szczegóły konkretnej pozycji książki (po prawej).
Wdrażanie funkcji w chmurze
Aby wdrożyć funkcję w Cloud Functions, użyjemy w katalogu function-import
tego polecenia:
$ gcloud functions deploy bulk-import \ --gen2 \ --trigger-http \ --runtime=nodejs20 \ --allow-unauthenticated \ --max-instances=30 --region=${REGION} \ --source=. \ --entry-point=parseBooks
Wdrażamy funkcję z nazwą symboliczną bulk-import
. Ta funkcja jest wyzwalana przez żądania HTTP. Korzystamy ze środowiska wykonawczego Node.JS 20. Wdrażamy funkcję publicznie (najlepiej powinniśmy zabezpieczyć ten punkt końcowy). Określamy region, w którym ma się znajdować funkcja. Wskazujemy źródła w katalogu lokalnym i używamy parseBooks
(wyeksportowanej funkcji JavaScriptu) jako punktu wejścia.
Po kilku minutach funkcja zostanie wdrożona w chmurze. Funkcja powinna być widoczna w interfejsie konsoli Cloud:
W danych wyjściowych wdrożenia powinien być widoczny adres URL funkcji zgodny z określoną konwencją nazewnictwa (https://${REGION}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/${FUNCTION_NAME}
). Adres URL aktywatora HTTP znajdziesz też w interfejsie konsoli Cloud na karcie aktywatora:
Możesz też pobrać adres URL za pomocą wiersza poleceń, używając gcloud
:
$ export BULK_IMPORT_URL=$(gcloud functions describe bulk-import \ --region=$REGION \ --format 'value(httpsTrigger.url)') $ echo $BULK_IMPORT_URL
Przechowujemy ją w zmiennej środowiskowej BULK_IMPORT_URL
, abyśmy mogli ponownie ją wykorzystać do testowania wdrożonej funkcji.
Testowanie wdrożonej funkcji
Korzystając z podobnego polecenia curl, które użyliśmy wcześniej do przetestowania funkcji działającej lokalnie, przetestujemy wdrożoną funkcję. Jedyną zmianą będzie adres URL:
$ curl -d "@../data/books.json" \ -H "Content-Type: application/json" \ $BULK_IMPORT_URL
Jeśli operacja się uda, powinien zwrócić następujący wynik:
{"status":"OK"}
Po wdrożeniu i gotowości funkcji importowania oraz przesłaniu przykładowych danych nadszedł czas na opracowanie interfejsu API REST do udostępniania tego zbioru danych.
7. Umowa dotycząca interfejsu API REST
Chociaż nie definiujemy umowy dotyczącej interfejsu API na przykład za pomocą specyfikacji Open API, przyjrzymy się różnym punktom końcowym interfejsu API typu REST.
Interfejs API wymienia obiekty JSON, które obejmują:
isbn
(opcjonalnie) – 13-znakowy kodString
reprezentujący prawidłowy kod ISBN,author
– niepuste poleString
reprezentujące imię i nazwisko autora książki,language
– niepuste poleString
zawierające język, w którym została napisana książka;pages
– wartość dodatniaInteger
dla liczby stron książki,title
– niepuste poleString
z tytułem książki,year
– wartośćInteger
oznaczająca rok publikacji książki.
Przykładowy ładunek książki:
{
"isbn": "9780435272463",
"author": "Chinua Achebe",
"language": "English",
"pages": 209,
"title": "Things Fall Apart",
"year": 1958
}
POBIERZ /books
Pobierz listę wszystkich książek, potencjalnie przefiltrowanych według autora lub języka, podzielonej na strony w okresie do 10 wyników naraz.
Ładunek: brak.
Parametry zapytania:
author
(opcjonalnie) – filtruje listę książek według autora,language
(opcjonalnie) – filtruje listę książek według języka,page
(opcjonalny, domyślny = 0) – wskazuje pozycję strony wyników do zwrócenia.
Zwraca tablica JSON obiektów książki.
Kody stanu:
200
– gdy żądanie pobierze listę książek,400
– jeśli wystąpi błąd;
POST /books i POST /books/{isbn}
Opublikuj nowy ładunek książki, z parametrem ścieżki isbn
(w takim przypadku kod isbn
nie jest wymagany w ładunku książki) lub bez niego (w takim przypadku kod isbn
musi znajdować się w ładunku książki).
Ładunek treści: obiekt książki.
Parametry zapytania: brak.
Zwraca: brak.
Kody stanu:
201
– po zapisaniu książki406
– jeśli kodisbn
jest nieprawidłowy,400
– jeśli wystąpi błąd;
POBIERZ /books/{isbn}
Pobiera książkę z biblioteki, która jest identyfikowana za pomocą kodu isbn
, przekazywana jako parametr ścieżki.
Ładunek: brak.
Parametry zapytania: brak.
Zwraca obiekt JSON książki lub obiekt błędu, jeśli książka nie istnieje.
Kody stanu:
200
– jeśli książka znajduje się w bazie danych,400
– jeśli wystąpi błąd,404
– jeśli nie udało się znaleźć książki,406
– jeśli kodisbn
jest nieprawidłowy.
PUT /books/{isbn}
Aktualizuje istniejącą książkę wskazywaną przez parametr isbn
przekazywany jako parametr ścieżki.
Ładunek treści: obiekt książki. Można przekazać tylko te pola, które wymagają aktualizacji, a pozostałe są opcjonalne.
Parametry zapytania: brak.
Zwraca zaktualizowaną książkę.
Kody stanu:
200
– po zaktualizowaniu książki,400
– jeśli wystąpi błąd,406
– jeśli kodisbn
jest nieprawidłowy.
USUŃ /books/{isbn}
Usuwa istniejącą książkę wskazaną za pomocą parametru isbn
przekazywanego jako parametr ścieżki.
Ładunek: brak.
Parametry zapytania: brak.
Zwraca: brak.
Kody stanu:
204
– po usunięciu książki400
– jeśli wystąpi błąd;
8. Wdrażanie i udostępnianie interfejsu API typu REST w kontenerze
Zapoznaj się z kodem
Dockerfile
Przyjrzyjmy się najpierw elementowi Dockerfile
, który odpowiada za konteneryzację naszego kodu aplikacji:
FROM node:20-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "node", "index.js" ]
Używamy obrazu "slim" Node.JS 20. Pracujemy w katalogu /usr/src/app
. Kopiujemy plik package.json
(szczegóły poniżej), który definiuje między innymi nasze zależności. Instalujemy zależności za pomocą dodatku npm install
, kopiując kod źródłowy. Na koniec wskazujemy sposób uruchamiania aplikacji za pomocą polecenia node index.js
.
package.json
Teraz możemy przyjrzeć się plikowi package.json
:
{
"name": "run-crud",
"description": "CRUD operations over book data",
"license": "Apache-2.0",
"engines": {
"node": ">= 20.0.0"
},
"dependencies": {
"@google-cloud/firestore": "^4.9.9",
"cors": "^2.8.5",
"express": "^4.17.1",
"isbn3": "^1.1.10"
},
"scripts": {
"start": "node index.js"
}
}
Określamy, że chcemy korzystać z Node.JS 14, tak jak w przypadku Dockerfile
.
Nasza aplikacja internetowego interfejsu API zależy od tych czynników:
- moduł NPM Firestore pozwalający na dostęp do danych książki w bazie danych,
- Biblioteka
cors
do obsługi żądań CORS (międzynarodowe udostępnianie zasobów), ponieważ nasz interfejs API REST jest wywoływany z kodu klienta frontendu aplikacji internetowej App Engine. - Platforma Express, która będzie naszą internetową platformą do projektowania interfejsu API,
- Moduł
isbn3
, który pomaga w sprawdzaniu kodów ISBN książek.
Określamy również skrypt start
, który będzie przydatny do lokalnego uruchamiania aplikacji do celów programistycznych i testowania.
index.js
Przejdźmy do sedna kodu i przyjrzyjmy się bliżej elementowi index.js
:
const Firestore = require('@google-cloud/firestore');
const firestore = new Firestore();
const bookStore = firestore.collection('books');
Wymagany jest moduł Firestore oraz odwołanie do kolekcji books
, w której są przechowywane dane książki.
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
const querystring = require('querystring');
const cors = require('cors');
app.use(cors({
exposedHeaders: ['Content-Length', 'Content-Type', 'Link'],
}));
Do wdrożenia interfejsu API REST używamy Express, czyli platformy internetowej. Używamy modułu body-parser
do analizowania ładunków JSON wymienianych za pomocą naszego interfejsu API.
Moduł querystring
przydaje się do manipulowania adresami URL. Będzie to możliwe podczas tworzenia nagłówków Link
do podziału na strony (więcej informacji na ten temat później).
Następnie konfigurujemy moduł cors
. Przekazujemy nagłówki, które mają być przekazywane za pomocą CORS, ponieważ większość z nich jest usuwana, ale tutaj chcemy zachować zwykłą długość i typ treści oraz nagłówek Link
określony na potrzeby podziału na strony.
const ISBN = require('isbn3');
function isbnOK(isbn, res) {
const parsedIsbn = ISBN.parse(isbn);
if (!parsedIsbn) {
res.status(406)
.send({error: `Invalid ISBN: ${isbn}`});
return false;
}
return parsedIsbn;
}
Do analizy i sprawdzenia kodów ISBN użyjemy modułu NPM isbn3
. Opracujemy też małą funkcję użytkową, która będzie analizowała kody ISBN, a jeśli kody ISBN będą nieprawidłowe, w odpowiedzi umieścimy kod stanu 406
.
GET /books
Przyjrzyjmy się fragmentowi punktu końcowego GET /books
:
app.get('/books', async (req, res) => {
try {
var query = new Firestore().collection('books');
if (!!req.query.author) {
console.log(`Filtering by author: ${req.query.author}`);
query = query.where("author", "==", req.query.author);
}
if (!!req.query.language) {
console.log(`Filtering by language: ${req.query.language}`);
query = query.where("language", "==", req.query.language);
}
const page = parseInt(req.query.page) || 0;
// - - ✄ - - ✄ - - ✄ - - ✄ - - ✄ - -
} catch (e) {
console.error('Failed to fetch books', e);
res.status(400)
.send({error: `Impossible to fetch books: ${e.message}`});
}
});
Przygotowujemy zapytanie do bazy danych. To zapytanie będzie zależało od opcjonalnych parametrów zapytania, które pozwalają filtrować według autora lub języka. Wyświetlamy także listę 10 książek we fragmentach.
Jeśli podczas pobierania książek wystąpi błąd, zwracamy kod stanu 400.
Powiększmy przyciętą część tego punktu końcowego:
const snapshot = await query
.orderBy('updated', 'desc')
.limit(PAGE_SIZE)
.offset(PAGE_SIZE * page)
.get();
const books = [];
if (snapshot.empty) {
console.log('No book found');
} else {
snapshot.forEach(doc => {
const {title, author, pages, year, language, ...otherFields} = doc.data();
const book = {isbn: doc.id, title, author, pages, year, language};
books.push(book);
});
}
W poprzedniej sekcji przefiltrowano według tych danych: author
i language
, ale w tej sekcji posortujemy listę książek według daty ostatniej aktualizacji (najpierw ostatnia aktualizacja). Wynik dzielimy też na strony, definiując limit (liczbę elementów do zwrócenia) i przesunięcie (punkt początkowy, od którego ma zostać zwrócona kolejna grupa książek).
Wykonujemy zapytanie, pobieramy zrzut danych i umieszczamy wyniki w tablicy JavaScript, która zostanie zwrócona na końcu funkcji.
Zakończmy opisywanie tego punktu końcowego, korzystając z dobrej metody: użyj nagłówka Link
do definiowania linków URI prowadzących do pierwszej, poprzedniej, następnej lub ostatniej strony danych (w naszym przypadku podamy tylko informacje o poprzedniej i następnej stronie).
var links = {};
if (page > 0) {
const prevQuery = querystring.stringify({...req.query, page: page - 1});
links.prev = `${req.path}${prevQuery != '' ? `?${prevQuery}` : ''}`;
}
if (snapshot.docs.length === PAGE_SIZE) {
const nextQuery = querystring.stringify({...req.query, page: page + 1});
links.next = `${req.path}${nextQuery != '' ? `?${nextQuery}` : ''}`;
}
if (Object.keys(links).length > 0) {
res.links(links);
}
res.status(200).send(books);
Na początku logika może się wydawać nieco skomplikowana, ale robimy to, dodając poprzedni link, jeśli nie jesteśmy na pierwszej stronie danych. Dodajemy też link next, jeśli strona zawiera już wszystkie dane (np. zawiera maksymalną liczbę książek określoną przez stałą PAGE_SIZE
zakładając, że istnieje inna taka strona z dodatkowymi danymi). Następnie za pomocą funkcji resource#links()
Express tworzymy właściwy nagłówek z właściwą składnią.
W przypadku Twoich informacji nagłówek linku będzie wyglądał mniej więcej tak:
link: </books?page=1>; rel="prev", </books?page=3>; rel="next"
POST /books
iPOST /books/:isbn
Oba punkty końcowe służą do utworzenia nowej książki. Jeden z nich przekazuje kod ISBN w ładunku książki, a drugi jako parametr ścieżki. W obu przypadkach wywołaj naszą funkcję createBook()
:
async function createBook(isbn, req, res) {
const parsedIsbn = isbnOK(isbn, res);
if (!parsedIsbn) return;
const {title, author, pages, year, language} = req.body;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.set({
title, author, pages, year, language,
updated: Firestore.Timestamp.now()
});
console.log(`Saved book ${parsedIsbn.isbn13}`);
res.status(201)
.location(`/books/${parsedIsbn.isbn13}`)
.send({status: `Book ${parsedIsbn.isbn13} created`});
} catch (e) {
console.error(`Failed to save book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to create book ${parsedIsbn.isbn13}: ${e.message}`});
}
}
Sprawdzamy, czy kod isbn
jest prawidłowy. W przeciwnym razie zwracany przez funkcję (i ustawia kod stanu 406
). Pobieramy pola książki z ładunku przekazanego w treści żądania. Szczegóły książki zapiszemy w Firestore. Zwrócono 201
w przypadku powodzenia, 400
w przypadku niepowodzenia.
Po pomyślnym powrocie ustawiamy też nagłówek lokalizacji, aby dać wskazówki dla klienta interfejsu API, w którym znajduje się nowo utworzony zasób. Nagłówek będzie wyglądać tak:
Location: /books/9781234567898
GET /books/:isbn
Pobierzmy z Firestore książkę identyfikowaną za pomocą numeru ISBN.
app.get('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
const docSnapshot = await docRef.get();
if (!docSnapshot.exists) {
console.log(`Book not found ${parsedIsbn.isbn13}`)
res.status(404)
.send({error: `Could not find book ${parsedIsbn.isbn13}`});
return;
}
console.log(`Fetched book ${parsedIsbn.isbn13}`, docSnapshot.data());
const {title, author, pages, year, language, ...otherFields} = docSnapshot.data();
const book = {isbn: parsedIsbn.isbn13, title, author, pages, year, language};
res.status(200).send(book);
} catch (e) {
console.error(`Failed to fetch book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to fetch book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
Jak zawsze sprawdzamy, czy numer ISBN jest prawidłowy. Aby pobrać książkę, wykonujemy zapytanie do Firestore. Właściwość snapshot.exists
pozwala sprawdzić, czy książka rzeczywiście została znaleziona. W przeciwnym razie odsyłamy błąd i kod stanu „Nie znaleziono” 404
. Pobieramy dane książki i tworzymy reprezentujący ją obiekt JSON, który ma zostać zwrócony.
PUT /books/:isbn
Aktualizujemy istniejącą książkę za pomocą metody PUT.
app.put('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.set({
...req.body,
updated: Firestore.Timestamp.now()
}, {merge: true});
console.log(`Updated book ${parsedIsbn.isbn13}`);
res.status(201)
.location(`/books/${parsedIsbn.isbn13}`)
.send({status: `Book ${parsedIsbn.isbn13} updated`});
} catch (e) {
console.error(`Failed to update book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to update book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
Aktualizujemy pole daty/godziny updated
, aby zapamiętać datę i godzinę ostatniej aktualizacji tego rekordu. Korzystamy ze strategii {merge:true}
, która zastępuje istniejące pola nowymi wartościami. W przeciwnym razie wszystkie pola zostaną usunięte i zostaną zapisane tylko nowe pola w ładunku, co spowoduje wymazanie istniejących pól z poprzedniej aktualizacji lub początkowego utworzenia.
Ustawiliśmy też nagłówek Location
tak, by wskazywał identyfikator URI książki.
DELETE /books/:isbn
Usuwanie książek jest całkiem proste. W odwołaniach do dokumentu nazywamy po prostu metodę delete()
. Zwracamy kod stanu 204, ponieważ nie zwracamy żadnych treści.
app.delete('/books/:isbn', async (req, res) => {
const parsedIsbn = isbnOK(req.params.isbn, res);
if (!parsedIsbn) return;
try {
const docRef = bookStore.doc(parsedIsbn.isbn13);
await docRef.delete();
console.log(`Book ${parsedIsbn.isbn13} was deleted`);
res.status(204).end();
} catch (e) {
console.error(`Failed to delete book ${parsedIsbn.isbn13}`, e);
res.status(400)
.send({error: `Impossible to delete book ${parsedIsbn.isbn13}: ${e.message}`});
}
});
Uruchamianie serwera Express / Node
Uruchamiamy serwer, domyślnie nasłuchując na porcie 8080
:
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Books Web API service: listening on port ${port}`);
console.log(`Node ${process.version}`);
});
Uruchamianie aplikacji lokalnie
Aby uruchomić aplikację lokalnie, zainstalujemy najpierw zależności z:
$ npm install
Możemy zacząć od:
$ npm start
Serwer uruchomi się w trybie localhost
i domyślnie nasłuchuje na porcie 8080.
Możesz też utworzyć kontener Dockera i uruchomić obraz kontenera za pomocą tych poleceń:
$ docker build -t crud-web-api . $ docker run --rm -p 8080:8080 -it crud-web-api
Użycie Dockera w kontenerze to także świetny sposób na sprawdzenie, czy konteneryzacja naszej aplikacji będzie działać prawidłowo, ponieważ kompilujemy ją w chmurze za pomocą Cloud Build.
Testowanie interfejsu API
Niezależnie od tego, jak uruchamiamy kod interfejsu API REST (bezpośrednio przez węzeł czy za pomocą obrazu kontenera Dockera), teraz możemy wykonywać wobec niego kilka zapytań.
- Utwórz nową książkę (numer ISBN w ładunku głównym):
$ curl -XPOST -d '{"isbn":"9782070368228","title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \ -H "Content-Type: application/json" \ http://localhost:8080/books
- Utwórz nową książkę (numer ISBN w parametrze ścieżki):
$ curl -XPOST -d '{"title":"Book","author":"me","pages":123,"year":2021,"language":"French"}' \ -H "Content-Type: application/json" \ http://localhost:8080/books/9782070368228
- Usuwanie książki (utworzonej przez nas):
$ curl -XDELETE http://localhost:8080/books/9782070368228
- Pobieranie książki według numeru ISBN:
$ curl http://localhost:8080/books/9780140449136 $ curl http://localhost:8080/books/9782070360536
- Aktualizowanie istniejącej książki przez zmianę jej tytułu:
$ curl -XPUT \ -d '{"title":"Book"}' \ -H "Content-Type: application/json" \ http://localhost:8080/books/9780003701203
- Pobierz listę książek (pierwszych 10):
$ curl http://localhost:8080/books
- Znajdź książki napisane przez konkretnego autora:
$ curl http://localhost:8080/books?author=Virginia+Woolf
- Wymień książki napisane po angielsku:
$ curl http://localhost:8080/books?language=English
- Wczytaj czwartą stronę książek:
$ curl http://localhost:8080/books?page=3
Możemy też łączyć parametry zapytania author
, language
i books
, aby zawęzić wyszukiwanie.
Tworzenie i wdrażanie skonteneryzowanego interfejsu API typu REST
Cieszymy się, że interfejs API typu REST działa zgodnie z planem, dlatego nadszedł właściwy moment na jego wdrożenie w chmurze, w Cloud Run.
Zrobimy to w 2 krokach:
- Najpierw utwórz obraz kontenera za pomocą Cloud Build za pomocą tego polecenia:
$ gcloud builds submit \ --tag gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api
- Następnie wdrażając usługę przy użyciu tego drugiego polecenia:
$ gcloud run deploy run-crud \ --image gcr.io/${GOOGLE_CLOUD_PROJECT}/crud-web-api \ --allow-unauthenticated \ --region=${REGION} \ --platform=managed
Po pierwszym poleceniu Cloud Build skompiluje obraz kontenera i będzie go hostować w Container Registry. Następne polecenie wdraża obraz kontenera z rejestru i wdraża go w regionie chmury.
W interfejsie konsoli Cloud możemy dokładnie sprawdzić, czy nasza usługa Cloud Run znajduje się teraz na liście:
Ostatni krok, który wykonasz w tym miejscu, to pobranie adresu URL niedawno wdrożonej usługi Cloud Run za pomocą tego polecenia:
$ export RUN_CRUD_SERVICE_URL=$(gcloud run services describe run-crud \ --region=${REGION} \ --platform=managed \ --format='value(status.url)')
W następnej sekcji będziemy potrzebować adresu URL naszego interfejsu API typu REST Cloud Run, ponieważ kod frontendu App Engine będzie współdziałał z tym interfejsem API.
9. Hostowanie aplikacji internetowej do przeglądania biblioteki
Ostatnim elementem układanki, który ma wzbogacić ten projekt, jest zapewnienie internetowego frontendu, który będzie współdziałał z naszym interfejsem API REST. W tym celu wykorzystamy Google App Engine z kodem JavaScript klienta, który będzie wywoływać interfejs API za pomocą żądań AJAX (za pomocą interfejsu API Fetch po stronie klienta).
Nasza aplikacja, chociaż została wdrożona w środowisku wykonawczym Node.JS App Engine, składa się w większości z zasobów statycznych. Nie ma dużej ilości kodu w backendzie, ponieważ większość interakcji użytkownika jest realizowana w przeglądarce przez JavaScript po stronie klienta. Nie będziemy korzystać z żadnej rozbudowanej platformy JavaScript frontendowej, ale użyjemy kodu JavaScript „vanilla” z kilkoma komponentami internetowymi, które zostaną dodane do interfejsu użytkownika z użyciem biblioteki komponentów sieciowych Shoelace:
- pole wyboru języka książki:
- komponent karty do wyświetlania szczegółów konkretnej książki (w tym kodu kreskowego reprezentującego jej numer ISBN), który korzysta z biblioteki JsBarcode:
- oraz przycisk wczytywania kolejnych książek z bazy danych:
Po połączeniu wszystkich tych elementów strona internetowa, na której można przeglądać naszą bibliotekę, będzie wyglądać tak:
Plik konfiguracji app.yaml
Zacznijmy od zapoznania się z bazą kodu tej aplikacji App Engine od przyjrzenia się jej plikowi konfiguracji app.yaml
. Jest to plik specyficzny dla App Engine i umożliwia skonfigurowanie takich elementów jak zmienne środowiskowe, różne „moduły obsługi” aplikacji czy określenie, że niektóre zasoby są zasobami statycznymi, które będą obsługiwane przez wbudowaną sieć CDN App Engine.
runtime: nodejs14
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
handlers:
- url: /js
static_dir: public/js
- url: /css
static_dir: public/css
- url: /img
static_dir: public/img
- url: /(.+\.html)
static_files: public/html/\1
upload: public/(.+\.html)
- url: /
static_files: public/html/index.html
upload: public/html/index\.html
- url: /.*
secure: always
script: auto
Określamy, że nasza aplikacja to Node.JS i chcemy użyć wersji 14.
Następnie definiujemy zmienną środowiskową wskazującą adres URL naszej usługi Cloud Run. Musimy zaktualizować zmienną CHANGE_ME, podając prawidłowy URL (poniżej znajdziesz informacje, jak to zmienić).
Następnie definiujemy różne moduły obsługi. Pierwsze trzy wskazują lokalizację kodu po stronie klienta HTML, CSS i JavaScript w folderze public/
i jego podfolderach. Czwarty z nich wskazuje, że główny adres URL naszej aplikacji App Engine powinien wskazywać stronę index.html
. Dzięki temu nie będziemy widzieć sufiksu index.html
w adresie URL podczas uzyskiwania dostępu do katalogu głównego witryny. Ostatni jest domyślnym, który skieruje wszystkie pozostałe adresy URL (/.*
) do naszej aplikacji Node.JS (tj. jej części „dynamiczne” w odróżnieniu od opisanych zasobów statycznych).
Zaktualizujmy teraz adres URL interfejsu Web API w usłudze Cloud Run.
W katalogu appengine-frontend/
uruchom to polecenie, aby zaktualizować zmienną środowiskową wskazującą adres URL naszego interfejsu API typu REST opartego na Cloud Run:
$ sed -i -e "s|CHANGE_ME|${RUN_CRUD_SERVICE_URL}|" app.yaml
Możesz też ręcznie zmienić ciąg CHANGE_ME
w app.yaml
, podając prawidłowy URL:
env_variables:
RUN_CRUD_SERVICE_URL: CHANGE_ME
Plik Node.JS package.json
{
"name": "appengine-frontend",
"description": "Web frontend",
"license": "Apache-2.0",
"main": "index.js",
"engines": {
"node": "^14.0.0"
},
"dependencies": {
"express": "^4.17.1",
"isbn3": "^1.1.10"
},
"devDependencies": {
"nodemon": "^2.0.7"
},
"scripts": {
"start": "node index.js",
"dev": "nodemon --watch server --inspect index.js"
}
}
Jeszcze raz podkreślamy, że aplikacja powinna działać w środowisku Node.JS 14. Polegamy na platformie Express oraz module isbn3
NPM na potrzeby weryfikacji książek. Kody ISBN.
W zależnościach programistycznych użyjemy modułu nodemon
, aby monitorować zmiany w plikach. Choć możemy uruchamiać aplikację lokalnie przy użyciu npm start
, wprowadzić pewne zmiany w kodzie, zatrzymać aplikację przy użyciu ^C
i uruchomić ją ponownie, jest to trochę uciążliwe. Zamiast tego możemy użyć następującego polecenia, aby automatycznie ponownie załadować aplikację po wprowadzeniu zmian:
$ npm run dev
Kod Node.JS index.js
const express = require('express');
const app = express();
app.use(express.static('public'));
const bodyParser = require('body-parser');
app.use(bodyParser.json());
Wymagana jest platforma internetowa Express. Wskazujemy, że katalog publiczny zawiera zasoby statyczne, które mogą być udostępniane (przynajmniej wtedy, gdy działa lokalnie w trybie programisty) przez oprogramowanie pośredniczące static
. Na koniec wymagamy body-parser
, aby analizuje nasze ładunki JSON.
Spójrzmy na kilka zdefiniowanych przez nas tras:
app.get('/', async (req, res) => {
res.redirect('/html/index.html');
});
app.get('/webapi', async (req, res) => {
res.send(process.env.RUN_CRUD_SERVICE_URL);
});
Pierwsza pasująca do pliku /
przekieruje do index.html
w naszym katalogu public/html
. Ponieważ w trybie programisty nie działamy w środowisku wykonawczym App Engine, nie otrzymujemy routingu adresów URL App Engine. Zamiast tego po prostu przekierowujemy główny adres URL do pliku HTML.
Drugi punkt końcowy, który zdefiniujemy /webapi
, zwróci adres URL naszego interfejsu API typu REST Cloud RUN. Dzięki temu kod JavaScript po stronie klienta będzie wiedzieć, gdzie należy wywołać listę książek.
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Book library web frontend: listening on port ${port}`);
console.log(`Node ${process.version}`);
console.log(`Web API endpoint ${process.env.RUN_CRUD_SERVICE_URL}`);
});
Na koniec używamy aplikacji internetowej Express i domyślnie nasłuchujemy na porcie 8080.
Strona index.html
Nie przyjrzymy się każdemu wierszowi tej długiej strony HTML. Zwróćmy uwagę na kilka najważniejszych wierszy.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/themes/base.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.37/dist/shoelace.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/barcodes/JsBarcode.ean-upc.min.js"></script>
<script src="/js/app.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css">
Pierwsze 2 wiersze importują bibliotekę komponentów sieciowych Shoelace (skrypt i arkusz stylów).
W następnym wierszu zaimportujemy bibliotekę JsBarcode, by utworzyć kody kreskowe kodów ISBN książek.
Ostatnie wiersze importują własny kod JavaScript i arkusz stylów CSS, które znajdują się w naszych podkatalogach public/
.
W kodzie body
strony HTML używamy komponentów Shoelace wraz z ich tagami elementów niestandardowych, np.:
<sl-icon name="book-half"></sl-icon>
...
<sl-select id="language-select" placeholder="Select a language..." clearable>
<sl-menu-item value="English">English</sl-menu-item>
<sl-menu-item value="French">French</sl-menu-item>
...
</sl-select>
...
<sl-button id="more-button" type="primary" size="large">
More books...
</sl-button>
...
Używamy też szablonów HTML i ich funkcji wypełniania przedziałów do reprezentowania książki. Utworzymy kopie tego szablonu, by zapełnić listę książek, i zastąpimy wartości w boksach informacjami o książkach:
<template id="book-card">
<sl-card class="card-overview">
...
<slot name="author">Author</slot>
...
</sl-card>
</template>
Kwestionowanie kodu HTML jest już prawie gotowe. Została jeszcze jedna sprawa: kod JavaScript app.js
po stronie klienta, który wchodzi w interakcje z naszym interfejsem API REST.
Kod JavaScript app.js po stronie klienta
Zaczynamy od detektora zdarzeń najwyższego poziomu, który czeka na wczytanie treści DOM:
document.addEventListener("DOMContentLoaded", async function(event) {
...
}
Następnie możemy skonfigurować kilka kluczowych stałych i zmiennych:
const serverUrlResponse = await fetch('/webapi');
const serverUrl = await serverUrlResponse.text();
console.log('Web API endpoint:', serverUrl);
const server = serverUrl + '/books';
var page = 0;
var language = '';
Najpierw pobierzemy adres URL naszego interfejsu API typu REST za pomocą kodu węzła App Engine, który zwraca zmienną środowiskową ustawioną na początku w polu app.yaml
. Dzięki zmiennej środowiskowej – punktowi końcowemu /webapi
– wywoływanemu z kodu JavaScriptu po stronie klienta – nie musieliśmy zakodować na stałe adresu URL interfejsu API REST w naszym kodzie frontendu.
Definiujemy również zmienne page
i language
, które służą do śledzenia podziału na strony i filtrowania języka.
const moreButton = document.getElementById('more-button');
moreButton.addEventListener('sl-focus', event => {
console.log('Button clicked');
moreButton.blur();
appendMoreBooks(server, page++, language);
});
Do przycisku ładowania książek dodajemy moduł obsługi zdarzeń. Kliknięcie spowoduje wywołanie funkcji appendMoreBooks()
.
const langSelect = document.getElementById('language-select');
langSelect.addEventListener('sl-change', event => {
page = 0;
language = event.srcElement.value;
document.getElementById('library').replaceChildren();
console.log(`Language selected: "${language}"`);
appendMoreBooks(server, page++, language);
});
Podobnie jak w przypadku pola wyboru, dodajemy moduł obsługi zdarzeń, który powiadamia o zmianach w wybranym języku. Podobnie jak w przypadku przycisku, wywołujemy funkcję appendMoreBooks()
, przekazując URL interfejsu API REST, bieżącą stronę i wybór języka.
Spójrzmy na tę funkcję, która pobiera i dołącza książki:
async function appendMoreBooks(server, page, language) {
const searchUrl = new URL(server);
if (!!page) searchUrl.searchParams.append('page', page);
if (!!language) searchUrl.searchParams.append('language', language);
const response = await fetch(searchUrl.href);
const books = await response.json();
...
}
Powyżej tworzymy dokładny URL, którego będziemy używać do wywoływania interfejsu API REST. Istnieją 3 parametry zapytania, które zwykle możemy określić, ale w tym miejscu w interfejsie użytkownika określiliśmy tylko 2 z nich:
page
– liczba całkowita wskazująca bieżącą stronę podziału książek na strony,language
– ciąg tekstowy języka do filtrowania według języka pisanego.
Następnie używamy interfejsu Fetch API, aby pobrać tablicę JSON ze szczegółami naszej książki.
const linkHeader = response.headers.get('Link')
console.log('Link', linkHeader);
if (!!linkHeader && linkHeader.indexOf('rel="next"') > -1) {
console.log('Show more button');
document.getElementById('buttons').style.display = 'block';
} else {
console.log('Hide more button');
document.getElementById('buttons').style.display = 'none';
}
W zależności od tego, czy w odpowiedzi znajduje się nagłówek Link
, wyświetlimy lub ukryjemy przycisk [More books...]
, ponieważ nagłówek Link
wskazuje, czy można jeszcze wczytać więcej książek (w nagłówku Link
będzie się znajdował adres URL next
).
const library = document.getElementById('library');
const template = document.getElementById('book-card');
for (let book of books) {
const bookCard = template.content.cloneNode(true);
bookCard.querySelector('slot[name=title]').innerText = book.title;
bookCard.querySelector('slot[name=language]').innerText = book.language;
bookCard.querySelector('slot[name=author]').innerText = book.author;
bookCard.querySelector('slot[name=year]').innerText = book.year;
bookCard.querySelector('slot[name=pages]').innerText = book.pages;
const img = document.createElement('img');
img.setAttribute('id', book.isbn);
img.setAttribute('class', 'img-barcode-' + book.isbn)
bookCard.querySelector('slot[name=barcode]').appendChild(img);
library.appendChild(bookCard);
...
}
}
W powyższej sekcji funkcji dla każdej książki zwróconej przez interfejs API REST sklonujemy szablon z kilkoma komponentami internetowymi reprezentującymi książkę i wstawimy w boksy szablonu szczegółowe informacje na ten temat.
JsBarcode('.img-barcode-' + book.isbn).EAN13(book.isbn, {fontSize: 18, textMargin: 0, height: 60}).render();
Aby kod ISBN był nieco ładniejszy, używamy biblioteki JsBarcode do tworzenia kodu kreskowego, np. na tylnej okładce prawdziwych książek.
Lokalne uruchamianie i testowanie aplikacji
Na razie wystarczy kod. Czas zobaczyć, jak działa aplikacja. Najpierw przeprowadzimy to lokalnie w Cloud Shell, a dopiero potem w rzeczywistości.
Moduły NPM wymagane przez naszą aplikację instalujemy za pomocą:
$ npm install
Uruchamiamy aplikację przy użyciu zwykłego systemu:
$ npm start
Z kolei automatyczne ponowne wczytywanie zmian dzięki funkcji nodemon
:
$ npm run dev
Aplikacja działa lokalnie i możemy uzyskać do niej dostęp w przeglądarce na stronie http://localhost:8080
.
Wdrażanie aplikacji App Engine
Skoro mamy pewność, że nasza aplikacja działa prawidłowo lokalnie, czas wdrożyć ją w App Engine.
Aby wdrożyć aplikację, uruchom następujące polecenie:
$ gcloud app deploy -q
Aplikacja powinna zostać wdrożona po około minucie.
Aplikacja będzie dostępna pod adresem URL kształtu: https://${GOOGLE_CLOUD_PROJECT}.appspot.com
.
Poznawanie interfejsu naszej aplikacji internetowej App Engine
Teraz możesz:
- Aby wczytać więcej książek, kliknij przycisk
[More books...]
. - Wybierz konkretny język, aby zobaczyć książki tylko w tym języku.
- Możesz wyczyścić zaznaczenie, używając małego krzyżyka w polu wyboru, aby wrócić do listy wszystkich książek.
10. 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}
11. Gratulacje!
Dzięki Cloud Functions, App Engine i Cloud Run stworzyliśmy zestaw usług, które pozwalają udostępniać różne punkty końcowe Web API i frontend internetowy oraz przechowywać, aktualizować i przeglądać bibliotekę książek zgodnie z dobrymi wzorcami projektowania interfejsu API REST.
Omówione zagadnienia
- Cloud Functions
- Cloud Firestore
- Cloud Run
- App Engine
Co dalej
Jeśli chcesz dokładniej zapoznać się z tym konkretnym przykładem i go rozwinąć, oto lista kwestii, które warto zbadać:
- Skorzystaj z bramki interfejsów API, aby udostępnić wspólną fasadę interfejsu API dla funkcji importu danych i kontenera interfejsu API REST, dodając takie funkcje jak obsługa kluczy API w celu uzyskiwania dostępu do interfejsu API lub definiowanie ograniczeń liczby żądań dla użytkowników interfejsu API.
- Wdróż moduł węzła Swagger-UI w aplikacji App Engine, aby udokumentować i udostępnić testowy interfejs API REST.
- We frontendzie (oprócz dotychczasowych funkcji przeglądania) dodaj dodatkowe ekrany edycji danych i utwórz nowe wpisy książki. Ponieważ korzystamy też z bazy danych Cloud Firestore, możesz korzystać z funkcji w czasie rzeczywistym, aby aktualizować wyświetlane dane książek po wprowadzeniu zmian.