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.jsstwó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.