Bundlery i Webpack
W poprzednich rozdziałach rozmawialiśmy o modułach, które umożliwiają nam podział naszego kodu na oddzielne pliki. Bardzo fajna sprawa. Problemem tutaj jest to, że funkcjonalność tak zadziała tylko najnowszych przeglądarkach, a dodatkowo przeglądarka będzie musiała się odwołać do każdego pliku z osobna, co zwiększy liczbę requestów.
Dlatego właśnie przy pracy z modułami większość developerów korzysta z dodatkowych narzędzi, które umożliwiają im tak zwane bundlowanie.
Bundlery
Bundlery to dodatkowe narzędzia, których główną funkcjonalnością jest... bundlowanie.
Narzędzia takie czytają wskazany przez nas plik JavaScript, a następnie wykonuje na nim odpowiednie czynności. Jeżeli dla przykładu znajdzie w kodzie importowanie innego pliku JavaScript, dołączy go generując tym samym jeden wspólny wynikowy plik JS. Gdy natrafi importowanie pliku CSS, SCSS czy IMG, także dołączy je do pliku wynikowego przy okazji konwertując ich kod na prawidłowy zapis Javascript. Dodatkowo mogą optymalizować kod, odświeżać stronę, generować automatycznie klasy itp. Takie wrzucanie wszystkiego w jeden wynikowy plik JavaScript bardzo przypomina generowanie klasycznych programów, gdzie bardzo często cały kod i zasoby lądują w jednym wspólnym pliku.
Na rynku mamy wiele takich narzędzi, a wśród nich najpopularniejszymi są webpack, parcel js, vite js, snowpack, browserity czy rollupjs. Jest ich więcej i większość z nich działa ogólnie dość podobnie, ale to właśnie te wymienione przewijają się w naszej branży najczęściej.
Webpack
Jednym z najpopularniejszych jest Webpack. Wynika to trochę z mody, trochę z faktu, że trafił na rynek w dobrym momencie, ale też na pewno dużych możliwości konfiguracyjnych tego narzędzia. Można powiedzieć, że to taki Photoshop wśród bundlerów (chociaż to wcale nie oznacza, że musisz go używać by być pro).
Instalacja webpacka
Aby zainstalować webpacka musimy w danym projekcie zainstalować dwa moduły. Jeden to sam webpack, a drugi pozwoli nam używać webpacka z terminala:
npm i webpack webpack-cli -D
Odpalamy webpacka
Webpack działa tak, że wskazujemy mu plik wejścia (plik w którym my piszemy nasze skrypty), a następnie wskazujemy mu plik, do którego zostanie zapisany skonwertowany kod.
Przypuśćmy, że nasza struktura katalogów wygląda tak jak poprzednio:
src
├── scss
│ └── main.scss
├── js
│ ├── app.js
│ └── other.js
├── images
│ ├── image1.jpg
│ └── image2.png
└── index.html
dist
├── images
└── index.html
Mamy więc katalog src, w którym mamy wszystkie pliki na których pracujemy. Mamy też katalog dist, w którym będzie aplikacja wynikowa.
Przy odpalaniu webpacka z terminala nie musimy podawać żadnych parametrów. Wystarczy komenda npx webpack
. Domyślnie plikiem wejściowym jest src.js
, a wyjściowym dist/main.js
. Możemy to zmienić z poziomu terminala podając dwa parametry:
npx webpack src/js/app.js -o dist/bundle.js
Po odpaleniu powyższej komendy w terminalu projektu powinna nam się pojawić informacja o zakończonej kompilacji.
Aby nie wpisywać za każdym razem całego polecenia (plus dodatkowych dość ważnych opcji których powyżej brakuje), możemy stworzyć skrypt, który będzie to robił za nas. Tak samo jak w poprzednim rozdziale dodajemy więc odpowiedni zapis do pliku package.json:
{
...
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production"
},
...
}
Pierwszy ze skryptów odpali webpacka w taki sposób, że kod będzie skompilowany w "czytelny" sposób. Wersja produkcyjna skompiluje kod zminimalizowany.
Niestety po odpaleniu powyższych skryptów, w konsoli pojawią się błędy.
Nasza struktura projektu jest nieco inna od domyślnej wymaganej przez Webpack (domyślnie webpack stara się działać na pliku ./src.js
). Aby to naprawić, możemy podać ścieżkę jako atrybut (tak jak powyżej). Przy mikro projektach może być. Przy każdym bardziej realnym lepiej przygotować odpowiedni plik konfiguracyjny.
Konfiguracja webpacka
Podobnie do Gulpa, Webpack także wymaga pliku konfiguracyjnego, który powie mu dokładnie, co ma robić. W przypadku Webpacka plik taki nosi nazwę webpack.config.js (1)
Tworzymy więc w głównym katalogu naszego projektu plik webpack.config.js, a następnie w jego wnętrzu wstawiamy poniższy kod:
const path = require("path");
module.exports = {
entry: "./src/js/app.js",
output: {
filename: "bundle.min.js",
path: path.resolve(__dirname, "dist"),
},
watch: false,
mode: "development",
devtool: "source-map",
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
]
}
}
Poniżej omówimy powyższą konfigurację, a i ją trochę udoskonalimy.
Entry i output
Konfiguracja webpacka zaczyna się od wskazania pliku wejściowego, od którego webpack zacznie czytać nasz kod. Służy do tego klucz entry
.
Kolejną rzeczą jest wskazanie pliku wyjściowego czyli output
. Można tutaj podawać w filename konkretną ścieżkę do pliku, a można i tak jak powyżej - podać katalog i nazwę pliku.
Ścieżka do katalogu musi być absolutna, dlatego wyliczamy ją za pomocą path:
const path = require("path");
...
module.exports = {
entry: "./js/app.js",
output: {
filename: "bundle.min.js",
path: path.resolve(__dirname, "dist")
},
...
}
Podawanie ścieżki do katalogu wynika z tego, że plików wejściowych może być kilka. Wtedy wyjść także będzie kilka:
entry: {
home : "./js/home.js",
app : "./js/app.js",
},
output: {
filename: "out-[name].js", //zostaną nam wygenerowane pliki out-home.js i out-app.js
path: path.resolve(__dirname, "dist")
},
Dodatkowo możemy tutaj użyć dodatkowych składowych do generowania nazw wyjściowych plików.
Watch i source-map
Kolejna właściwość - watch
- włącza nam obserwowanie zmian w plikach. Po odpaleniu webpacka będzie on działał w tle i czekał na zmiany w plikach źródłowych. Gdy takie zauważy, odpali kompilację. Dzięki temu wystarczy, że raz go odpalimy i skupimy się na dalszej pracy. Aby zakończyć tak odpalonego webpacka, wystarczy w terminalu nacisnąć Ctrl + C kilka razy.
Kolejną właściwością jest source-map
. Wyobraź sobie, że w naszym kodzie źródłowym zrobiliśmy błąd. Po dołączeniu do index.html wynikowego pliku nie bylibyśmy w stanie stwierdzić, w którym pliku znajduje się błędny kod, bo przecież w przeglądarce mamy dołączony kod skompilowany. Dzięki source-maps webpack dołącza plik z mapą, która mapuje skompilowany kod na pliki źródłowe. Dzięki temu badając JavaScript w przeglądarce debugger będzie nam wskazywał ścieżki do plików źródłowych.
Sekcja module
Kolejna duża sekcja to module
.
...
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
},
{
...
},
{
...
}
]
}
...
Webpack czyta nasz kod. Jeżeli trafi na instrukcję require/import
, dołączy do danego pliku importowany kod. W przypadku webpacka możemy do Javascriptu dołączać dowolny typ pliku.
Aby tak dołączane pliki stały się poprawnym kodem Javascript, muszą być na niego odpowiednio przekształcone.
Służą do tego tak zwane loadery.
Jak widzisz w powyższym listingu, sekcja module składa się z tablicy rules, w której określamy zasady jakie mają być użyte dla używanych przez nas formatów plików. Każda taka zasada składa się z
test
- gdzie podajemy testowane pliki. Jeżeli webpack wykryje plik o takim rozszerzeniu w require/import, użyje odpowiedniego loadera, który podajemy we właściwości use
.
W naszym przypadku interesują nas głównie pliki JavaScript, które domyślnie webpack po prostu by złączył z sobą. My chcemy jednak by webpack dodatkowo taki kod transpilował na starszy zapis (dla lepszego wsparcia przeglądarek) za pomocą Babel. Żeby tego dokonać, mówimy webpackowi, że gdy wykryje dołączane pliki z rozszerzeniem JS (właściwość test), ma użyć loadera babel-loader (właściwość use).
Żeby używać loadera babel, musimy go zainstalować. Wpisujemy więc w google webpack loader babel, przechodzimy na stronę https://github.com/babel/babel-loader a następnie zgodnie z instrukcją instalujemy:
npm install -D babel-loader @babel/core @babel/preset-env
Wraz z babel-loader i babel-core zainstalowaliśmy preset-env. Presety - jak w każdym programie czy grze - to przygotowane dla nas gotowe ustawienia. Babel to mechanizm, który służy do transformacji naszego kodu Javascript np - na inną wersję. Żeby taka transformacja mogła przebiec, musimy powiedzieć Babelowi co i jak zamieniać i jakich pluginów ma do tego używać. Zamiast tego możemy też skorzystać właśnie z gotowych presetów.
I tak jeżeli używamy Reacta, zapewne zainteresuje nas preset preset-react, jeżeli typescript, preset babel-preset-typescript.
Jednym z bardziej polecanych presetów jest preset-env (ten który powyżej zainstalowaliśmy). Jego główną zaletą jest to, że podobnie jak w przypadku autoprefixera, możemy tutaj podać przeglądarki na których powinien działać docelowy kod.
Preset preset-env podobnie jak autoprefixer do określenia przeglądarek wykorzystuje mechanizm browserlist. Używaliśmy go w jednym z poprzednich rozdziałów. Tak samo jak poprzednio - dodajmy do pliku package.json sekcję browserlist:
{
"name": "projekt",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production"
},
"browserslist": [
"defaults"
],
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
...
}
}
Obsługa CSS
Jeżeli interesuje nas bundlowanie plików CSS, powinniśmy po pierwsze zainstalować odpowiednie loadery:
npm i css-loader style-loader -D
a następnie dodać do naszej konfiguracji odpowiednie zasady:
...
module: {
rules: [
...
{
test: /\.css$/i,
use: [
"style-loader",
"css-loader"
],
},
]
}
...
Jeżeli dla jednego typu plików używamy kilku loaderów, odpalane są one od dołu do góry. Pierwszy zamieni kod css na odpowiedni zapis w Javascript. Drugi - style-loader
- wrzuci style w nagłówek strony.
Dodatkowo każdy taki loader może być odpalany z dodatkowymi opcjami. Wtedy zapisujemy go w postaci obiektu:
...
module: {
rules: [
...
{
test: /\.css$/i,
use: [
{
loader: "style-loader",
options: {
//opcje danego loadera
}
},
"css-loader"
],
},
]
}
...
Obsługa SCSS
Podobnie do powyższego możemy dodać do naszej konfiguracji możliwość używania SCSS.
Na początku instalujemy odpowiednie pakiety:
npm i sass-loader sass -D
A następnie dodajemy odpowiednie loadery do zasad:
...
module: {
rules: [
...
{
test: /\.s[ac]ss$/i,
use: [
"style-loader",
"css-loader",
{
loader: "sass-loader",
options: {
implementation: require("sass"), //Wolimy dart-sass
},
},
],
},
]
}
...
PostCSS i Autoprefixer
Dodajmy do naszej konfiguracji autoprefixera, który automatycznie będzie dodawał do css odpowiednie prefixy (-webkit, -moz itp).
Po pierwsze musimy zainstalować odpowiednie loadery (1):
npm install --save-dev postcss-loader postcss autoprefixer
a następnie zmodyfikować powyższe zasady dotyczące css i scss:
...
module: {
rules: [
...
{
test: /\.s[ac]ss$/i,
use: [
"style-loader",
"css-loader",
{
loader: "sass-loader",
options: {
implementation: require("sass"),
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"autoprefixer",
],
],
},
},
},
],
},
]
}
...
Jeżeli teraz chcielibyśmy dodać podobny mechanizm dla plików CSS, nasz kod niebezpiecznie by się rozrósł. Alternatywą jest stworzenie oddzielnego pliku postcss.config.js
i umieszczenie w nim konfiguracji loadera postcss:
module.exports = {
plugins: [
[
"autoprefixer",
],
],
};
Od tej pory możemy odwoływać się w konfiguracji webpacka do danego loadera, a webpack sam automatycznie spróbuje wyszukać dany plik z konfiguracją postcss.
...
module: {
rules: [
...
{
test: /\.css$/i,
use: [
"style-loader",
"css-loader",
"postcss-loader"
],
},
{
test: /\.s[ac]ss$/i,
use: [
"style-loader",
"css-loader",
{
loader: "sass-loader",
options: {
implementation: require("sass"),
},
},
"postcss-loader"
],
},
]
}
...
Można też zostać przy dłuższym kodzie i nie tworzyć dodatkowych plików...
Grafiki i inne pliki
Bundlując kod musisz mieć na uwadze, że każdy rodzaj pliku, który użyjesz w kodzie musi być przez ciebie opisany w konfiguracji. Gdy dla przykładu zaimportujesz do Javascriptu plik CSS, w którym używasz jako tła pliku jpg, svg i podobnego, dla odpowiednich formatów powinieneś dodać odpowiednie loadery. Dla większości formatów możemy użyć wspólnego loadera zwącego się file loader. Jego głównym zadaniem jest skopiowanie danych plików z katalogu src do wynikowego dist.
Zainstalujmy więc odpowiednie pakiety:
npm install file-loader --save-dev
a następnie dodajmy wpis do naszej konfiguracji:
...
module: {
rules: [
...
{
test: /\.(png|jpe?g|gif|webp|avif|svg)$/i,
use: [
{
loader: "file-loader",
options: { //grafiki chcemy w katalogu dist/images
context: "public",
name: "/images/[name]-[hash].[ext]",
},
},
],
},
]
}
...
Pluginy
Do powyższej konfiguracji możemy też dodać dodatkowe pluginy, które służą do naginania działania webpacka. Przykładowo domyślnie webpack wrzuca kod css bezpośrednio do bundlowanego pliku, a przy odpaleniu strony dynamicznie wrzuca go w nagłówek strony.
A co jeżeli chcielibyśmy mieć style w osobnym pliku? Tu właśnie z pomocą przychodzą pluginy.
Niektóre z nich przychodzą wraz z webpackiem, a niektóre musimy ręcznie zainstalować - tak samo jak powyższe loadery.
//przykładowy kod
const HtmlWebpackPlugin = require("html-webpack-plugin"); //niektóre pluginy trzeba zainstalować i zaimportować
const webpack = require("webpack"); //a niektóre są wbudowane w webpacka
const path = require("path");
module.exports = {
...
module: {
...
},
plugins: [ //odpalamy odpowiednie pluginy
new HtmlWebpackPlugin()
]
}
W naszym przypadku użyjemy dwóch dość popularnych dodatków. Pierwszy z nich to html-webpack-plugin
, który w katalogu wynikowym generuje html automatycznie wstawiając do niego wygenerowany plik javascript. Drugi to mini-css-extract-plugin
, który wyciąga wygenerowany css z bundlowanego pliku js, a następnie zapisuje go w oddzielnym pliku css.
Po pierwsze musimy zainstalować odpowiednie pakiety:
npm i -D html-webpack-plugin mini-css-extract-plugin
Każdy plugin możemy dodatkowo skonfigurować w sekcji plugins. W przypadku html-webpack-plugins odpowiednie parametry znajdziemy tutaj, natomiast ten drugi tutaj.
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require("path");
module.exports = {
...
module: {
rules: [
...
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new HtmlWebpackPlugin({
title: "My App",
filename: "index.html",
template: "src/index.html"
})
]
}
Webpack dev server
Podczas pracy dobrze jest mieć automatyczne odświeżanie, dzięki czemu nasza praca będzie przyjemniejsza.
Jednym z możliwych rozwiązań jest zastosowanie do tego webpack dev server, który automatycznie będzie odświeżał na stronie nasze skrypty gdy wykryje zmianę która do nich wprowadzamy.
Ważną sprawą jest, że po odpaleniu takiego servera bundlowanie skryptów odbywa się wirtualnie w pamięci komputera. Na naszej stronie w przeglądarce wszystko będzie widoczne jak należy, ale wynikowy kod nie będzie zapisywany na dysku do realnych plików js, a będzie brany bezpośrednio z pamięci komputera.
Co to oznacza? Jeżeli podczas pracy developerskiej będziemy korzystać z takiego serwera, to na jej zakończenie i tak będziemy musieli odpalić jeden ze skryptów, który realnie przebuduje pliki na dysk.
Aby zainstalować taki server, przechodzimy na stronę https://github.com/webpack/webpack-dev-server i instalujemy serwer zgodnie z instrukcją:
npm i webpack-dev-server -D
Aby teraz go użyć, wystarczy zamiast samego webpacka, odpalić nasz serwer. Dodajmy do skryptów w package.json odpowiedni skrypt:
...
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"serve": "webpack-dev-server"
},
...
Dodatkowo możemy też dodać do naszej konfiguracji odpowiednie opcje zmieniające działanie serwera:
module.exports = {
...
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
compress: true,
port: 9000,
hot: true
},
...
}
Parametr hot
oznacza hot-replacement. Webpack przy dużych skryptach będzie starał się aktualizować tylko kawałki kodu, które realnie się zmieniły - dodatkowo będzie to robił bez przeładowania strony.
Gotowa konfiguracja
Konfigurację z powyższego tekstu znajdziesz tutaj.