To jest repozytorium szkoleniowe w tematyce aplikacji czasu rzeczywistego. W ramach szkolenia zostanie zaimplementowana aplikacja do wzywania pomocy podczas eventów szkoleniowych. Do implementacji została wykorzystana paczka ws, będąca niskopoziomową, lekką implementacją techonologii WebSockets w środowisku node.
Serwer aplikacji został napisany w typescript, natomiast front w czystym JS, z wykorzystaniem HTML5 i css3.
Repozytorium składa się z branchy podzielonych na kolejne etapy, następujące po sobie, zachowujące spójność, pomiędzy którymi można swobodnie się przełączać i kontynuować szkolenie od wybranego momentu. Ponadto każdy etap posiada przykład rozwiązania etapu w postaci diffa pomiędzy branchami kolejnych etapów.
Do szkolenia potrzebne są:
-
Node w wersji nie starszej niż
11.x.x -
Przeglądarka internetowa
-
Najnowszy
ChromelubFirefox -
Edytor kodu wspierający
typescript-
vscode -
WebStorm
-
-

-
Sklonować repozytorium
-
Zainstalować zależności zależności
yarnalbonpm i
-
Zmienić branch na
etap-0
Dodać prosty serwer HTTP serwujący pliki statyczne z folderu public.
-
Hello world serwera HTTP (
/src/index.ts)-
Utworzyć serwer przy wykorzystaniu funkcji
createServerze wbudowanego modułuhttp-
Zapisać do stałej
server -
Przekazać handler serwera w postaci funkcji
(request, response) => {...}-
Dodać blok
try catch, który obejmie cały kod handlera-
w przypadku błędu:
-
wyświetlić błąd do konsoli
-
wysłać odpowiedź w postać
e.toString()gdzieeto wyłapany błąd przezcatch
-
-
-
Handler serwera w odpowiedzi na wszystkie zapytania zwraca tekst
Test server- Wykorzystać metodę
endobiekturesponse
- Wykorzystać metodę
-
-
-
Dodać nasłuchiwanie serwera na porcie wykorzystując metodę
listenobiektuserver-
Przekazać jako pierwszy argument stałą
PORT -
Jako drugi funkcję, która wywoła się po uruchomieniu serwera
- Użyć
console.logaby sprawdzić czy serwer rozpoczął nasłuchiwanie na porcie
- Użyć
-
-
-
Zmodyfikować handler serwera HTTP, tak aby zwracał pliki statyczne
-
Zapisać do stałej
urladres URL z obiektu zapytania wykorzystującrequest.url-
W przypadku gdy adres jest równy
/ustawić wartośćindex.html- Wykorzystać operator
? :
- Wykorzystać operator
-
-
Utworzyć stałą
urlPartsz wartościąurl.split('.') -
Utworzyć stałą
fileExtensionz wartością ostatniego elementu tablicyurlParts -
Utworzyć stałą
contentTypez wartością z mapyFILE_EXTENSION_TO_CONTENT_TYPE, której kluczami są rozszerzenia plików -
Ustawić status odpowiedzi wykorzystując metodę
response.writeHeadobiektu odpowiedzi-
Jako pierwszy argument przekazać status odpowiedzi równy
200 -
Jako drugi argument przekazać obiekt
{ 'Content-Type': contentType }
-
-
Wczytać plik
-
Wykorzystać funckje
readFileSyncze wbudowanego modułufs-
Zapisać do stałej
file -
Jako pierwszy argument przekazać absolutną ścieżkę do pliku
- Do pobrania ścieżki projektu skorzystać z
proces.cwd()
- Do pobrania ścieżki projektu skorzystać z
-
-
-
Wykorzystać metodę
response.endaby zakończyć zapytanie przekazując do metody zawartość wczytanego pliku
-
Ustanowić stałe połaączenie pomiędzy klientem a serwerem wykorzystując WebSockety
-
Utworzyć nową instancję serwera
new WebSocket.Servero nazwiewebSocketsServer- Przekazać do konstruktora obiekt konfiguracyjny z kluczem
server, wskazujący na referencje do serwera HTTP
- Przekazać do konstruktora obiekt konfiguracyjny z kluczem
-
Dodać do serwera WebSockets nasłuchiwanie na event połączenia o nazwie
connectionprzy wykorzystaniu metodyon-
Klasa
Serverz pakietuwsdziedziczy do klasieEventEmmiter -
Handler eventu
connectionjako argument wywołania dostaje socket, który reprezentuje połączenie z klientem-
Wypisać do konsoli
socket connected -
Odesłać wiadomość powitalną o treści
welcomewykorzystującsocket.send
-
-
-
Dodać do
webSocketsServernasłuchiwanie na eventmessageprzy wykorzystaniu metodyon- Wypisać do konsoli dane eventu dostępne w argumencie handlera
-
Dodać do
webSocketsServernasłuchiwanie na eventcloseprzy wykorzystaniu metodyon- Wypisać do konsoli
socket closed
- Wypisać do konsoli
-
W handlerze eventu
DOMContentLoadedstworzyć nowe polaczeniem do serweraWebSockets-
Utworzyć instancję socketa wykorzystując klasę
WebSocketso nazwiesocket- Konstruktor przyjmuje argument typu
string, który reprezentuje adres serwera WebSocketsws://localhost:5000
- Konstruktor przyjmuje argument typu
-
Zaimplementować obsługę eventów:
-
onopen- wywoływany po ustanowieniu połączenia z serwerem- W reakcji na event:
console.log(['WebSocket.onopen'], event);
- W reakcji na event:
-
onmessage- wywoływany przy każdej wiadomości serwera- W reakcji na event:
console.log(['WebSocket.onmessage'], event);
- W reakcji na event:
-
onerror- wywoływany przy każdym błędzie komunikacji z serwerem- W reakcji na event:
console.log(['WebSocket.onerror'], event);
- W reakcji na event:
-
onclose- wywoływany w sytuacji kiedy serwer zakończy połączenie z socketem- W reakcji na event:
console.log(['WebSocket.onclose'], event);
- W reakcji na event:
-
-
Obsłużyć logowanie użytkowników poprzez stworzenie obiektu reprezentującego użytkownika i dodanie go do odpowiedniej kolekcji, odpowiednio na uczestników oraz trenerów.
- Dodać funkcję do wysyłania eventów do serwera WebSocket:
const sendEvent = (action, payload) => {
try {
socket.send(JSON.stringify({ action, payload }));
}
catch (e) {
console.error(e);
}
};-
Na ekranie powitalnym (funkcja
renderLandingView)-
WAŻNE:
renderTemplateByIdmusi być zawsze wywołane w pierwszej kolejności, inaczej elementy ekranu nie będą wyrenderowane, nie będzie można z nimi nic zrobić -
Dodać nasłuchiwanie na kliknięcie w element z
id="loginParticipant"wykorzystującaddEventListenerorazgetNodeById- Przekazać nową funkcję jako handler funkcję
renderParticipantLoginView
- Przekazać nową funkcję jako handler funkcję
-
Analogicznie zrobić dla elementu z
id="loginTrainer"
-
-
Ekran logowania uczestnika (funkcja
renderParticipantLoginView)-
Dodać nasłuchiwanie na event
submitna elemencie formularza zid="participantLoginForm"-
Zablokować domyślne działanie zdarzenia poprzez
event.preventDefault(); -
Wykorzystać
FormDatado zebrania danych z formulurza-
const formData = new FormData(event.target); -
Dostęp do danych
formData.get(group)gdziegroupdo wartość atrybutunameelementu input formularza -
Nazwy pól w formularzu:
-
name -
group
-
-
-
Wykorzystać funkcję
sendEventdo wysłania eventu do serwera WebSocket, przekazać obiekt z kluczami:-
actiono wartościPARTICIPANT_LOGIN -
payloado wartości danych z formularza w postaci obiektu, gdzie nazwy pól to klucze
-
-
-
-
Ekran logowania trenera
-
Wykonać analogicznie dla ekranu logowania uczestnika, z takimi różnicami
-
Akcja
TRAINER_LOGIN -
Wysłać tylko pole
name
-
-
-
Usunąć wysłanie wiadomości powitalnej
socket.send('welcome');
-
Dodać obiekt na poziomie pliku, który będzie reprezentował stan serwera
- Obiekt zawiera dwie kolekcje zawierające podłączonych użytkowników
const state: State = { participants: [], trainers: [], };
-
Obiekt reprezentujący podłączonego użytkownika
-
id- identyfikator użytkownika -
data- dane zebrane podczas logowania -
socket- referencja do socketa użytkownika
-
-
Na poziomie pliku dodać funkcję wysyłające event dbającą o obsługę błędu
const sendEvent = (socket: WebSocket, event: Event): void => { try { socket.send(JSON.stringify(event)); } catch (e) { console.error(e); } };
-
Po połączeniu (event
connection) stworzyć w domknięciu stałą reprezentującą połączonego użytkownikaconst connectedUser: User = { id: `user-id-${Date.now()}`, data: { name: '', group: '', }, socket, };
-
W evencie
message:-
Sparsować argument eventu zrzutowany do
string(.toString()) wykorzystującJSON.parse- Zapisać do stałych pola obiektu
actionorazpayload
- Zapisać do stałych pola obiektu
-
Dodać prosty system akcji przy wykorzystaniu instrukcji warunkowej
switch-
Wykorzystać
actionw instrukcjiswitchswitch (action as Action) { case 'PARTICIPANT_LOGIN': { ... break; } case 'TRAINER_LOGIN': { ... break; } default: { console.error('unknown action'); } }
-
Dodać obsługę akcji
PARTICIPANT_LOGIN-
Zaktualizować dane połączonego użytkownika
connectedUser.datazawartościąpayload -
Dodać połączonego użytkownika do kanału uczestników
state.participants -
Wysłać akcję
PARTICIPANT_LOGGEDz pustympayload
-
-
Dodać obsługę akcji
TRAINER_LOGIN-
Zaktualizować dane połączonego użytkownika
connectedUser.datazawartościąpayload -
Dodać połączonego użytkownika do kanału uczestników
state.trainers -
Wysłać akcję
TRAINER_LOGGEDz pustympayload
-
-
-
-
Obsługa wysyłania sygnału pomocy przez uczestnika.
-
W evencie
onmessagedodać prosty system nasłuchiwania na akcje, analogiczny do tego z serweraswitch (action) { case 'PARTICIPANT_LOGGED': { break; } }
-
Dodać obługę akcji
PARTICIPANT_LOGGED- Wywołać funkcję
renderIssueSubmitView
- Wywołać funkcję
-
Dodać obsługę akcji
ISSUE_RECEIVED- Wywołać funkcję
renderIssueReceivedView
- Wywołać funkcję
-
Na ekranie zgłaszania sygnału pomocy (
renderIssueSubmitView)-
Dodać obsługę eventu
submitformularza oid="issueSubmitForm"-
Zablokować domyślne działanie eventu
event.preventDefault(); -
Wysłać event z
actiono wartościTRAINER_NEEDEDipayloadz wartością inputaproblem
-
-
-
Dodać kolekcje reprezentującą zgłoszenia uczestników do stanu serwera pod kluczem
issuesw postaci:-
id- unikalny identyfikator -
status- statnus zgłoszenia -
userId- identyfikator uczestnika -
userName- nazwa uczestnika -
userGroup- grupa uczestnika -
problem- opis problemu
-
-
Dodać obsługę akcji
TRAINER_NEEDED-
W odpowiedzi na event dodać nowy element do kolekcji zgłoszeń
-
Wysłać do użytkownika event z akcją
ISSUE_RECEIVED
-
Wyświetlić listę zgłoszeń na ekranie trenera.
-
Wysyłanie listy zgłoszeń
-
Do akcji
TRAINER_LOGGEDdodaćpayloadz kolekcją zgłoszeń -
Po wystąpieniu akcji
TRAINER_NEEDEDwysłać do wszystkich trenerów akcjiISSUESzpayloadjako wszystkie zgłoszenia
-
-
Dodać obsługę akcji
ISSUES- wywołać funkcję
renderTrainerDashboardViewi przekazać jejpayload
- wywołać funkcję
-
Dodać obsługę akcji
TRAINER_LOGGED- wywołać funkcję
renderTrainerDashboardViewi przekazać jejpayload
- wywołać funkcję
-
Dodać referencję do elementów z
id="issueListItem"iid="issueList"const issueListItemTemplate = getNodeById('issueListItem'); const issueListNode = getNodeById('issueList');
-
Przeiterować się z użyciem
forEachpo argumeciedata, który jest tablicą zgłoszeń w postaci wysłanej przez serwerdata.forEach(it => { ... });
-
Podczas każdej iteracji tworzyć nowy element na podstawie szablonu
issueListItemTemplateconst issueListItemNode = document.importNode(issueListItemTemplate.content, true); -
W stworzonym elemencie ustawić zawartość tekstu, nadpisująć zawartość pola
textContentelementu-
issueListItemNode.querySelector('.issueListItemName').textContent = it.userName;-
Element posiada klasy, dzięki którym można zidentyfikować element do wyświetlenia danach:
-
.issueListItemName- kolumna z nazwą uczestnika -
.issueListItemGroup- kolumna z grupą uczestnika -
.issueListItemProblem- kolumna z problemem uczestnika -
.issueListItemStatus- kolumna ze statusem zgłoszenia
-
-
-
Dodać do parenta
issueListNode.appendChild(issueListItemNode);
-
-
Dodać obsługę przyjęcia zgłoszenia przez trenera.
-
Na ekranie listy zgłoszeń, podczas iteracji po zgłoszenia
-
Dodać referenję do przycisku
Przyjmij zgłoszenieconst takeIssueButtonNode = issueListItemNode.querySelector('.issueListItemActions button');
-
Dodać
switchpracujący na statusie zgłoszeniait.statuspoissueListNode.appendChild(issueListItemNode);-
Dla statusu
PENDING:-
Dodać nasłuchiwanie na kliknięcie na elemencie
takeIssueButtonNode- W odpowiedzi na kliknięcie wysłać event z akcją
ISSUE_TAKENz identyfikatorem zgłoszeniait.idjakopayload
- W odpowiedzi na kliknięcie wysłać event z akcją
-
-
dla
default:- do elementu
takeIssueButtonNodedodać klasęhidewykorzystując.classList.add('hide')
- do elementu
-
-
-
Dodać obsługę akcji
ISSUE_TAKEN-
Znaleźć w kolekcji zgłoszenie wykorzystując
payloadzawierający identyfikator zgłoszenia i zapisać do stałejissue-
idzgłoszenia równepayload -
statusróżne odSOLVED -
Jeśli się nie udało przerwać
switchif (!issue) break;
-
-
Zmienić status zgłoszenia na
TAKEN- Zauktualizować wartość przez referencję
-
Wysłać akcje
ISSUESdo wszystkich trenerów z nową listą zgłoszeń
-
Obsłużyć rozwiązanie problemu.
-
Dodać do akcji
ISSUE_TAKENodesłanie do użytkownika eventu z przyjęciem zgłoszenia-
Znaleźć użytkownika wykorzystując
issue.userIdi zapisać do stałejparticipant -
Jeśli nie znaleziono użytkownika przerwać
swtichprzy użyciubreak -
Wysłać do znalezionego użytkownika event z akcją
ISSUE_TAKENipayloadzawierającym nazwę trenera, który przyjął zgłoszenieconnectedUser.data.name
-
-
Dodać obsługę akcji
ISSUE_TAKEN- Wywołać
renderIssueTakenViewzpayloadzawierającym nazwę trenera, który przyjął zgłoszenie
- Wywołać
-
Na ekranie przyjętego zgłoszenia (
renderIssueTakenView)-
Znaleźć element o
id="issueTakenHeader"- Ustawić pole
textContentnaTrener ${trainerName} przyjął Twoje zgłoszenie, zaraz podejdzie.
- Ustawić pole
-
Dodać nasłuchiwanie na kliknięcie w przycisk
Problem rozwiązany-
Wysłać event z akcją
ISSUE_SOLVEDz pustympayload -
Zmienić na ekran zgłaszania problemu (
renderIssueSubmitView)
-
-
-
Dodać obsługę akcji
ISSUE_SOLVED-
Akcja działa analogicznie do akcji
ISSUE_TAKENz tymi różnicami:-
Nie wysyłamy żadnego eventu do uczestnika, którego dotyczyło zgłoszenie
-
Status zgłoszenia zmienić na
SOLVED
-
-
-
Na ekranie trenera, podczas iteracji po zgłoszeniach
-
Dodać referencję do formularza ze wskazówką
const issueListHintFormNode = issueListItemNode.querySelector('.issueListHintForm');
-
Dodać na formularzu nasłuchiwanie na event
submit-
Zablokować domyśle zachowanie eventu
event.preventDefault();
-
Zebrać dane z formularza
const formData = new FormData(event.target);
-
Wysłać event z akcją
HINT_SENTipayloadw postaci:-
hint- wartość z pola formularzahint -
userId- identyfikator użytkowanika (it.userId)
-
-
-
Dodać nowy
casedla statusu o wartościTAKEN -
Zadbać o ukrywanie przycisku gdy status równy
TAKEN- Ukryć element przycisku dodając do niego klasę
hide
- Ukryć element przycisku dodając do niego klasę
-
Zadbać o ukrywanie formularza gdy status równy
PENDING -
Ukryć element formularza dodając do niego klasę
hide -
Ukryć formularz oraz przycisk domyślnie
-
-
Dodać obsługę akcji
HINT_SENT-
Znaleźć uczestnika wykorzystująć
payload.userId- Jeśli nie znaleziono przerwać
switch
- Jeśli nie znaleziono przerwać
-
Znaleźć aktywne zgłoszenie uczestnika
-
userIdzgłoszenia równeparticipant.id -
statusróżne odSOLVED -
Jeśli nie znaleziono przerwać
switch
-
-
Wysłać do uczestnika event z akcją
HINTipayloadrównympayload.hint -
Zmienić status zgłoszenia na
HINT -
Wysłać do wszystkich trenerów zmienioną listę zgłoszeń
-
-
Dodać obsługę akcji
HINT-
Wyświetlić ekran podpowiedzi (
renderHintReceivedView)- Przekazać
payloaddo ekranu
- Przekazać
-
Na ekranie podpowiedzi
-
Wyświetlić treść podpowiedzi dostępnej w argumencie funkcji ekranu
hint- Znaleźć element o
id="hint"i ustawićtextContentna zawartość podpowiedzi
- Znaleźć element o
-
Dodać nasłuchiwanie na kliknięcie na element o
id="hintSuccess"-
Wysłać event z akcją
ISSUE_SOLVED -
Wyświetlić ekran zgłaszania (
renderIssueSubmitView)
-
-
Dodać nasłuchiwanie na kliknięcie na element o
id="hintFail"-
Wysłać event z akcją
HINT_FAIL -
Wyświetlić ekran oczekiwania na trenera (
renderIssueReceivedView)
-
-
-
-
Dodać obsługę akcji
HINT_FAIL-
Znaleźć aktywne zgłoszenie uczestnika
-
userIdzgłoszenia równeconnectedUser.id -
statusróżne odSOLVED -
Jeśli nie znaleziono przerwać
switch
-
-
Zmienić status zgłoszenia na
PENDING -
Wysłać do wszystkich trenerów zmienioną listę zgłoszeń
-
-
Obsługa rozłączenia użytkownika
-
Po rozłączeniu (event
close) usunąc rozłączonego użytkownika-
Przefiltrować kolekcję
state.participantsporównującsocket- Wynikiem filtrowania nadpisać kolekcję
-
Przefiltrować kolekcję
state.trainersporównującsocket- Wynikiem filtrowania nadpisać kolekcję
-
-
-
Dodać walidację czy użytkownik o danej nazwie już istnieje
-
Wyświetlić listę uczestnik na ekranie zgłoszeń trenera
-
Dodać więcej danych do tabeli zgłoszeń:
-
Data zgłoszenia i data ostatniej modyfikacji
-
Nazwa trenera, który przyjął zgłoszenie
-
Dodać ekran
Moje zgłoszenia, który by wyświetlał się po zalogowaniu uczestnika i po rozwiązaniu problemu- Na ekranie przycisk
Nowe zgłoszeniedo przejścia na ekran zgłaszania pomocy
- Na ekranie przycisk
-
Dodać obsługę ponownego połączenia użytkownika
-
Dodać testy integracyjne
