Webpack czy Browserify & Gulp: co jest lepsze?

Opublikowany: 2022-03-11

Ponieważ aplikacje internetowe stają się coraz bardziej złożone, ich skalowalność staje się sprawą najwyższej wagi. Podczas gdy w przeszłości wystarczyło pisanie ad-hoc JavaScript i jQuery, obecnie budowanie aplikacji internetowej wymaga znacznie większej dyscypliny i formalnych praktyk tworzenia oprogramowania, takich jak:

  • Testy jednostkowe, aby upewnić się, że modyfikacje kodu nie psują istniejącej funkcjonalności
  • Linting, aby zapewnić spójny styl kodowania wolny od błędów
  • Kompilacje produkcyjne różniące się od wersji deweloperskich

Sieć oferuje również kilka własnych, unikalnych wyzwań programistycznych. Na przykład, ponieważ strony internetowe wysyłają wiele asynchronicznych żądań, wydajność aplikacji internetowej może być znacznie obniżona z powodu konieczności żądania setek plików JS i CSS, z których każdy ma swoje własne niewielkie obciążenie (nagłówki, uściski dłoni itd.). Ten konkretny problem często można rozwiązać, łącząc pliki razem, więc żądasz tylko jednego spakowanego pliku JS i CSS, a nie setek pojedynczych.

Kompromisy w narzędziach do łączenia: Webpack vs Browserify

Którego narzędzia do tworzenia pakietów należy użyć: Webpack czy Browserify + Gulp? Oto przewodnik po wyborze.
Ćwierkać

Często używa się również preprocesorów językowych, takich jak SASS i JSX, które kompilują się do natywnego JS i CSS, a także transpilerów JS, takich jak Babel, aby korzystać z kodu ES6 przy zachowaniu kompatybilności z ES5.

Sprowadza się to do znacznej liczby zadań, które nie mają nic wspólnego z pisaniem logiki samej aplikacji internetowej. W tym miejscu wkraczają moduły uruchamiające zadania. Celem modułu uruchamiającego zadania jest zautomatyzowanie wszystkich tych zadań, aby można było korzystać z ulepszonego środowiska programistycznego, koncentrując się na pisaniu aplikacji. Po skonfigurowaniu programu uruchamiającego zadania wystarczy wywołać pojedyncze polecenie w terminalu.

Będę używał Gulpa jako programu do uruchamiania zadań, ponieważ jest bardzo przyjazny dla programistów, łatwy do nauczenia i łatwy do zrozumienia.

Szybkie wprowadzenie do Gulp

API Gulpa składa się z czterech funkcji:

  • gulp.src
  • gulp.dest
  • gulp.task
  • gulp.watch

Jak działa łyk

Oto przykładowe zadanie, które wykorzystuje trzy z tych czterech funkcji:

 gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });

Kiedy wykonywane jest my-first-task , wszystkie pliki pasujące do wzorca glob /public/js/**/*.js są minimalizowane, a następnie przenoszone do folderu build .

Piękno tego tkwi w .pipe() . Bierzesz zestaw plików wejściowych, przesyłasz je przez serię przekształceń, a następnie zwracasz pliki wyjściowe. Aby było jeszcze wygodniej, rzeczywiste przekształcenia potoku, takie jak minify() , są często wykonywane przez biblioteki NPM. W rezultacie w praktyce bardzo rzadko trzeba napisać własne przekształcenia poza zmienianiem nazw plików w potoku.

Następnym krokiem do zrozumienia Gulpa jest zrozumienie szeregu zależności między zadaniami.

 gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });

Tutaj my-second-task uruchamia funkcję zwrotną dopiero po zakończeniu zadań lint i bundle . Pozwala to na oddzielenie obaw: tworzysz serię małych zadań z jedną odpowiedzialnością, takich jak konwersja LESS do CSS , i tworzysz rodzaj głównego zadania, które po prostu wywołuje wszystkie inne zadania za pomocą tablicy zależności między zadaniami.

Wreszcie mamy gulp.watch , który obserwuje zmiany we wzorcu pliku glob i po wykryciu zmiany uruchamia serię zadań.

 gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })

W powyższym przykładzie wszelkie zmiany w pliku pasującym do /public/js/**/*.js wywołałyby zadanie lint i reload . Powszechnym zastosowaniem gulp.watch jest uruchamianie przeładowań na żywo w przeglądarce, funkcja tak przydatna dla rozwoju, że nie będziesz w stanie bez niej żyć, gdy już ją doświadczysz.

I tak po prostu rozumiesz wszystko, co naprawdę musisz wiedzieć o gulp .

Gdzie mieści się pakiet Webpack?

Jak działa pakiet internetowy

W przypadku korzystania ze wzorca CommonJS łączenie plików JavaScript w pakiet nie jest tak proste, jak ich łączenie. Zamiast tego masz punkt wejścia (zwykle nazywany index.js lub app.js ) z serią instrukcji require lub import u góry pliku:

ES5

 var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');

ES6

 import Component1 from './components/Component1'; import Component2 from './components/Component2';

Zależności muszą zostać rozwiązane przed pozostałym kodem w app.js , a te zależności mogą same mieć dalsze zależności do rozwiązania. Ponadto możesz require tej samej zależności w wielu miejscach w aplikacji, ale chcesz rozwiązać tę zależność tylko raz. Jak możesz sobie wyobrazić, gdy masz drzewo zależności o głębokości kilku poziomów, proces łączenia kodu JavaScript staje się dość złożony. Tutaj wkraczają pakiety, takie jak Browserify i Webpack.

Dlaczego programiści używają Webpack zamiast Gulp?

Webpack to pakiet, podczas gdy Gulp to narzędzie do uruchamiania zadań, więc można oczekiwać, że te dwa narzędzia będą powszechnie używane razem. Zamiast tego istnieje rosnący trend, szczególnie wśród społeczności React, do używania Webpack zamiast Gulp. Dlaczego to?

Mówiąc najprościej, Webpack jest tak potężnym narzędziem, że może już wykonywać większość zadań, które w innym przypadku wykonałbyś za pomocą programu do uruchamiania zadań. Na przykład Webpack zawiera już opcje minifikacji i map źródłowych dla twojego pakietu. Ponadto Webpack może być uruchamiany jako oprogramowanie pośredniczące za pośrednictwem niestandardowego serwera o nazwie webpack-dev-server , który obsługuje zarówno przeładowanie na żywo, jak i przeładowanie na gorąco (o tych funkcjach omówimy później). Korzystając z programów ładujących, można również dodać transpilację ES6 do ES5 oraz pre- i postprocesory CSS. To tak naprawdę pozostawia testy jednostkowe i linting jako główne zadania, których Webpack nie może obsłużyć samodzielnie. Biorąc pod uwagę, że zmniejszyliśmy co najmniej pół tuzina potencjalnych zadań Gulp do dwóch, wielu programistów decyduje się zamiast tego bezpośrednio używać skryptów NPM, ponieważ pozwala to uniknąć narzutu związanego z dodaniem Gulpa do projektu (o czym również porozmawiamy później) .

Główną wadą korzystania z Webpack jest to, że jest dość trudny do skonfigurowania, co jest nieatrakcyjne, jeśli próbujesz szybko uruchomić projekt.

Nasze 3 konfiguracje programu uruchamiającego zadania

Skonfiguruję projekt z trzema różnymi konfiguracjami uruchamiania zadań. Każda konfiguracja wykona następujące zadania:

  • Skonfiguruj serwer deweloperski z możliwością ponownego ładowania na żywo zmian w obserwowanych plikach
  • Łącz nasze pliki JS i CSS (w tym transpilację ES6 do ES5, konwersję SASS do CSS i mapy źródłowe) w skalowalny sposób na zmiany obserwowanych plików
  • Uruchamiaj testy jednostkowe jako samodzielne zadanie lub w trybie zegarka
  • Uruchom linting jako samodzielne zadanie lub w trybie zegarka
  • Zapewnij możliwość wykonania wszystkich powyższych czynności za pomocą jednego polecenia w terminalu
  • Masz kolejne polecenie do tworzenia pakietu produkcyjnego z minifikacją i innymi optymalizacjami

Nasze trzy konfiguracje to:

  • Łyk + Przeglądarka
  • Łyk + Pakiet sieciowy
  • Pakiet internetowy + skrypty NPM

Aplikacja użyje Reacta jako front-end. Pierwotnie chciałem zastosować podejście niezależne od frameworka, ale użycie Reacta w rzeczywistości upraszcza odpowiedzialność programu uruchamiającego zadania, ponieważ potrzebny jest tylko jeden plik HTML, a React działa bardzo dobrze ze wzorcem CommonJS.

Omówimy zalety i wady każdej konfiguracji, abyś mógł podjąć świadomą decyzję, jaki typ konfiguracji najlepiej odpowiada potrzebom Twojego projektu.

Skonfigurowałem repozytorium Git z trzema gałęziami, po jednym dla każdego podejścia (link). Testowanie każdej konfiguracji jest tak proste, jak:

 git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)

Przyjrzyjmy się szczegółowo kodowi w każdej gałęzi…

Wspólny kod

Struktura folderów

 - app - components - fonts - styles - index.html - index.js - index.test.js - routes.js

index.html

Prosty plik HTML. Aplikacja React jest ładowana do <div></div> i używamy tylko jednego dołączonego pliku JS i CSS. W rzeczywistości w naszej konfiguracji programistycznej Webpack nie będziemy nawet potrzebować pliku bundle.css .

index.js

Działa to jako punkt wejścia JS naszej aplikacji. Zasadniczo, po prostu ładujemy React Router do div z app id, o której wspominaliśmy wcześniej.

trasy.js

Ten plik definiuje nasze trasy. Adresy URL / , /about i /contact są mapowane odpowiednio na składniki HomePage , AboutPage i ContactPage .

index.test.js

Jest to seria testów jednostkowych, które testują zachowanie natywnego JavaScriptu. W rzeczywistej aplikacji o jakości produkcyjnej napisałbyś test jednostkowy dla każdego komponentu React (przynajmniej tych, które manipulują stanem), testując zachowanie specyficzne dla Reacta. Jednak na potrzeby tego postu wystarczy po prostu mieć funkcjonalny test jednostkowy, który można uruchomić w trybie zegarka.

komponenty/App.js

Można to traktować jako pojemnik na wszystkie nasze wyświetlenia strony. Każda strona zawiera składnik <Header/> oraz this.props.children , którego wynikiem jest sam widok strony ( ContactPage , jeśli w /contact w przeglądarce).

komponenty/strona główna/HomePage.js

To jest nasz domowy widok. Zdecydowałem się na react-bootstrap , ponieważ system gridowy Bootstrap jest doskonały do ​​tworzenia responsywnych stron. Przy prawidłowym użyciu programu bootstrap liczba zapytań o media, które musisz napisać dla mniejszych okien ekranu, jest znacznie zmniejszona.

Pozostałe składniki ( Header , AboutPage , ContactPage ) mają podobną strukturę (znaczniki react-bootstrap , brak manipulacji stanem).

Porozmawiajmy teraz więcej o stylizacji.

Podejście do stylizacji CSS

Moim preferowanym podejściem do stylizowania komponentów Reacta jest posiadanie jednego arkusza stylów na komponent, którego style są ograniczone do zastosowania tylko do tego konkretnego komponentu. Zauważysz, że w każdym z moich komponentów React, div najwyższego poziomu ma nazwę klasy pasującą do nazwy komponentu. Na przykład HomePage.js ma swoje znaczniki opakowane przez:

 <div className="HomePage"> ... </div>

Istnieje również powiązany plik HomePage.scss o następującej strukturze:

 @import '../../styles/variables'; .HomePage { // Content here }

Dlaczego to podejście jest tak przydatne? Powoduje to wysoce modularny CSS, w dużej mierze eliminujący problem niechcianego zachowania kaskadowego.

Załóżmy, że mamy dwa komponenty React, Component1 i Component2 . W obu przypadkach chcemy nadpisać rozmiar czcionki h2 .

 /* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }

Rozmiar czcionki h2 Component1 i Component2 jest niezależny od tego, czy składniki sąsiadują ze sobą, czy też jeden składnik jest zagnieżdżony w drugim. Idealnie oznaczałoby to, że styl komponentu jest całkowicie samowystarczalny, co oznacza, że ​​komponent będzie wyglądał dokładnie tak samo bez względu na to, gdzie zostanie umieszczony w znacznikach. W rzeczywistości nie zawsze jest to takie proste, ale z pewnością jest to ogromny krok we właściwym kierunku.

Oprócz stylów dla poszczególnych komponentów lubię mieć folder styles zawierający globalny arkusz stylów global.scss , wraz z częściami SASS, które obsługują określoną odpowiedzialność (w tym przypadku _fonts.scss i _variables.scss dla czcionek i zmiennych ). Globalny arkusz stylów pozwala nam zdefiniować ogólny wygląd i działanie całej aplikacji, podczas gdy części pomocnicze mogą być w razie potrzeby importowane przez arkusze stylów poszczególnych komponentów.

Teraz, gdy wspólny kod w każdej gałęzi został dogłębnie zbadany, skupmy się na pierwszym podejściu uruchamiającym zadanie / pakietem.

Konfiguracja Gulp + Browserify

gulpfile.js

Wynika to z zaskakująco dużego pliku gulp, z 22 importami i 150 wierszami kodu. Tak więc, dla zwięzłości, omówię szczegółowo tylko zadania js , css , server , watch i default .

Pakiet JS

 // Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }

Takie podejście jest raczej brzydkie z wielu powodów. Po pierwsze, zadanie podzielone jest na trzy oddzielne części. Najpierw tworzysz obiekt pakietu Browserify b , przekazując niektóre opcje i definiując niektóre programy obsługi zdarzeń. Następnie masz samo zadanie Gulp, które musi przekazać nazwaną funkcję jako funkcję zwrotną zamiast ją wstawiać (ponieważ b.on('update') używa tego samego wywołania zwrotnego). Nie ma to elegancji zadania Gulp, w którym po prostu przechodzisz do gulp.src i wprowadzasz pewne zmiany.

Inną kwestią jest to, że zmusza nas to do innego podejścia do ponownego ładowania html , css i js w przeglądarce. Patrząc na nasze zadanie watch Gulp:

 gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

Gdy plik HTML zostanie zmieniony, zadanie html zostanie ponownie uruchomione.

 gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

Ostatni potok wywołuje livereload() , jeśli NODE_ENV nie jest production , co powoduje odświeżenie w przeglądarce.

Ta sama logika jest używana w zegarku CSS. Gdy plik CSS zostanie zmieniony, zadanie css jest uruchamiane ponownie, a ostatni potok w zadaniu css uruchamia livereload() i odświeża przeglądarkę.

Jednak zegarek js w ogóle nie wywołuje zadania js . Zamiast tego moduł obsługi zdarzeń b.on('update', bundle) obsługuje przeładowanie przy użyciu zupełnie innego podejścia (mianowicie wymiany modułu na gorąco). Niespójność w tym podejściu jest irytująca, ale niestety konieczna, aby mieć przyrostowe kompilacje. Gdybyśmy naiwnie wywołali po prostu livereload() na końcu funkcji bundle , spowodowałoby to przebudowanie całego pakietu JS po każdej indywidualnej zmianie pliku JS. Takie podejście oczywiście się nie skaluje. Im więcej masz plików JS, tym dłużej trwa ponowne pakowanie. Nagle twoje ponowny pakiet 500 ms zaczyna trwać 30 sekund, co naprawdę hamuje zwinny rozwój.

Pakiet CSS

 gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });

Pierwszym problemem jest kłopotliwe włączenie CSS dostawcy. Za każdym razem, gdy do projektu dodawany jest nowy plik CSS dostawcy, musimy pamiętać o zmianie naszego pliku gulpfile, aby dodać element do tablicy gulp.src , zamiast dodawać import w odpowiednim miejscu w naszym rzeczywistym kodzie źródłowym.

Innym głównym problemem jest zawiła logika w każdej rurze. Musiałem dodać bibliotekę NPM o nazwie gulp-cond , aby skonfigurować logikę warunkową w moich potokach, a wynik końcowy nie jest zbyt czytelny (wszędzie potrójne nawiasy!).

Zadanie serwera

 gulp.task('server', () => { nodemon({ script: 'server.js' }); });

To zadanie jest bardzo proste. Zasadniczo jest to opakowanie wokół wywołania wiersza poleceń nodemon server.js , które uruchamia server.js w środowisku węzła. nodemon jest używany zamiast node , więc wszelkie zmiany w pliku powodują jego ponowne uruchomienie. Domyślnie nodemon restartuje działający proces po każdej zmianie pliku JS, dlatego ważne jest, aby dołączyć plik nodemon.json , aby ograniczyć jego zakres:

 { "watch": "server.js" }

Przejrzyjmy nasz kod serwera.

serwer.js

 const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();

To ustawia katalog podstawowy serwera i port na podstawie środowiska węzła i tworzy instancję express.

 app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));

Dodaje to oprogramowanie pośredniczące connect-livereload (niezbędne do naszej konfiguracji ponownego ładowania na żywo) i statyczne oprogramowanie pośredniczące (niezbędne do obsługi naszych zasobów statycznych).

 app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });

To tylko prosta trasa API. Jeśli przejdziesz do localhost:3000/api/sample-route w przeglądarce, zobaczysz:

 { website: "Toptal", blogPost: true }

W prawdziwym zapleczu miałbyś cały folder poświęcony trasom API, oddzielne pliki do nawiązywania połączeń z bazą danych i tak dalej. Ta przykładowa trasa została uwzględniona tylko po to, aby pokazać, że możemy łatwo zbudować backend na skonfigurowanym przez nas interfejsie.

 app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });

Jest to trasa typu catch-all, co oznacza, że ​​bez względu na to, jaki adres URL wpiszesz w przeglądarce, serwer zwróci naszą samotną stronę index.html . W takim przypadku React Router odpowiada za rozwiązanie naszych tras po stronie klienta.

 app.listen(port, () => { open(`http://localhost:${port}`); });

To mówi naszej ekspresowej instancji, aby nasłuchiwała określonego portu i otworzyła przeglądarkę w nowej karcie pod określonym adresem URL.

Jak dotąd jedyną rzeczą, której nie lubię w konfiguracji serwera, jest:

 app.use(require('connect-livereload')({port: 35729}));

Biorąc pod uwagę, że już używamy gulp-livereload w naszym pliku gulpfile, tworzy to dwa oddzielne miejsca, w których należy użyć livereload.

Na koniec, ale nie mniej ważne:

Zadanie domyślne

 gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });

Jest to zadanie, które uruchamia się po wpisaniu gulp w terminalu. Jedną z osobliwości jest potrzeba użycia runSequence , aby zadania były uruchamiane sekwencyjnie. Zwykle szereg zadań jest wykonywanych równolegle, ale nie zawsze jest to pożądane zachowanie. Na przykład musimy uruchomić clean zadanie przed html , aby upewnić się, że nasze foldery docelowe są puste przed przeniesieniem do nich plików. Po wydaniu gulp 4 będzie natywnie obsługiwał metody gulp.series i gulp.parallel , ale na razie musimy wyjść z tym drobnym dziwactwem w naszej konfiguracji.

Poza tym jest to całkiem eleganckie. Całe tworzenie i hosting naszej aplikacji odbywa się za pomocą jednego polecenia, a zrozumienie dowolnej części przepływu pracy jest tak proste, jak zbadanie pojedynczego zadania w sekwencji uruchamiania. Ponadto możemy podzielić całą sekwencję na mniejsze części, aby uzyskać bardziej szczegółowe podejście do tworzenia i hostowania aplikacji. Na przykład moglibyśmy skonfigurować oddzielne zadanie o nazwie validate , które uruchamia zadania lint i test . Albo możemy mieć zadanie host , które uruchamia server i watch . Ta możliwość organizowania zadań jest bardzo potężna, zwłaszcza gdy aplikacja jest skalowana i wymaga bardziej zautomatyzowanych zadań.

Programowanie a kompilacje produkcyjne

 if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';

Korzystając z biblioteki yargs NPM, możemy dostarczyć flagi wiersza poleceń do Gulpa. Tutaj poinstruuję plik gulpfile, aby ustawić środowisko węzła na produkcyjne, jeśli --prod jest przekazywane do gulp w terminalu. Nasza zmienna PROD jest następnie używana jako warunek warunkowy do rozróżniania zachowań związanych z rozwojem i produkcją w naszym pliku gulpfile. Na przykład jedną z opcji, które przekazujemy do naszej konfiguracji browserify jest:

 plugin: PROD ? [] : [hmr, watchify]

Mówi to browserify , aby nie używał żadnych wtyczek w trybie produkcyjnym i używał wtyczek hmr i watchify w innych środowiskach.

Ten warunek PROD jest bardzo przydatny, ponieważ oszczędza nam konieczności pisania oddzielnego pliku gulp dla produkcji i rozwoju, który ostatecznie zawierałby wiele powtórzeń kodu. Zamiast tego możemy zrobić takie rzeczy, jak gulp --prod , aby uruchomić domyślne zadanie w środowisku produkcyjnym, lub gulp html --prod , aby uruchomić tylko zadanie html w środowisku produkcyjnym. Z drugiej strony, widzieliśmy wcześniej, że zaśmiecanie naszych potoków Gulp wyrażeniami takimi jak .pipe(cond(!PROD, livereload())) nie są najbardziej czytelne. Ostatecznie to kwestia preferencji, czy chcesz użyć podejścia opartego na zmiennych logicznych, czy też skonfigurować dwa oddzielne pliki gulpfile.

Zobaczmy teraz, co się stanie, gdy nadal używamy Gulpa jako naszego programu uruchamiającego zadania, ale zastępujemy Browserify Webpackiem.

Konfiguracja Gulp + Webpack

Nagle nasz plik gulpfile ma tylko 99 wierszy i 12 importów, co stanowi znaczną redukcję w porównaniu z poprzednią konfiguracją! Jeśli sprawdzimy zadanie domyślne:

 gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });

Teraz nasza pełna konfiguracja aplikacji internetowej wymaga tylko pięciu zadań zamiast dziewięciu, co stanowi ogromną poprawę.

Ponadto wyeliminowaliśmy potrzebę livereload . Nasze zadanie watch to teraz po prostu:

 gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });

Oznacza to, że nasz obserwator Gulp nie wywołuje żadnego zachowania związanego z ponownym kupowaniem. Dodatkową korzyścią jest to, że nie musimy już przenosić index.html z app do dist ani build .

Wracając do redukcji zadań, nasze zadania html , css , js i fonts zostały zastąpione jednym zadaniem build :

 gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });

Wystarczająco proste. Uruchom kolejno zadania clean i html . Po ich zakończeniu chwyć nasz punkt wejścia, prześlij go przez pakiet Webpack, przekazując plik webpack.config.js , aby go skonfigurować, i wyślij otrzymany pakiet do naszego baseDir ( dist lub build , w zależności od env węzła).

Przyjrzyjmy się plikowi konfiguracyjnemu Webpack:

webpack.config.js

Jest to dość duży i onieśmielający plik konfiguracyjny, więc wyjaśnijmy kilka ważnych właściwości ustawianych w naszym obiekcie module.exports .

 devtool: PROD ? 'source-map' : 'eval-source-map',

To ustawia typ map źródłowych, których będzie używać Webpack. Webpack nie tylko obsługuje mapy źródłowe od razu po wyjęciu z pudełka, ale w rzeczywistości obsługuje szeroką gamę opcji map źródłowych. Każda opcja zapewnia inny balans między szczegółowością mapy źródłowej a szybkością przebudowy (czas potrzebny na ponowne pakowanie po zmianach). Oznacza to, że możemy użyć „taniej” opcji map źródłowych do rozwoju, aby osiągnąć szybkie przeładowanie, oraz droższej opcji map źródłowych w produkcji.

 entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]

To jest nasz punkt wejścia do pakietu. Zwróć uwagę, że przekazywana jest tablica, co oznacza, że ​​można mieć wiele punktów wejścia. W tym przypadku mamy oczekiwany punkt wejścia app/index.js , a także punkt wejścia webpack-hot-middleware , który jest używany jako część naszej konfiguracji ponownego ładowania modułu.

 output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },

W tym miejscu zostanie wyprowadzony skompilowany pakiet. Najbardziej zagmatwaną opcją jest publicPath . Ustawia podstawowy adres URL miejsca, w którym Twój pakiet będzie hostowany na serwerze. Na przykład, jeśli twoja publicPath to /public/assets , pakiet pojawi się w /public/assets/bundle.js na serwerze.

 devServer: { contentBase: PROD ? './build' : './app' }

To mówi serwerowi, który folder w twoim projekcie ma być używany jako katalog główny serwera.

Jeśli kiedykolwiek zdezorientujesz się, w jaki sposób Webpack mapuje utworzony pakiet w Twoim projekcie na pakiet na serwerze, po prostu pamiętaj o następujących kwestiach:

  • path + filename : Dokładna lokalizacja pakietu w kodzie źródłowym projektu
  • contentBase (jako root, / ) + publicPath : Lokalizacja pakietu na serwerze
 plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],

Są to wtyczki, które w pewien sposób zwiększają funkcjonalność Webpacka. Na przykład za webpack.optimize.UglifyJsPlugin odpowiada webpack.optimize.UglifyJsPlugin.

 loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]

To są ładowarki. Zasadniczo przetwarzają one wstępnie pliki, które są ładowane za pomocą instrukcji require() . Są one nieco podobne do rur Gulp, ponieważ można połączyć ze sobą ładowarki.

Przyjrzyjmy się jednemu z naszych obiektów ładujących:

 {test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}

Właściwość test informuje Webpack, że dany loader ma zastosowanie, jeśli plik pasuje do podanego wzorca wyrażenia regularnego, w tym przypadku /\.scss$/ . Właściwość loader odpowiada czynności wykonywanej przez program ładujący. Tutaj łączymy ze sobą programy ładujące style , css , resolve-url i sass , które są wykonywane w odwrotnej kolejności.

Muszę przyznać, że składnia loader3!loader2!loader1 nie uważam za zbyt elegancką. W końcu, kiedy musisz kiedykolwiek czytać coś w programie od prawej do lewej? Mimo to loadery są bardzo potężną funkcją webpacka. W rzeczywistości program ładujący, o którym właśnie wspomniałem, pozwala nam importować pliki SASS bezpośrednio do naszego JavaScript! Na przykład możemy zaimportować nasze arkusze stylów dostawców i globalne w naszym pliku punktu wejścia:

index.js

 import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));

Podobnie w naszym komponencie Header możemy dodać import './Header.scss' aby zaimportować arkusz stylów skojarzony z komponentem. Dotyczy to również wszystkich innych naszych komponentów.

Moim zdaniem można to niemal uznać za rewolucyjną zmianę w świecie programowania JavaScript. Nie musisz martwić się łączeniem CSS, minifikacją lub mapami źródłowymi, ponieważ nasz program ładujący obsługuje to wszystko za nas. Nawet gorące przeładowanie modułu działa dla naszych plików CSS. Możliwość obsługi importu JS i CSS w tym samym pliku sprawia, że ​​programowanie staje się koncepcyjnie prostsze: większa spójność, mniej przełączania kontekstów i łatwiejsze wnioskowanie.

Aby przedstawić krótkie podsumowanie działania tej funkcji: Webpack wbudowuje CSS do naszego pakietu JS. W rzeczywistości Webpack może to zrobić również dla obrazów i czcionek:

 {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}

Moduł ładujący URL instruuje pakiet Webpack, aby wstawiał nasze obrazy i czcionki jako adresy URL danych, jeśli mają one mniej niż 100 KB, w przeciwnym razie wyświetlają je jako osobne pliki. Oczywiście możemy również skonfigurować rozmiar odcięcia na inną wartość, np. 10 KB.

I to w skrócie konfiguracja Webpacka. Przyznam, że jest sporo konfiguracji, ale korzyści z jej używania są po prostu fenomenalne. Chociaż Browserify ma wtyczki i przekształcenia, po prostu nie można ich porównać z programami ładującymi Webpack pod względem dodatkowej funkcjonalności.

Webpack + Konfiguracja skryptów NPM

W tej konfiguracji używamy bezpośrednio skryptów npm zamiast polegać na pliku gulp do automatyzacji naszych zadań.

pakiet.json

 "scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }

Aby uruchomić kompilacje deweloperskie i produkcyjne, wprowadź odpowiednio npm start i npm run start:prod .

Jest to z pewnością bardziej zwarte niż nasz plik gulp, biorąc pod uwagę, że zmniejszyliśmy 99 do 150 linii kodu do 19 skryptów NPM lub 12, jeśli wykluczymy skrypty produkcyjne (z których większość po prostu odzwierciedla skrypty programistyczne ze środowiskiem węzła ustawionym na produkcyjny ). Wadą jest to, że te polecenia są nieco tajemnicze w porównaniu z naszymi odpowiednikami zadań Gulp i nie są tak ekspresyjne. Na przykład nie ma możliwości (przynajmniej o tym wiem), aby pojedynczy skrypt npm uruchamiał niektóre polecenia szeregowo, a inne równolegle. To albo jedno, albo drugie.

Takie podejście ma jednak ogromną zaletę. Używając bibliotek NPM, takich jak mocha , bezpośrednio z wiersza poleceń, nie musisz instalować równoważnego wrappera Gulp dla każdej z nich (w tym przypadku gulp-mocha ).

Zamiast instalować NPM

  • łyk-eslint
  • gulp-mocha
  • łyk-nodemon
  • itp

Instalujemy następujące pakiety:

  • eslint
  • mokka
  • nodemon
  • itp

Cytując post Cory House, Dlaczego zostawiłem Gulp and Grunt dla skryptów NPM :

Byłem wielkim fanem Gulpa. Ale w moim ostatnim projekcie skończyłem z setkami wierszy w moim pliku gulpfile i około tuzinem wtyczek Gulp. Miałem problemy z integracją Webpack, Browsersync, hot reloading, Mocha i wieloma innymi za pomocą Gulpa. Czemu? Cóż, niektóre wtyczki miały niewystarczającą dokumentację dla mojego przypadku użycia. Niektóre wtyczki ujawniały tylko część API, której potrzebowałem. Jeden miał dziwny błąd, w którym oglądał tylko niewielką liczbę plików. Kolejne pozbawione kolorów kolory podczas wysyłania do wiersza poleceń.

Określa trzy podstawowe problemy związane z Gulp:

  1. Zależność od autorów wtyczek
  2. Frustrujące w debugowaniu
  3. Rozdzielona dokumentacja

Zgadzałbym się z tym wszystkim.

1. Zależność od autorów wtyczek

Za każdym razem, gdy biblioteka taka jak eslint zostanie zaktualizowana, powiązana biblioteka gulp-eslint wymaga odpowiedniej aktualizacji. Jeśli opiekun biblioteki straci zainteresowanie, wersja biblioteki Gulp nie będzie zsynchronizowana z wersją natywną. To samo dotyczy tworzenia nowej biblioteki. Jeśli ktoś utworzy bibliotekę xyz i to się przyjmie, nagle potrzebujesz odpowiedniej biblioteki gulp-xyz , aby użyć jej w swoich zadaniach gulp.

W pewnym sensie to podejście po prostu się nie skaluje. Idealnie, chcielibyśmy podejścia takiego jak Gulp, które może korzystać z bibliotek natywnych.

2. Frustrujące do debugowania

Chociaż biblioteki takie jak gulp-plumber pomagają znacznie złagodzić ten problem, prawdą jest, że raportowanie błędów w gulp po prostu nie jest zbyt pomocne. Jeśli nawet jeden potok zgłosi nieobsługiwany wyjątek, otrzymasz ślad stosu dla problemu, który wydaje się całkowicie niezwiązany z przyczyną problemu w kodzie źródłowym. W niektórych przypadkach może to sprawić, że debugowanie stanie się koszmarem. Żadna ilość wyszukiwania w Google lub Stack Overflow nie może naprawdę pomóc, jeśli błąd jest wystarczająco tajemniczy lub mylący.

3. Rozłączona dokumentacja

Często stwierdzam, że małe biblioteki gulp mają bardzo ograniczoną dokumentację. Podejrzewam, że dzieje się tak dlatego, że autor zwykle tworzy bibliotekę przede wszystkim na własny użytek. Ponadto często trzeba zajrzeć do dokumentacji zarówno wtyczki Gulp, jak i samej biblioteki natywnej, co oznacza dużo przełączania kontekstu i dwa razy więcej do zrobienia.

Wniosek

Wydaje mi się całkiem jasne, że Webpack jest lepszy niż Browserify, a skrypty NPM są lepsze niż Gulp, chociaż każda opcja ma swoje zalety i wady. Gulp jest z pewnością bardziej wyrazisty i wygodny w użyciu niż skrypty NPM, ale płacisz cenę za całą dodaną abstrakcję.

Nie każda kombinacja może być idealna dla Twojej aplikacji, ale jeśli chcesz uniknąć przytłaczającej liczby zależności programistycznych i frustrującego środowiska debugowania, najlepszym rozwiązaniem jest pakiet Webpack ze skryptami NPM. Mam nadzieję, że ten artykuł okaże się przydatny w doborze odpowiednich narzędzi do kolejnego projektu.

Związane z:
  • Utrzymuj kontrolę: przewodnik po pakietach internetowych i reagowaniu, cz. 1
  • Łyk pod maską: tworzenie narzędzia do automatyzacji zadań opartego na strumieniu