JavaScript poza przeglądarką - wprowadzenie do Node.js i npm

JavaScript poza przeglądarką - wprowadzenie do Node.js i npm

W której części aplikacji webowej swoją rolę odgrywa JavaScript? Pierwsza poprawna odpowiedź brzmi – w części aplikacji uruchamianej po stronie klienta – w końcu zajmuje się nim przeglądarka internetowa. To prawda, współczesna przeglądarka musi zawierać w sobie silnik do obsługi tego języka. Jest jednak druga, dla niektórych mniej oczywista odpowiedź – w części uruchamianej po stronie serwera bądź też uruchamianej jako odrębna usługa bez przeglądarki. Czy JS poza przeglądarką jest jednak w ogóle potrzebny? O tym w dzisiejszym poście.

Problemy pierwszego świata

Znacie te wszystkie memy spod znaku first world problems? Możemy tutaj podpiąć wszystkie wymyślane na siłę problemy ludzi żyjących w dostatku i bogactwie:

Czy Node.js to kolejny z first-world problems?

Czy sytuacja z wyciąganiem JavaScriptu poza przeglądarkę nie wygląda podobnie? No bo przecież sprawy układają się całkiem dobrze – front-endowcy używają najnowszych frameworków do tworzenia swoich aplikacji, przeglądarki sprawnie interpretują nasze skrypty, za back-end odpowiadają Pythony, Railsy i .NETy – po co upychać JS’a w środowisko do którego nie był on przeznaczony? Powodów jest co najmniej kilka.

Jednym z nich ujednolicenie stacka technologicznego którym posługujemy się tworząc aplikacje webowe. Typowa lista technologii wykorzystywanych w takich rozwiązaniach może wyglądać w ten sposób:

  • Front-end – JavaScript
  • Back-end – Python / Ruby / C#
  • Baza danych – MSSQL, PostgreSQL, Oracle

Do każdego z tych trzech poziomów potrzebujemy specjalistów od bardzo różniących się od siebie języków. Czy taki kombajn to zawsze to czego potrzebujemy? Czasami nasz back-end jest na tyle prosty i lekki, że sztuką dla sztuki jest wprowadzanie kolejnego języka tylko po to aby napisać część serwerową naszej aplikacji. Po stronie bazy nie zawsze musimy stawiać bazy relacyjnej – często bazy noSQL spełnią nasze wymagania. Jak pewnie zgadujesz wszystkie te warstwy może z powodzeniem obsługiwać JavaScript – to obecnie najpopularniejszy język wykorzystywany na front-endzie którego możemy dzisiaj używać na wszystkich niższych warstwach.

Kolejny argument wynika z potencjału i popularności języka. Ludzie tworzący aplikacje oparte o JS’a w skali nieco większej niż aplikacja “To do” od dawna pragnęli wykorzystywać jego możliwości i siłę społeczności poza przeglądarką. Takie elementy języka jak dynamiczne typowanie, możliwości programowania funkcyjnego czy bezproblemowa praca z najpopularniejszymi formatami danych (JSON) wydawały się robić z JavaScriptu idealnego kandydata do pracy na serwerach.

Niestety, funkcjonalności takie jak chociażby dostęp do systemu plików były przez długi czas tylko marzeniem. Operowanie na plikach? Zaawansowana obsługa protokołów sieciowych? Praca jako usługa na serwerze? Współdzielenie kodu i managery paczek? Pewnie, weź swojego C#’a, Pythona czy Ruby’ego bo w JavaScripcie na pewno ci tego nie zrobię.

Na szczęście i na to znaleziono rozwiązanie.

Dodatkowo warto też wspomnieć o nieustannej potrzebie tworzenia i szlifowania narzędzi wspierających codzienną pracę programistów posługujących się JavaScriptem. Dla oszczędzenia czasu nasze testy jednostkowe chcielibyśmy uruchamiać w tle. Podobnie z narzędziami do sprawdzania składni, budowania naszego kodu i optymalizowania go. Procesy nie wymagające zaawansowanego interfejsu użytkownika powinny być możliwe do uruchamiania jako usługi a nie aplikacje.

Wszystko to o czym napisałem powyżej stało się możliwe dzięki stworzeniu Node.js – środowisku z którym prędzej czy później spotkasz się tworząc aplikacje w JS’ie. Dzisiaj zaprezentuję ci czym jest ten tajemniczy Node i w jaki sposób możemy go wykorzystać w naszej pracy.

Czym jest Node.js

Pierwszą podpowiedź dotyczącą tego czym jest Node.js znajdziemy na stronie poświęconej opisywanemu dzisiaj rozwiązaniu:

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js’ package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

Trzy podstawowe punkty które należy wyciągnąć z tej definicji to:

  • built on Chrome’s V8 JavaScript engine

Jesteśmy przyzwyczajeni do tego, że przeglądarki bez najmniejszego problemu przetwarzają tworzone przez nas skrypty języka JavaScript. Co w momencie, kiedy chcielibyśmy je uruchamiać poza przeglądarką? Są dwie możliwości – albo zaczniemy projektować parser tego języka i środowisko które będzie go rozumiało, albo postąpimy jak rozsądny programista i poszukamy istniejących rozwiązań.

Pomyślmy… skąd wziąć coś co rozumie JS’a? No tak, niby przeglądarki go rozumieją, ale… WAIT A MINUTE! A gdyby tak spojrzeć w kod źródłowy takiego chociażby Chrome’a i zobaczyć jak on radzi sobie z przetwarzaniem tego języka? Czyżby to coś nazywało się V8, miało otwarty kod źródłowy, działało turboszybko i było silnikiem do obsługi JavaScript’u? Dokładnie tak!

(dla ciekawskich – V8 to nie jedyny silniki którego kod można swobodnie przeglądać – obok mamy chociażby Microsoftową Chakrę)

V8 to solidna podstawa całego Node’a, ale sama interpretacja języka którym chcemy się posługiwać nie wystarczy do stworzenia pełnoprawnego środowiska, które dodatkowo ma przecież działać po stronie serwera. Poza wspomnianym silnikiem w pracy Node’a ogromną rolę odgrywają komponenty stworzone w językach C oraz C++ (mówiłem, że ma być turboszybko), takie jak przede wszystkim biblioteka libuv, crypto czy http-parser. Node.js posiada zaimplementowaną obsługę wywoływania takich bibliotek tworzonych w innych językach poprzez mechanizm tzw. bindingów oraz addonów.

  • event-driven, non-blocking I/O model

Typowe podejście do operacji wejścia/wyjścia (I/O) takich jak np. praca na plikach (odczyt, zapis) w wielu językach programowania realizowane jest synchronicznie (blocking). W momencie zgłoszenia chęci dostępu do pliku czy bazy danych wszystko co możemy zrobić to… czekać. Kiedy dany zasób będzie dostępny nasz program wykona kolejne instrukcje i w taki sposób linijka po linijce będziemy się posuwać do przodu. W żaden sposób nie przeskoczymy niestety momentu oczekiwania:

  1. Chciałbym wpisać do pliku dane.txt moje imię i nazwisko
  2. (czekam na dostęp do pliku… czekam… czekam…)
  3. Uzyskałem dostęp do pliku dane.txt
  4. Wpisuję moje imię i nazwisko
  5. Wykonuję kolejne operacje

Ryan Dahl, twórca Node.js’a kwestionował takie podejście. W swojej pierwszej prezentacji poświęconej Node.js mówił, że taki za taki stan rzeczy odpowiadają dwie kwestie – kultura i infrastruktura.

Kultura dlatego, ponieważ “żądanie wyniku natychmiast” było przez długi czas uważane za jedyne słuszne podejście do operacji I/O. Niestety, na to rzekome “natychmiast” traciliśmy zazwyczaj bardzo dużo czasu związanego z oczekiwaniem na dany zasób.

Infrastruktura natomiast dlatego, że przez długi czas twórcy języków programowania źle podchodzili do asynchroniczności – albo nie było możliwości na np. dostęp do bazy danych czy pliku w sposób inny niż synchroniczny (brak callbacków, funkcji anonimowych), albo liczba skomplikowanych rozwiązań skutecznie odrzucała nieznających tematu programistów.

JavaScript natomiast, jako język wykorzystywany do obsługi interfejsów użytkownika które z definicji muszą być “non-blocking”, wpisywał się idealnie w rozwiązanie problemów związanych z I/O. Najprościej można to sobie wyobrazić myśląc o obsłudze kliknięcia myszką na stronie:

Taki fragment kodu w żaden sposób nie blokuje głównego wątku programu. Po prostu zgłaszamy chęć “reakcji” na dany event. Nie wiemy kiedy on nastąpi – ale jeśli nastąpi, to chcemy wykonać określoną akcję.

Gdzie tutaj przewaga JavaScriptu nad innymi językami? Wystarczy spojrzeć na parametry z jakimi wywołujemy metodę addEventListener – jako pierwszy parametr przekazujemy string, a jako drugi… funkcję. Funkcję, która spełnia rolę event listenera (czegoś, co wykona się w następstwie wybranego eventu).

Praca z funkcjami anonimowymi które spełniają rolę callbacków w JS’ie jest czymś naturalnym – wiele języków nie pozwala jednak na tak swobodne przekazywanie funkcji jako parametrów do innych funkcji (co byłoby pomocne chociażby w przypadku asynchroniczności).

Przekładając to na pracę z I/O w Node.js znajdziemy coś bardzo podobnego (tutaj np. dostęp do pliku):

Oczywiście tutaj chcemy powiedzieć jasno, że w tym konkretnym momencie interesuje nas np. dostęp do pliku, ale realizacja tego zadania może być wykonana asynchronicznie – dzięki podejściu z wykorzystaniem callbacków osobną operacją będzie zgłoszenie chęci dostępu do pliku, a osobną wykonanie operacji na nim.

W Node.js postanowiono więc wykorzystać “całą dobroć” jaką dostarcza nam język JavaScript do realizacji zadań związanych z operacjami wejścia/wyjścia w sposób nieblokujący głównego wątku aplikacji.

  • package ecosystem – npm

The npm registry hosts over a quarter million packages of reusable code — the largest code registry in the world.

npm to manager paczek dla aplikacji tworzonych w języku JavaScript. Jest to obecnie jedno z największych źródeł bibliotek i rozwiązań open-source dostępnych we współczesnym świecie programowania. W zasobach npm’a znajdują się zarówno doskonale znane frameworki i biblioteki jak Angular czy React, jak i pojedyncze funkcje spełniające rolę “utilsów” w codziennej pracy programisty.

Początkowo npm był traktowany jako manager paczek przeznaczony wyłącznie dla rozwiązań serwerowych, jednak z biegiem czasu wyparł on takie rozwiązania jak Bower i obecnie uznaje się go za podstawowe narzędzie każdego front-endowca.

Podstawowym elementem pracy z npm’em jest plik package.json, który zawiera całą specyfikację paczek i zależności używanych w projekcie. Zamieszczamy w nim informacje o nazwie projektu, autorze, używanej licencji czy słowach kluczowych danego projektu. Przede wszystkim zawiera on jednak listę wszystkich wersjonowanych zależności na których opiera się dany projekt, z rozróżnieniem ich typu (dependencies, czyli zależności niezbędne do działania naszej aplikacji oraz devDependencies, czyli tzw. tooling).

Node.js w praktyce

Po wprowadzeniu do tematu zobaczmy jak wykorzystać Node’a w praktyce.

Instalacja

Node.js możemy pobrać ze wspomnianej wcześniej strony głównej ale dla nieco bardziej zaawansowanych polecam się zapoznać z nvm’em – narzędziem do kontroli wersji Node’a z którą pracujemy – dostępne jest ono tutaj oraz tutaj dla Windows.

W przypadku instalacji Node’a ze strony głównej pobieramy instalator odpowiedniej wersji (u mnie 6.9) i postępujemy według wskazówek:

Po przejściu przez kilka początkowych ekranów instalatora możemy podejrzeć jakie komponenty wchodzą w skład instalatora Node.js:

Przede wszystkim widzimy, że zainstalujemy zaraz środowisko uruchomieniowe (runtime), do tego managera paczek npm, skróty do dokumentacji i dodamy do zmiennych środowiskowych zmienną node. Klikamy Next, następnie Install i czekamy kilka chwil do zakończenia procesu instalacji.

Pierwsze testy

Poprawną instalację Node.js najłatwiej zweryfikować z poziomu konsoli. W Windowsie może to być zwykły cmd lub cmder – uruchamiamy więc konsolę, wpisujemy polecenie node i sprawdzamy, czy kursor zmienił swoją postać.

Tryb w którym się teraz znajdujemy to tzw. REPL – read-eval-print-loop. Co możemy tutaj robić? Pisać… zwykły JavaScript! Dokładnie tak, nie jesteśmy już dłużej przywiązani do przeglądarki internetowej. Wpisywane fragmenty kodu będą na bieżąco przetwarzane przez Node’a a my będziemy otrzymywać wyniki w konsoli.

Zacznijmy od pobrania informacji o środowisku na którym uruchomiony jest node. Aby to zrobić możemy użyć modułu os udostępniającego nam informacje o systemie operacyjnym i właściwościach maszyny (o modułach w dalszej części posta).

Najpierw za pomocą funkcji require pobieramy referencję do modułu który nas interesuje, a następnie wywołujemy dostępne funkcje takie jak type czy platform aby dowiedzieć się czegoś o naszym systemie operacyjnym. Wygląda na to, że Node.js może faktycznie pobierać informacje o platformie na której pracuje – w przypadku pracy na serwerach informacje o dostępnej pamięci, procesorze czy wersji OS’a mogą być dla autora danego rozwiązania kluczowe!

 

Zobaczmy teraz co stanie się jeśli zechcemy użyć zmiennych globalnych znanych z przeglądarek takich jak documentwindow. Chyba nie trudno zgadnąć, co?

Document Object Model, czyli znany z przeglądarek DOM, jest tutaj po prostu niedostępny. Tak jak już mówiłem – pomiędzy Node.js a przeglądarkami biegnie mocno zaznaczona granica. DOM jest właśnie tym, co udostępniają nam przeglądarki internetowe, a Node.js z założenia ma być niezależnym środowiskiem uruchomieniowym – natywne wsparcie dla takich obiektów jak document nie miałoby tutaj sensu. Oczywiście istnieją rozwiązania które pozwalają nam operować na czymś co symuluje DOM, jednak nie są to funkcjonalności wbudowane w Node.js.

Funkcjonalnością znaną z przeglądarek którą możemy jednak przetestować jest chociażby logowanie do konsoli:

 

Czy pisanie kodu na żywo to jedyny sposób w jaki możemy testować i używać Node.js? Oczywiście, że nie. W przypadku bardziej skomplikowanych funkcjonalności pisanie w ten sposób byłoby bardzo uciążliwe – o automatyzacji naszej pracy moglibyśmy zapomnieć. Właśnie dlatego w Node.js możemy posługiwać się gotowymi skryptami. Wystarczy, że w utworzymy testowy skrypt z fragmentem kodu JS’a i podamy go jako argument w konsoli do polecenia node.

Weźmy chociażby wypisanie na konsolę imienia i nazwiska. W tym celu tworzymy plik start.js który wygląda w ten sposób:

Po wykonaniu polecenia node start.js zobaczymy wypisany na konsoli string zawierający moje imię i nazwisko:

To działa!

 

W pierwszym przykładzie gdzie używaliśmy modułu os do pobrania informacji o systemie operacyjnym napisałem, że temat modułów zostanie rozwinięty w dalszej części posta. Warto temu elementowi Node.js przyjrzeć się w osobnym punkcie i teraz jest na to dobra pora.

Czym są więc moduły? Moduły w Node.js “opakowują” daną funkcjonalność możliwą do wykorzystania w innych miejscach. Są one reprezentowane są przez fizyczne pliki z kodem JavaScript.

Zobaczmy teraz jak możemy się nimi posługiwać. Zdefiniujmy najpierw w nowym pliku math-utils.js przykładowe funkcje do dodawania i mnożenia i udostępnijmy je jako moduł:

Wszystko co w danym pliku będzie zawierał obiekt module.exports będzie możliwe do użycia na zewnątrz, poprzez funkcję require.

W naszym pliku start.js odwołujemy się do modułu math-utils i używamy dostępnych funkcji jak poniżej:

Wynikiem są wypisane na konsoli liczby 7 oraz 21:

W ten sposób możemy w łatwy sposób posługiwać się reużywalnymi fragmentami kodu w naszym projekcie czy też używać funkcji udostępnianych przez zewnętrzne biblioteki. Oczywiście przykład który ja pokazałem jest bardzo prosty – w przypadku rzeczywistych projektów modułami mogą być zdecydowanie bardziej zaawansowane fragmenty kodu.

(uwaga – w przypadku pracy z JavaScriptem w przeglądarce nie możemy zostawić kodu w tej postaci – funkcja require nie będzie po prostu dostępna więc moduły musimy dostarczać w inny sposób)

Jak korzystać z managera paczek npm

Po zapoznaniu się z podstawami node’a oraz po przetestowaniu jego działania na żywo sprawdźmy teraz jak wykorzystać manager paczek npm do ułatwienia sobie pracy w przypadku korzystania z zewnętrznych zależności, bibliotek i frameworków.

Aby w naszym projekcie z powodzeniem wykorzystywać npm’a musimy go najpierw zainicjalizować. W tym celu w katalogu głównym wykonujemy polecenie npm init i odpowiadamy na kolejno pojawiające się pytania:

Po przejściu przez całą procedurę utworzy się wspomniany wcześniej plik package.json w którym automatycznie rejestrowane będą wszystkie dodawane przez nas biblioteki i zależności zewnętrzne.

Zobaczmy teraz, jak z użyciem npm’a dodać do naszego projektu pierwszą bibliotekę – zdecydujemy się na lodash i wykorzystamy ją do prostych operacji na tablicach.

W konsoli wpisujemy polecenie: npm install lodash –save i czekamy na dokończenie instalacji.

Zawartość pliku package.json wygląda teraz następująco:

Na dole w sekcji dependencies mamy dowód na to, że faktycznie z powodzeniem dodaliśmy lodasha do naszego projektu. Możemy też zobaczyć, że w katalogu w którym pracujemy utworzył się nowy folder – node_modules. Jest to miejsce gdzie domyślnie npm umieszcza wszystkie pobierane przez nas paczki.

Przykład który teraz pokażę będzie polegał na wykonaniu prostych operacji na tablicach i sprawdzeniu dwóch kwestii – pierwsza dotyczy tego, czy w naszej tablicy istnieją użytkownicy płci żeńskiej (funkcja some), druga dotyczy sprawdzenia, czy każdy z użytkowników spełnia określony warunek (funkcja every).

Aby wykorzystać dodaną przez nas bibliotekę która udostępnia te dwie funkcje używamy poznanej już funkcji require:

Skrypt uruchamiamy tak jak poprzednio – node [nazwa_pliku]. Rezultatem jest odpowiedź na dwa pytania dotyczące naszych użytkowników:

Na podstawie tego fragmentu można jeszcze powiedzieć o dwóch ciekawostkach dotyczących modularyzacji kodu.

Pierwsza dotyczy optymalizacji tego co napisaliśmy. Czy naprawdę zawsze musimy pobierać całą bibliotekę do wykorzystania jej dwóch całkowicie niezależnych funkcji? Odpowiedź brzmi… nie, nie zawsze. Dlatego właśnie większość współczesnych bibliotek rozbija swoje funkcjonalności na niezależne części (moduły) tak aby każdą z nich wykorzystywać wg potrzeb. W przypadku tzw. “bundlowania” naszych plików narzędziami takimi jak Webpack skutkuje to mniejszym rozmiarem skryptów które użytkownik pobiera wchodząc np. na naszą stronę, przez co jej zawartość jest dostępna dla niego zdecydowanie szybciej.

Nasz przykład możemy przerobić w ten sposób:

Za pomocą funkcji require pobieramy dwie potrzebne funkcje i to wszystko do czego przyda nam się lodash. Kolejny krok to pytanie… czy przyda nam się cały lodash? 😉 Jeśli nie, może lepiej dodawać do projektu tylko niezbędne funkcje zamiast całej biblioteki?

 

Druga ciekawostka to jednocześnie pytanie i odpowiedź. Dlaczego w przypadku biblioteki lodash nie musimy podawać pełnej ścieżki do modułu którego chcemy użyć? Odpowiedzią jest zastosowana konwencja o której więcej mówi nam dokumentacja. Oto jeden z fragmentów:

When the code in the foo package does require(‘bar’), it will get the version that is symlinked into /usr/lib/node/foo/1.2.3/node_modules/bar. Then, when the code in the bar package calls require(‘quux’), it’ll get the version that is symlinked into /usr/lib/node/bar/4.3.2/node_modules/quux.

Dzięki przyjętemu nazewnictwu Node jest w stanie rozwiązywać niezbędne zależności o ile spełniają one opisane w dokumentacji wymagania co do położenia względem wersji i nazwy paczki.

 

Biblioteka lodash to tylko jeden z typów zależności które możemy dodawać do projektu za pomocą npm’a. Zewnętrznymi zależnościami które dzięki niemu “doinstalujemy” do projektu mogą być też wszelkiego typu biblioteki, mogą to być narzędzia do testowania które będziemy instalować z flagą –save-dev, mogą to być typowe utilsy czy moduły które dopieszczamy miesiącami i chcemy je bezproblemowo wykorzystywać ponownie.

Używając tego typu managera paczek zyskujemy przede wszystkim jedno miejsce do kontrolowania wszystkich zależności i ich wersji, dzięki czemu ich ew. wymiana i aktualizacja staje się o wiele łatwiejsza niż w przypadku podejścia “każdy po swojemu”.

Ważny punkt – folderu node_modules nie dodajemy do systemu kontroli wersji. Dodajemy za to plik package.json. Dzięki temu poleceniem npm install będziemy mogli w folderze z czystym projektem odtworzyć całą strukturę niezbędnych zależności.

Bez obaw – to tylko pierwszy krok 😉

Tak tak, dobrze przeczytałeś. Wiedza, którą pochłonąłeś w tym poście to tylko pierwszy, ale niezbędny krok do poznania całego morza narzędzi i rozwiązań opartych o Node.JS. Dzięki zrozumieniu podstaw będziemy teraz mogli pokazać ci przykłady z życia wykorzystujące chociażby npm’a, który jest standardem używanym przez nas na co dzień w rzeczywistych projektach. Trudno i raczej mało rozsądnie byłoby nam opowiadać o Angularze2, Webpacku czy aplikacjach back-endowych bez przedstawienia wszystkiego tego co powyżej. Teraz już z górki 🙂

Jeśli chcesz wiedzieć jeszcze więcej, to na koniec krótki spis rozwiązań za którymi stoi gdzieś w tle Node.JS:

  • frameworki wspomagające tworzenie aplikacji back-endowych w języku JavaScript (express, sails)
  • bundlery, czyli “paczkomaty” łączące zależności w jedną całość (webpack)
  • test-runnery umożliwiające testowanie naszego kodu (mocha, jasmine)
  • lintery i narzędzia do statycznej analizy kodu (eslint)
  • narzędzia wspomagające pracę nad projektami z poziomu konsoli (angular-cli)

Przed nami kolejne posty dotyczące JavaScriptu. Wspomniane narzędzia i technologie będziemy się starali w miarę możliwości rozbierać na czynniki pierwsze. Przede wszystkim jednak pokażemy wam jak wpływają one na codzienną, praktyczną pracę z JavaScriptem.

Do zobaczenia!

  • Adrian Dobrowolski

    odpowiedz na drugie pytanie jest ‘nie’

    • Przemek Smyrdek

      Możesz rozwinąć? 🙂

      • Adrian Dobrowolski

        masz zapytanie czy wszyscy sa pelnoletni const isEveryUserOverEighteen = lodash.every(userData, user => user.age >= 18);
        nizej napisales ze tak, co jest nieprawdą, bo masz Pawła który ma 15 lat 🙂 { id: 5, name: ‘Paweł’, gender: ‘male’, age: 15 }

        • Przemek Smyrdek

          Racja! Dzięki za czujność, Paweł usunięty 😉

  • > NPM, czyli node package manager

    No właśnie ani nie NPM, ani nie node package manager. npm to npm – tyle → https://docs.npmjs.com/misc/faq#if-npm-is-an-acronym-why-is-it-never-capitalized

    • Przemek Smyrdek

      Masz rację – npm to npm. Dbając o merytorykę poprawiam 😉