Node.js i npm
Ogólnie o Node.js
Node.js to środowisko, które pozwala nam odpalać Javascript poza przeglądarką - bezpośrednio z terminala. Dla nas - frontendowców - jest to w zasadzie nierozłączna rzecz w codziennej pracy i prędzej czy później każdy z nas będzie miał z nim styczność.
Środowisko to charakteryzują trzy główne rzeczy.
Pierwsza z nich to asynchroniczność. Node tak samo jak Javascript w przeglądarce jest jednowątkowy. Aby móc wykonywać wiele operacji równocześnie konieczne jest używanie asynchroniczności. Rozmawiamy o tych sprawach w tym rozdziale.
Drugą charakterystyczną cechą jest to, że w odróżnieniu od klasycznego dołączania skryptów do pliku html, w Node każdy plik javascript stanowi oddzielny moduł (oddzielne zamknięte pudełko), z którego musimy exsportować/importować interesujące nas rzeczy. Mówimy sobie o tym tutaj.
Trzecią cechą jest możliwość prostego doinstalowywania dodatkowych pakietów (funkcjonalności).
Wraz z Node.js dostajemy też Node Package Manager (w skrócie NPM), czyli mały program, który służy do zarządzania dodatkowymi funkcjonalnościami.
Takie managery to w sumie nic nowego. Dla Linuxa mamy APT (apt-get), dla MacOs Homebrew, dla PHP Composera itp. Już niedługo nawet Windows dostanie coś swojego.
Brakuje nam więc jakiejś funkcjonalności? Jednym poleceniem możemy to sobie doinstalować. Jeżeli dodatkowo taka funkcjonalność do swojego działania wymaga innych pakietów, manager automatycznie je doinstaluje.
W dzisiejszych czasach środowisko Node.js tak szybko się rozwija, że w zasadzie znajdziemy tutaj pakiety do każdej funkcjonalności, która cię interesuje. Od sliderów, poprzez karuzele, biblioteki do pobierania danych itp. Wygoda tego rozwiązania sprawiła, że większość narzędzi, których na co dzień używają frontendowcy to właśnie takie rozbudowane pakiety, które doinstalowujemy sobie w razie potrzeby.
Instalacja Node i uruchamianie skryptów
Zaczynamy jednak od zainstalowania Node.js.
Wchodzimy na stronę https://nodejs.org/en/ i dokonujemy instalacji.
Różni się ona w zależności od systemu. Na Windowsie nie powinno być raczej żadnego problemu - ot ściągamy instalację, next, next i tyle. Ewentualnie po instalacji czasami trzeba się ponownie zalogować do systemu by zaktualizowało zmienne Path, dzięki czemu możemy później używać Node w terminalu. Raz jeden widziałem też przypadek, gdzie antywirus blokował proces instalacji.
Systemy Linux/Osx są bardziej restrykcyjne jeżeli chodzi o uprawnienia, i w wielu sytuacjach tradycyjna instalacja Node może później przynieść problemy z brakiem dostępu przy próbie instalacji globalnych pakietów.
Aby uniknąć tego problemu, możemy zainstalować Node na kilka sposobów.
Poniżej przepis dla odpowiednich systemów.
Od tej pory będziemy mogli odpalać pliki ze skryptami z poziomu terminala:
node nazwa_pliku.js
//nie musimy podawać rozszerzenia .js
node nazwa_pliku
NPM i instalowanie pakietów
Wspomniany powyżej NPM dostajemy wraz z Node.js
Menadżerów dla Node.js mamy na rynku kilka. NPM jest najpopularniejszy, ale równie często używane są yarn, pnpm czy chociażby ich starszy brat bower (a znajdzie się i kilka innych). Osobiście początkowo nie zawracał bym sobie tym głowy i pozostał przy domyślnym. W poniższym tekście skoncentruję się właśnie na nim.
Funkcjonalności, które możemy instalować dla Node.js są zgrupowane we wspólnym rejestrze pakietów dostępnych na stronie https://www.npmjs.com/. Aby znaleźć dany pakiet albo skorzystamy z wyszukiwarki na górze tej strony, albo zwyczajnie wyszukamy ją w necie wpisując npm nazwa_funkcjonalności
np. npm slider
. Problemem tutaj najczęściej będzie to, że pasujących wyników trafimy setki, a jak to z różnymi rozwiązaniami bywa - nie każde będzie dopracowane i nie każde będzie spełniać nasze oczekiwania. Pozostaje testowanie i nabieranie doświadczenia (czyli powolna zamiana w seniora).
W środowisku Node.js pakiety możemy instalować globalnie na cały komputer oraz lokalnie per dany projekt.
Te instalowane globalnie po zainstalowaniu dostępne są z każdego miejsca w komputerze. Do ich instalacji służy flaga -g
:
npm install nazwa_paczki -g
//lub skrótowo
npm i nazwa_paczki -g
Po zainstalowaniu takiego pakietu będziemy mogli odpalić go w terminalu poprzez podanie jego nazwy. Przykładem takiego pakietu może być live-server, czyli serwer lokalny, który daje nam automatyczne odświeżanie strony gdy piszemy nasz kod. Wystarczy takie narzędzie zainstalować poleceniem npm i live-server -g
, a następnie szybko odpalać w danym katalogu za pomocą polecenia live-server
.
Na co dzień tylko niektóre pakiety będziemy instalować globalnie. Zazwyczaj będą to dodatkowe narzędzia, które chcielibyśmy łatwo odpalać z każdego miejsca (np. wspomniany live-server, browser-sync, rimraf itp).
Pracując już na konkretnym projekcie pakiety dla niego będziemy chcieli instalować raczej lokalnie - per dany katalog. Żeby to zrobić będąc w danym katalogu w terminalu wpisujemy polecenie:
npm i nazwa_pakietu_1 nazwa_pakietu_2
Pakiet taki zostanie zainstalowany do katalogu node_modules który zostanie utworzony w tym samym katalogu.
W katalogu node_modules będzie zazwyczaj dużo plików. Nigdy tego katalogu nie powinniśmy wrzucać do repozytorium. Osoba która ściągnie nasz projekt i tak zainstaluje sobie pakiety na podstawie omawianego poniżej pliku package.json. Dlatego też node_modules jak najszybciej powinniśmy dodać do .gitignore
Aby odpalić tak zainstalowany pakiet skorzystamy z nieco innych metod. Ale to za chwilę...
Plik package.json
Zanim zainstalujemy pierwszy lokalny pakiet, warto utworzyć w danym projekcie plik package.json. Jest to plik konfiguracyjny dla danego projektu (a będąc bardziej szczegółowym jest to pliku konfiguracyjny naszego pakietu. Nasz projekt też możemy traktować jako swoisty pakiet, który w przyszłości moglibyśmy publikować do głównego rejestru wszystkich paczek).
Aby utworzyć taki plik, powinniśmy w danym katalogu użyć polecenia:
npm init
Zostaniemy zapytani o kilka rzeczy jak nazwa pakietu, opis, autor itp.
Wystarczy kilka razy nacisnąć Enter. Możemy też użyć polecenia npm init -y
które stworzy taki plik bez zadawania nam pytań.
Po odpaleniu powyższych komend, powinniśmy dostać plik package.json z mniej więcej poniższą zawartością:
{
"name": "test-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Plik taki jest o tyle ważny, że zapisywane są w nim instalowane przez nas lokalne pakiety. Jeżeli teraz coś doinstaluję w danym projekcie, odpowiednie informacje zostaną dodane do powyższego pliku:
npm i express body-parser
{
"name": "test-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1"
}
}
Po wstępnej konfiguracji projektu i zainstalowaniu potrzebnych paczek plik taki trafi później na repozytorium, a osoba która ściągnie dany projekt wszystkie potrzebne rzeczy zainstaluje pojedynczą komendą:
npm install
//lub
npm i
Domyślnie instalowane lokalne pakiety dodawane są do sekcji dependencies
w pliku package.json. Oznacza to, że danych pakietów chcemy używać w czasie tworzenia (development), ale też podczas jego działania (production). To do jakiej fazy zakwalifikujemy dany pakiet oznacza dodatkowa flaga, którą używamy podczas instalacji:
-g | pakiet instalowany globalnie na cały komputer. O tym już mówiliśmy. |
--save-dev lub -D | oznacza, że dany pakiet będzie przez nas używany w fazie developingu, czyli podczas tworzenia projektu |
--save-prod lub -P | dany pakiet będzie używany w czasie tworzenia strony ale też na produkcji. Pakiety takie zostaną zapisane do sekcji dependencies. Pakiety są domyślnie instalowane do tej sekcji, dlatego możemy tą flagę pominąć. W poprzednich wersjach npm flaga ta miała postać --save |
--save-optional lub -O | dany pakiet będzie używany tylko w przypadku użycia konkretnych funkcjonalności. Nie jest wymagana do działania projektu. |
--no-save | dany pakiet nie zostanie zapisana do pliku package.json |
Przykładowo po zainstalowaniu 4 pakietów zostaną one zapisane w odpowiednich sekcjach:
npm i express body-parser
npm i webpack webpack-cli -D
{
"name": "test-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1"
},
"devDependencies": {
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
}
}
Od strony działania pakietów nie ma znaczenia do której sekcji trafi dany pakiet w pliku package.json. Można to traktować jako sprawy porządkowe. W razie gdy dany pakiet zostanie zapisany w złej sekcji tego pliku, możemy go albo przeinstalować dodając odpowiednią flagę, albo ręcznie edytować ten plik po prostu przenosząc odpowiednie nazwy z sekcji do sekcji.
Umieszczanie odpowiednich pakietów w odpowiednich sekcjach za pomocą flag może się przydać w sytuacji, gdy chcemy nasz projekt postawić na serwerze. W katalogu node_modules zazwyczaj jest bardzo, bardzo dużo plików. Zamiast je wszystkie przerzucać na serwer, możemy połączyć za pomocą terminala (np. za pomocą Putty) z serwerem i zainstalować je bezpośrednio na nim.
Aby nie instalować paczek które wykorzystywaliśmy w fazie tworzenia (tych z sekcji devDependencies) posłużymy się poleceniem:
npm i --only=production
//lub
npm i --only=prod
Dodatkowo jeżeli na serwerze zmienna środowiskowa NODE_ENV
ustawiona jest na production, wtedy polecenie npm i
zainstaluje tylko paczki z sekcji dependencies.
Ale tak naprawdę nawet jeżeli wszystkie paczki mielibyśmy w sekcji dependencies, nic wielkiego by się nie stało - po prostu wszystkie byśmy zainstalowali poleceniem npm i
. Także jest to trochę marginalna sprawa (przy czym polecam trzymać porządek).
Aktualizacja pakietów
Gdy zainstalujemy dany pakiet, instalowany jest jego najnowsza wersja, a w pliku dodawany jest zapis o jej wersji:
"devDependencies": {
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
}
Możemy też zainstalować daną wersję pakietu podając konkretną wersję po nazwie:
npm view gulp version --json //sprawdzam jakie mamy wersje
npm i gulp@3.8.8
Wersje są zapisane w składni semver. Zapis taki składa się z trzech części oddzielonych kropkami. Pierwsza liczba oznacza wersję główną (major), druga podwersję (minor), a trzecia aktualizacje, lub poprawy błędów (patch, bug fixes).
Z przodu wersji mogą pojawić się dwa znaki:
Znak ^
, oznacza to, że dany pakiet będzie mógł być uaktualniony dla podwersji - np. wersja ^5.4.0
może być zaktualizowana do 5.5.0
ale nie do 6...
.
Znak ~
oznacza, że dany pakiet może być zaktualizowany tylko do nowszej wersji patchowej, czyli 5.4.0
do 5.4.2
ale nie do 5.5..
Aby zaktualizować dane pakiety użyjemy komend:
npm outdated //pokaże stan zainstalowanych pakietów
npm update //aktualizuje pakiety
Aby odinstalować dane pakiety, skorzystamy z polecenia:
npm uninstall nazwa_pakietu nazwa_pakietu2
Jeżeli dane pakiety chcielibyśmy zaktualizować do nowszej wersji głównej, albo wcześniej odinstalujemy i zainstalujemy je ponownie, albo też skorzystamy z przygotowanych do tego... pakietów. Jednym z nich jest npm-check. Po odpaleniu komendą npx npm-check -u
w terminalu pokaże się lista pakietów, które wymagają aktualizacji. Po wyborze odpowiednich zostaną one zaktualizowane.
Kolejną możliwością jest npm-check-updates. Po odpaleniu zaktualizuje wersje w package.json. Po tej czynności musimy jeszcze odpalić npm install
by realnie zaktualizować dane pakiety.
Przy czym niepisana zasada mówi o tym, że jak coś działa, to nie trzeba tego ruszać. Czasami aktualizacja pakietu do nowej wersji może pociągnąć z sobą wiele problemów - np. przestaną działać inne pakiety, które były zależne od danej wersji. Ogólnie rób to z głową.
Plik package-lock.json
Gdy zainstalujesz pierwsze pakiety, zauważysz, że obok katalogu node_modules został też utworzony plik package-lock.json.
Wyobraź sobie, że zacząłeś pracę nad projektem. Instalujesz pakiet np. express poleceniem npm i express
.
W twoim pliku package.json pojawił się odpowiedni zapis:
{
...
"dependencies": {
"express": "^4.17.1"
}
}
Twój współpracownik ściąga za jakiś czas dany projekt, po czym odruchowo instaluje wszystkie wymagane pakiety poleceniem npm i
. W pliku package.json nic się oczywiście nie zmieni, ponieważ nic nowego nie doinstalowywał.
Ale cóż to - gdy wpisze w terminalu polecenie npm list --depth=0
, okaże się, że zainstalowała mu się nowsza wersja 4.18.1, która w międzyczasie została wydana. Odpala projekt i coś nie działa jak należy.
I tu właśnie przychodzi z pomocą plik package-lock.json. Plik ten trzyma informacje o strukturze katalogu node_modules, o dokładnych zainstalowanych wersjach pakietów i wszystkich operacjach jakie przeprowadzane były na tym katalogu (instalowanie, odinstalowywanie pakietów itp). Dzięki niemu wszystkie osoby z zespołu instalując pakiety zainstalują je w dokładnie takich samych wersjach jak u innych.
npx
Po zainstalowaniu paczki lokalnie, nie możemy jej odpalać bezpośrednio za pomocą jej nazwy (możesz to zrobić tylko dla paczek globalnych). Aby je uruchomić możemy skorzystać albo z polecenia npx, albo z odpowiednich skryptów.
W nowych wersjach npm dostajemy dostęp do polecenia npx.
Aby użyć tego polecenia wystarczy wpisać:
npx nazwa_paczki
//np
npx touch my-file.txt
Jeżeli w danym katalogu jest zainstalowany lokalny pakiet o danej nazwie, polecenie to spróbuje go odpalić. Jeżeli takiego pakietu nie ma, ściągnie dany pakiet do pamięci, odpali, a po użyciu usunie go.
Poza szybkim uruchamianiem różnych narzędzi (np. npx live-server
) narzędzie to możemy wykorzystać też do testów odmiennych wersji danego pakietu.
W wersji npm6 i wyższych po użyciu tego polecenia zostaniemy dodatkowo poinformowani o konieczności instalacji danej paczki oraz o potwierdzenie tej operacji.
Tak naprawdę nic nie będzie instalowane (paczka ściągana jest tylko do tymczasowego katalogu cache), a sam komunikat został wprowadzony w celu poprawy bezpieczeństwa (tak byśmy przez przypadek nie odpalali niechcianych paczek). Jeżeli chcemy pominąć to pytanie, dodajmy flagę -y
:
npx touch -y my-file.txt
npx -y create-react-app my-app
Jeżeli nie chcesz by takie pytanie się pojawiało, możesz to zmienić za pomocą odpowiedniej flagi zgodnie z opisem.
Skrypty w package.json
Jeżeli jeszcze raz spojrzymy na nasz plik package.json, zobaczymy tam sekcję scripts:
{
"name": "test-project",
"version": "1.0.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
...
}
}
W sekcji tej możemy zamieszczać własne skrypty, które potem będziemy odpalać bezpośrednio z terminala. Najczęściej są one wykorzystywane do uruchamiania lokalnych pakietów, które dość często wymagają dodatkowych parametrów.
Aby odpalić skrypt o danej nazwie, w terminalu wpiszemy polecenie:
npm run nazwa_skryptu
//np.
npm run test
Nasze skrypty mogą mieć dowolne nazwy. Istnieją też predefiniowane. I tak jeżeli stworzymy skrypt o nazwie start, możemy go odpalić komendą npm run start
ale także npm start
. Podobne aliasy istnieją dla start, restart, stop, config, test i script.
Cechę tą wykorzystuje sporo popularnych narzędzi np. create-react-app, które po zainstalowaniu odpala się poleceniem między innymi poleceniem npm start
.
Dodatkowo dla każdego skryptu o danej nazwie możemy też stworzyć dwa dodatkowe z nazwami rozpoczynającymi się od pre
i post
. Pierwszy z nich będzie odpalany tuż przed naszym skryptem, a drugi tuż po:
{
"name": "test-project",
"version": "1.0.0",
"scripts": {
"prestart" : "echo '--- rozpoczynam ---'",
"start" : "echo '--- działam ---'",
"poststart" : "echo '--- kończę ---'"
}
}
I teraz w terminalu odpalenie naszego skryptu za pomocą komendy npm start
odpali po kolei prestart
, start
a następnie poststart
.
Więcej na ten temat możesz przeczytać tutaj.
W poniższym przykładowym projekcie skrypty będziemy głównie wykorzystywać do uruchamiania dodatkowych pakietów. Pamiętaj jednak, ze możesz je użyć do odpalania dowolnego kodu, który wklepujesz w terminal.
{
"name": "test-project",
"version": "1.0.0",
"scripts": {
"clean": "rm -rf dist/*",
"makeFiles": "mkdir dist/css && mkdir dist/js"
"start": "npm run clean && npm run makeFiles"
},
"devDependencies": {
...
}
}