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
Chrome
lubFirefox
-
Edytor kodu wspierający
typescript
-
vscode
-
WebStorm
-
-
©: https://res.cloudinary.com/
-
Sklonować repozytorium
-
Zainstalować zależności zależności
yarn
albonpm 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
createServer
ze 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()
gdziee
to wyłapany błąd przezcatch
-
-
-
Handler serwera w odpowiedzi na wszystkie zapytania zwraca tekst
Test server
- Wykorzystać metodę
end
obiekturesponse
- Wykorzystać metodę
-
-
-
Dodać nasłuchiwanie serwera na porcie wykorzystując metodę
listen
obiektuserver
-
Przekazać jako pierwszy argument stałą
PORT
-
Jako drugi funkcję, która wywoła się po uruchomieniu serwera
- Użyć
console.log
aby sprawdzić czy serwer rozpoczął nasłuchiwanie na porcie
- Użyć
-
-
-
Zmodyfikować handler serwera HTTP, tak aby zwracał pliki statyczne
-
Zapisać do stałej
url
adres URL z obiektu zapytania wykorzystującrequest.url
-
W przypadku gdy adres jest równy
/
ustawić wartośćindex.html
- Wykorzystać operator
? :
- Wykorzystać operator
-
-
Utworzyć stałą
urlParts
z wartościąurl.split('.')
-
Utworzyć stałą
fileExtension
z wartością ostatniego elementu tablicyurlParts
-
Utworzyć stałą
contentType
z wartością z mapyFILE_EXTENSION_TO_CONTENT_TYPE
, której kluczami są rozszerzenia plików -
Ustawić status odpowiedzi wykorzystując metodę
response.writeHead
obiektu odpowiedzi-
Jako pierwszy argument przekazać status odpowiedzi równy
200
-
Jako drugi argument przekazać obiekt
{ 'Content-Type': contentType }
-
-
Wczytać plik
-
Wykorzystać funckje
readFileSync
ze 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.end
aby 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.Server
o 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
connection
przy wykorzystaniu metodyon
-
Klasa
Server
z pakietuws
dziedziczy do klasieEventEmmiter
-
Handler eventu
connection
jako argument wywołania dostaje socket, który reprezentuje połączenie z klientem-
Wypisać do konsoli
socket connected
-
Odesłać wiadomość powitalną o treści
welcome
wykorzystującsocket.send
-
-
-
Dodać do
webSocketsServer
nasłuchiwanie na eventmessage
przy wykorzystaniu metodyon
- Wypisać do konsoli dane eventu dostępne w argumencie handlera
-
Dodać do
webSocketsServer
nasłuchiwanie na eventclose
przy wykorzystaniu metodyon
- Wypisać do konsoli
socket closed
- Wypisać do konsoli
-
W handlerze eventu
DOMContentLoaded
stworzyć nowe polaczeniem do serweraWebSockets
-
Utworzyć instancję socketa wykorzystując klasę
WebSockets
o 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:
renderTemplateById
musi 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ącaddEventListener
orazgetNodeById
- 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
submit
na elemencie formularza zid="participantLoginForm"
-
Zablokować domyślne działanie zdarzenia poprzez
event.preventDefault();
-
Wykorzystać
FormData
do zebrania danych z formulurza-
const formData = new FormData(event.target);
-
Dostęp do danych
formData.get(group)
gdziegroup
do wartość atrybutuname
elementu input formularza -
Nazwy pól w formularzu:
-
name
-
group
-
-
-
Wykorzystać funkcję
sendEvent
do wysłania eventu do serwera WebSocket, przekazać obiekt z kluczami:-
action
o wartościPARTICIPANT_LOGIN
-
payload
o 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
action
orazpayload
- Zapisać do stałych pola obiektu
-
Dodać prosty system akcji przy wykorzystaniu instrukcji warunkowej
switch
-
Wykorzystać
action
w instrukcjiswitch
switch (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.data
zawartościąpayload
-
Dodać połączonego użytkownika do kanału uczestników
state.participants
-
Wysłać akcję
PARTICIPANT_LOGGED
z pustympayload
-
-
Dodać obsługę akcji
TRAINER_LOGIN
-
Zaktualizować dane połączonego użytkownika
connectedUser.data
zawartościąpayload
-
Dodać połączonego użytkownika do kanału uczestników
state.trainers
-
Wysłać akcję
TRAINER_LOGGED
z pustympayload
-
-
-
-
Obsługa wysyłania sygnału pomocy przez uczestnika.
-
W evencie
onmessage
dodać 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
submit
formularza oid="issueSubmitForm"
-
Zablokować domyślne działanie eventu
event.preventDefault();
-
Wysłać event z
action
o wartościTRAINER_NEEDED
ipayload
z wartością inputaproblem
-
-
-
Dodać kolekcje reprezentującą zgłoszenia uczestników do stanu serwera pod kluczem
issues
w 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_LOGGED
dodaćpayload
z kolekcją zgłoszeń -
Po wystąpieniu akcji
TRAINER_NEEDED
wysłać do wszystkich trenerów akcjiISSUES
zpayload
jako wszystkie zgłoszenia
-
-
Dodać obsługę akcji
ISSUES
- wywołać funkcję
renderTrainerDashboardView
i przekazać jejpayload
- wywołać funkcję
-
Dodać obsługę akcji
TRAINER_LOGGED
- wywołać funkcję
renderTrainerDashboardView
i 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
forEach
po 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
issueListItemTemplate
const issueListItemNode = document.importNode(issueListItemTemplate.content, true);
-
W stworzonym elemencie ustawić zawartość tekstu, nadpisująć zawartość pola
textContent
elementu-
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łoszenie
const takeIssueButtonNode = issueListItemNode.querySelector('.issueListItemActions button');
-
Dodać
switch
pracujący na statusie zgłoszeniait.status
poissueListNode.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_TAKEN
z identyfikatorem zgłoszeniait.id
jakopayload
- W odpowiedzi na kliknięcie wysłać event z akcją
-
-
dla
default
:- do elementu
takeIssueButtonNode
dodać klasęhide
wykorzystując.classList.add('hide')
- do elementu
-
-
-
Dodać obsługę akcji
ISSUE_TAKEN
-
Znaleźć w kolekcji zgłoszenie wykorzystując
payload
zawierający identyfikator zgłoszenia i zapisać do stałejissue
-
id
zgłoszenia równepayload
-
status
różne odSOLVED
-
Jeśli się nie udało przerwać
switch
if (!issue) break;
-
-
Zmienić status zgłoszenia na
TAKEN
- Zauktualizować wartość przez referencję
-
Wysłać akcje
ISSUES
do wszystkich trenerów z nową listą zgłoszeń
-
Obsłużyć rozwiązanie problemu.
-
Dodać do akcji
ISSUE_TAKEN
odesłanie do użytkownika eventu z przyjęciem zgłoszenia-
Znaleźć użytkownika wykorzystując
issue.userId
i zapisać do stałejparticipant
-
Jeśli nie znaleziono użytkownika przerwać
swtich
przy użyciubreak
-
Wysłać do znalezionego użytkownika event z akcją
ISSUE_TAKEN
ipayload
zawierającym nazwę trenera, który przyjął zgłoszenieconnectedUser.data.name
-
-
Dodać obsługę akcji
ISSUE_TAKEN
- Wywołać
renderIssueTakenView
zpayload
zawierającym nazwę trenera, który przyjął zgłoszenie
- Wywołać
-
Na ekranie przyjętego zgłoszenia (
renderIssueTakenView
)-
Znaleźć element o
id="issueTakenHeader"
- Ustawić pole
textContent
naTrener ${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_SOLVED
z pustympayload
-
Zmienić na ekran zgłaszania problemu (
renderIssueSubmitView
)
-
-
-
Dodać obsługę akcji
ISSUE_SOLVED
-
Akcja działa analogicznie do akcji
ISSUE_TAKEN
z 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_SENT
ipayload
w postaci:-
hint
- wartość z pola formularzahint
-
userId
- identyfikator użytkowanika (it.userId
)
-
-
-
Dodać nowy
case
dla 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
-
userId
zgłoszenia równeparticipant.id
-
status
różne odSOLVED
-
Jeśli nie znaleziono przerwać
switch
-
-
Wysłać do uczestnika event z akcją
HINT
ipayload
ró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ć
payload
do ekranu
- Przekazać
-
Na ekranie podpowiedzi
-
Wyświetlić treść podpowiedzi dostępnej w argumencie funkcji ekranu
hint
- Znaleźć element o
id="hint"
i ustawićtextContent
na 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
-
userId
zgłoszenia równeconnectedUser.id
-
status
róż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.participants
porównującsocket
- Wynikiem filtrowania nadpisać kolekcję
-
Przefiltrować kolekcję
state.trainers
poró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łoszenie
do przejścia na ekran zgłaszania pomocy
- Na ekranie przycisk
-
Dodać obsługę ponownego połączenia użytkownika
-
Dodać testy integracyjne