Webpack w pigułce
Webpack w pigułce
Tym razem bierzemy się za temat konfiguracyjny, dzisiejszy materiał będzie nieco dłuższy i pokażemy Ci naszą propozycję na skonfigurowanie projektu przy użyciu Webpacka
Ważne! Artykuł został napisany, gdy najnowszą wersją webpacka jest wersja v5
Wstęp
W dzisiejszych czasach mamy wiele narzędzi pozwalających nam szybciej wystartować z nowym projektem i mało, kto samodzielnie konfiguruje całe środowisko
Na przykład, w ekosystemie Reacta mamy Create React App. Dzięki niemu za pomocą jednego polecenia dostajemy gotową, podstawową konfiguracje.
Jego odpowiednikiem dla technologii Server Side Rendering czy też Static Site Generation może być Create Next App. Po skorzystaniu z generatora dostajemy gotowy setup i zapominamy do czasu następnego użycia. A czy zastanawiałeś się jak to działa “pod maską”?
W przypadku skomplikowanego i bardzo spersonalizowanego projektu często gotowe zestawy narzędziowe będą niewystarczające a umiejętność konfiguracji całego środowiska od zera może okazać się ogromną zaletą.
Module Bundlery
Podstawą takich narzędzi jak CRA jest module bundler
Zanim przejdziemy do wyjaśnienia, czym są module bundlery, dowiedzmy się, jakie korzyści płyną z ich użycia
Pewnie wielokrotnie, w trakcie eksperymentów lub nauki tworzyłeś plik index.html wraz z osadzonym skryptem. Tak jak np. na screenie poniżej:
<html lang="en"> <head> <title>Instytut Fullstack Code Snippet</title> </head> <body> <script> const arrayOfStrings = ['instytut', 'fullstack', 'webpack']; arrayOfStrings.forEach((item) => { console.log(item); }) </script> </body> </html>
Jest to rozwiązanie bardzo wygodne, ale kompletnie nie sprawdzi się w przypadku większych projektów. Trzymanie całego kodu w jednym pliku bardzo utrudniłoby proces rozwoju aplikacji.
Alternatywą jest podzielenie projektu na pliki i zaimportowanie ich do głównego indeksu:
<body> <script src="./feature.js"></script> <script src="./feature2.js"></script> </body>
Niestety każdy kolejny skrypt to dodatkowy request dla przeglądarki powodujący spadek wydajności:
Tutaj na ratunek przychodzą nam właśnie module bundlery
Ich głównym zadaniem jest jak sama nazwa wskazuje „bundlowanie”, czyli wczytywanie plików, wykonywanie na nich jakichś operacji a następnie wypluwanie ich w przetworzonej formie
Takich bundlerów jest bardzo wiele, przykładowo są to: webpack, parcel, rollup.js i tak dalej…, ale w tym artykule skupimy się tylko na webpacku
Pod tajemnicą wykonywanych „operacji” kryją się między innymi takie czynności jak:
- Łączenie wielu plików w jeden plik wynikowy
- Minifikacja/obfuskacja kodu
- Ładowanie obrazków, czcionek i innych assetów
- Optymalizacja kodu
- Uruchomienie serwera developerskiego
Możliwości module bundlerów jest oczywiście o wiele więcej, tutaj wymieniłem tylko kilka z nich. To, z czego chcemy skorzystać należy określić w pliku konfiguracyjnym. Dla webpacka domyślnie jest to webpack.config.js
Webpack od wersji 4 nie wymaga od nas pliku konfiguracyjnego. W przypadku jego braku założy on, że plikiem wejściowym jest index.js w folderze src oraz że plikiem wyjściowym będzie main.js w folderze dist zminifikowany i zoptymalizowany produkcyjnie
Nie wchodząc w głębokie szczegóły samego Webpacka zajmijmy się konfiguracją naszej aplikacji
Konfiguracja aplikacji React
Aby rozpocząć nowy projekt zainicjujmy plik package.json poleceniem:
npm init -y
Teraz stwórzmy sobie odpowiednią strukturę katalogów, moja propozycja wygląda następująco:
- config – tutaj będziemy trzymać pliki konfiguracyjne webpacka
- public – folder, w którym umieścimy template głównego pliku html oraz inne assety np. obrazki
- src – główny katalog aplikacji, w którym będziemy trzymać komponenty, ich lokalne style i logikę biznesową
- styles – miejsce do umieszczenia globalnych styli
- types – przestrzeń na trzymanie typów oraz interfejsów dla Typescripta
Jest to subiektywna i minimalistyczna struktura utworzona na potrzeby tego artykułu, w większości przypadków każdy projekt będzie miał specyficzną dla siebie konfigurację
Następnie zainstalujmy Reacta poleceniem:
npm i react react-dom
W naszej przykładowej aplikacji warto skonfigurować także TypeScripta, więc doinstalujmy go wraz z typami dla Reacta:
npm i typescript @types/react --save-dev
W kolejnym kroku instalujemy paczkę babel wraz z odpowiednimi presetami.
Pomoże nam ona tłumaczyć JSX oraz nasz „nowoczesny js” na taki zrozumiały dla przeglądarek:
npm i @babel/core @babel/preset-env @babel/preset-react --save-dev
W katalogu src utwórzmy podkatalog components, a w nim kolejny podkatalog Button, w którym będziemy trzymać nasz przykładowy komponent (plik tsx oraz style).
Następnie bezpośrednio w folderze src utwórzmy pliki index.tsx oraz App.tsx. Ten pierwszy będzie głównym plikiem, w którym wyrenderujemy całą aplikację, a drugi głównym komponentem, w którym w przyszłości będziesz mógł dodawać Routing, Context API itp., ale to już temat na osobny artykuł
Wnętrze pliku index.tsx prezentuje się następująco:
import React from 'react' import ReactDOM from 'react-dom/client' import {App} from "./App"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </React.StrictMode> );
Wewnątrz App.tsx importujemy pozostałe komponenty, czyli w tym przypadku nasz Button.tsx:
import React from 'react' import {Button} from "./components/Button/Button"; export const App = () => { return ( <> Basic Webpack Config for React App <Button btnText={"React button"}/> </> ) }
Do stylowania komponentów możemy wykorzystać CSS Modules. W tym celu, w katalogu komponentu obok pliku .tsx utwórzmy również buton.module.css:
import React from 'react' import styles from './button.module.css' export const Button = ({btnText}) => { return ( <div> <button className={`${styles.btn}`}>{btnText}</button> </div> ) }
W folderze public stwórzmy template dla pliku HTML o nazwie index.html, a także możemy zagnieździć pod folder assets, w którym na ten moment umieścimy przykładowe obrazki.
Przykład index.html poniżej:
<html lang="en"> <head> <title>Instytut Fullstack React App from scratch</title> </head> <body> <div id="root"></div> </body> </html>
Poza lokalnymi stylami dla komponentów będziemy używać globalnych arkuszy scss, które umieścimy w katalogu styles o nazwie globalStyles.scss:
* { box-sizing: border-box; } html { font-size: 16px; } body { background-color: aqua; }
Następnie zaimportujmy go w index.tsx:
import '../styles/customStyles.scss'
W katalogu głównym projektu stwórzmy jeszcze plik .babelrc, w którym umieścimy konfigurację presetów babela:
{ "presets": ["@babel/preset-env", "@babel/preset-react"] }
Także w katalogu głównym zainicjujmy plik tsconfig.json, możemy do tego użyć polecenia:
tsc --init
Lub wpisać własną konfigurację od zera ręcznie:
{ "compilerOptions": { "target": "es5", "module": "commonjs", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "jsx": "react", }, "exclude": [ "node_modules" ], }
Teraz powinniśmy mieć mniej więcej taką strukturę katalogów:
|-- config |-- package.json |-- public | |-- assets | `-- index.html |-- src | |-- App.tsx | |-- components | | `-- Button | | |-- Button.tsx | | `-- button.modules.css | `-- index.tsx |-- styles | `-- globalStyles.scss `-- types
Powyższa konfiguracja jest poprawna, ale uruchamiając plik index.html szybko zorientujemy się, że całość nie działa…
W tym momencie na pomoc przychodzi Webpack
Dodanie Webpacka
Zainstalujmy webpacka i jego CLI (command line interface), jako zależności deweloperskie poleceniem:
npm i webpack webpack-cli --save-dev
Następnie w folderze config stwórzmy 3 pliki:
- webpack.common.js
- webpack.dev.js
- webpack.prod.js
Jak się pewnie domyślasz wewnątrz prod.js będziemy trzymać konfigurację na środowiska produkcyjne, analogicznie w dev.js znajdzie się config developerski a common.js będzie zawierał części wspólne żeby ich nie duplikować
Do scalania configów posłuży nam paczka webpack merge, którą zainstalujemy poleceniem:
npm i webpack-merge --save-dev
Teraz przyjrzyjmy się nieco bliżej samemu plikowi konfiguracyjnemu, jest to po prostu moduł JS, który może zwrócić między innymi:
- Obiekt
- Funkcję, która zwraca obiekt
- Promise
My w pliku common zwrócimy po prostu obiekt, a w przypadku dev i prod wywołanie funkcji merge
Plik konfiguracyjny webpacka zawiera kilka istotnych sekcji, przez które teraz przejdziemy
Entry
Tutaj określimy plik lub zestaw plików wejściowych, na początek wystarczy dosłownie jedna linijka
Output
W tej sekcji wskażemy nazwę i lokalizację naszego pliku wynikowego
Loaders
Ze względu na to, że webpack rozumie tylko pliki z rozszerzeniem .js oraz .json musimy dodać również loadery, jeśli chcemy załadować np. style lub svg. To samo tyczy się TypeScripta.
Globalne style będziemy chcieli pisać przy użyciu składni scss, ale również będzie wsparcie dla arkuszy module.css
Optimization
Tutaj określmy czy chcemy minifikować nasze pliki, dzielić je na chunki itp.
Plugins
Oprócz lodaderów mamy również dostępnych wiele pluginów. Jednym z nich, który będzie bardzo przydatny jest HtmlWebpackPlugin służący do utworzenia pliku HTML w folderze wynikowym oraz dołączenie do niego zbundlowanego skryptu. Co więcej pozwala on na skonfigurowanie naszego pliku lub nawet wygenerowanie go na podstawie jakiegoś szablonu.
Zainstalujmy go poleceniem:
npm i html-webpack-plugin --save-dev
Dev Server
Aby skutecznie rozwijać aplikację przydatny będzie serwer deweloperski
Jego konfigurację umieścimy w sekcji devServer, gdzie możemy między innymi sprecyzować port, na którym ma działać, folder, z którego ma serwować zawartość, przede wszystkim określmy port, na którym ma startować
Uruchomimy go na porcie 3001. Ponadto, ustawiając atrybut hot na true możemy aktywować hot module realoading. Jest to taki „watch na sterydach” Webpack będzie wtedy nasłuchiwał na zmiany w naszych plikach i na bieżąco przeładowywał pliki, w których wystąpiły zmiany bez pełnego przeładowania strony, przez co wprowadzone poprawki w kodzie będą niemalże od razu widoczne i bardzo usprawni to lokalny development
Pliki konfiguracyjne
Skoro poznaliśmy poszczególne sekcje, z których składa się plik konfiguracyjny dla webpacka przejdźmy do stworzenia pierwszego z nich, czyli webpack.common.js. Jest to konfiguracja wspólna dla środowisk developerskich oraz produkcyjnych.
Ustalimy tu tylko pewne reguły, które byłyby identyczne dla wszystkich środowisk i tym samym unikniemy duplikacji.
Najpierw wskażemy punkt wejściowy naszej konfiguracji gdyż będzie on zawsze taki sam:
entry: path.resolve(__dirname, '../src/index.tsx'),
Będziemy również korzystać z Typescripta i nowoczesnego JS’a więc użyjemy do tego specjalnych loaderów:
npm i ts-loader babel-loader --save-dev
Za pomocą regexów ustawiamy odpowiednie loadery dla poszczególnych plików:
module: { rules: [ { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: 'ts-loader', }, { test: /\.(js|jsx)$/, exclude: /node_modules/, use: { loader: "babel-loader", } } ], },
We wspólnej konfiguracji dodajmy jeszcze wcześniej wspomniany HtmlWebpackPlugin i wskażmy mu nasz index.html z katalogu public:
plugins: [ new HtmlWebpackPlugin({ title: "Webpack Instytut Fullstack", template: path.resolve(__dirname, "../public/index.html") }) ],
I to by było na tyle z części wspólnej, w następnej sekcji skupimy się na konfiguracji developerskiej
Konfiguracja developerska
Zaczynamy od scalenia webpack.common z webpack.dev. W tym celu musimy użyć funkcji merge. Aby to zrobić instalujemy paczkę webpack-merge:
npm i webpack-merge --save-dev
Użyjmy jej w pliku webpack.dev.js
module.exports = merge(common, {
Opcjonalnie możemy ustawić property mode z wartością development:
mode: 'development',
Jest to ustawienie mówiące webpackowi, aby dodatkowo używał swoich wbudowanych optymalizacji
W tym pliku również musimy nareszcie określić jak ma wyglądać plik wynikowy, ustalmy, że będzie to plik o dynamicznie tworzonej nazwie (hash:8 oznacza, że hash będzie się składał tylko z 8 znaków) w folderze dist:
output: { path: path.resolve(__dirname, '../dist'), filename: '[name].[hash:8].js', },
Dla konfiguracji developerskiej możemy też określić jak chcemy ładować nasze style, doinstalujmy w tym celu 4 paczki:
npm i style-loader css-loader sass-loader node-sass --save-dev
Sass-loader ładuje pliki scss a node-sass jest jego dependency, więc też potrzebujemy tej paczki. Z kolei css-loader zbierze pliki CSS z projektu o zamieni je na string (css-loader domyślnie wspiera CSS modules). Na sam koniec style-loader doda nasze style do pliku index.html domyślnie, jako <style>
tag
Następnie ustalamy rozszerzenia plików, które będą ładowane:
module: { rules: [ { test: /\.(scss|css)$/, use: [ "style-loader", "css-loader", 'sass-loader' ], } ], },
Istotna jest tutaj kolejność (webpack najpierw ładuje pliki loaderem na samym dole), czyli najpierw załadujemy pliki scss/css a później dodamy je do pliku index.html
Dodatkowo żeby Typescript nie szalał przy importowaniu CSS Modules w katalogu types stwórzmy plik declaration.d.ts z taką zawartością:
declare module '*.css' { const content: Record<string, string>; export default content; } declare module '*.scss' { const content: Record<string, string>; export default content; }
Kolejną wartą uwagi opcją dla konfiguracji developerskiej jest ustawienie source-map, żeby łatwiej było debugować nasz kod:
devtool: 'source-map',
Ostatnią sekcją, której dodanie usprawni naszą pracę jest wcześniej wspomniany serwer developerski, którego doinstalujemy:
npm i webpack-dev-server --save-dev
a następnie dodajmy do naszej konfiguracji ustalając port i opcję hotModuleReplacement:
devServer: { port: 3001, hot: true, },
Konfiguracja produkcyjna
Przyszła pora na konfigurację dla środowisk produkcyjnych, fragment z mergowaniem konfiguracji common będzie taki sam jak wcześniej, zmienimy natomiast mode na production oraz zmodyfikujemy nieco output:
output: { path: path.resolve(__dirname, '../dist'), filename: '[name].[hash:8].js', chunkFilename: '[id].[hash:8].js' },
W dalszym ciągu będzie to folder dist i dynamicznie nazwany plik, ale w celach optymalizacyjnych dodamy dzielenie pliku wynikowego na chunki
Zmieni się także ładowanie styli, w tym celu doinstalujmy dodatkowy loader:
npm i mini-css-extract-plugin --save-dev
Zamienimy poprzedni style-loader nowym, gdyż zamiast ładować pliki tagiem <style>
chcemy style minifikować i nieco bardziej optymalnie załadować
module: { rules: [ { test: /\.(scss|css)$/, use: [ MiniCssExtractPlugin.loader, "css-loader", 'sass-loader' ], }, ], },
Na produkcji nie potrzebujemy już bardzo dokładnych source-map do debugowania, więc możemy wykorzystać nieco ”tańszą” opcję:
devtool: 'inline-source-map',
W kontekście dalszej optymalizacji minifikujmy nasz kod (w tym CSS-y) i dzielmy go na mniejsze fragmenty (chunki), w tym celu doinstalujmy sobie paczkę do minifikacji CSS-ów:
npm i css-minimizer-webpack-plugin --save-dev
Następnie dodajmy do webpacka sekcję optimization:
optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin(), ], splitChunks: { chunks: 'all', }, },
Do pluginów dodajmy tylko jeden, który posłużył nam wcześniej, jako loader do CSS-a:
plugins: [ new MiniCssExtractPlugin() ],
Konfiguracja uruchomieniowa
Mamy już napisany config to teraz przyszła pora na konfigurację uruchomieniową, użyjemy w tym celu pliku package.json i sekcji scripts
Będziemy potrzebować na początek w zasadzie 3 skryptów:
- Startu aplikacji
- Zbudowania projektu z konfiguracją developerską
- Zbudowania aplikacji z konfiguracją produkcyjną
Wygląda to tak:
"scripts": { "start": "webpack serve --config config/webpack.dev.js --open", "build:dev": "webpack --config config/webpack.dev.js", "build:prod": "webpack --config config/webpack.prod.js" },
Teraz możemy uruchamiać naszą aplikację lub ją budować poleceniami npm run … lub po prostu klikając w package.json, jeśli używamy IDE, które wspiera npm scripts
Bonus
Pisząc aplikacje w React zdarzy się, że będziemy chcieli używać plików SVG tak jak zwykłych komponentów w JSX. W tym celu musimy doinstalować odpowiednią paczkę:
npm i @svgr/webpack --save-dev
A następnie dodać loader do webpacka (ja proponuję do konfiguracji common):
{ test: /\.svg$/, use: ['@svgr/webpack'], }
Dodatkowo należy utworzyć w katalogu types plik custom.d.ts z następującą zawartością, aby Typescript nie sypał nam błędów przy imporcie:
declare module '*.svg' { const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>; export default content; }
Teraz możemy zaimportować nasz SVG i użyć go w kodzie
import React from 'react' import {Button} from "./components/Button/Button"; import Shape from '../public/assets/shape.svg' export const App = () => { return ( <> Basic Webpack Config for React App <Button btnText={"React button"}/> <Shape/> </> ) }
Podsumowanie
Powyżej pokazałem Ci podstawową konfigurację webpacka dla aplikacji pisanej w React.
Artykuł nie wyczerpuje całości tematu. Na temat pozostałych pluginów, loaderów, opcji konfiguracyjnych można by napisać książkę.
Jeśli nie masz czasu krok po kroku przejść przez ten poradnik całość kodu znajdziesz na naszym GitLabie: LINK