Bazując na aplikacji utworzonej w jednym z poprzednich wpisów – Autoryzacja w Firebase, zajmiemy się dziś Realtime Database. To jedna z dwóch baz danych jakie ma do zaoferowania Firebase. Podobnie jak we wcześniejszym wpisie na którym będziemy bazować, tym razem również, aplikacja będzie pisana w czystym JavaScript. Wykonamy CRUDa (Create, Read, Update, Delete) do bazy danych Realtime Database. Aplikacja będzie polegała na przechowywaniu naszych ulubionych filmów wraz z ich ocenami.
Słów kilka o Realtime Database
Bazę tą można traktować jak obiekt JSON przechowywany w chmurze. Nie mamy tu tabel ani rekordów. Po dodaniu danych do bazy, stają się one częścią przechowywanej struktury posiadającej klucz identyfikujący. Firebase daje możliwość zagnieżdżenia danych aż do 32 poziomu. Jednak zaleca się tworzenie najprostszych struktur ze względu na wydajność tworzonej aplikacji. Podczas pobierania danych z bazy, pobierane są również wszystkie jego węzły podrzędne. Co przy dużych projektach może powodować spowolnienie aplikacji. Poniżej znajduje się przykładowa struktura danych.
{ "posts": { "post_id": { "details": { "title": "", "author": "", "tags": [] } "article": "", "comments": { "comment_id": { "body": "", "author": "" }, "comment_id": { … }, "comment_id": { … }, … } } "post_id": { … }, "post_id": { … }, … } }Korzystając z takiej struktury danych, nie potrzebujemy pobierać komentarzy chcąc jedynie wyświetlić tytuł, autora oraz tagi. W sytuacji kiedy chcemy wyświetlić wszystkie dane wystarczy pobrać cały węzeł posts/post_id.
Prezentacja aplikacji
Poniżej znajdują się screeny prezentujące działanie aplikacji.
Strona główna po zalogowaniu.
Formularz do edytowania danych.
Okno potwierdzenia usunięcia wybranego filmu.
Implementacja
Na sam początek dodamy w pliku index.html dodatkowy moduł Firebase wykorzystywany do obsługi Realtime Database oraz Font Awesome. W nagłówku dodajemy ikony.
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">Na samym końcu znacznika <body> dodajemy moduł firebase-database.js.
<script src="https://www.gstatic.com/firebasejs/6.1.1/firebase-database.js"></script>Teraz możemy w pliku app.js dodać klasę Movie, która będzie odpowiedzialna za przechowywanie informacji o filmie.
class Movie { constructor(title, rating, key) { this.title = title; this.rating = rating; this.key = key; } }Mamy tu trzy pola: tytuł, ocena oraz klucz, który przypisywany jest przez Firebase.
Wypełnienie dashboardu
Przechodzimy do pliku index.html i dodajemy część odpowiedzialną za formularz dodawania kolejnych filmów oraz wyświetlanie listy już dodanych.
<div> <div>Ulubione filmy</div> <div> <div> <form> <div> <div> <input type="text" id="movieTitle" placeholder="Tytuł filmu"> </div> <div> <select id="movieRating"> <option value="0" selected disabled>Ocena</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> <option value="6">6</option> <option value="7">7</option> <option value="8">8</option> <option value="9">9</option> <option value="10">10</option> </select> </div> <div> <button type="button" id="movieAddBtn">Dodaj</button> </div> </div> </form> </div> <ul id="movieList"></ul> </div> </div>Implementacja modala
Teraz możemy przystąpić do implementacji modala. W tworzonej aplikacji będziemy potrzebowali dwóch takich elementów. Pierwszy będzie wykorzystywany do edytowania danych, drugi zaś do potwierdzenia usunięcia.
Zaczniemy od utworzenia kodu HTML.
<div id="modal"> <div id="editView"> <div> <h5>Edytuj ulubiony film</h5> </div> <div> <form> <div> <div> <input type="text" id="movieTitleEdit" placeholder="Tytuł filmu"> </div> <div> <select id="movieRatingEdit"> <option value="0" selected disabled>Ocena</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> <option value="6">6</option> <option value="7">7</option> <option value="8">8</option> <option value="9">9</option> <option value="10">10</option> </select> </div> </div> </form> </div> <div> <button type="button" id="editCloseBtn" data-dismiss="modal">Anuluj</button> <button type="button" id="editConfirmBtn">Zapisz zmiany</button> </div> </div> <div id="deleteView"> <div> <h5>Czy na pewno chcesz usunąć film?</h5> </div> <div> <p>Jeśli potwierdzisz film zostanie usunięty bezpowrotnie. Czy jesteś tego pewien?</p> </div> <div> <button type="button" id="deleteCloseBtn" data-dismiss="modal">Anuluj</button> <button type="button" id="deleteConfirmBtn">Usuń</button> </div> </div> </div>Jak możemy zobaczyć powyżej, mamy dwa modale. Pierwszy z nich to editView, drugi to deleteView. Abyśmy mogli edytować dodany wcześniej film w modalu potrzebujemy pola input do zmiany tytułu oraz select wykorzystywany do zmiany oceny.
Modal odpowiedzialny za usuwanie sprowadza się jedynie do wyświetlenia tekstu Czy na pewno chcesz usunąć film? jeżeli potwierdzisz film zostanie usunięty bezpowrotnie. Czy jesteś tego pewien?
Dla obu modali w części będącej stopką znajdują się dwa przyciski odpowiedzialne za anulowanie zmian oraz ich akceptację.
Aby poprawnie się wyświetlały należy dodać kod CSS.
#modal { position: fixed; left: 0; top: 0; width: 100vw; height: 100vh; z-index: 10; display: none; justify-content: center; align-items: center; background: rgba(0, 0, 0, 0.15); } .modal-container { height: 50%; width: 50%; background: #fff; display: flex; justify-content: center; align-items: center; flex-direction: column; } .modal-container div { width: 100%; } .modal-title, .modal-body { width: 100%; text-align: center } #editView, #deleteView { display: none; }Teraz możemy przejść do pliku app.js, gdzie dodamy klasę Modal. Odpowiedzialna będzie za obsługę modali w aplikacji.
class Modal { static getID() { return sessionStorage.getItem('id'); } static setID(id) { sessionStorage.setItem('id', id); } static modalHandler() { return document.getElementById('modal'); } static editView() { return document.getElementById('editView'); } static deleteView() { return document.getElementById('deleteView'); } static showEditModal(title, rating, key) { Modal.setID(key); Modal.modalHandler().style.display = 'flex'; Modal.editView().style.display = 'flex'; document.getElementById('movieTitleEdit').value = title; document.getElementById('movieRatingEdit').value = rating; } static showDeleteModal(key) { Modal.setID(key); Modal.modalHandler().style.display = 'flex'; Modal.deleteView().style.display = 'flex'; } static hideModal() { Modal.modalHandler().style.display = 'none'; Modal.editView().style.display = 'none'; Modal.deleteView().style.display = 'none'; } }Za pomocą dwóch pierwszych metod będziemy odczytywać i zapisywać w pamięci sesyjnej klucz identyfikujący dane do zmodyfikowania lub usunięcia. Kolejne trzy metody odpowiadają za zwrócenie elementu o danym id. Metody showEditModal() i showDeleteModal() odpowiadają za wyświetlenie odpowiednio okna do edycji oraz okna potwierdzającego usunięcie elementu z bazy. Ostatnia metoda zajmuję się ukrywaniem modalów.
Modyfikacja klasy UI
Korzystając z klasy UI z poprzedniego wpisu, rozszerzymy jej funkcjonalność dzięki dziedziczenia. Dzięki temu w kodzie aplikacji będziemy odnosić się do modali dzięki klasy UI, a nie Modal. Następnie dodamy trzy nowe metody: addMovie(), updateMovie() i removeMovie(). Będą one odpowiedzialne za dodawanie, edytowanie oraz usuwanie elementów z listy ulubionych filmów. Poniżej znajduję się kod implementujący tą funkcjonalność.
static addMovie(movie) { const li = document.createElement('li'); li.setAttribute('id', `m${movie.key}`); li.innerHTML = ` <div>${movie.title}</div> <div>${movie.rating}</div> <div> <i aria-hidden="true" onclick="UI.showEditModal('${movie.title}', ${movie.rating}, '${movie.key}')"></i> <i aria-hidden="true" onclick="UI.showDeleteModal('${movie.key}')"></i> </div> `; document.getElementById('movieList').appendChild(li); } static updateMovie(movie) { const li = document.getElementById(`m${movie.key}`); li.innerHTML = ` <div>${movie.title}</div> <div>${movie.rating}</div> <div> <i aria-hidden="true" onclick="UI.showEditModal('${movie.title}', ${movie.rating}, '${movie.key}')"></i> <i aria-hidden="true" onclick="UI.showDeleteModal('${movie.key}')"></i> </div> `; } static removeMovie(key) { const movie = document.getElementById(`m${key}`); movie.parentNode.removeChild(movie); }Pierwsza metoda tworzy nowy element li, dodaje do tego elementu id składające się z litery m oraz klucza uzyskanego z odpowiedzi od Firebase przykładowe id – m-aksdakjask. Następnie uzupełnia go tytułem, oceną oraz przyciskami uruchamiającymi okno edycji oraz okno potwierdzające usunięcie.
Druga metoda znajduje element o id przekazanym dzięki parametru i aktualizuje go.
Ostatnia metoda polega na znalezieniu elementu o id przekazanym w parametrze i usunięciem go.
Implementacja Realtime Database
Implementacje zaczniemy od utworzenia metod nasłuchujących zmian w bazie. W dokumentacji możemy znaleźć informacje o 4 typach nasłuchiwanych zmian: dodawaniu, edytowaniu, usuwaniu i zmianie kolejności:
- child_added – pobiera listę obiektów oraz nasłuchuje dodanie nowych. Wykonuję się dla wszystkich elementu listy oraz po dodaniu do listy nowego obiektu. Otrzymuje od serwera obiekt z listy.
- child_changed – nasłuchuje zmian w elementach listy. Otrzymuje od serwera zmodyfikowany obiekt.
- child_removed – nasłuchuje usuwania elementów. Otrzymuje od serwera usunięty obiekt.
- child_moved – nasłuchuje zmian w kolejności elementów na uporządkowanej liście.
Poniżej znajduję się schemat korzystania z modułu database().
firebase.database().ref(url).on(type, callback);Za pomocą metody database() odwołujemy się do modułu obsługującego bazę danych. Następnie w metodzie ref(url) podajemy ścieżkę pod jaką znajduję się lista elementów, w naszym przypadku będzie to lista ulubionych filmów. Na koniec wywołujemy metodę on(type, callback), która przyjmuje dwa parametry nasłuchiwany typ oraz funkcję wykonującą się po otrzymaniu odpowiedzi od serwera. Poniżej znajduję się kod odpowiedzialny za nasłuchiwaniu zmian w bazie.
static addData() { firebase.database().ref(`blog/users/${firebase.auth().currentUser.uid}/movies`).on('child_added', data => { const movie = new Movie(data.val().title, data.val().rating, data.key); UI.addMovie(movie); }); } static changeData() { firebase.database().ref(`blog/users/${firebase.auth().currentUser.uid}/movies`).on('child_changed', data => { UI.updateMovie(new Movie(data.val().title, data.val().rating, data.key)); }); } static deleteData() { firebase.database().ref(`blog/users/${firebase.auth().currentUser.uid}/movies`).on('child_removed', data => { UI.removeMovie(data.key); }); }Pierwsza metoda uruchamiana jest po dodaniu elementu do bazy. Na podstawie zwróconego obiektu przez Firebase zostaje utworzony obiekt klasy Movie, a następnie zostaje wywołana metoda addMovie(). Druga metoda odpowiada za wprowadzenie zmian w liście po dokonaniu aktualizacji danych w bazie. Ostatnia metoda wykonuję się w sytuacji, kiedy z bazy zostanie usunięty element.
Kolejnym krokiem jaki musimy zrobić jest utworzenie metod odpowiadających za modyfikacje bazy danych.
static addFavoriteMovie() { const movieTitle = document.getElementById('movieTitle').value; const movieRating = document.getElementById('movieRating').value; if (!movieTitle || !movieRating) { return; } const movie = new Movie(movieTitle, movieRating); firebase.database().ref(`blog/users/${firebase.auth().currentUser.uid}/movies`).push().set({ title: movie.title, rating: movie.rating }) .then(response => { document.getElementById('movieTitle').value = ''; document.getElementById('movieRating').value = '0'; }) .catch(error => { showMessageAlert(error.message); }); } static updateFavoriteMovie() { const title = document.getElementById('movieTitleEdit').value; const rating = document.getElementById('movieRatingEdit').value; if (!title || !rating) { return; } firebase.database().ref(`blog/users/${firebase.auth().currentUser.uid}/movies/${Modal.getID()}`).update({ title, rating }) .then(response => {}) .catch(err => {}); } static deleteFavoriteMovie() { if (!Modal.getID()) { return; } firebase.database().ref(`blog/users/${firebase.auth().currentUser.uid}/movies/${Modal.getID()}`).remove() .then(response => {}) .catch(err => {}); }Metoda addFavoriteMovie() odpowiada za dodanie danych do bazy. Na początku pobierane są wartości z formularza, a następnie sprawdzany jest warunek czy któreś z pól nie jest puste. Z pobranych danych tworzony jest obiekty klasy Movie. Mając już przygotowane dane wykorzystujemy moduł database(), w metodzie ref() określamy ścieżkę pod jaką ma zostać dodany obiekt do kolekcji. Następnie wywołujemy metodę pusz() oraz set(). W tej ostatniej przekazujemy obiekt jaki ma zostać zachowany w bazie. W przypadku poprawnego dodanie obiektu do bazy formularz zostanie wyczyszczony. W razie wystąpienia problemów zostanie wyświetlony komunikat z informacją zwróconą przez Firebase.
W przypadku metody updateFavoriteMovie() podobnie jak wcześniej pobieramy dane z formularza i sprawdzamy czy któreś z pól nie jest puste. Następnie po określeniu ścieżki wywołujemy metodę update() przekazując w parametrze obiekt, który ma zostać zmieniony w bazie.
Ostatnia metoda – deleteFavoriteMovie(), odpowiada za usuwanie z bazy obiektu. W metodzie ref() określamy ścieżkę do obiektu, który ma zostać usunięty, a następnie wywołujemy metodę remove().
Pozostało nam zrobić jest wywołanie metod nasłuchujących zmian w bazie oraz dodanie obsługi kliknięcia dla nowych przycisków. Nasłuchiwanie dodajemy w zdarzeniu DOMContentLoaded, tak by po załadowaniu drzewa DOM nastąpiło wczytanie dodanych przez nas filmów. Kod tego zdarzenia znajduję się poniżej.
document.addEventListener('DOMContentLoaded', () => { firebase.auth().onAuthStateChanged(user => { if (user) { UI.updateUsername(); UI.showDashboardView(); Firebase.addData(); Firebase.changeData(); Firebase.deleteData(); } else { UI.showLoginView(); } }); });Względem poprzedniego wpisu, dodane zostały metody Firebase.addData(), Firebase.changeData(), Firebase.deleteData(). Teraz po wczytaniu aplikacji zostaną załadowane dane z bazy i zaprezentowane użytkownikowi.
Ostatnią rzeczą jaką musimy zrobić to obsługa nowych przycisków.
document.getElementById('movieAddBtn').addEventListener('click', e => { Firebase.addFavoriteMovie(); }); document.getElementById('editConfirmBtn').addEventListener('click', e => { Firebase.updateFavoriteMovie(); UI.hideModal(); }); document.getElementById('editCloseBtn').addEventListener('click', e => { UI.hideModal(); }); document.getElementById('deleteConfirmBtn').addEventListener('click', e => { Firebase.deleteFavoriteMovie(); UI.hideModal(); }); document.getElementById('deleteCloseBtn').addEventListener('click', e => { UI.hideModal(); });Pierwsza metoda odpowiada za obsługę kliknięcia przycisku dodającego nowe dane do bazy. Po kliknięciu wywołana zostaje metoda Firebase.addFavoriteMovie(). Następnie mamy obsługę potwierdzenia aktualizacji danych. Przycisk ten znajduję się w modalu odpowiedzialnym za edytowanie danych. Po kliknięciu go następuje wywołanie metody Firebase.updateFavoriteMovie() oraz ukrycie modala. Jak możemy zobaczyć, odwołujemy się do metody hideModal() dzięki klasy UI. Przycisk o identyfikatorze deleteConfirmBtn odpowiada za potwierdzenie usunięcia filmu z bazy i ukryciu okna potwierdzenia. Przyciski editCloseBtn i deleteCloseBtn odpowiedzialne są za anulowanie działań i ukrycie okien.
Podsumowanie
W tym wpisie przygotowaliśmy aplikację wykorzystującą Realtime Database. Jak możemy zobaczyć obsługa tej bazy jest bardzo prosta i daje wiele możliwości. Utworzenie odpowiedniej struktury danych jest najważniejsze dla wydajności tworzonego systemu, ponieważ nieodpowiednia struktura może zdecydowanie spowolnić działanie aplikacji.
Dokumentacja Realtime Database
Opis zalecanej struktury danych