Aplikacje WWW
Table of Contents
Zajęcia 6-8
Krok 1: Uruchamiamy tsc
Aby móc korzystać z kompilatora TypeScriptu
musimy go
zainstalować. Wprawdzie mamy już jedną kopię załączoną do VS Code
,
ale potrzebna nam będzie jeszcze jedna, z której będziemy korzystali z
linii poleceń.
Zanim zaczniemy trzeba sprawdzić, czy niczego nie popsuliśmy i działa
nvm
zainstalowany na pierwszych zajęciach:
nvm current
Jeśli nie wychodzi v10.15.1
albo coś podobnego, to trzeba poprawnie
zainstalować nvm
, patrz zajęcia pierwsze.
Kompilator zainstalujemy globalnie, żeby wygodniej było go używać;
przy okazji dodamy program TSLint
, który będzie wyszukiwał jeszcze
więcej problemów z kodem. TSLint
warto też dodać do VS Code
, żeby
informacje o błędach mieć na bieżąco.
npm install -g typescript tslint
code --install-extension "ms-vscode.vscode-typescript-tslint-plugin"
Po wpisaniu tych poleceń z konsoli powinniśmy móc uruchomić tsc
i
tslint
, a w edytorze powinien być dodatek TSLint. Program tslint
trzeba będzie skonfigurować do użycia z naszym kodem.
TypeScript:
- https://pl.wikipedia.org/wiki/TypeScript
- https://www.typescriptlang.org/
- TypeScript w 5 min
- Sandbox
- TypeScript od podstaw
- Slajdy z wykładów (wszystkie 3 czesci)
Wiecej informacji o nvm:
- https://www.sitepoint.com/beginners-guide-node-package-manager/
- https://www.nettecode.com/node-js-npm-na-start/
Krok 2: Tworzymy nowy projekt i piszemy pierwszy program
Będziemy do tego potrzebowali nowego katalogu i pomocy programu npm
:
mkdir nowy_projekt
cd nowy_projekt
npm init -y
W poważniejszych projektach warto wyedytować plik package.json
tak,
by zawierał rozsądne informacje, ale do naszych potrzeb nie jest to
niezbędne.
Otwieramy katalog zawierający package.json
w VS Code
. Do
pierwszych eksperymentów stwórzmy nową stronę (plik .html
),
np. korzystając z szablonu w VS Code
. Stwórzmy plik main.css
,
początkowo pusty, ale zamiast pliku main.js
stwórzmy main.ts
. W
pliku main.ts
napiszmy:
console.log("Ależ skomplikowany program!");
Niestety, gdy otworzymy plik .html
w przeglądarce i otworzymy
konsolę w narzędziach deweloperskich, zobaczymy, że nie udało się
załadować pliku main.js
. Musimy więc stworzyć taki plik, jako wynik
kompilacji main.ts
. Na konsoli piszemy:
tsc main.ts
W katalogu pojawia się plik main.js
, identyczny z main.ts
, a po
przeładowaniu strony konsola wyświetla komunikat. Udało się nam
uruchomić pierwszy program w TypeScripcie!
Zobaczmy jeszcze co o tym programie myśli TSLint
. Najpierw musimy
stworzyć domyślną konfigurację:
tslint -i
a potem wywołać go na naszym pliku:
tslint main.ts
TSLint
w domyślnej konfiguracji nie lubi programów korzystających z
konsoli. Widać to też w edytorze - napis console.log
jest
podkreślony, a okienko z listą błędów zawiera komunikat wypisywany
przez TSLint
. Spróbujmy go jednak przekonać, że konsola jest
fajna. Dopiszmy do tslint.json
linijkę:
"rules": { "no-console": false },
I już możemy się cieszyć brakiem błędów zgłaszanych przez program. Dobrze jest jednak pamiętać, że zwykle wyłączenie komunikatu o błędzie nie jest najlepszą metodą rozwiązania problemu.
Stwórzmy funkcję, która będzie logowała nasze komunikaty i jej wywołanie:
function zaloguj(...komunikaty: string[]) { console.log("Ależ skomplikowany program!", ...komunikaty); } zaloguj("Ja", "cię", "nie", "mogę");
Skompilujmy i uruchommy. Wprawdzie działa super, ale gdyby się coś
popsuło, to musielibyśmy skorzystać z debuggera. To zajrzyjmy na
zakładkę debuggera w narzędziach deweloperskich. Ops, tym razem plik
main.js
nie wygląda zbyt podobnie do naszego main.ts
. Gdybyśmy
chcieli widzieć nasz program musimy stworzyć source map, czyli plik
opisujący związek oryginalnych źródeł z wygenerowanym .js
. Pewnie
można ręcznie, ale prościej poprosić kompilator:
tsc --sourceMap main.ts
Teraz w debuggerze mamy zarówno wygenerowany plik .js
(przydatny,
gdy tłumaczenie nie jest takie, jak oczekiwaliśmy i chcemy to
wyjaśnić) jak i .ts
, z którego zwykle korzystamy gdy chcemy się
zorientować co się dzieje z naszym programem.
Krok 2: chcemy wczytać dane w formacie JSON
Zwykle dostaniemy je z sieci, ale tu stwórzmy w naszym programie zmienną:
let jsonString: string = `{ "piloci": [ "Pirx", "Exupery", "Idzikowski", "Główczewski" ], "lotniska": { "WAW": ["Warszawa", [3690, 2800]], "NRT": ["Narita", [4000, 2500]], "BQH": ["Biggin Hill", [1802, 792]], "LBG": ["Paris-Le Bourget", [2665, 3000, 1845]] } }`;
Chcielibyśmy dostać jakąś strukturę danych, w której będą te wszystkie informacje. Niby nic prostszego:
let dataStructure = JSON.parse(jsonString); console.log(dataStructure);
Gdy spojrzymy na wywnioskowany typ dataStructure, to będzie to
any
. Nie lubimy any
, bo nie wiadomo co można z nim
zrobić. Przygotujmy więc odpowiednie struktury danych. Chcemy mieć
interfejs ILotnisko
opisujący dane lotniska, typ Pilot
opisujący
pilota i interfejs ILiniaLotnicza
opisujący całą zawartość
jsonString
.
[Tu piszemy te typy]
Możemy teraz dodać typ do deklaracji:
let dataStructure: ILiniaLotnicza = JSON.parse(jsonString);
i oczywiście zmienna będzie już tego typu. Możemy sprawdzić ilu jest pilotów:
console.log(jsonData.piloci.length);
Wygląda na to, że jest świetnie, na konsoli nic się wprawdzie nie zmieniło, bo po tłumaczeniu na JS nie ma już informacji o typach, ale w edytorze działa podpowiadanie, a więc pewnie i kontrola typów.
Prawie. Wykasujmy cały klucz piloci
z jsonString
i skompilujmy
program.
TypeError
! Jak to? Niestety, gdy przypisujemy wartość typu any
do
zmiennej jakiegoś konkretnego typu, kompilator wierzy, że wiemy co
robimy. JSON.parse
nie sprawdza, czy dane mają jakąś konkretną
zawartość, a wyłącznie czy są poprawne składniowo. Musimy się
napracować i sprawdzić sami. Napiszmy funkcję, która sprawdza, czy
dane są poprawne i zwróci true
lub false
.
[Tu piszemy kod, który sprawdzi, czy dane zawierają wszystkie potrzebne informacje]
Teraz po wywołaniu tej funkcji możemy być pewni, że dane są poprawne. Chyba że pomyliliśmy się w treści funkcji - warto by ją przetestować, ale o testowaniu będzie później.
Gdy zadeklarujemy funkcję następująco:
function sprawdzDaneLiniiLotniczej(dane: any): dane is ILiniaLotnicza {
to nawet prosta deklaracja zmiennej:
let daneLiniiLotniczej = sprawdzDaneLiniiLotniczej(JSON.parse(jsonString));
wystarczy kompilatorowi do wywnioskowania, że dane są faktycznie typu
ILiniaLotnicza
.
Krok 3: DOMowe sprawy
Wróćmy do strony z ostatnich zajęć o HTML/CSS. Zrobiliśmy tam popup, który był mało użyteczny. Wyświetlał się na stałe i nic nie można było z nim zrobić. Spróbujmy doprowadzić do tego, żeby popup wyświetlał się tylko po tym, gdy spróbujemy wysłać formularz rejestracji z pustym imieniem albo nazwiskiem pasażera lub z datą lotu wcześniejszą niż aktualna. Tekst w popupie powinien opisywać napotkany błąd. No i chcemy móc zamknąć popup gdy już przeczytamy o błędach.
Spróbujmy dostać się z poziomu JS do jakiegoś elementu strony - np. do przycisku wysyłającego formularz. Znamy jego selektor (jeśli nie, to można go znaleźć z pomocą narzędzi deweloperskich), spróbujmy więc dostać się do obiektu JS związanego z wyświetlanym elementem.
DOM - co to jest?
W konsoli możemy wyszukać element wpisując coś w rodzaju:
let el = document.querySelector("input[type=submit]");
Teraz możemy obejrzeć dostępne z JS właściwości tego elementu.
Niestety wyświetlona lista to tylko atrybuty, bez metod. Metody możemy
zobaczyć wpisując w konsoli el.
, i patrząc na wyniki
autouzupełniania. Jest ich raczej dużo, więc będziemy szukali i
używali tylko tych potrzebnych do wykonania tego zadania.
Spróbujmy ukryć przycisk. Można usunąć go z drzewa:
el.remove();
ale gdy możemy chcieć wyświetlić go ponownie wygodniej jest zmienić
właściwość display
z CSS. Można to zrobić bezpośrednio:
el.style.display = "none";
albo modyfikować klasy do których należy element:
el.classList.add("hidden");
Oczywiście w ten sposób możemy dowolnie modyfikować wygląd elementu. Warto sprawdzić w Inspektorze jak wyglądają wprowadzane w taki sposób zmiany.
Po eksperymentach z użyciem konsoli dopiszmy do skryptu wczytywanego
przez naszą stronę ukrywanie przycisku. Niestety, na konsoli pojawia
się błąd, a przycisk ciągle widać. Wynika to z tego, że skrypt jest
wczytywany i wykonuje się zanim przeglądarka stworzy całe drzewo DOM,
bo jest wczytywany na początku strony, w <head>
. Przenieśmy znacznik
<script>
na sam koniec strony, tuż przed </body>
, i już wszystko
będzie OK. Inną metodę radzenia sobie z tym problemem poznamy później.
Wyświetlmy teraz zawartość pola input
z imieniem. Jeśli coś do niego
wpiszemy, to w Inspektorze będzie widać, że jest atrybut value
,
który zawiera ten tekst. Spróbujmy dopisać do naszego skryptu
logowanie zawartości tego pola. Niestety edytor zupełnie nie chce
zauważyć, że znaleziony przez querySelector
element ma atrybut
value
. Gdy wpiszemy nazwę ręcznie, to kod oczywiście zadziała, ale
chcielibyśmy używać poprawnych typów. Potrzebujemy poznać typ
elementu, żeby móc go użyć w edytorze. Z powodu skomplikowanej
historii obiektów w JS typ możemy sprawdzić pisząc:
el.constructor.name;
jeśli el
to nasz element. Gdy w edytorze napiszemy więc:
let el = document.querySelector("input[name=imie]") as HTMLInputElement;
to podpowiadanie składni zacznie działać, a TypeScript będzie
wiedział, że jesteśmy pewni, że ten element ma taki typ. Jeśli nie
jesteśmy pewni, warto skorzystać z operatora instanceof
żeby się
upewnić.
Teraz zmodyfikujmy jakiś tekst na stronie. Wybierzmy dowolny akapit
(<p>
), a jeśli takiego nie ma - to go dopiszmy. Jak pobrać jego
zawartość i jak ją zmienić? Można skorzystać z wielu różnych
atrybutów, m.in. textContent
, innerText
, innerHTML
. Ich opisy
można znaleźć na MDN.
Dodawanie elementów też nie jest trudne. Musimy utworzyć element, np. następująco:
let nowyElement = document.createElement("<div>");
a potem zmodyfikować jego zawartość; atrybuty możemy dodawać przez
setAttribute
, a dodatkowe elementy i teksty w środku stworzonego
elementu przez przypisanie do właściwości innerHTML
. Można też je
tworzyć wywołując znowu createElement
.
Utworzony element trzeba dodać do drzewa. Służą do tego metody
appendChild
i insertBefore
.
Stwórzmy nowy akapit i dodajmy go na końcu strony, tuż przed
</body>
.
Krok 4: czas na nas
Czasem będziemy chcieli wykonać jakąś akcję po określonym
czasie. Możemy skorzystać z funkcji setTimeout
, żeby spowodować
wywołanie naszego kodu mniej więcej po podanym w milisekundach
czasie. Dopiszmy do naszej strony wywołanie, które po 2 sekundach od
załadowania strony wypisze na konsoli tekst o tym, że minęły 2 sekundy
od załadowania strony. Wyjdzie coś w rodzaju:
setTimeout(() => {
console.log("No już wreszcie.");
}, 2000);
Uwaga:
- setTimeout
to właściwie window.setTimeout
- zastanów się czemu wywołanie nastąpi po mniej więcej podanym czasie (stackoverflow?)
- nie zapomnij zrobiś callback!
Do wyłączenia wcześniej ustawionego zegarka służy clearTimeout
, a
jeśli chcemy wywołań cyklicznie co określony czas, to możemy użyć
setInterval~/~clearInterval
albo wołać setTimeout
po zakończeniu
każdego kolejnego wywołania.