Konfiguracja własnego SPA - Webpack cz. 2

Myślałeś kiedyś o tym jak działa CRA czy rozwiązania Single Page Application under the hood? W kolejnym wpisie z serii o Webpacku, skonfigurujemy go do imitacji zachowania Create React App. Konfiguracja Webpacka od zera z wyjaśnieniem poszczególnych kroków.

Wstęp

Po teoretycznym rozdziale o Webpacku, który możesz przeczytać tutaj (do czego zachęcam), nadszedł czas na jego praktyczne zastosowanie. Dziś stworzymy projekt Single Page Application za pomocą konfiguracji Webpacka. Założeń jest niewiele - React i Typescript, wszystko od zera. Nauka narzędzia w praktyce. Brzmi dobrze? Przejdźmy więc do rzeczy!

Inicjowanie projektu

Stwórzmy sobie folder i zainicjujmy w nim npm, bo będziemy oczywiście używać node i instalować pakiety npm.

npm init -y

Docelowo będziemy chcieli pracować na Typescripcie. Zainstalujmy go zatem od razu w projekcie:

npm install typescript

Aby stworzyć plik tsconfig.json, bez którego się nie obędzie, wpiszemy:

npx tsc --init

Utworzy nam to podstawowy plik konfiguracyjny do Typescripta, który będzie nam potrzebny nieco później. Jedyne, co na ten moment zrobimy w configu, to powiedzenie typescriptowi, żeby "widział" kod jsx jako ten z Reacta. Dzięki temu zapobieżemy problemowi renderowania JSXa. A więc: 

// tsconfig.json
{
// ...
  "jsx": "react"
// ...
}

W porządku. Czas na Reacta, a więc:

npm install react @types/react react-dom @types/react-dom

Zauważ, że powyższym poleceniem instalujemy również typy do Reacta, co jest istotne z uwagi na TSa. Mamy już Reacta oraz Typescripta. Nic nie stoi zatem na przeszkodzie, by z nich skorzystać. Stwórzmy folder src oraz plik index.html oraz index.tsx. Plik index.html będzie templatem dla naszego SPA i na jego podstawie Webpack będzie budował resztę aplikacji. index.tsx natomiast posłuży jako główny plik wyjściowy do tworzenia komponentów i contentu apki.

<!-- src/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
    <title>Webpack tutorial</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Co tutaj ważne, to element div o id root, do którego wstrzykniemy Reacta. Zwróć uwagę, że nie umieszcamy tutaj żadnego znacznika script z JSem. Tym zajmie się Webpack.

// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <React.StrictMode>
    Hello!
  </React.StrictMode>,
  document.getElementById('root')
);

Jak widzisz, Reacta renderujemy tylko w tym jedynym miejscu - do tego był nam potrzebny element element z id. Mamy już wszystko, co potrzebne, by tworzyć aplikację reactową wraz z Typescriptem. Teraz czas na Webpacka!

Konfiguracja Webpacka

Czas zainstalować Webpacka. Jak wspomniałem w poprzednim wpisie, będziemy go potrzebować jedynie podczas developmentu aplikacji, a więc zainstalujemy go jako devDependency (wraz z cli):

npm install -D webpack webpack-cli

A następnie zainstalujemy pierwszą dependencję potrzebną do bundlowania naszych plików:

npm install -D typescript ts-loader

Jest to ts-loader, który będzie nam bundlował kod Typescripta (jednocześnie transpilując jego składnię do JSa). Gdybyśmy używali zwykłego JavaScriptu w projekcie, w tym wypadku potrzebowalibyśmy loadera babel-loader. Teraz czas na plik webpack.config.js. Jest to najważniejszy plik, w którym dzieje się cała magia. To tutaj określamy całą konfigurację Webpacka i to w jaki sposób będzie generował nam aplikację. Umieśćmy w nim następujący początkowy config:

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'index.bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
  },
};

Wyjaśnijmy sobie, co tu się zadziało. Klucz entry mówi Webpackowi, jaki plik powinien uznać za wejściowy. Wcześniej stworzyliśmy plik src/index.tsx, który w zamierzeniu jest naszym wyjściowym plikiem całej aplikacji. To właśnie jego musimy wskazać jak entry point dla Webpacka. output natomiast, jak pewnie się domyślasz, jest naszym "wyplutym", produkcyjnym plikiem, do którego Webpack wpycha cały przetrawiony kod. W output musimy podać ścieżkę oraz - opcjonalnie - nazwę pliku. Plików wejściowych jak i wyjściowych może być wiele. Jeżeli cię to interesuje, to odsyłam do oficjalnej dokumentacji Webpacka.

Klucz module określa zasady dla modułów JSowych. To tutaj wskazujemy nasz loader i wskazujemy pliki brane pod uwagę w kontekście importów. W resolve natomiast określamy wszystkie rozszerzenia plików, które Webpack w ogóle powinien brać pod uwagę podczas parsowania plików. Tutaj także konfigurujemy aliasy (służące np. do absolutnych importów), co zapewne zrobimy w jednym z przyszłych wpisów. Jest to podstawowy config, dzięki któremu jesteśmy w stanie wykorzystać Webpacka do zbudowania wyniku. A zrobimy to za pomocą polecenia:

npx webpack build

Spróbuj i zobacz, co się stało. W tym momencie powinien wygenerować się plik build/index.bundle.js, a w nim zawartość w postaci przetranspilowanego kodu. Jest on zminifikowany i gotowy produkcyjnie. Możesz się zastanawiać, z czego wzięła się tak ogromna ilość kodu. Wynika to z tego, że Webpack "zasysa" wszystkie zaimportowane dependencje i umieszcza je w produkcyjnym kodzie. W tym przypadku mamy tylko Reacta, który przecież jest rozbudowaną biblioteką, więc nie ma co się dziwić.

Udało się nam wykorzystać Webpacka do budowy kodu po raz pierwszy, jednak jeszcze trochę zostało do zrobienia, aby pracowało się z nim lepiej.

Używamy Typescripta i loader ts-loader. Loader ten współpracuje z plikiem konfiguracyjnym TSa, o którym wcześniej wspomniałem, czyli tsconfig.json. Umieśćmy w nim następującą zawrtość:

{
  "compilerOptions": {
    "target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
    "module": "es6" /* Specify what module code is generated. */,
    "jsx": "react" /* Specify what JSX code is generated. */,
    "strict": true /* Enable all strict type-checking options. */,
    "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
    "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
    "outDir": "./build/" /* Specify an output folder for all emitted files. */,
    "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */
  }
}

Mamy tutaj kilka podstawowych opcji, które zapewnią prawidłowe działanie TSa w projekcie z Reactem. I nic nam nie powinno teraz krzyczeć. Jest to o tyle ważne, że ts-loader działa ściśle z tym plikiem, a bez poprawnie skonfigurowanego TSa edytor może rzucać błędami. Bardziej ciekawskich odsyłam do Internetu.

Wykorzystajmy teraz nasz plik index.html. Tworzymy SPA, więc jest to w tym wypadku nasz jedyny plik html. To tutaj wstrzykniemy zbundlowanego JSa. I wykorzystamy do tego pierwszy plugin:

npm install -D html-webpack-plugin

W configu Webpacka umieścimy więc następujący kod:

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
// ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html',
    }),
  ]
}

Umieśćmy teraz nowy skrypt w package.json, żebyśmy nie musieli wpisywać za każdym razem komendy npx:

// package.json
{
// ...
"scripts": {
    "build": "webpack build"
  },
// ...
}

Teraz możemy wpisać w terminalu:

npm run build

Powinien się wygenerować katalog build wraz z index.bundle.js oraz index.html wewnątrz. Dzięki pluginowi html-webpack-plugin Webpack automatycznie umieszcza zbundlowany plik JSa. Możemy teraz uruchomić index.html w przeglądarce i zobaczyć, czy React działa poprawnie.

zdjęcie tematyczne

Wszystko wskazuje na to, że tak! Zanim jednak zakończymy ten wpis, jest jeszcze kilka małych rzeczy do zrobienia.

Routing oraz devServer

Aby tworzenie projektu było wygodne, musimy skonfigurować webpacka, żeby nasłuchiwał zmian i wyświetlał nam je w czasie rzeczywistym. Nie będziemy przecież przebudowywać projektu po każdym razie, gdy coś zmienimy w kodzie. Zainstalujmy więc webpack-dev-server:

npm install -D webpack-dev-server

Oraz zmodyfikujmy nieco package.json:

// package.json
{
// ...
"scripts": {
    "build": "webpack build --mode=production",
    "dev": "webpack serve --mode=development"
  },
// ...
}

Dodaliśmy nowy skrypt oraz do obu istniejących dodaliśmy flage mode, wskazując w jakim trybie serwujemy Webpacka. Ta flaga wskazuje, w jaki sposób Webpack wygeneruje kod podczas developmentu oraz buildu produkcyjnego. Przykładowo, w trybie developerskim, kod nie będzie zminifikowany. Jest to o tyle istotne, że podczas developmentu może i skrypt będzie wykonywał się nieco dłużej, ale pozwoli to nam prościej namierzyć błędy w kodzie wskazane podczas runtimu.

W configu dodajmy jeszcze następującą zawartość:

// webpack.config.js

devServer: {
    historyApiFallback: true,
  }

Jest nam to potrzebne do routingu, do którego użyjemy na pewno ci znany react-router-dom:

npm install react-router-dom

Stwórzmy plik app.tsx w src a w nim umieśćmy następującą zawartość:

// src/app.tsx
import React, { FC } from 'react';
import { BrowserRouter, NavLink, Route, Routes } from 'react-router-dom';

const App: FC = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='/' element={<NavLink to='/about'>Navigate</NavLink>} />
        <Route path='about' element={<div>About</div>} />
      </Routes>
    </BrowserRouter>
  );
};

export default App;

Następnie zaimportujmy i użyjmy tego komponentu w roocie:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

Wpiszmy:

npm run dev

Lokalny runtime powinien się teraz odpalić, a my możemy do woli korzystać z możliwości Reacta!

Zakończenie

Jeżeli dotrwałeś do tego momentu, gratulacje! Dziś stworzyliśmy własny projekt Single Page Application za pomocą Webpacka, a nie używając gotowych boilerplatów, takich jak Create-React-App. Jest to szczątkowa konfiguracja i sporo tu jeszcze zostało do zrobienia, ale jak na jeden wpis to wystarczy. W następnej części skonfigurujemy inne narzędzie służące do rozwijania aplikacji, takie jak scss, czy import różnych plików, no. graficznych. Tymczasem, zachęcam do samodzielnego zgłębiania specyfikacji Webpacka. Do zobaczenia!

Źródła

© Damian Kalka 2022
Wszelkie prawa zastrzeżone