Laboratorium 5
Zajęcia 5
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.
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
.
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
.
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 mimo prostej deklaracji zmiennej:
let daneLiniiLotniczej = JSON.parse(jsonString);
po zastosowaniu type guard kompilator będzie wiedział, że zmienna jest już właściwego typu:
if(sprawdzDaneLiniiLotniczej(daneLiniiLotniczej)) {
let juzNaPewnoDaneLinii = daneLiniiLotniczej;
}
Zmienna juzNaPewnoDaneLinii
będzie 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.
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:
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 bonus: 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);
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.