< Zpět na články

Webpack - moderní Web Development

Marek JančaMarek Janča
11. května 2017

Webpack se stal v poslední době často skloňovanou technologií a je srovnáván s dalšími utilitami jako je Gulp nebo Grunt. V tomto článku se pokusím vysvětlit princip a filosofii Webpacku v kontextu vývoje aplikací a shrnout pár případů, kdy a jak chceme a nechceme Webpack používat.

Články vychází z textu, který jsem sepsal k ukázkovým konfiguracím Webpacku na Githubu, které vznikly v rámci vzdělávacích meetingů u nás v Ackee ❤.

Co je Webpack

Co jsou moduly

Node.js od svého počátku podporuje CommonJS code spliting pro rozdělení programového kódu do modulů závislostí, které mohou být v případě potřeby nahrány a= použity:

var $ = require('jquery');— jQuery je nahráno jako závislost v nějakém modulu a je možné ho použít,
module.export = jquery; — jQuery je exportováno z modulu a může být někde použito jako závislost.

Code splitting a rozdělení do modulů chceme dělat zejména z důvodu přehlednosti a jednoduché správy kódu. Je to klasická inženýrská praxe známá z jazyků jako je C, C++, PHP, Ruby, Python, Java ad., a proto ji zavedl i Node.js jakožto terminálový interpret Javascriptu.

Problémem Javascriptu interpretovaného prohlížečem je, že nativně žádný code splitting nepodporuje. Všechny scripty jsou zpracovávány v rámci jednoho kontextu v prohlížeči v pořadí v jakém jsou parsovány z HTML. Pak vzniká problém s globálními proměnnými a kontextem existence proměnných a ve větším projektu nemusí být jasné, odkud se daná závislost bere a proč kód funguje.

Příprava modulů pro prohlížeč

Zde přichází na řadu Webpack, který se snaží o podporu code splittingu v prohlížeči tak, že nám dovolí psát moduly stejně, jako kdybychom psali Javascript pro Node.js interpret (tj. CommonJS, ale od verze 2.0 umí nativně i ES modules a AMD). Máme tedy javascriptový kód napsaný pomocí např. CommonJS modulů jako v Node.js. Jak tento kód předat prohlížeči, aby s ním uměl pracovat?

Webpack zpracuje kód napsaný modulárně a vytvoří z něj balíček (pro jednoduchost řekněme jeden js soubor, ale nemusí to být nutně pravda). Navíc dovoluje použít jako moduly také npm balíčky jako třeba React nebo jQuery, které pak není nutné nahrávat jako další script do HTML dokumentu. To znamená: konec globálních proměnných, outscopingu a nepredikovatelného kódu.

Někteří mohou namítnout, že pro tyto účely už existuje utilita Browserify a mají pravdu. Jenže Webpack jde ještě dál. Základním účelem Webpacku je sice práce s javascriptovými moduly a vytváření balíčků pro prohlížeč, ale Webpack je vytvořený tak, že umožňuje práci s různými druhy assetů. Při správné konfiguraci je schopný zpracovat tyto assety a vytvořit balíček (nebo balíčky), které se jednoduše nahrají na webserver a vše funguje tak, jak má. To znamená, že je možné nahrát jako závislost do nějakého js modulu třeba Sass nebo css.

Co jsou Webpack moduly

Webpack moduly jsou cokoliv, co lze v kódu, který Webpack zpracovává, requirenout (importnout). Tudíž to mohou být CommonJS moduly, ES moduly, AMD (tyto tři umí Webpack nativně), ale také Sass @import v Sass kódu (ne nativně, ale pomocí rozšíření) a Webpack je bude umět při správné konfiguraci projít a zpracovat.

Webpack dovoluje importovat do js i assety, které nejsou Javascript, např.:

  • jsx, coffee;
  • css, sass, less, stylus, postcss;
  • png, jpeg, svg;
  • json, yaml, xml a další.

Je nutné si pamatovat, že výsledkem Webpacku je vždy primárně js balíček (nebo balíčky) pro použití v prohlížeči. To znamená, že je nutné s ostatními assety něco udělat. Musí být zpracovány a převedeny na js nebo nějakým způsobem vyextrahovány ze zdrojových modulů do samostatných souborů. Na tuto práci používá Webpack loaderypluginy, které mu umožňují provádět specifický preprocessing nad moduly a balíčky před tím, než je výstup uložen do filesystému.

Shrnutí Webpacku

  • primární účel = vytváření js balíčků z modulárního js kódu pro použití v prohlížeči
  • tím, jak je vytvořen, umožňuje transformovat, zpracovat, modifikovat či zabalit do balíčku téměř jakýkoliv druh assetu jako modulu
  • pro preprocessing assetů používá loaderypluginy
  • vstupem je modulární Webpack kód (tj. moduly různých typů), výstupem je js balíček (a také další soubory, pokud je správně aplikován preprocessing)

Konfigurace Webpacku

Konfigurační soubor Webpacku je Node.js modul, který exportuje konfigurační objekt. Můžeme v něm psát jakýkoli javascriptový kód interpretovatelný Node.js, což nám poskytuje mnoho možností. Například můžeme soubory, které chceme nastavit jako vstupy Webpacku, načítat z filesystému dynamicky.

Výchozím názvem konfiguračního souboru je webpack.config.js (pokud není uveden v příkazu, hledá Webpack tento soubor) a Webpack spustíme pomocí webpack --config ./configs/webpack.config.js .

Konfigurační objekt obsahuje 4 nejdůležitější klíče:

  • entry obsahuje nastavení vstupu Webpacku,
  • output obsahuje nastavení výstupu Webpacku,
  • module obsahuje nastavení práce nad moduly (zejména nastavení loaderů),
  • plugins obsahuje pluginy a jejich nastavení.

Entry

Entry obsahuje nastavení vstupu Webpacku. Říká, odkud má Webpack začít vytvářet balíček (balíčky). Každý jeden soubor uvedený v entry je kořenem grafu závislostí, který Webpack projde a vytvoří výsledný balíček.

Pokud použijeme paralelu s Node.js, pak si lze každý soubor na vstupu Webpacku představit jako soubor, který předáváme Node.js k interpretaci. Výsledný balíček Webpack můžeme předat prohlížeči a bude fungovat jako validní modulární kód.

Entry má následující typy:

  • string — cesta k souboru => jeden entry point, jeden balíček
  • pole — pole cest k souborům => více entry pointů, jeden balíček
  • object — objekt pojmenovaných cest k souborům => více entry pointů, více balíčků

V rámci konfigurace může být uveden klíč context, relativní cesty v entry se pak vztahují vůči němu.

Pravidlo: jeden entry point na HTML stránku, SPA = jeden globální entry point, MPA = více entrypointů

Příklad konfigurace entry:

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  // entry: ['./index.js', './login.js'],  
  // entry: {  
  //   index: './index.js',  
  //   login: './login.js',  
  // },  
}

Output

Output obsahuje nastavení výstupu Webpacku. Jedná se o objekt s klíči:

path

Cesta do složky, kam se uloží výsledek Webpacku. Může být ovlivněna pomocí nastavení context.

filename

Název výstupu:

  • pokud je vstupem string nebo pole, pak je zde uveden string, pod kterým se balíček uloží do path,
  • pokud je vstupem objekt, pak se zde uvede template string, který říká, jak se mají výsledné balíčky jmenovat. Např. [name].js říká, že se mají balíčky jmenovat podle klíčů ze vstupního objektu. Může být použito také např.[chunkhash].js, což uloží balíček pod hashem, který mu vypočítá Webpack.

publicPath

Veřejná cesta k souborům v rámci výstupu z Webpacku (jak jsme si řekli, mohou být výstupem i např. css soubory) je používaná loadery (např url-loader) a pluginy. Jedná se o cestu, pod kterou bude balíček vystaven na serveru. Je závislá na kořenu webserveru, na kterém je balíček vystaven. Je-li tento kořen shodný s path, pak by měla být publicPath nastavena na **/** .

Příklad konfigurace output:

module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  // entry: ['./index.js', './login.js'],  
  // entry: {  
  //   index: './index.js',  
  //   login: './login.js',  
  // },  
  output: {  
    path: path.join(__dirname, 'build'),  
    publicPath: '/',  
    filename: 'bundle.js',  
    // filename: '[name].js',  
    // filename: '[chunkhash].js',  
  }  
}

Pokud chceme používat Webpack pouze na vytváření balíčků z modulárního ES5 javascriptového kódu, pak nám stačí v konfiguraci uvést pouze entry a output.

Loadery

Loadery jsou transformace aplikované per module. Jsou aplikovány na moduly různých typů před tím, než je vytvořen balíček. V položce module se nastavují pravidla, která říkají, v jakém případě se má loader aplikovat.
Každé pravidlo se skládá z testu regulárním výrazem, který říká, jestli se mají uvedené loadery na modul aplikovat či nikoliv. Loader má svůj název a nastavení pomocí options:

module.exports = {  
module: {  
  rules: [  
    {  
      test: /\.js$/  
      use: [  
        {  
          loader: 'babel-loader',  
          options: {  
            presets: ['react']  
          }  
        }  
      ]  
    },  
    {  
      test: /\.sass$/,  
      use: [  
        {  
          loader: 'style-loader',  
        },  
        {  
          loader: 'css-loader',  
        },  
        {  
          loader: 'sass-loader',  
        }  
      ]  
    }  
  ]  
}  
}

Pokud je uvedeno více loaderů, pak se na modul aplikují postupně zezdola. Ve výše uvedeném příkladu se na soubory končící .sass aplikuje postupně sass, css a style loader.

Loadery nastavené v konfiguraci jsou aplikovány automaticky pokud je splněn regulární výraz pro import modulu. Např. pro výše uvedenou konfiguraci by se aplikoval loader v případě require('../sass/main.sass') .

Loadery však nemusí být nutně nastaveny v konfiguraci, ale lze je aplikovat přímo v require klauzuli require('style-loader!css-loader!sass-loader!../sass/main.sass') . Toto použití loaderů je upřednostněno před konfigurací a jsou aplikovány od modulu směrem doleva. Konfigurace loaderu se pak provádí přes query string require('babel-loader?presets=['react']!./component.jsx') .

Pomocí loaderů lze tedy transpilovat JSX na ES5 js, ES6 js na ES5 js, Sass na CSS, obrázky na base64 stringy, json soubory na js objekty atd.

Vždy musí být výsledkem posledního loaderu v pipě Javascript (protože Webpack je Javascript module bundler).

Je tedy nutné převést všechny moduly, které nejsou Javascript interpretovatelný prohlížečem, na ES5 Javascript, protože Webpack primárně tvoří balíčky Javascriptu. Proto například style-loader vezme CSS a převede jej na Javascript, který dané CSS vloží dynamicky do hlavičky HTML dokumentu. Něco podobné se musí provést i s ostatními non-js moduly.

Pluginy

Pluginy jsou transformace per bundle (balíček). Používají se pro funkcionalitu, kterou není schopen provést loader. Takovou funkcionalitou může být například extrakce stylů, vyčlenění vendorů do samostatných balíčků, obfuskace (minifikace a uglifikace) Javascriptu či nastavení globálních proměnných.
Pluginy jsou konfigurovány v položce plugins. Jedná se o pole, do kterého předáváme instance jednotlivých pluginů, protože můžeme chtít jeden plugin použít víckrát v různých případech.

Příklad použití pluginu:

module.exports = {  
  plugins: [  
    new ExtractTextPlugin({  
      name: 'bundle.css'  
    }),  
    new webpack.optimize.UglifyJsPlugin({  
      minimize: true  
    }),  
 ]  
}

Jak bylo uvedeno v předchozím článku, je možné vytvářet pomocí Webpacku i jiné balíčky než Javascript. Pokud například nechceme, aby se CSS přidávaly do js balíčku jako kód (což provede style-loader), ale byly vyextrahovány jako samostatný CSS bundle, můžeme použít výše uvedený ExtractTextPlugin. Webpack pak pomocí pluginu vygeneruje do output.path soubor bundle.css obsahující všechna CSS z Javascript balíčku, protože se plugin aplikuje nad celým js balíčkem.

Jak to celé zhruba funguje

  1. Webpack vytvoří grafy závislostí, jejichž kořeny jsou soubory uvedené v entry.
  2. Prochází postupně směrem do hloubky moduly, které jsou do entry pointů requirovány a aplikuje na ně podle stanovených pravidel loadery, které mohou dělat transpilaci či jinou transformaci modulů.
  3. Když Webpack projde graf závislostí pro vybraný entry point, už by mohl vytvořit balíček. Nejprve však aplikuje specifikované pluginy. Zde může docházet k minifikaci či uglifikaci (obfuskaci) nebo třeba extrakci vendor balíčků.
  4. Nakonec Webpack uloží balíček podle specifikace v output.

Příklad konfigurace

const webpack = require('webpack');  
const path = require('path');  
const Extract = require('extract-text-webpack-plugin');
module.exports = {  
  context: path.resolve(__dirname),  
  entry: './index.js',  
  output: {  
    path: path.join(__dirname, 'build'),  
    filename: 'index.js',  
  },  
  module: {  
    rules: [  
      {  
        test: /**\.**js$/,  
        use: [  
          {  
            loader: 'babel-loader',  
            options: {  
              presets: ['latest', 'react']  
            }  
          }  
        ]  
      }, {  
        test: /**\.**sass$/,  
        loader: Extract.extract({  
          fallback: 'style-loader',  
          use: 'css-loader!sass-loader'  
        })  
      },  
    ]  
  },  
  plugins: [  
    new Extract({  
      filename: 'bundled-sass.css',  
    }),  
    new webpack.optimize.UglifyJsPlugin({  
      minimize: true,  
    }),  
    new webpack.EnvironmentPlugin([  
      'NODE_ENV',  
    ]),  
  ]  
};

Module bundler versus task runner

Gulp a Grunt jsou task runnery. Používají se k automatizaci úloh, které uživatel definuje pomocí programování nebo konfigurace. Mezi úlohy může patřit kompilace, transpilace, minifikace, linting, testování atd. Důležité však je, že každá úloha pracuje s jedním konkrétním assetem (CSS, JavaScript atd.) a je na programátorovi, aby zajistil, že bude výsledek fungovat, tj. že budou například správně nastavené cesty k obrázkům.

Jak již bylo zmíněno, Webpack je primárně utilita pracující s JavaScriptem a umožňuje psát modulární kód, který pro prohlížeč zabalí do balíčku. Webpack navíc svou filosofií dovoluje do javascriptového kódu přidávat i jiné druhy assetů a poté je pomocí loaderů a pluginů zpracovat. Z tohoto pohledu je spuštění Webpacku jedna konkrétní úloha.

Gulp ani Grunt nezkoumají kód, který jim je předán, jen nad ním provedou definované úlohy. Na rozdíl od nich Webpack kód analyzuje a podle konfigurace jej upravuje a zpracovává.

To je tedy hlavní rozdíl. Webpack je konkrétní úloha spuštěná nad javascriptovým kódem. Je to podobné, jako když pustíte úlohu pro **sass** kompiler, jen malinko složitější, protože dovoluje komplexnější transformace nad předaným kódem.

Kdy použít Webpack při vývoji aplikací

  • Pokud chcete psát modulární javascriptový kód, který bude umět zpracovat prohlížeč.
  • Pokud chcete používat standardy JavaScriptu, které ještě nejsou implementovány (ES6+).
  • Pokud píšete SPA například v Reactu, pak se Webpack přímo vybízí.
  • Pokud píšete MPA a chcete psát moderní kód atd.

Zatím jsem zmínil pouze JavaScript, protože ostatní assety by měly být, dle mého názoru, brány v úvahu, jen pokud budete používat Webpack pro JavaScript, protože je do JavaScriptu budete muset importovat. Nedává smysl používat Webpack na SASS, pokud projekt obsahuje pouze SASS a CSS. To radši pusťte pouze sass --watch sass:css .

Já osobně používám Webpack neustále, ať píšu jen drobné kusy js kódu či velký projekt, protože mi dovoluje psát čistý, čitelný a moderní kód a navíc jej bez velké námahy rovnou obfuskuje.
Jelikož Webpack používám pravidelně, rovnou mám přidané loadery pro SASS a PostCSS a tím pádem mohu psát i moderní StyleSheety.
Přidanou hodnotou je i optimalizace dalších assetů jako jsou obrázky a fonty.

Má smysl používat task runnery s Webpackem?

Ano, má. Pokud používáte například Gulp a chcete přidat do svého projektu moderní modulovatelný javascriptový kód, tak nevidím důvod Webpack nepoužít, protože existuje například plugin do Gulpu, který dovoluje spouštět Webpack jako úlohu.
Pokud se vám navíc nelíbí importování SASSu do JavaScriptu, tak v případě použití Webpacku s Gulpem můžete oddělit transpilaci SASSu do vlastní úlohy.

Ale

Jak bylo zmíněno výše, patří mezi úlohy, které provádí task runnery, například minifikace, transpilace, linting nebo testování. To umí Webpack při troše konfigurace také, tudíž není nutné používat Webpack s task runnery.
Stačí pouze spustit například export NODE_ENV=production; webpack --progress --config webpack.config.js nebo přidat výše uvedený příkaz do package.json a pustit yarn run deploy.prod a výsledek může být stejný jako s task runnerem. Je pouze na vás, jaké řešení vám více vyhovuje nebo lépe zapadá do vaší firemní dev flow.

Používejte Yarn

Situaci, kdy při vývoji aplikace funguje celý projekt jak má, ale při nasazení do produkce pomocí CI najednou nefunguje nic, jsem zažil mnohokrát. Skoro vždy za to mohly balíčky nainstalované z NPM.

Důvod je prostý: při nasazení přes CI se instalují balíčky podle verzí uvedených v package.json, což může způsobit nainstalování novějšího balíčku, než který máte aktuálně u sebe a všechno přestane fungovat. Řešení? Yarn.

Yarn je balíčkovací systém pro Node.js. Jeden takový už máme: npm. Yarn, stejně jako npm, funguje nad NPM repository a používá package.json. To znamená, že s ním nainstalujete to samé, co s npm. Proč jej tedy používat? Má trochu jinou filosofii, jiné příkazy, ale hlavně má lockfile.

Pokud pustíte yarn install a existuje vedle package.json i soubor yarn.lock, pak se neinstalují balíčky podle package.json, ale podle yarn.lock. Zde jsou zamknuté verze z vaší instalace, což znamená, že při CI se nainstalují přesně ty samé balíčky, jako máte vy. Navíc pokud na projektu pracuje více lidí, mají všichni stejné verze balíčků a všem by měl projekt fungovat stejně.
Jedná se o funkcionalitu, kterou má třeba composer pro PHP už dávno, v npm bohužel chybí.

Yarn navíc tvrdí, že je rychlejší než npm a také, že umí pracovat v offline módu, což ale podle mě není tak velké plus jako zmíněný lockfile.

Používejte yarn, zjednoduší vám život!

Dodatek 6.6. 2017: Dodatečně bych rád zmínil, že NPM má sice shrinkwrap, ale ten donedávna nefungoval jako lockfily například výše zmíněného composeru.

Update 2019: Pokud vás zajímá, co se událo od roku 2017, přečtěte si navazující článek o novinkách a posledních změnách ve Webpacku.

Jestli vás článek bavil, mrkněte, jak probíhá vývoj aplikací u nás v Ackee. To je teprve jízda!

Marek Janča
Marek Janča
Frontend Developer

Máte zájem o spolupráci? Pojďme to probrat osobně!