AJAX

Ajax czyli Asynchronous JavaScript and XML to technologia, która umożliwia nam ściąganie i wysyłanie części danych bez potrzeby przeładowania całej strony.

Zanim przejdziemy do pracy z Ajaxem, musimy przejść przez trochę teorii, która będzie nam się non stop przewijać przy pracy z ajaxem.

Protokół HTTP

Działanie internetu w większości opiera się o protokół HTTP.
Ale jak on właściwie działa?

Gdy robimy zapytanie o jakiś zasób (np. wchodzimy na daną stronę), przeglądarka wysyła do serwera żądanie wraz z nagłówkami. Wśród tych nagłówków są informacje takie jak rodzaj przeglądarki, jakie typy danych ona obsługuje, rodzaj cache itp.

nagłowki request

Możesz sobie to sprawdzić. Wejdź w debuger do zakładki network i odśwież stronę. Zobaczysz, jak twoja przeglądarka wysyła zapytania o kolejne zasoby. Pierwszym z nich będzie ta strona.

Serwer odbiera te żądanie, przetwarza, po czym wysyła odpowiedź, która składa się z kilku części:

  • Statusu odpowiedzi
  • 0 lub więcej nagłówków (np. jakiego typu jest odpowiedź, jej długość, ważność itp)
  • Ciała odpowiedzi (które nie zawsze występuje)

Taka odpowiedź może wyglądać jak poniżej. Także możesz sobie ją sprawdzić w zakładce network debugera.

nagłowki request

Gdy przeglądarka dostatnie taką odpowiedź, zaczyna czytać ciało odpowiedzi.
Dochodzi do momentu, gdzie np. za pomocą link dołączone są style do strony, czy kolejna grafika w kodzie html. Przeglądarka wysyła kolejne żądanie (wraz z nagłówkami), serwer je przetwarza i odsyła stosowną odpowiedź.
W tych dołączonych stylach autor użył @import, które służy do dołączania innych plików css. Przeglądarka natrafia na takie @include i znowu wysyła odpowiednie żądanie do odpowiedniego serwera. Ale dalej w kodzie autor umieścił grafiki img. Przeglądarka natrafia na nie i znowu - wysyła odpowiednie żądania, a w odpowiedzi dostaje jakiś wynik. Ten wynik zależny jest od tego co wyśle serwer. I tak w koło, aż cały dokument zostanie wczytany...

W powyższym opisie kilka rzeczy ma szczególne znaczenie. Pierwszą z nich jest status, czyli oznaczenie, czy dane połączenie się zakończyło sukcesem (202) lub np. niepowodzeniem, bo dany adres nie istnieje (404).

Najczęściej spotykane statusy odpowiedzi to:

200Wszystko ok, połączenie zakończyło się sukcesem
301Strona została przeniesiona na inny adres
404Nie ma takiej strony
418Jestem czajniczkiem
500Błąd serwera

Wśród nagłówków warto zwrócić uwagę na kilka z nich.

Nagłówek content/type, określa typ MIME danych, czyli typ dokumentu. Jeżeli dla przykładu serwer wyśle nam stronę html, ale jako typ ustawi text/plain, wtedy w naszej przeglądarce strona wyświetli się jako plik tekstowy. Podobnie z całą resztą zasobów - gdy serwer wyśle grafikę png jako text/plain, w naszej przeglądarce zobaczymy tekstowy śmietnik. Idąc dalej nasz serwer mógłby wysłać skrypt php jako png - wystarczy, że wyśle go wraz z nagłówkami image/png

Raz od święta może się zdarzyć, że uda ci się w wyszukiwarce znaleźć grafiki jpg, które mają przezroczyste tło. Stosownego przykładu z głowy ci nie podam, ale sam natrafiałem na takie twory. Wynika to właśnie z tego, że serwer, który wysłał ci dany zasób pomylił się i wysłał plik png z content/type ustawionym na jpg. Podobnie może się dziać z wieloma innymi zasobami - dla przykładu można wysyłać skrypty php które podszywają się pod grafiki...

Kolejną bardzo ważną właściwością każdego połączenia jest jego typ. Jeżeli wejdziesz do debugera i zakładki Network, a następnie odświeżysz tą stronę, zobaczysz, że większość połączeń będzie typu GET. Dlatego, że pobieramy dane. Takich typów jest kilka:

GET Ten typ połączenia służy do pobieranie danych. Można wykorzystać go też do wysyłania danych, które to stosujemy przy formularzach w HTML. Wtedy dane te są dołączane do adresu strony (łączymy się na adres z danymi, dzięki czemu serwer może sobie je pobrać). Tego typu używa się do pracy na stosunkowo małej ilości danych, ponieważ liczba liter w adresie wynosi ok 2000, więc i dane nie mogą być bardzo długie.
POST wykorzystywany do wysyłania danych. Ten typ danych jest dołączany do ciała requestu, a nie do adresu strony
PUT typ połączenia używany do zamiany rekordu w bazie danych. Stosowany przy edycjach rekordów.
PATCH Podobny do PUT, z tym że wykorzystuje się do edycji/aktualizacji konkretnej właściwości obiektu w bazie danych - np. nazwiska użytkownika
DELETE Określa typ połączenia służący do usuwania danych
PUT można traktować jak zastąpienie starej wartości nową. Można więc porównać to do ustawienia zmiennej z tablicą nowej wartości z nowymi danymi. PATCH natomiast zmienia tylko składowe, czyli porównując zmienia tylko jakieś dane w tej tablicy.
Ale to tylko teoria. W praktyce do edycji danych na serwerze używa się tego i tego (przy czym put o wiele częściej).

REST

Powyżej omówiliśmy typy połączeń. Kolejnym tematem, który będzie nam się przewijał przy pracy z Ajax będzie REST czyli Representational State Transfer.

Co to takiego? Pewna konwencja, wzór, a w zasadzie cytując wikipedię styl architektury.

Wyobraź sobie, że w celu pobierania/wysyłania danych będziemy dynamicznie łączyć się na adres:


const ourApiUrl = "http://kursjs.pl/bohaterowie";

To co powyżej to główny adres naszej "aplikacji na serwerze" (a właściwie miejsca do którego możemy się łączyć by pobierać czy wysyłać dane).
Architektura REST to zbiór zasad, które określają, jak powinieneś zbudować odpowiednie endpointy (adresy), by inni programiści mogli się w nich odnaleźć.

Metoda HTTP Adres Opis
GET http://kursjs.pl/bohaterowie Pobranie całej listy bohaterów
GET http://kursjs.pl/bohaterowie/1 Pobranie danych bohatera o id 1
POST http://kursjs.pl/bohaterowie Wysłanie/dodanie nowego bohatera
PUT http://kursjs.pl/bohaterowie/1 Aktualizacja/edycja bohatera o id 1
PATCH http://kursjs.pl/bohaterowie/1 Aktualizacja jednej właściwości bohatera o id 1
DELETE http://kursjs.pl/bohaterowie/1 Usunięcie bohatera o id 1

Teraz jeżeli dasz mi adres "http://kursjs.pl/bohaterowie" to ja wiedząc, że stosujesz REST mogę założyć, że gdy chcę dodać nowego użytkownika, dane powinienem wysłać za pomocą połączenia typu POST na adres ""http://kursjs.pl/bohaterowie". Ale już edytując dane jakiegoś użytkownika, pewnie wyślę dane za pomocą PATCH na adres "http://kursjs.pl/bohaterowie/10". Jasne - bez opisu bym nie podchodził, ale przynajmniej jakieś założenia mogę zrobić...

Bardzo fajny artykuł na ten temat znajduje się pod adresem https://www.smashingmagazine.com/2018/01/understanding-using-rest-api/.

Przykładowych API do których możemy się łączyć jest mnóstwo i bardzo łatwo je w necie znaleźć. Wystarczy wpisać w google "public api". Nasa ma swoje, jakieś ministerstwa pogody itp. Tutaj masz przykładową listę darmowych api z których będziesz mógł pobierać dane. Nie zawsze jest to proste. Dość często trzeba się dodatkowo rejestrować, a i część z takich darmowych API jest dość średnia w wyglądzie i trzeba się przeczesywać przez ich nierzadko dość toporne opisy.

XML

Asynchronous JavaScript and XML - widzisz tą końcówkę? Oznacza ona, że nasze dynamiczne połączenia będą operować głównie na XML.
Faktycznie - na początku tak było. Gdy powstawała ta technologia, głównym formatem przesyłanych danych był XML.

Problem (a w zasadzie nie problem, co cecha) z tym formatem jest taki, że po pierwsze jego zapis jest dość długi, co czasami może sprawić że sama składnia będzie dłuższa od realnych danych:


<?xml version="1.0"?>
<catalog>
   <book id="bk101">
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <description>An in-depth look at creating applications
      with XML.</description>
   </book>
   <book id="bk102">
      <author>Ralls, Kim</author>
      <title>Midnight Rain</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <description>A former architect battles corporate zombies</description>
   </book>
</catalog>

Zobacz miejsca w powyższym formacie straciliśmy na same znaczniki.

Dodatkowo poza długim zapisem pobieranie takich danych przypomina spacerowanie po drzewie dom, co też nie jest najprzyjemniejszą rzeczą na świecie - zwłaszcza gdy działasz na sporej ilości danych. Poniżej bardzo prosty przykład, a już musimy stosować dość skomplikowane zapisy:


function loadDoc() {
    const xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            myFunction(this);
        }
    };
    xhttp.open("GET", "books.xml", true);
    xhttp.send();
}

function myFunction(text) {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(text.response, "text/xml");
    const books = xmlDoc.querySelectorAll('book');
    [].forEach.call(books, function(book) {
        const title = book.querySelector('title').firstChild.nodeValue;
        const author = book.querySelector('author').firstChild.nodeValue;
        const genre = book.querySelector('genre').firstChild.nodeValue;
        const price = Number(book.querySelector('price').firstChild.nodeValue);
        const description = book.querySelector('description').firstChild.nodeValue;
        console.log(title, author, gender, price, description);
    });
}

loadDoc();
I nie, XML wcale nie jest złem wcielonym. Autor przez długi czas korzystał z tego formatu i żyło się z tym całkiem fajnie. Potem pojawił się poniższy i w pierwszych momentach oczywiście ździwienie - bo przecież XML jest całkiem ok.

JSON

XML działa całkiem dobrze, ale można i lepiej. Dość szybko wymyślony został inny format - JSON, który bardzo uprościł prace na przesyłanych danych.

Format JSON do złudzenia przypomina klasyczne obiekty w JavaScript


{
    "catalog" : [
        {
            "id" : "bg101",
            "author" : "Gambardella, Matthew",
            "title" : "XML Developer's Guide",
            "genre" : "Computer",
            "price" : 44.95,
            "description" : "An in-depth look at creating applications with XML."
        },
        {
            "id" : "bg102",
            "author" : "Ralls, Kim",
            "title" : "Midnight Rain",
            "genre" : "Fantasy",
            "price" : 5.95,
            "description" : "A former architect battles corporate zombies"
        }
    ]
}

Jest jednak kilka różnic, które go odróżnia od obiektów:

  • Oczywiście żadnych funkcji - to tylko dane, więc mają mieć tylko dane
  • Nazwy kluczy piszemy w cudzysłowach
  • Żadnych komentarzy
  • Żadnych pojedynczych cudzysłowów - tylko podwójne
  • Po ostatniej właściwości nie może być przecinka (w js może być)
  • Teksty piszemy w jednej linii - nawet te na wiele linii. Musimy tutaj zastosować jakieś znaki nowej linii, które potem odpowiednio skonwertujemy.

Skoro JSON tak przypomina obiekty JavaScript, to czy praca na nich też przypomina pracę na obiektach JavaScript? Jak najbardziej.


//pobieram dane drugiej książki z powyższego jsona
const el = catalog[0];

const id = ca.id;
const author = el.author;
const title = el.title;
const genre = el.genre;
const price = el.price;
const description = el.description;

//lub za pomocą destrukturyzacji w ES6
{id, author, title, genre, price, description} = catalog[0];

Format json najczęściej zapisuje się w plikach z rozszerzeniem .json.

W dzisiejszych czasach możesz go spotkać na każdym kroku. I mamy go w każdym projekcie Node (package.json), a i praktycznie każda nowa aplikacja korzysta z takich plików. Ot chociażby Visual Studio Code, ale i podobne.

Aby sprawdzić poprawność naszego JSON, wystarczy skorzystać z narzędzia https://jsonlint.com/.

W ramach mini treningu użyj tego narzędzia i popraw poniższy JSON:


{
    "data" : {
            "id" : 1000,
            'name' : "Marcin",
            "surname" : "Domanski",
            "user_type" : "admin",
            "privileges" : [
                "edit_post",
                "add_post",
                "delete_post"
                'delete_user',
            ]
        },
}

Obiekt JSON

W pracy z formatem JSON w JavaScript bardzo pomocny okaże się obiekt JSON. Nie jest to klasa obiektów (więc nie używamy go z new), a pojedynczy obiekt, który udostępnia nam 2 metody: stringify i parse.

Pierwsza z nich zamienia dany obiekt na tekstowy zapis w formacie JSON. Druga z nich zamienia zakodowany wcześniej tekst na obiekt JavaScript:


const ob = {
    name : "Piotr",
    surname : "Nowak"
}

const obStr = JSON.stringify(ob);
console.log(obStr); //"{"name":"Piotr","subname":"Nowak"}"

console.log( JSON.parse(obStr) ); //nasz wcześniejszy obiekt

Do czego to może się przydać? W przypadku Ajaxa będziemy go wykorzystywać do wysyłania danych na serwer w formacie JSON.

Wysłanie prostych nie zagnieżdżonych danych nie jest problemem - będziemy wykonywać do tradycyjnie. Jeżeli jednak chcemy wysłać dane które są zagnieżdżone (np. obiekt w obiekcie), wtedy bez JSON.stringify się nie obejdzie.

Innym praktycznym zastosowaniem obiektu JSON jest praca z dataset, gdzie dane są przechowywane jako string, dlatego gdy chcemy przechować tam obiekt, musimy go zamienić na zapis tekstowy.


const ob = {
    name : "Piotr",
    surname : "Nowak",
    car : {
        brand : "Fiat",
        color: "red"
    }
}


//przykład z dataset
element.dataset.info = ob; //nie zrobiliśmy konwersji
console.log(element.dataset.info); //[object Object]

element.dataset.info = JSON.stringify(ob); //nie zrobiliśmy konwersji
console.log(JSON.parse(element.dataset.info)); //powyższy obiekt


//przykład z ajax

//tak nie wyślemy
$.ajax({
    url : '...',
    data : ob
});

//tak jak najbardziej
$.ajax({
    url : '...',
    contentType : 'application/json',
    data : JSON.stringify(ob);
});

Czym się łączyć

W czystym Javascript do obsługi połączeń XHR (czyli dynamicznego pobierania/wysyłania danych) możemy skorzystać z 2 rzeczy: obiektu XMLHttpRequest oraz Fetch. Ten pierwszy istnieje nierozłączenie od początku istnienia Ajax. Ten drugi to nowsze rozwiązanie, które w wielu sytuacjach staje się prostsze w użyciu (dzięki domyślnej obsłudze obietnic), ale w praktyce jest po prostu nieco innym podejściem do rozwiązywania mniej więcej podobnych problemów.

Przy najprostszych skryptach użycie obydwu tych metod jest całkiem proste. Problem pojawia się w bardziej zaawansowanych aplikacjach, gdzie musimy pobierać i wysyłać wiele danych, musimy dbać o autoryzację, obsługę błędów, wyjątków itp. Okazuje się, że oba te rozwiązania przydało by się okryć dodatkowymi funkcjami i stosownymi warunkami - czyli stworzyć pewną abstrakcję, która ułatwi nam późniejsze ich używanie (1, 2).

Między innymi dlatego właśnie dość często wybierane są gotowe rozwiązania, które dają nam gotową nakładkę, która ułatwia pracę z XHR w realnych projektach. Nie jest to rzecz, którą używać musimy, ale może nam po prostu ułatwić pracę z przyszłym projektem.

Jedną z najczęściej wybieranych metod jest Ajax w jQuery, która jest bajecznie prosta.

Jeżeli nie używasz jQuery, możesz pokusić się o wypróbowanie Axios lub Superagent - obie równie popularne a i równie proste w użyciu.

Ale dalej - nie są to rzeczy, których używać musimy, a podejście do przesyłania danych zależy głównie od nas.

Pobieranie danych i CORS

Żeby pobrać i wysyłać dane, musimy mieć miejsce do którego będziemy takie połączenia wykonywać.

Jeżeli robisz swoje strony jako pliki html na pulpicie (albo w dowolnym katalogu na dysku) i chcesz dynamicznie pobrać na stronę plik, który leży tuż obok naszego pliku html (a może gdzieś indziej na dysku), to ci się to nie uda.

Przeglądarki w dzisiejszych czasach mają zabezpieczenia, by nie było możliwe pobieranie danych skądkolwiek - w tym bezpośrednio z dysku komputera. Nie ważne, że zastosujesz relatywne ścieżki, wskażesz idealnie miejsce na dysku - nie da się.

Ale czemu właściwie się nie da?

Rozchodzi się o zabezpieczenia typu CORS - Cross-Origin Resource Sharing.
Połączenia typu XHR można normalnie wykonywać tylko w obrębie własnej domeny (np. jako autor tej strony mogę się łączyć tylko na swój serwer http://kursjs.pl, który leży właśnie na tej samej domenie), lub do serwerów, które dadzą ci znać, że możesz od nich pobierać dane.
A jak dadzą znać? Wysyłając odpowiedni nagłówek:


Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods: "GET,POST,OPTIONS,DELETE,PUT"

Pierwszy nagłówek wskazuje na domeny, które mogą wykonywać dynamiczne połączenia na ten serwer. Gwiazdka oznacza dowolną domenę. Drugi nagłówek określa możliwe typy połączeń.

Poniżej przykładowa odpowiedź z jednego z publicznych api:

Nagłówki cors

W bardzo wielu przypadkach gdy się połączysz z jakimś serwisem by pobrać dane, w twojej konsoli pojawi się złowieszczy komunikat:

Cors error

Oznacza on, że nie możesz się dynamicznie łączyć z tym serwerem. Jeżeli wiesz dobrze, że adres jest poprawny i powinieneś móc się połączyć, od razu męcz administratorów serwera, bo prawdopodobnie dali ciała i nie wysyłają ci odpowiednich nagłówków...

Istnieją też nieco inne - mniej oficjalne rozwiązania tego problemu. Można na przykład spróbować skorzystać ze strony https://cors-anywhere.herokuapp.com/, która pozwala omijać CORS. Wystarczy do jej adresu dodać adres serwera, który ma problem z CORS:


const apiUrl = "http://my-bad-server.com/movies";
const corsAnywhereUrl = "https://cors-anywhere.herokuapp.com/";
const url = corsAnywhereUrl + apiUrl;

fetch(url).then(...)

Dla niektórych przeglądarek istnieją też dodatki, które pozwalają wyłączyć te zabezpieczenia.
Dla Chrome to np. ten a dla Firefoxa np. ten. Dla reszty przeglądarek łatwo coś odnaleźć szukając "nazwa przeglądarki disable cors plugin".
Czy polecam to podejście? Gdy trzeba coś na szybko przetestować - da radę. Ale pamiętaj, że użytkownik nie będzie miał zainstalowanego takiego rozszerzenia, więc tak naprawdę problemu nie rozwiązujesz.

Innym rozwiązaniem CORS jest stworzenie pośredniego serwera agregującego. Problem CORS dotyczy tylko przeglądarek. Strona backendowa spokojnie może takie połączenia wykonywać. Wystarczy stworzyć mini serwer (np. w Express), który będzie łączył się z danym serwerem, pobierał stamtąd dane, a my będziemy się łączyć z naszym.

Serwer lokalny

Wracamy na chwilę do naszych plików na pulpicie. Wiesz już dlaczego nie możesz pobrać pliku który leży tuż obok? Właśnie z tego samego powodu - CORS. Dane nie są serwowane z żadnej domeny (a co to za domena rozpoczynająca się od file://...) i przeglądarka to blokuje.

Jeżeli chcemy rozwiązać powyższy problem, nasza lokalna strona powinna być serwowana z tak zwanego serwera lokalnego.

Jeżeli o niego chodzi, możliwości jest bardzo wiele.

Osobiście korzystam z kilku rozwiązań - w zależności od pogody i tego nad jakim projektem aktualnie pracuję.

Jeżeli mamy zainstalowane Node.js, można dla przykładu globalnie zainstalować sobie https://www.npmjs.com/package/http-server, który potem bardzo łatwo odpalić.

Można też zainstalować sobie Gulpa i BrowserSync, który po uruchomieniu też odpala lokalny server. Mega fajne rozwiązanie.

Jakiś czas temu wpadł mi w oko darmowy serwer o miło brzmiącej nazwie Feniks. Spróbowałem i działa jak należy, chociaż osobiście korzystam z powyższych i nie potrzebuję dodatkowych narzędzi.

Jeżeli pracujecie w PHP (tak ja ja) pewnie korzystacie już z jakiegoś serwera - np. Xampp czy Wamp (i podobnych).

Osoby, które korzystają z Webstorma, wystarczy, że odpalą daną stronę korzystając z ikonek w prawym górnym rogu na ekranie z html. W Visual Studio Code można za to wykorzystać dodatek LivePreview, który odpala ją na lokalnym serwerze.

Także dla każdego coś miłego...

json-server

Powyżej podałem ci stronę, gdzie możesz sobie przejrzeć kilka darmowych API.

Dość często będziemy stykać się z sytuacją, gdzie żadne API nie będzie dla nas wystarczające, a najlepszym rozwiązaniem będzie stworzenie własnego.

I znowu - jak w przypadku serwera lokalnego - dla każdego inne rozwiązanie się sprawdzi.

Polecam przejrzeć kilka filmów pokazanych przez znanego Youtubera - Trawersy Media (gość robi naprawdę kawał dobrej roboty).

Jeżeli pracujesz w PHP, pewnie zainteresuje cię ten film

Jeżeli używasz Node, może zainteresować cię użycie servera Express do budowy RestApi. Tutaj możesz znaleźć paczkę z przykładowymi ustawieniami dla Express.

Nie oszukujmy się. Stworzenie własnego API jest dość pracochłonne, bo mówimy to o całym pakiecie funkcjonalności takich jak edycja, dodawanie, stronicowanie, wyszukiwanie itp.

Kolejnym możliwym rozwiązaniem jest użycie json-server. Jest to gotowe narzędzie, które daje nam wszystkie potrzebne funkcjonalności.

Instalujemy nasz serwer globalnie na cały komputer poleceniem:


npm install json-server -g

Możemy teraz w dowolnym katalogu odpalić go za pomocą polecenia:


json-server --watch nazwa-pliku.json

Gdzie nazwa-pliku.json to plik json, który będzie naszą bazą danych. Przykładowo gdybyśmy chcieli by w naszym API były 2 tabele z użytkownikami i filmami, wtedy taki plik mógłby mieć postać:


{
    "users" : [
        {
            "id" : 0,
            "name" : "Marcin",
            "surname" : "Nowak"
        },
        {
            "id" : 0,
            "name" : "Piotr",
            "surname" : "Kowalski"
        }
    ],
    "movies" : [
        {
            "id" : 0,
            "name" : "Gayver",
            "genre" : "sf"
        },
        {
            "id" : 1,
            "name" : "Poznaj mojego tatę",
            "surname" : "comedy"
        }
    ]
}

Po prawidłowym odpaleniu takiego serwera w konsoli pojawią się linki, na które możemy się łączyć:


Resources
http://localhost:3000/users
http://localhost:3000/movies

Home
http://localhost:3000

Ważne by podczas pracy nie zamykać danego okna terminala. Warto też przy pobieraniu danych z takiego serwera odpalać naszą stronę za pomocą serwera lokalnego.

Postman

Zanim przejdziemy do właściwej pracy ostatnie mini narzędzie, które może nam się przydać. W powyższym przykładzie zrobiłem testowe pobieranie użytkowników z json-servera za pomocą mini skryptu.

Często zamiast pisać skrypt do testowania połączeń, lepiej skorzystać z programu Postman, który służy do testowania zapytań.

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, to zadania do tego rozdziału znajdują się w w repozytorium pod adresem: https://github.com/kurs-javascript/js-ajax

Dowiedz się więcej na ten temat tutaj.

A może pasuje ci ściągnąć zadania do wszystkich rozdziałów na raz? Jeżeli tak, to skorzystaj z repozytorium pod adresem https://github.com/kurs-javascript/js-all.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem. Aha - i ta strona korzysta z ciasteczek.