Popularny post Karrol Napisano Marzec 7, 2021 Popularny post Udostępnij Napisano Marzec 7, 2021 Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową #4 - Projekt praktyczny, Hosting Wiemy już czym jest Firebase oraz jak połączyć go z urządzeniami bazującymi na ESP. Teraz zajmiemy się kontaktem z użytkownikiem. Dowiemy się jak zintegrować Auth, RTDB i Firestore ze stroną internetową, tworząc interfejs dla użytkownika. UWAGA: Do pełnego zrozumienia tej część kursy potrzebna jest podstawowa znajomość html i js. Naszym celem jest zapoznanie z Firebase, więc nie będziemy zwracać szczególnej uwagi na pełną poprawność kodów html i js. W internecie jest dostępnych wiele kursów na ten temat i myślę, że każdy zainteresowany znajdzie odpowiedni dla siebie. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » KONFIGURACJA W FIREBASE Na początku musimy dodać aplikację webową do naszego projektu. W tym celu przechodzimy do strony głównej projektu w Firebase i klikamy Dodaj aplikację -> Aplikacja sieciowa oraz nadajemy nazwę naszej aplikacji. Dodawanie nowej aplikacji sieciowej. W następnym kroku wyświetlą się nam dane, których będziemy potrzebować do łączenia plików strony internetowej z Firebase. Widok po dodaniu aplikacji. W każdej chwili możemy też je znaleźć przechodząc do ustawień projektu. Widok danych dostępowych w ustawieniach projektu. ZACZYNAMY! Na początku tworzymy plik html i umieszczamy w nim podstawowy szablon. <html> <head> </head> <body> </body> </html> Między znacznikami <head> </head> dołączamy skrypt do obsługi Firebase oraz skrypty do konkretnych usług, których będziemy używać. Wersję 8.2.9 w poniższym kodzie można zamienić na nowszą. // Dołączenie skryptu do obsługi Firebase <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-app.js"></script> // Dołączenie skryptów dla konkretnych usług <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-firestore.js"></script> // Firestore <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-auth.js"></script> // Auth <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-database.js"></script> // RTDB Jak widać jest to kilka początkowych linijek ze skopiowanych wcześniej danych dostępowych. W tym przykładzie dołączyliśmy Firestore, Auth i RTDB. Sposób dołączania innych usług jest opisany w dokumentacji. Następnie wewnątrz znaczników <body></body> wstawiamy skrypt <script></script> z danymi naszego projektu: var firebaseConfig = { apiKey: "xxxxxxxxxxxxx", authDomain: "xxxx.firebaseapp.com", databaseURL: "https://xxxx-default-rtdb.firebaseio.com", projectId: "xxxx", storageBucket: "xxxx.appspot.com", messagingSenderId: "1234567890", appId: "xxxxxxxxxxxxx" }; Tuż pod spodem inicjalizujemy Firebase i wybrane usługi: // Inicjalizujemy Firebase firebase.initializeApp(firebaseConfig); // Inicjalizujemy Firestore const db = firebase.firestore(); db.settings({ timestampsInSnapshots: true }); // Inicjalizujemy Auth const auth = firebase.auth(); // Inicjalizujemy RTDB const rtdb = firebase.database(); W kolejnym kroku tworzymy plik JavaScript (.js), w którym będzie część logiczna naszej strony i dołączamy go do pliku html. Cały kod html wygląda następująco: <html> <head> <!-- Dołączamy skrypt do obsługi Firebase --> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-app.js"></script> <!-- Dodajemy skrypty do obsługi poszczególnych produktów https://firebase.google.com/docs/web/setup#available-libraries --> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-firestore.js"></script> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-auth.js"></script> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-database.js"></script> </head> <body> <!-- Tutaj jakaś zawartość strony... --> <script> // Wklejamy dane dostępowe do naszego projektu var firebaseConfig = { apiKey: "xxxxxx", authDomain: "xxxxx", databaseURL: "xxxxx", projectId: "xxxxx", storageBucket: "xxxxx", messagingSenderId: "xxxxx", appId: "xxxxx" }; // Inicjalizujemy Firebase firebase.initializeApp(firebaseConfig); // Inicjalizujemy Firestore const db = firebase.firestore(); db.settings({ timestampsInSnapshots: true }); // Inicjalizujemy Auth const auth = firebase.auth(); // Inicjalizujemy RTDB const rtdb = firebase.database(); </script> <!-- Dołączenie naszego pliku .js--> <script src="nazwa.js"></script> </body> </html> AUTH W serwisach internetowych uwierzytelnianie użytkowników jest jedną z podstawowych funkcji. Stworzenie samodzielnie bezpiecznego i sprawnego systemu do zarządzania użytkownikami może być skomplikowane i wymaga dużo pracy. Zaraz zobaczymy jak łatwo da się to zrobić przy użyciu Firebase Auth. Podczas inicjalizacji Auth stworzyliśmy uchwyt do tej usługi o nazwie auth: const auth = firebase.auth(); Z tego powodu wszystkie funkcje, które dotyczą Auth będziemy wywoływać w postaci: auth.nazwaFunkcji() SPRAWDZANIE STANU UŻYTKOWNIKA Na początku nauczymy się sprawdzać czy osoba przeglądająca naszą stronę jest zalogowanym użytkownikiem, czy nie. Służy do tego funkcja onAuthStateChanged(), która wykrywa w czasie rzeczywistym zmianę stanu zalogowania użytkownika. Wewnątrz tej funkcji możemy określić co ma się stać, gdy użytkownik jest zalogowany, a co gdy nie. auth.onAuthStateChanged(user => { if (user) { console.log('ZALOGOWANY: ', user); } else { console.log('NIEZALOGOWANY'); } }) W tym przykładzie user to obiekt przedstawiający naszego użytkownika. Jeżeli użytkownik jest zalogowany to przechowuje informacje o nim, a jeżeli nie jest zalogowany to user wynosi null. Z tego powodu stan zalogowania możemy sprawdzić zwykłą instrukcją warunkową if. Jeżeli użytkownik jest zalogowany to program wypisze w konsoli ZALOGOWANY: dane_użytkownika, a jeżeli nie jest zalogowany to wypisze NIEZALOGOWANY. Oczywiście zamiast tego możemy samodzielnie zdecydować co ma się zadziać np. wywołanie określonej funkcji, przekierowanie na inną stronę itp. Umieszczamy powyższy fragment kodu w pliku .js dołączonym do naszego pliku html i otwieramy go w przeglądarce. Powinna ukazać nam się pusta strona. To dlatego, że nie stworzyliśmy zawartości naszej strony w pliku html, a tylko wypisywaliśmy komunikaty do konsoli. Pusto... Jak się dostać do konsoli? Klikamy prawy przycisk myszy, wybieramy Zbadaj i przechodzimy do zakładki console. Powinniśmy teraz widzieć komunikat wypisany przez nasz program. Widok konsoli. Na razie pojawia się tam NIEZALOGOWANY, ponieważ nie poznaliśmy jeszcze mechanizmu logowania. Zaraz to zmienimy! LOGOWANIE I WYLOGOWYWANIE Do logowania użytkownika za pomocą adresu e-mail i hasła służy funkcja: auth.signInWithEmailAndPassword(email, haslo); Jeżeli dopiszemy to polecenie poniżej polecenia sprawdzającego stan zalogowania (email zamieniamy na “test@test.test”, a hasło na “123456”) i odświeżymy stronę to najpierw otrzymamy komunikat o niezalogowanym użytkowniku, a później o zalogowanym wraz z danymi użytkownika. Widok konsoli - 2 komunikaty. Wynika to z tego, że na początku byliśmy niezalogowani, a zalogowaliśmy się dopiero po sprawdzeniu stanu zalogowania (kod logowania jest poniżej kodu sprawdzania). Jeśli jednak teraz odświeżymy stronę to dostaniemy tylko komunikat o zalogowanym użytkowniku. Wynika to z tego, że przeglądarka zapamiętała nasze zalogowanie i nas nie wylogowała. Widok konsoli - przeglądarka nas pamięta. Jak zatem się wylogować? Służy do tego funkcja: auth.signOut(); Dodajmy ją zatem na końcu naszego kodu i sprawdźmy co się stanie. Widok konsoli. Co tu się wydarzyło!? Chyba nie o to nam chodziło… Najpierw pojawia się komunikat o zalogowanym użytkowniku, co się zgadza, bo byliśmy wcześniej zalogowani. Później komunikat o wylogowaniu, a na końcu znowu o zalogowaniu. Przecież w kodzie umieściliśmy te polecenia w odwrotnej kolejności! Okazuje się, że zalogowanie zajmuje dużo więcej czasu niż wylogowanie, więc wylogowanie dokonało się zanim udało nam się zalogować. Ktoś mógłby się zastanawiać, czy wywołanie funkcji signOut() nie powinno zaczekać na koniec wywołania funkcji signIn(). Otóż nie! Jest to bardzo niebezpieczne zjawisko i może powodować trudne do wykrycia błędy np. gdy postanowimy wyświetlać spersonalizowaną stronę użytkownika i kod ją generujący wywoła się zanim funkcja signIn() skutecznie go zaloguje. Błąd ten da się łatwo naprawić. Wykorzystamy do tego celu funkcję then(), która zapisana po funkcji signIn() zaczeka, aż wykona się ona w całości i dopiero pozwoli na wywołanie funkcji signOut(). Więcej o funkcji then() można dowiedzieć się tutaj. Zamiast signIn i signOut zapisujemy: auth.signInWithEmailAndPassword("test@test.test", "123456").then(function(){auth.signOut();}); Wylogowanie nastąpiło dopiero po zalogowaniu - sukces 😎 Oczywiście nie zawsze użytkownikowi udaje się poprawnie zalogować. Jeżeli przy logowaniu wystąpi błąd, to chcielibyśmy wiedzieć z czego on wynika. Wystarczy, że na końcu naszej funkcji logowania dopiszemy: .catch(function(error) {console.log(error);}); Teraz jeżeli nastąpi błąd podczas logowania to zostanie on wypisany w konsoli. Przykład błędu po podaniu nieprawidłowego hasła. Oprócz wypisania w konsoli całego błędu możemy odczytać tylko jego kod za pomocą error.code i na tej podstawie informować użytkownika o błędzie. Najczęstsze kody błędów to: auth/wrong-password - niepoprawne hasło auth/user-not-found - nie ma takiego użytkownika auth/invalid-email - niepoprawny adres e-mail DODAWANIE UŻYTKOWNIKÓW Z poziomu aplikacji webowej możemy też tworzyć konta nowym użytkownikom. Służy do tego funkcja: auth.createUserWithEmailAndPassword(email, hasło) Po stworzeniu konta użytkownika jest on automatycznie zalogowany. Polecam lekturę dokumentacji, gdzie dokładniej są opisane wszystkie możliwości Auth. RTDB Teraz zajmiemy się obsługą Realtime Database w JavaScript. Podczas inicjalizacji RTDB stworzyliśmy uchwyt do tej usługi o nazwie rtdb. Analogicznie do Auth, wszystkie funkcje dotyczące RTDB będą miały postać: rtdb.nazwaFunkcji(); W części I tego kursu ustawiliśmy reguły RTDB w taki sposób, aby do odczytu i zapisu danych wymagane było zalogowanie. Z tego powodu funkcje operujące na RTDB powinny zostać wywołane po funkcji logowania lub po funkcji sprawdzającej stan zalogowania, a najlepiej wewnątrz then(). POJEDYNCZY ODCZYT Do jednorazowego odczytu danych z bazy służy funkcja get(). Musimy ją poprzedzić funkcją ref() wskazującą na gałąź, którą chcemy odczytać: rtdb.ref('ścieżka').get() //np. rtdb.ref('led/led1').get() Aby uzyskać dostęp do odczytanych danych musimy jeszcze dopisać: .then(function(snapshot) { //treść funkcji }); Dane odczytane z bazy znajdują się wewnątrz obiektu snapshot. Teraz wypiszmy do konsoli wartość zapisaną w 'led/led1': auth.signInWithEmailAndPassword("test@test.test", "123456").then(function () { rtdb.ref('led/led1').get().then(function (snapshot) { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("Nie odczytano danych!"); } }).catch(function (error) { //łapanie błędów console.error(error); }); }).catch(function (error) { console.log(error); }); Widok konsoli z wartością ‘led/led1’. Widok konsoli, gdy zmienimy ścieżkę na ‘led’ (w bazie wewnątrz led znajdują się led1 z wartością true i led2 z wartością false). NASŁUCHIWANIE ZMIAN W CZASIE RZECZYWISTYM Funkcja get() odczytuje dane tylko raz - w momencie wywołania. Jednak czasem potrzebujemy na bieżąco dowiadywać się o nowych zmianach w bazie bez odświeżania strony np. na bieżąco śledzić zmiany temperatury. Umożliwia nam to funkcja on(): rtdb.ref('led').on('value', (snapshot) => { console.log(snapshot.val()); //co ma się wykonać po wykryciu zmiany }); Nową wartość w obserwowanej gałęzi możemy odczytać za pomocą snapshot.val(). Widok konsoli. Każda zmiana wartości w gałęzi ‘led’ to nowy komunikat. W takiej postaci funkcja on() reaguje na każdą zmianę wartości we wskazanej gałęzi. Czasami jednak interesuje nas tylko konkretny rodzaj modyfikacji np. tylko dodanie nowej gałęzi lub tylko zmiana istniejącej wartości. W tym celu należy zmienić ‘value’ na: ‘child_added’ - jeżeli interesuje nas tylko dodanie nowej gałęzi. W snapshot znajduje się tylko wartość nowej gałęzi. Po odświeżeniu strony wypisze pojedynczo wartość wszystkich istniejących gałęzi należących do podanej ścieżki np. oddzielnie wartości led1 i led2 dla ścieżki ‘led’. ‘child_changed’ - jeżeli istnieje nas tylko zmiana wartości w istniejących gałęziach. Po odświeżeniu strony nic nie zwraca dopóki jakaś gałąź nie zostanie zmodyfikowana. ‘child_removed’ - jeżeli interesuje nas tylko usunięcie gałęzi. Zwraca wartość usuniętej gałęzi. ZAPIS DANYCH DO BAZY Zapisać dane do RTDB możemy na 3 sposoby, tak jak w przypadku zapisu za pomocą ESP: set() - zapisuje lub zmienia dane w konkretnym miejscu (konkretna ścieżka) np. możemy zmienić temperaturę w /pomiary/czujnik1/temperatura. Metoda ta spowoduje nadpisanie danych w określonej lokalizacji, w tym wszelkich węzłów podrzędnych. Oznacza to, że zapisując tylko wartość temperatury do lokalizacji /pomiary/czujnik1 utracimy zapisaną wartość wilgotności. update() - zmienia dane w konkretnym miejscu bez nadpisywania pozostałych danych. Oznacza to, że zapisując tylko wartość temperatury do lokalizacji /pomiary/czujnik1 zapisana wartość wilgotności pozostanie bez zmian. push() - dodaje nową gałąź do bazy nadając jej unikalne id. Ma to zastosowanie np. jeżeli chcemy gromadzić historię pomiarów (a nie tylko ostatni). Dane do zapisu, które podajemy jako argument tych funkcji, muszą być w postaci JSON. var json = { wilgotnosc : 58 }; // zmienia się tylko wartość wilgotności - temperatura bez zmian rtdb.ref('pomiary/czujnik1').update(json); // tracimy wartość temperatury rtdb.ref('pomiary/czujnik1').set(json); // dodajemy nową gałąź o automatycznie generowanym id rtdb.ref('pomiary').push(json); Widok początkowy. Po wywołaniu update(). Po wywołaniu set(). Po wywołaniu push(). Jeżeli chcemy wiedzieć jakie id dokumentu wygenerowała funkcja push() możemy podczas jej wywołania dopisać .key: console.log(rtdb.ref('pomiary').push(json).key); FIRESTORE Teraz zajmiemy się obsługą Firestore w JavaScript. Wszystkie funkcje dotyczące Firestore będą miały postać: db.nazwaFunkcji(); W Firestore zamiast ref() używamy .collection("nazwa_kolekcji").doc("nazwa_dokumentu"). Dla Firestore także ustawiliśmy reguły bezpieczeństwa, więc także musimy najpierw zalogować użytkownika. POJEDYNCZY ODCZYT Do jednorazowego odczytu danych z bazy służy funkcja get(). Aby odczytać dane z konkretnego dokumentu musimy użyć jej w takiej postaci: db.collection("nazwa").doc("nazwa").get(); //np. db.collection("pomiary").doc("czujnik2").get(); Aby uzyskać dostęp do odczytanych danych musimy jeszcze dopisać: .then((doc) => {//treść funkcji }); Dane odczytane z bazy znajdują się wewnątrz doc. Za pomocą doc.exists możemy sprawdzić czy jakieś dane zostały pobrane z bazy. Aby odczytać dane musimy użyć: doc.data() W ten sposób uzyskujemy wszystkie dane wskazanego dokumentu w postaci json. Kolekcja pomiary, dokument czujnik2 - uzyskaliśmy wartość temperatury i wilgotności. Jeżeli interesuje nas konkretne pole w danym dokumencie musimy po kropce wpisać jego nazwę: doc.data().nazwa_pola Kolekcja pomiary, dokument czujnik2, doc.data().temperatura. Możemy także odczytać wszystkie dokumenty w kolekcji. W tym przypadku doc.data() i doc.data().nazwa_pola wywołujemy oddzielnie dla każdego odczytanego dokumentu: db.collection("pomiary").get().then((querySnapshot) => { querySnapshot.forEach((doc) => { // funkcja wykonująca się dla każdego dokumentu }); }); // np: // wypisanie id oraz zawartości wszystkich dokumentów w kolekcji “pomiary” db.collection("pomiary").get().then((querySnapshot) => { querySnapshot.forEach((doc) => { console.log(doc.id, " => ", doc.data()); }); }); Wypisanie id oraz zawartości wszystkich dokumentów w kolekcji “pomiary”. Istnieje także możliwość filtrowania wyników - zapytanie zwróci tylko dokumenty spełniające określone warunki. W tym celu pomiędzy collection() a get() musimy dopisać klauzulę where(): where("pole", "warunek", “wartość”) //np: where("temperatura", ">", 50) db.collection("pomiary").where("temperatura", ">", 50).get().then(...) where("temperatura", ">", 50) nie zwraca nam dokumentu czujnik2, bo jest w nim zapisana temperatura 34.5 Wszystkie dostępne warunki zapytań, konstruowanie zapytań złożonych oraz sortowanie kolejności zwracanych dokumentów możemy znaleźć w dokumentacji. NASŁUCHIWANIE ZMIAN W CZASIE RZECZYWISTYM Tak samo jak w RTDB, możemy w czasie rzeczywistym reagować na zmiany w bazie. Możemy ustalić co ma się wydarzyć, gdy dokona się jakakolwiek zmiana w podanej kolekcji lub reagować tylko na konkretny typ zmiany (np. dodanie nowego dokumentu lub modyfikacja już istniejącego): db.collection('pomiary').onSnapshot(snapshot => { let changes = snapshot.docChanges(); changes.forEach(change => { //funkcja wykonująca się dla każdego dokumentu, dla którego zaszła zmiana console.log(change.doc.id, " => ", change.doc.data()); // Możemy rozróżniać typy zmian if(change.type == 'added'){ console.log("Dodano"); } if (change.type === "modified") { console.log("Zmodyfikowano"); } }); }); Przykład dla powyższego kodu - wypisanie początkowe wszystkich dokumentów, modyfikacja wilgotności w czujnik2, dodanie czujnik4. ZAPIS DANYCH DO BAZY Podobnie jak w RTDB, w Firebase mamy 3 sposoby zapisu danych do bazy: set, update i add (zamiast push). Funkcja set nadpisuje istniejący lub tworzy nowy dokument o podanej nazwie (wszystkie wcześniejsze dane zapisane w tym dokumencie są tracone): db.collection("pomiary").doc("czujnik5").set({ temperatura: 12.35, wilgotnosc: 97 }) Dodano czujnik5. Ponowne wywołanie set(), ale tym razem tylko z parametrem temperatury, usunie zapisaną wartość temperatury: db.collection("pomiary").doc("czujnik5").set({ temperatura: 42.35 }) Funkcja add() dodaje do określonej kolekcji nowy dokument o automatycznie generowanym id: db.collection("pomiary").add({ temperatura: 32.35, wilgotnosc: 56 }) Funkcja update() aktualizuje istniejący dokument: db.collection("pomiary").doc("czujnik2").update({ temperatura: 37.75, wilgotnosc: 59 }) Zaktualizowane wartości temperatury i wilgotności. Jeżeli podamy do zaktualizowanie nie wszystkie istniejące pola, to pozostałe pola nie zmienią swojej wartości: db.collection("pomiary").doc("czujnik2").update({ wilgotnosc: 63 }) Jeśli podamy jej dokument, który nie istnieje to wyskoczy błąd: db.collection("pomiary").doc("czujnik99").update({ temperatura: 32.35, wilgotnosc: 56 }) (wiel)BŁĄD 🐪 Za to możemy dodać w dokumencie pola, które wcześniej nie istniały: db.collection("pomiary").doc("czujnik2").update({ nieistnieje: true }) Dodaliśmy pole, które wcześniej nie istniało. Dodatkowo update pozwala nam zapisywać czas serwera... db.collection("pomiary").doc("czujnik2").update({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }) Zapisaliśmy czas serwera. … a także inkrementować wartość wybranego pola: db.collection("pomiary").doc("czujnik2").update({ wilgotnosc: firebase.firestore.FieldValue.increment(1) //inkrementacja o 1 - jedynkę można zastąpić dowolną liczbą }) PODSUMOWANIE Jeżeli udało Ci się wytrzymać do tego momentu to gratuluję! W tych trzech częściach starałem się przekazać jak najwięcej informacji na temat Firebase przez co wyszły one bardzo treściwe. Obiecuję, że to już ostatnia tak nudna część 😉. W następnym odcinku bajki połączymy wszystkie zdobyte do tej pory informacje tworząc prosty system oparty na Firebase, który umieścimy na dedykowanym hostingu. Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową (właśnie to czytasz) 4 Cytuj Link do komentarza Share on other sites More sharing options...
Treker (Damian Szymański) Marzec 9, 2021 Udostępnij Marzec 9, 2021 @Karrol dzięki za kolejny ciekawy poradnik - artykuł został właśnie zaakceptowany, więc jest już widoczny publicznie 🙂 Cytuj Link do komentarza Share on other sites More sharing options...
Pomocna odpowiedź
Dołącz do dyskusji, napisz odpowiedź!
Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!