Webpack i Babel

Wsparcie

Gdy spojrzysz na tabelę ze strony http://kangax.github.io/compat-table/es6/ zobaczysz, że wsparcie dla ES6 jest już całkiem dobre.

Niektóre funkcjonalności nie są jeszcze wspierane - w szczególności implementacja modułów. Gdy piszę te słowa wciąż trwa dyskusja jak taka implementacja ma wyglądać w końcowej fazie. Safari już wprowadziło swoją implementację, Chrome i Firefox dają możliwość włączenia takiej funkcjonalności w ukrytych ustawieniach przeglądarki. Czy to oznacza, że problem jest rozwiązany? Okazuje się, że nie do końca, ponieważ serwowane oddzielnie moduły w chwili pisania tego tekstu sprawdzają się gorzej, niż zbundlowane skrypty. Zobaczymy co przyniesie przyszłość...

Pomijając moduły, część przeglądarek nie nadąża za standardami i zatrzymała się na wersji ES5. Dla przykładu używane przez nas notorycznie let i const są bardzo dobrze wspierane, ale już interpolacja stringów, czy klasy wymagają naszej pomocy.

Żeby móc używać dzisiaj takich funkcjonalności musimy - podobnie jak przy SASS zamienić nasz nowy kod na odpowiednio transpilowany kod, który zadziała na starszych przeglądarkach. Czyli musimy go zamienić na starszy zapis.

W dzisiejszych czasach do takiej transpilacji najczęściej używa się Babel, która zamienia nowy kod na odpowiedni zapis.

Jest wiele sposobów na użycie Babel.

Jednym z najbardziej popularnych sposobów jest użycie jej wraz z webpackiem.

Co to jest webpack? Jest to tak zwany bundler, czyli narzędzie, które czyta wskazany przez nas plik JavaScript, a następnie wykonuje na nim zadane czynności. Jeżeli dla przykładu znajdzie w kodzie importowanie innego modułu (innego pliku JavaScript), dołączy go do naszego kodu. Takich różnych akcji jest cała masa. Webpack może łączyć skrypty, minimalizować je, może zamieniać nasze SCSS na odpowiedni kod JavaScript (tak byśmy mogli takie style potem używać w js), może optymalizować kod html, dołączać do js zakodowane grafiki itp.

W wyniku takich akcji nasze źródłowe skrypty są zamieniane na jeden wynikowy, zminimalizowany i zoptymalizowany plik.

Webpack słynie z tego, że jego konfiguracja do najprostszych nie należy. Nie do końca się z tym zgadzam. Konfiguracja jest raczej spoko, problem jest raczej z pluginami, które dołączamy do naszej konfiguracji. Każdy sobie rzepkę - co jest często zmorą dzisiejszego frontendu. Każdy plugin konfiguruje się inaczej, trzeba doszukiwać się czemu coś działa albo nie działa, jak trzeba coś podłączyć. A utrudniają to pisane na kolanie dokumentacje. Ale żyć się da - trzeba po prostu raz się przebić przez ten bigos.

Instalacja NodeJavaScript

Zaczynamy od zainstalowania NodeJavaScript na swoim komputerze. Wchodzimy na stronę https://nodejs.org/en/ i dokonujemy instalacji. Różni się ona w zależności od systemu. Na Windowsie i OS X nie powinno być żadnego problemu.

W przypadku Ubuntu sprawdzają się różne metody. Na oficjalnej stronie podawana jest metoda z użyciem JCurl.

Ale uwaga. Metoda ta przynosi często późniejsze problemy, dlatego ja osobiście jej nie polecam. Rozchodzi się o to, że instalując w ten sposób Node, tworzone są katalogi. Problem pojawia się w przypadku, kiedy chcemy zainstalować później za pomocą Node jakieś globalne moduły. Okazuje się, że Node nie ma dostępu do swoich własnych katalogów (bo nie on jest ich właścicielem). Powoduje to, że podczas instalacji globalnych modułów w terminalu zamiast potwierdzenia instalacji pojawia nam się masa czerwonych błędów wynikających z "permission denied".

Aby temu zapobiec warto skorzystać z nvm - menadżera wersji node.

Instalujemy go zgodnie z poleceniem ze strony: https://github.com/creationix/nvm#install-script. Jeżeli nie mamy zainstalowanego Curl, wtedy skorzystajmy po prostu z instalacji za pomocą wget.

Po instalacji - tak jak mówi instrukcja - musimy wyłączyć i włączyć terminal. Po tych czynnościach będziemy mogli zainstalować node za pomocą polecenia:


nvm install node

Czasami po ponownym włączeniu terminala wpisanie powyższej komendy pokaże nam informację, że nie ma takiego polecenia. Wtedy trzeba wcześniej użyć polecenia:


source ~/.bashrc

Wszystko jest opisane w instrukcji na stronie https://github.com/creationix/nvm#install-script.

Instalacja Webpacka

Po instalacji node przechodzimy do instalacji webpacka.

Tak samo jak resztę pakietów Node, webpacka możemy zainstalować globalnie lub lokalnie per projekt. Obie metody są dobre i zależą od podejścia do projektu. Ja preferuję metodę "per projekt", ponieważ powoduje ona zapisanie w pliku package.json instalowanych paczek (patrz poniżej).

Wchodzimy do katalogu z naszym projektem i wpisujemy w konsoli:


npm init

Co spowoduje inicjację projektu. Będziemy musieli odpowiedzieć na kilka pytań odnośnie naszego projektu. Musimy czytać, co pojawia się w konsoli, bo dla przykładu pierwsze pytanie dotyczy nazwy naszego projektu i domyślna nazwa brana jest na bazie nazwy katalogu. Jeżeli nasz katalog zawiera duże litery lub spacje, wtedy nie będziemy mogli wybrać domyślnej wartości, a wpisać własną nazwę. Na szczęście wszystkie komunikaty pojawiają się w terminalu.
Nie musimy dokładnie odpowiadać na pytania. Kilka szybkich enterów powinno załatwić sprawę.

Możemy też zamiast npm init wpisać npm init -y, co spowoduje automatyczne odpowiedzi na wszystkie pytania.

Po inicjacji pojawi się nam w katalogu plik packages.json, który zawiera nie tylko informacje na temat projektu (te które właśnie wpisywaliśmy), ale rzeczy, które zainstalujemy dla naszego projektu (np webpack, webpack-cli, gulp itp). Plik ten spokojnie możesz edytować za pomocą dowolnego edytora (ale uważnie, bo tutaj każdy przecinek ma znaczenie).

Dzięki powyższemu plikowi, osoba która ściągnie nasz projekt z repozytorium nie bedzie musiała się przebijać przez cały proces szukania i instalacji odpowiednich modułów, a wykona tylko jedno polecenie


npm install

które automatycznie zainstaluje jej wszystkie niezbędne moduły.

Instalacja webpacka

Przechodzimy do instalacji webpacka.

Będąc dalej w terminalu w naszym katalogu wpisujemy:


npm install webpack webpack-cli --save-dev

alternatywą do powyższego polecenia jest


npm i webpack webpack-cli -D

co zainstaluje nam webpacka oraz jego klienta w danym projekcie. Możemy to sprawdzić na dwa sposoby. Po pierwsze powinien pojawić się nam katalog node_modules, w którym pojawi się bardzo dużo katalogów i plików (baaardzo dużo). Wśród nich będzie katalog webpack.
Drugim sposobem jest podglądnięcie pliku package.json.
Powinna się w nim pojawić sekcja devDependencies, która będzie zawierać w sobie wpis o webpacku:


{
  "name": "projekt",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {},
  "devDependencies": {
    "webpack": "^4.4.1",
    "webpack-cli": "^2.0.13"
  }
}

Właśnie w tej sekcji będą się pojawiać wpisy o instalowanych przez nas rzeczach. Dzięki temu inne osoby ściągając nasz projekt nie będą musiały za każdym razem instalować wszystkich rzeczy z osobna, a zainstalują wszystko naraz za pomocą polecenia npm i.

Odpalamy webpacka

Webpack ogólnie działa tak, że wskazujemy mu plik wejścia (plik w którym my piszemy skrypty), a następnie wskazujemy mu plik, do którego zostanie zapisany transpilowany kod.

Przypuśćmy, że nasza struktura katalogów wygląda tak:

Webpack struktura projektu

Mamy więc katalog src, w którym mamy wszystkie pliki na których pracujemy. Mamy też katalog dist, w którym będzie strona wynikowa, a w nim katalog js, w którym pojawi się transpilowany plik bundle.js, który dołączymy w naszym index.html.

Podstawowe odpalenie webpacka polega na odwołaniu się do webpacka, a następnie podanie 2 parametrów: pliku wejściowego i pliku wyjściowego. Webpack został zainstalowany w node_modules, a jego skrypt mieści się dokładnie w node_modules/webpack/bin. Aby więc odpalić ręcznie webpacka musimy użyć polecenia:


node node_modules/webpack/bin/webpack.js src/js/app.js js/bundle.js
Jeżeli zainstalowalibyśmy webpacka globalnie (zamiast -D dalibyśmy -g), wtedy nie musielibyśmy podawać całej ścieżki do pliku webpacka, a zamiast tego moglibyśmy wpisywać:

webpack src/js/app.js js/bundle.js

Po odpaleniu powyższej komendy w terminalu projektu powinna nam się pojawić informacja o zakończonej kompilacji.

Jak widzisz, powyższe polecenie do najprostszych nie należy. Spróbujmy to usprawnić.

Jednym ze sposobów jest wykorzystanie skryptów npm. Jeżeli otworzysz plik package.json, znajdziesz w nim sekcję script:


...
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
},
...

Jest to przykładowy skrypt o nazwie test. Zamieńmy jego treść na:


...
"scripts": {
    "test": "echo 'To nasz testowy skrypt'"
},
...

i odpalmy poleceniem:


npm run test

Skrypt ten tylko wypisał w konsoli powyższy tekst. Skrypty więc służą do odpalania jakiś komend w terminalu. Wykorzystajmy je do odpalania webpacka. Skrypt test nie będzie nam już potrzebny, dlatego zamieńmy go poniższym:


...
"scripts": {
    "dev": "webpack --mode development",
    "prod": "webpack --mode production",
},
...

Powyższe 2 skrypty odpalają webpacka. Odpalając moduły za pomocą skryptów z pliku package.json nie musisz podawać im dokładnej ścieżki do moduły (czyli nie musimy wskazywać dokładnie gdzie leży webpack), ponieważ npm wie gdzie, że ma ich szukać w node_modules w odpowiednim katalogu (webpack).

Pierwszy ze skryptów odpali webpacka w taki sposób, że kod będzie skompilowany w "czytelny" sposób. Wersja produkcyjna skompiluje kod zminimalizowany.

Uruchamiamy więc nasz skrypt poleceniem:


npm run dev
//lub
npm run prod

Po odpaleniu powyższego skryptu, w konsoli pojawią się błędy.

Webpack daje nam znać, że brakuje pliku konfiguracji. Gdy pierwszy raz odpaliliśmy webpacka, podawaliśmy mu 2 atrybuty - plik wejściowy i wyjściowy. Odpalając webpacka w powyższy sposób, musimy mu dostarczyć plik konfiguracyjny.

Konfiguracja webpacka

Podobnie do Gulpa, webpack też wymaga pliku konfiguracyjnego, który powie mu dokładnie co ma robić. W gulpie plik taki nazywa się gulpfile.js, natomiast dla webpacka plik konfiguracyjny nosi nazwę webpack.config.js

Tworzymy więc za pomocą dowolnego edytora w głównym katalogu naszego projektu (tam gdzie mamy package.json) plik webpack.config.js

W jego wnętrzu wpisujemy poniższy kod:


module.exports = {
    entry: './src/js/app.js',
    output: {
        path: `${__dirname}/dist/js`,
        filename: 'bundle.js'
    },
    watch: true,
    mode: "development", //ta opcja zostanie pominięta jeżeli użyjemy npm run build
    devtool: "source-map",
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [["env", {
                            targets: {
                                browsers: ['> 1%']
                            }
                        }]]
                    }
                }
            }
        ]
    }
}

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. Możemy ją podać tak jak powyżej korzystając ze zmiennej __dirname, lub skorzystać z oferowanego przez Node modułu path (który jest dostępny domyslnie w Node):


const path = require('path');

...

module.exports = {
    entry: "./js/app.js",
    output: {
        filename: "out[name].js",
        path.resolve(__dirname, "js")
    },
    ...
}

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: {
    a : "./js/app1.js",
    b : "./js/app2.js",
},
output: {
    filename: "out[name].js", //zostaną nam wygenerowane pliki out-a.js i out-b.js
    path: `${__dirname}/js`,
},

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 transpilację. 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 debuger będzie nam wskazywał ścieżki do plików źródłowych.

Kolejna duża sekcja to module.

Webpack czyta nasz kod. Jeżeli trafi na instrukcję require, dołączy do danego pliku importowany kod. Jeżeli trafi np. na importowany plik scss, css czy np. grafikę, które tutaj mogą być importowane do js, wtedy musi wykonać odpowiednie akcje. Właśnie w tej sekcji konfiguracji podajemy mu jak webpack ma reagować na odpowiednie formaty plików które są dołączane w kodzie.

W naszym przypadku interesują nas pliki JavaScript, które domyślnie webpack po prostu by złączył z sobą. My chcemy jednak by webpack dodatkowo taki kod transpilował za pomocą Babel. Używamy do tego celu tak zwanego loadera. Loadery to właśnie "dodatki" do webpacka, które mówią mu co ma robić z danymi formatami plików. Pliki JavaScript loader babel transpiluje na ES5, ale np. loader sass dołączane do JavaScript pliki z sasem zamienił by na odpowiedni kod.

Takich loaderów jest masa i wszystko zależy czego potrzebujesz. W praktyce wystarczy wpisać w google "webpack loader ..." gdzie kropki to interesujący nas format.

Żeby używać loadera babel, musimy go zainstalować. Wpisujemy więc w google webpack loader bable i przechodzimy na stronę https://github.com/babel/babel-loader. Musimy ten loader zainstalować - tak samo jak webpacka. W terminalu wpisujemy więc:


npm install babel-loader babel-core babel-preset-env -D

Wraz z babel-loader i babel-core zainstalowaliśmy preset. Presety dla babela są to dodatki (pluginy) działające mniej więcej tak samo jak autoprefixer. Czym jest ten autoprefixer? Jest to mechanizm (występujący jako narzędzia online, czy dodatki do różnych narzędzi jak edytory, gulp itp), który zamienia napisany przez nas kod scss/less/postcss na taki zapis, by był wspierany przez wskazane przez nas przeglądarki. My więc piszemy nasz kod beż żadnych prefixów korzystając z najnowszej składni CSS, a autoprefixer za naszym plecami automatycznie zamienia taki zapis dodając odpowiednie prefixy jak np. -moz-, -webkit itp.

Podobnie jest z presetami do babela. My piszemy nasz kod za pomocą nowej składki ES6 (i nowszych), a dany preset automatycznie konwertuje go do odpowiedniego zapisu tak by działał on na wskazanych przez nas przeglądarkach.

Takich presetów jest kilka. Część z nich kompiluje dany kod na konkretną wersję JavaScript.
Preset który my powyżej zainstalowaliśmy jest połączeniem takich presetów i pozwala nam podczas konfiguracji wybrać jakie przeglądarki chcemy wspierać. W chwili pisania tego tekstu jest zalecany do użycia.

Powiedzmy, że chcemy wspierać przeglądarki, które mają 1% rynku i dodatkowo 2 ostatnie wersje przeglądarek:


...
module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: [["env", {
                        targets: {
                            browsers: ['> 1%', 'last 2 versions']
                        }
                    }]]
                }
            }
        }
    ]
}
...

Pełną listę podobnych zapisów możesz znaleźć tutaj. Podobne definicje stosuje się też przy autoprefixerze, który opiera się o ten sam mechanizm.

W internecie znajdziesz masę tutoriali, które korzystają z innych presetów np. preset-env2015. Nie jest to błędem. Po prostu autorzy użyli taki preset a nie inny. Ja sam bardzo długo używałem preset-es2015 i nigdy nie narzekałem na działanie. Ale skoro nowszy pozwala dodatkowo wybierać jakie przeglądarki chcemy wspierać, to czemu by z niego nie skorzystać...

Odpal teraz webpacka poleceniem:


npm run dev
//lub
npm run prod

Jeżeli chcesz przerwać działanie webpacka, w terminalu naciśnij Ctrl + C.

Tutaj możesz ściągnąć powyżej opisany mini projekt. Po ściągnięciu wypakuj go do katalogu z projektem, a następnie zainstaluj wszystkie paczki za pomocą polecenia npm i (lub sudo npm i).

Po instalacji możesz już działać odpalając webpacka powyżej opisanymi poleceniami.

Inne rozwiązania

To co pokazałem ci powyżej, jest jedną z miliona możliwości używania webpacka.

Niektórzy instalują go globalnie, tworzą plik konfiguracyjny (tak jak powyżej), a potem odpalają go bez skryptu bezpośrednio z konsoli poleceniem:


webpack

co dam nam w konsoli:

webpack console

Inni instalują webpacka globalnie i wogóle nie tworzą pliku konfiguracji odrazu odpalając go z komendy (podając też wszystkie konieczne parametry)

Ostatnio też powstał webpack-cli - coś jak gulp-cli, czyli mini narzędzie, które pozwala odpalać z terminala webpacka wraz z odpowiednimi opcjami konfiguracyjnymi.

Dodatkowo jakiś czas temu dodano do npm komendę npx, która pozwala odpalać lokalnie zainstalowane paczki z poziomu terminala:


npx webpack

Ja osobiście używam na co dzień Gulpa. Stworzyłem sobie więc podobny do powyższego plik konfiguracyjny webpacka, a następnie zintegrowałem go z Gulpem.


...
gulp.task('es6', function(cb) {
    return webpack(require('./webpack.config.js'), function(err, stats) {
        if (err) throw err;
        console.log(stats.toString());
        cb();
        browserSync.reload();
    })
})

gulp.task('watch', function() {
    gulp.watch('src/scss/**/*.scss', ['sass']);
    gulp.watch('src/js/**/*.js', ['es6']);
    gulp.watch("dist/**/*.html").on('change', browserSync.reload);
});

gulp.task('default', function() {
    console.log(gutil.colors.yellow('------ start ------'));
    gulp.start(['sass', 'es6', 'browseSync', 'watch']);
});

...

Dzięki temu odpalając Gulpa poleceniem gulp odpalam wszystkie taski - do kompilowania scss na css (z autoprefixerem), obserwowania html, czy pokazany powyżej odpalający webpacka.
Metoda dla mnie bardzo wygodna, ale czy dla ciebie się sprawdzi? Wszystko zależy od twojego warsztatu webmastera i tego do czego używasz podobnych narzędzi.

Niektórzy są frontendowcami bardziej graficznymi i ich zabawa z narzędziami nie bawi. Osoby takienajczęściej wybierają narzędzia takie jak prepros czy codekitapp. Ja osobiście czasami w niektórych projektach odpalam sobie taki program i zamiast dłubać w konfiguracji narzędzi i od razu rzucam się na żywioł kodu. Wszystko zależy od sytuacji i nas samych.
Nie jest tak i nigdy nie będzie, że do celu prowadzi tylko jedna słuszna ścieżka, a cała reszta rozwiązań jest zła.

Jest wiele bardzo dobrych artykułów na temat webpacka. Jednym z nich jest świetny tekst na stronie https://medium.com/javascript-training/beginner-s-guide-to-webpack-b1f1a3638460