Webpack je nástroj, který prostě musíte mít, pokud to s vývojem frontendu myslíte vážně. Používáme ho ve @SkrzCz na všech projektech. Než jsme se k němu dostali, prošli jsme si slepými cestičkami. Co jsme udělali dobře, co špatně, co nám pomohlo.
1. Cesta k Webpacku
Skrz DEV Cirkus
10.6.2015
V Node.JS na server-side existuje jednoduchý způsob, jak začít používat cizí knihovny - `npm install …` a
poté stačí napsat `require(“…”)`. Pokud jde o browser, už to tak jednoduché není. a) Žádný standardní
způsob není. b) “assety” před tím, než se pošlou do prohlížeče je potřeba minifikovat (odstranit bílé
znaky, komentáře, zkrátit názvy proměnných). c) A nakonec optimalizovat pro posílání přes HTTP (spojit
soubory dohromady), upravit názvy, aby podporovaly dobré kešování apod. V této prezentaci probereme,
jak jsme ve Skrzu došli k Webpacku.
2. Co je Skrz? Skrz je katalog akčního zboží, takový online akční
leták. Musí poskytovat skvělou user experience pro své uživatele,
tudíž se řeší nejrůznější Javascriptové vychytávky, animace,
AJAXová volání. Ale zároveň musí být obsah dostupný pro
vyhledávače - tzn. že renderovaní se musí řešit na serveru.
4. Takhle nějak vypadal Javascriptový kód. Moc a moc úrovní odsazení, drlouhá jQuery
špageta. Jestli uživatel je/není na nějaké stránce se neřešilo, vždycky se posílal stejný
Javascript, který obsahoval vše - rozlišení, jestli se nějaký kód má, nebo nemá aplikovat
na dané stránce se řešilo přes různé ověřování, jestli existují elementy pro všechny
selektory, které daná fcionalita potřebovala = hromada ifů. Výsledkem bylo dlouhé
načítání, špatný výkon.
5. Takhle nějak vypadalo řešení knihoven a závislostí. Podobně to asi někdy řešil každý. Na produkci
se akorát zapla proměná `settings.user_compile_js`.
Minifikaci a spojování Javascriptových souborů řešila vlastní skript v PHP - tzv. packer. Tomu se v
packer.conf souboru určilo, co všechno za soubory má projít a jak je má spojit. Často je spojil
špatně, vyhodil znaky, které neměl apod. - takže až na produkci se zjistilo, že kód nefunguje.
6. -1 rok
LiveScript + LESS + AMD
Grunt + Require.JS
Březen až květen 2014 se řešil redesign Skrzu. Ještě důležitější než nový kabátek to ale znamenalo přepis hodně
stránek a nové technologie. Místo home-made frameworku Ulriky se začala na backendu používat Symfony. Většina
stránek začala brát data z Elasticsearch místo MySQL. A na client-side místo čistého Javascriptu se začal používat
LiveScript, místo čistého CSS zase LESS. Nejdůležitější však bylo strukturování LiveSciptu do AMD (asynchronous
module definition) - což dalo dobře základ škálovatelnému řešení rozdělení závilostí. Celé to spojoval Grunt a
Require.JS.
7. NO-FRAMEWORK
framework
aka vlastní řešení
Jak jsem říkal, jako požitek pro uživatele je pro Skrz důležitá přístupnost pro roboty - tzn. renderování musí probíhat
na serveru. V době redesignu byly v Javascriptu nejpoužívanější frameworky Backbone a Angular. Oba se snažily řešit
renderování na klientu a podpora pro renderování na serveru bylo nedostatečná. A pokud se odmyslelo renderování
na klientu, přidávaly pramálo užitečného pro strukturování aplikace - neřešili problém, který jsme měli ve Skrzu. My
pouze potřebujeme vyrenderované HTML na serveru “rozhýbat” na klientu, sem tam načíst něco AJAXem. Vznikl tedy
vlastní způsob, jak stránku rozděli na komponenty a jak je zaregistrovat a napojit na ně Javascript.
8. V aplikaci je jeden entry point - main.ls (tečka LS protože LiveScript). V elementu s ID
js-options je serializovaný JSON s proměnnými týkajících se celé stránky předaných z
backendu. Obsahuje např. ID uživatele. V Javascriptu používáme dependency
injection container. V této ukázce je to LillyContainer (Lilly je název jedné z aplikací).
Ten se předá aplikaci a pak se aplikaci prožene fronta událostí, které do té doby na
stránce vznikly.
9. Dependency injection container obsahuje metody s prefixem “get” a
“create”. “get” metody znamenají, že daná instance je singleton - bude
vytvořena pouze jednou za celý běh aplikace. “create” metody jsou
továrničky. Nejvíce pro tzv. “widgety” - to jsou právě jednotlivé části, na
které je stránka rozdělená.
10. Na začátku každé stránky (hned po otevíracím tagu <body>) se inicializuje kód fronty
událostí. Něco jako mají trackovací kódy Google Analytics (_gaq), Facebooku (_fbq).
Cílem (ke kterému jsme se bohužel ještě nedostali), je mít jeden `<script … async>` v
hlavičce, eventy tedy hned po načtení části stránky mohou hned napárovat
Javascriptovvý widget (který se stará o interakci) na naparsovaný DOM element.
11. Události jsou prostě kusy Javascriptu přímo v HTML, které zavolají vždy nějakou metodu na globalním
objektu `skrz`. Nejdůležitější/nejpoužívanější je událost/metoda `skrz.widget(…)`. Jako první argument
má název widgetu (přilepte na začátek “create” a na konec “Widget” a máte název metody z
dependency injection kontejneru) a jako druhý argument model v JSONu. Mohli jste si všimnout, že v
kontejneru mají ještě widget-továrničky parametr `el`. To je odkaz na parent DOM element, ve kterém je
volání `skrz.widget` (ten se odvodí auto-magicky díky způsobu, jakým prohlížeče zpracovávají DOM).
12. Wiget je třída, v konstruktoru dostane tři argumenty `el`, `model` a `children`.
`el` je element, ve kterém byla vyvolána eventa `skrz.widget`. `model` je
druhý argument předaný události. Např. máme `ItemWidget` a ten má v
model ID nabídky. Widgety tvoří strom, podobně jako DOM tvoří strom.
`children` jsou widgety podřazené současnému.
Lifecycle widgetu je jednoduchý - po vytvoření kontejnerem se zavolá
metoda `bind`. Ta se stará o interaktivitu. Najde si různé elementy. Máme
konvenci, že elementy používané z Javascriptu musí mít třídu s prefixem `j-
*`.
Opakem `bind` je `unbind`, která se zavolá, pokud je widget odstraňován.
Widgety tvoří zapouzdřené celky, které se jednoduše upravují. Starají se
pouze přidání interaktivity nad HTML renderované na serveru.
13. -0.9972 roku
Require.JS
Widgety byly super a rozdělení do více souborů jakbysmet. Ale asi za
1 den, když se mělo natáhnout třeba 30 souborů s 30 různými
widgety, jsme narazili na problém s připojením k internetu v
předchozích kancelářích Skrzu na Bohdalci - prostě se nestáhl jeden
z 30 souborů a najednou tu byl těžko debugovatelný problém.
Vykašlali jsme se tedy na Require.JS…
14. …a místo něj se inspirovali Stichem (https://github.com/
sstephenson/stitch), což je Node.JS middleware, který spojí X
JS souborů dohromady. Přepsali jsme řešení do PHP a
donedávna úspěšně používali.
Mělo to však několik problémů:
a) Stitch nechápal Javascript, pouze vzal soubory v adresáři,
dal kolem každého boilerplate a spojil je dohromady.
Výsledkem bylo mnoho opakujících se řetězců s názvem
modulu, a tdy zbytečně velká velikost souboru.
b) Řešil pouze Javascript (resp. LiveScript), na LESS jsme
používali stále Grunt tasky.
15. -3 měsíce
Webpack
S velkou popularitou a množstvím přednášek ohledně ReactJS, jsem narazil
na přednášku od Pete Hunta, který ve Facebooku řeší Instagram webový
frontend.
Facebook používá k minifikaci, modularizaci a servírování Javascriptu
adaptabilní a prediktivní řešení. Je ale hodně provázané s celým zbytkem
Facebooku. Pro Instagram potřebovali něco daleko jednoduššího, co by
mohli rychle nasadit. A našli Webpack.
16. Google ->
“pete hunt webpack
howto”
Zadejte do Google “pete hunt webpack howto” = nejjednodušší způsob,
jak s Webpackem začít. A taky, jak jsme začali my.
17. package.json:
{
"name": "Lilly",
"version": "1.0.0",
"devDependencies": {
"webpack-dev-server": "^1.7.0",
"webpack": "^1.7.3"
},
"dependencies": {
"LiveScript": "^1.3.1",
"bootstrap": "^3.3.2",
"bootstrap-webpack": "0.0.3",
"css-loader": "^0.9.1",
"expose-loader": "^0.6.0",
"extract-text-webpack-plugin": "^0.3.8",
"file-loader": "^0.8.1",
"font-awesome": "^4.3.0",
"font-awesome-webpack": "gowravshekar/font-awesome-webpack#e22214a",
"imports-loader": "^0.6.3",
"jquery": "^1.11.2",
"less": "^2.0.0",
"less-loader": "^2.0.0",
"livescript-loader": "^0.1.3",
"source-map-loader": "^0.1.3",
"style-loader": "^0.8.3",
"uglify-js": "^2.4.16",
"url-loader": "^0.5.5",
"webpack": "^1.5.3"
},
"scripts": {
"dev": "BUILD_DEV=true webpack-dev-server -d --hot --inline --progress --colors --port 8443 --inline --https --content-base is/ --
output-public-path https://localhost:8443/assets/",
"build": "npm run build:queue && webpack -p --progress --profile --colors",
"clean": "rm -f www/assets/*",
"build:queue": "lsc --compile --bare --print client-src/Skrz/Inlined/queue.ls | uglifyjs --compress --mangle > client-src/Skrz/Inlined/
queue.js"
}
}
V praxi to vypadá následovně: v
package.json máme natažené všechny
potřebné závislosti.
A pak už 2 důležité scripty:
a) `dev` spustí Webpack dev server -
NodeJS server, který poslouchá na
určitém portu. Umí livereload stránky
při změně a další vychytávky.
Nebuďte líní, pokud budete s
webpackem začínat, si dev server
nastavit.
b) `build` spojuje a minifikuje soubory
pro použití na produkci. V případě
skrzu to trvá několik minut, ale
výsledek je skvělý.
Že je pro nás webpack tak skvělý je
hlavně díky rozhodnutí strukturovat kód
pomocí AMD modulů. Ale stejně tak
webpack podporuje synchronní `require`
jako je v Node.JS a další způsoby.
18. webpack.config.js:
var webpack = require("webpack");
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: {
all: ["./client-src/Skrz/Bundle/LillyBundle/main"]
},
output: {
path: "./www/assets",
publicPath: "/assets/",
filename: "all.js"
},
module: {
loaders: [
{ test: /bootstrap/js//,
loader: ‘imports?jQuery=jquery' },
{ test: /.ls/,
loader: “livescript-loader" },
{ test: /.css$/,
loader: ExtractTextPlugin.extract("style-loader", “css-loader") },
{ test: /.less$/,
loader: ExtractTextPlugin.extract("style-loader", “css-loader!less-loader") },
{ test: /.(woff2?|ttf|eot|svg|jpg|png|gif|swf)(?.*)?$/,
loader: “file-loader" }
]
},
resolve: {
extensions: ["", ".js", ".json", ".ls"]
},
plugins: [
new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || "true"))
}),
new ExtractTextPlugin("[name].css")
]
};
Webpack se nastaví přes `webpack.config.js`. Důležitá je sekce loaders,
kde podle přípony souboru nastavíte, co v tom souboru je. Např. tady
soubory končící na `.ls` se interpretují jako LiveScript. Soubory končící na
`.less` jako LESS = ano, uděláte v Javacriptu `require(“./style.less”)` a
webpack soubor přeloží do CSS, všechny CSS pak posbírá
ExtractTextPlugin (viz sekce plugins) a vytvoří jeden CSS soubory se vším.
Zahodili jsme tedy Grunt a o všechno se teď stará webpack.
Ten navíc např. fonty referencované v CSS, obrázky uloží do souboru,
který má jako název MD5 hash obsahu a nahradí všechny výskyty v CSS -
můžete tedy nastavit na fonty/obrázky kešování forever, a pokud se
změní, vznikne nový soubor, a tím pádem se invaliduje keš.
19. <head>:
<link rel="stylesheet" href="{$assetsBaseUrl}/all.css">
za <body>:
<script>{$queue nofilter}</script>
<script>skrz.begin();</script>
před </body>:
<script>skrz.end();</script>
<script>skrz.widget("Body");</script>
<script type="application/json" id=“js-options”>{$options|json_encode nofilter}</script>
<script src="{$assetsBaseUrl}/all.js"></script>
Pak už jsem stačí nastavit `$assetsBaseUrl`
v šabloně podle toho, jestli se jedná o vývoj
nebo produkci.
20. Otázky?
Díky!
Jedna otázek byla: “Proč použít Webpack a nepoužít Gulp?”
A proč nepoužít obojí! Každá věc je totiž na něco jiného. Webpack se stará o
přípravu “assetů” pro development/produkci. Gulp je task runner. Jedním z těch
tasků může být např. spuštění Webpacku pro development nebo vybuildovaní
assetů pro produkci.
My jsme Gulp jako task runner nepotřebovali, protože většinu tasků máme v PHP, v
Javascriptu máme pouze ty, které byly vidět v `package.json` na jednom z
předchozím slajdů. `npm run …` nám na to úplně stačí.