VRath – konfiguracja webpacka

W poprzednim wpisie dotyczącym projektu VRath określiłem z jakich technologii będę korzystał i pojawił się także pierwszy sensowny commit. Dzisiaj kolejny etap budowy projektu – konfiguracja webpacka.

Dzielenie kodu na mniejsze kawałki to już praktycznie standard, zarówno w dużych jak i małych projektach. Programiści tworzą moduły, które odpowiadają za poszczególne funkcjonalności w aplikacjach, a z kolei same moduły z reguły także są dzielone na mniejsze pliki np. widoki, kontrolery, modele etc. Separacja pozwala szybciej odnaleźć się w projekcie, ułatwia testowanie i przyspiesza pracę nad kodem, który tworzony jest przez wiele osób. W świecie JavaScriptu jeszcze kilka lat temu taka separacja była utrudniona. Chcąc zachować podział aplikacji na kilka części należało utworzyć wiele odwołań do plików za pomocą tagu <script>, co niestety odbijało się czkawką na wydajności aplikacji, ponieważ przeglądarka musiała wykonać wiele zapytań. Dziś trudno sobie wyobrazić projekt, który miałby kilkaset bądź nawet kilka milionów linii kodu, podzielony na kilkadziesiąt tagów <script>, to byłoby nie do zaakceptowania. Na szczęście powstały narzędzia pozwalające połączyć poszczególne części aplikacji w jeden zasób. Przykładami takich narzędzi są Browserify oraz require.js. Oba narzędzia są już bardzo dojrzałe i posiadają rozbudowane społeczności, ale ja chciałbym skupić się na jeszcze jednym, który w ostatnim czasie stał się liderem tego typu rozwiązań, mianowicie na webpacku.



Można się zastanawiać co wyróżnia webpacka na tle swoich konkurentów. Wydaje mi się, że najmocniejszą cechą będzie konfigurowalność. Webpack z pomocą pluginów i loaderów jest w stanie nie tylko pracować z javascriptem, ale także pozwala na dociąganie plików CSS czy plików graficznych. Jego starsi bracia takiej możliwości nie posiadają, no chyba, że połączymy ich z managerem zadań takim jak gulp czy grunt. Im mniej narzędzi tym lepiej, bo łatwiej sprawować nad środowiskiem kontrolę, tak więc webpack wychodzi w tym starciu na plus.

Instalacja webpacka

Zatrzymajmy się z teorią w tym miejscu żeby nie było zbyt „sucho” i pozwólcie, że zacznę instalacje webpacka za pomocą yarna:

yarn add webpack --dev

Webpack został zainstalowany lokalnie dla projektu VRath wraz z wpisem do package.json. Instalacja lokalna powoduje, że nie mamy możliwości globalnie wywołać polecenia webpack, jednak ten sposób jest rekomendowany przez zespół webpacka. Aby skorzystać z polecenia webpack, możemy odwoływać się za każdym razem do ścieżki node_modules/webpack/, lub też zastosować skrypt npm, co z pewnością jest wygodniejsze, a i tak ze skryptów npm’owych skorzystam przy uruchamianiu chociażby lintera czy testów. Tak więc w package.json umieszam:

"scripts": {
    "start": "webpack --config webpack.config.js"
}

Wykonanie w konsoli npm start spowoduje błąd, ponieważ plik webpack.config.js jeszcze nie został utworzony, ale mam już pewność, że polecenie webpack jest poprawnie rozumiane.

Konfiguracja i pierwszy build

Wiem już, że poprawnie zainstalowałem webpacka, ale żeby móc wykorzystać jego możliwości to będę musiał utworzyć plik konfiguracyjny. Zgodnie z tym co umieściłem w package.json, mój plik konfiguracyjny będzie nazywał się webpack.config.js, a o to jego zawartość:

module.exports = {
	entry: "./src/assets/js/app.js"
};

Jest to najmniejsza konfiguracja potrzebna do uruchomienia webpacka. Jest to tzw. definicja wejścia (ang. entry), czyli miejsca od którego webpack rozpoczyna analizę drzewa zależności. Standardowo będzie to plik główny naszej aplikacji. Skoro mamy wejście to potrzebne jest nam również wyjście, do którego trafi nasz zasób:

const path = require("path");

module.exports = {
	entry: "./src/assets/js/app.js",
	output: {
		filename: "bundle.js",
		path: path.resolve(__dirname, "build")
	}
};

Wykorzystałem zalecenie użycia modułu path do wskazania ścieżki absolutnej folderu build, w którym ostatecznie wyląduje finalny zasób (zmienna __dirname jest dołączona za pośrednictwem środowiska node.js i określa aktualny katalog). Czas przetestować moją konfigurację, w tym celu utworzę dwa pliki:

1. Plik app.js stanowiący wejście do aplikacji:

var module = require("./module.js");

console.log(module.hello());

2. Plik module.js będący zależnością dla pliku app.js:

module.exports = {
	hello: function () {
		return "Hello World!";
	}	
};

Prosta sprawa. Jeden plik zaciąga drugi i wywołuje metodę hello(), która wypisuje na konsoli napis „Hello World!”. Aby utworzyć paczkę wynikową, którą nazwałem bundle.js wykonuję polecenie:

npm start

W projekcie pojawił się katalog build, a w nim plik bundle.js. Aby sprawdzić czy wszystko wykonuje się jak należy, wystarczy w konsoli wpisać:

node build/bundle.js

No i mamy Hello World! I na tym moglibyśmy zakończyć, ale mój kod wygląda trochę staroświecko. Przy przedstawianiu projektu zadeklarowałem się, że będzie on pisany z użyciem standardu ES6, dlatego teraz przepiszę oba pliki aby było nieco „nowocześniej”:

app.js:

import module from "./module.js";

console.log(module.hello());

module.js:

export default {
	hello: () => "Hello World!"
};

Ok jest tylko jeden problem, takiego kodu nie mogę użyć bezpośrednio w każdej przeglądarce, ponieważ standard ES6 jest w miare nową rzeczą, a kwestia importów jest dość problematyczna dla twórców przeglądarek. Z pomocą przychodzą w webpacku loadery (ang. loaders). Dokonują one transformacji kodu zanim dołączy on do finalnego zasobu. Najpopularniejszym sposobem na kompilację ES6 do standardu ES5 jest wykorzystanie dojrzałej biblioteki babel.js i na szczęście webpack dysponuje loaderem babel-loader, który ów bibliotekę wykorzystuje. Dodam go do projektu:

yarn add babel-loader babel-core babel-preset-env --dev

Oprócz samego loadera dodałem także babel-core, który jest wymaganą zależnością tego loadera, oraz babel-preset-env jako konfigurację babela. Następnie muszę zmodyfikować plik webpack.config.js:

const path = require("path");

module.exports = {
	entry: "./src/assets/js/app.js",
	output: {
		filename: "bundle.js",
		path: path.resolve(__dirname, "build")
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				exclude: [/node_modules/],
				use: [{
					loader: "babel-loader",
					options: { 
						presets: ["env"] 
					}
				}]
			}
		]
	}
};

W obiekcie module dodawane są reguły dla loaderów. W powyższym przykładzie dla wszystkich plików z rozszerzeniem .js wykorzystywany jest babel-loader. Za pomocą klucza test wyrażeniem regularnym wskazujemy, które pliki mają być poddane transformacji. Klucz exclude pozwala nam wykluczyć wybrane pliki/foldery, a use wskazuje loadery wraz z ich opcjami konfiguracyjnymi. Dla babel-loader’a należy ustawić odpowiedni preset, czyli konfigurację uruchamianą przy transformacji. Jedną z nowszych konfiguracji jest „env”, która stara się w sposób automatyczny sprawdzić możliwości środowiska i nie tranformować części kodu, które są już wspierane przez przeglądarki. W kolejnych artykułach będę do tego wracał, ponieważ dla reacta i plików .jsx potrzebne są odpowiednie wpisy w preset.

Podsumowanie

Wstępna konfiguracja środowiska zakończona. Następnym krokiem będzie podłączenie minifikacji kodu, dołączenie reacta i utworzenie po raz kolejny Hello World, ale tym razem z większą ilością zabawek 😉