Przewodnik po zarządzaniu zależnościami pakietów internetowych
Opublikowany: 2022-03-11Koncepcja modularyzacji jest nieodłączną częścią większości nowoczesnych języków programowania. Jednak JavaScript nie miał żadnego formalnego podejścia do modularyzacji aż do czasu pojawienia się najnowszej wersji ECMAScript ES6.
W Node.js, jednym z najpopularniejszych obecnie frameworków JavaScript, pakiety modułów umożliwiają ładowanie modułów NPM w przeglądarkach internetowych, a biblioteki zorientowane na komponenty (takie jak React) zachęcają i ułatwiają modularyzację kodu JavaScript.
Webpack to jeden z dostępnych pakietów modułów, który przetwarza kod JavaScript, a także wszystkie statyczne zasoby, takie jak arkusze stylów, obrazy i czcionki, do pliku w pakiecie. Przetwarzanie może obejmować wszystkie zadania niezbędne do zarządzania i optymalizacji zależności kodu, takie jak kompilacja, łączenie, minifikacja i kompresja.
Jednak konfiguracja Webpacka i jego zależności może być stresująca i nie zawsze jest prostym procesem, szczególnie dla początkujących.
Ten wpis w blogu zawiera wytyczne wraz z przykładami konfigurowania pakietu WebPack dla różnych scenariuszy i wskazuje najczęstsze pułapki związane z łączeniem zależności projektu przy użyciu pakietu Webpack.
W pierwszej części tego wpisu na blogu wyjaśniono, jak uprościć definicję zależności w projekcie. Następnie omówimy i zademonstrujemy konfigurację dzielenia kodu aplikacji wielostronicowych i jednostronicowych. Na koniec omówimy, jak skonfigurować Webpack, jeśli chcemy włączyć do naszego projektu biblioteki firm trzecich.
Konfigurowanie aliasów i ścieżek względnych
Ścieżki względne nie są bezpośrednio związane z zależnościami, ale używamy ich podczas definiowania zależności. Jeśli struktura pliku projektu jest złożona, znalezienie odpowiednich ścieżek modułów może być trudne. Jedną z najbardziej podstawowych zalet konfiguracji Webpack jest to, że pomaga ona uprościć definiowanie ścieżek względnych w projekcie.
Załóżmy, że mamy następującą strukturę projektu:
- Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js
Możemy odwoływać się do zależności za pomocą ścieżek względnych do potrzebnych nam plików, a jeśli chcemy zaimportować komponenty do kontenerów w naszym kodzie źródłowym, wygląda to tak:
Home.js
Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';
Modal.js
import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';
Za każdym razem, gdy chcemy zaimportować skrypt lub moduł, musimy znać lokalizację bieżącego katalogu i znaleźć względną ścieżkę do tego, co chcemy zaimportować. Możemy sobie wyobrazić, jak ten problem może nasilać się w złożoności, jeśli mamy duży projekt z zagnieżdżoną strukturą plików lub chcemy zrefaktoryzować niektóre części złożonej struktury projektu.
Możemy łatwo poradzić sobie z tym problemem za pomocą opcji solve.alias pakietu resolve.alias
. Możemy zadeklarować tzw. aliasy – nazwę katalogu lub modułu wraz z jego lokalizacją i nie opieramy się na ścieżkach względnych w kodzie źródłowym projektu.
webpack.config.js
resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }
W pliku Modal.js
możemy teraz znacznie prościej zaimportować datepicker:
import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';
Dzielenie kodu
Możemy mieć scenariusze, w których musimy dołączyć skrypt do końcowego pakietu, podzielić ostateczny pakiet lub załadować oddzielne pakiety na żądanie. Konfiguracja naszego projektu i konfiguracji Webpack dla tych scenariuszy może nie być prosta.
W konfiguracji Webpack opcja Entry
informuje Webpack, gdzie znajduje się punkt początkowy dla końcowego pakietu. Punkt wejścia może mieć trzy różne typy danych: String, Array lub Object.
Jeśli mamy jeden punkt wyjścia, możemy użyć dowolnego z tych formatów i uzyskać ten sam wynik.
Jeśli chcemy dołączyć wiele plików, które nie są od siebie zależne, możemy użyć formatu Array. Na przykład możemy dołączyć analytics.js
na końcu bundle.js
:
webpack.config.js
module.exports = { // creates a bundle out of index.js and then append analytics.js entry: ['./src/script/index.jsx', './src/script/analytics.js'], output: { path: './build', filename: bundle.js ' } };
Zarządzanie wieloma punktami wejścia
Załóżmy, że mamy wielostronicową aplikację z wieloma plikami HTML, takimi jak index.html
i admin.html
. Możemy wygenerować wiele pakietów, używając punktu wejścia jako typu Object. Poniższa konfiguracja generuje dwa pakiety JavaScript:
webpack.config.js
module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) } };
index.html
<script src=”build/index.js”></script>
admin.html
<script src=”build/admin.js”></script>
Oba pakiety JavaScript mogą dzielić wspólne biblioteki i komponenty. W tym celu możemy użyć CommonsChunkPlugin
, który wyszukuje moduły występujące w partiach z wieloma wpisami i tworzy wspólny pakiet, który można buforować na wielu stronach.
webpack.config.js
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js'); module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) }, plugins: [commonsPlugin] };
Teraz nie możemy zapomnieć o dodaniu <script src="build/common.js"></script>
przed dołączonymi skryptami.
Włączanie leniwego ładowania
Webpack może dzielić statyczne zasoby na mniejsze porcje, a to podejście jest bardziej elastyczne niż standardowe łączenie. Jeśli mamy dużą aplikację jednostronicową (SPA), proste połączenie w jeden pakiet nie jest dobrym podejściem, ponieważ ładowanie jednego dużego pakietu może być powolne, a użytkownicy zwykle nie potrzebują wszystkich zależności w każdym widoku.
Wyjaśniliśmy wcześniej, jak podzielić aplikację na wiele pakietów, połączyć wspólne zależności i czerpać korzyści z zachowania pamięci podręcznej przeglądarki. To podejście działa bardzo dobrze w przypadku aplikacji wielostronicowych, ale nie w przypadku aplikacji jednostronicowych.
W przypadku SPA powinniśmy zapewnić tylko te statyczne zasoby, które są wymagane do renderowania bieżącego widoku. Router po stronie klienta w architekturze SPA to doskonałe miejsce do obsługi dzielenia kodu. Gdy użytkownik wprowadzi trasę, możemy wczytać tylko te zależności potrzebne do wynikowego widoku. Alternatywnie możemy załadować zależności, gdy użytkownik przewija stronę w dół.
W tym celu możemy skorzystać z funkcji require.ensure
lub System.import
, które Webpack może wykryć statycznie. Webpack może wygenerować oddzielny pakiet na podstawie tego punktu podziału i wywołać go na żądanie.
W tym przykładzie mamy dwa kontenery React; widok administratora i widok pulpitu nawigacyjnego.
admin.jsx
import React, {Component} from 'react'; export default class Admin extends Component { render() { return <div > Admin < /div>; } }
dashboard.jsx
import React, {Component} from 'react'; export default class Dashboard extends Component { render() { return <div > Dashboard < /div>; } }
Jeśli użytkownik wprowadzi adres URL /dashboard
lub /admin
, ładowany jest tylko odpowiedni wymagany pakiet JavaScript. Poniżej możemy zobaczyć przykłady z routerem po stronie klienta i bez niego.
index.jsx
if (window.location.pathname === '/dashboard') { require.ensure([], function() { require('./containers/dashboard').default; }); } else if (window.location.pathname === '/admin') { require.ensure([], function() { require('./containers/admin').default; }); }
index.jsx
ReactDOM.render( <Router> <Route path="/" component={props => <div>{props.children}</div>}> <IndexRoute component={Home} /> <Route path="dashboard" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/dashboard').default) }, "dashboard")}} /> <Route path="admin" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/admin').default) }, "admin")}} /> </Route> </Router> , document.getElementById('content') );
Wyodrębnianie stylów do oddzielnych pakietów
W pakiecie Webpack programy ładujące, takie jak style-loader
i css-loader
, wstępnie przetwarzają arkusze stylów i osadzają je w wyjściowym pakiecie JavaScript, ale w niektórych przypadkach mogą powodować flashowanie treści bez stylu (FOUC).

Możemy uniknąć FOUC dzięki ExtractTextWebpackPlugin
, który umożliwia generowanie wszystkich stylów do oddzielnych pakietów CSS zamiast osadzania ich w końcowym pakiecie JavaScript.
webpack.config.js
var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { loaders: [{ test: /\.css/, loader: ExtractTextPlugin.extract('style', 'css')' }], }, plugins: [ // output extracted CSS to a file new ExtractTextPlugin('[name].[chunkhash].css') ] }
Obsługa bibliotek i wtyczek innych firm
Wielokrotnie musimy korzystać z zewnętrznych bibliotek, różnych wtyczek lub dodatkowych skryptów, ponieważ nie chcemy tracić czasu na tworzenie tych samych komponentów od podstaw. Dostępnych jest wiele starszych bibliotek i wtyczek, które nie są aktywnie utrzymywane, nie rozumieją modułów JavaScript i zakładają globalną obecność zależności pod predefiniowanymi nazwami.
Poniżej kilka przykładów z wtyczkami jQuery, z wyjaśnieniem, jak poprawnie skonfigurować Webpack, aby móc wygenerować końcowy pakiet.
Podaj wtyczkę
Większość wtyczek innych firm opiera się na obecności określonych globalnych zależności. W przypadku jQuery, wtyczki polegają na zdefiniowanej zmiennej $
lub jQuery
, a my możemy użyć wtyczek jQuery, wywołując w naszym kodzie $('div.content').pluginFunc()
.
Możemy użyć wtyczki ProvidePlugin
, aby poprzedzić var $ = require("jquery")
za każdym razem, gdy napotka globalny identyfikator $
.
webpack.config.js
webpack.ProvidePlugin({ '$': 'jquery', })
Gdy pakiet Webpack przetwarza kod, szuka obecności $
i zapewnia odwołanie do globalnych zależności bez importowania modułu określonego przez funkcję require
.
Importer-ładowarka
Niektóre wtyczki jQuery zakładają $
w globalnej przestrzeni nazw lub polegają na this
, że jest to obiekt window
. W tym celu możemy wykorzystać imports-loader
, który wstrzykuje zmienne globalne do modułów.
example.js
$('div.content').pluginFunc();
Następnie możemy wstrzyknąć zmienną $
do modułu, konfigurując imports-loader
:
require("imports?$=jquery!./example.js");
To po prostu poprzedza var $ = require("jquery");
do example.js
.
W drugim przypadku użycia:
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }
Używając symbolu =>
(nie mylić z funkcjami ES6 Arrow), możemy ustawić dowolne zmienne. Ostatnia wartość redefiniuje zmienną globalną this
, aby wskazywała na obiekt window
. To to samo, co owinięcie całej zawartości pliku (function () { ... }).call(window);
i wywołanie this
funkcji z window
jako argumentem.
Możemy również wymagać bibliotek korzystających z formatu modułu CommonJS lub AMD:
// CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });
Niektóre biblioteki i moduły mogą obsługiwać różne formaty modułów.
W następnym przykładzie mamy wtyczkę jQuery, która wykorzystuje format modułów AMD i CommonJS i ma zależność jQuery:
jquery-plugin.js
(function(factory) { if (typeof define === 'function' && define.amd) { // AMD format is used define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS format is used module.exports = factory(require('jquery')); } else { // Neither AMD nor CommonJS used. Use global variables. } });
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: "imports?define=>false,exports=>false" }] }
Możemy wybrać, jakiego formatu modułu chcemy użyć dla konkretnej biblioteki. Jeśli zadeklarujemy define
to equal false
, Webpack nie przeanalizuje modułu w formacie modułu AMD, a jeśli zadeklarujemy exports
zmiennych na equal false
, Webpack nie będzie parsował modułu w formacie modułu CommonJS.
Odsłoń-loader
Jeśli potrzebujemy wystawić moduł do kontekstu globalnego, możemy użyć expose-loader
. Może to być pomocne, na przykład, jeśli mamy zewnętrzne skrypty, które nie są częścią konfiguracji Webpack i opierają się na symbolu w globalnej przestrzeni nazw lub używamy wtyczek przeglądarki, które potrzebują dostępu do symbolu w konsoli przeglądarki.
webpack.config.js
module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }
Biblioteka jQuery jest teraz dostępna w globalnej przestrzeni nazw dla innych skryptów na stronie internetowej.
window.$ window.jQuery
Konfigurowanie zależności zewnętrznych
Jeśli chcemy dołączyć moduły ze skryptów hostowanych zewnętrznie, musimy je zdefiniować w konfiguracji. W przeciwnym razie Webpack nie może wygenerować końcowego pakietu.
Skrypty zewnętrzne możemy skonfigurować za pomocą opcji externals
w konfiguracji Webpacka. Na przykład możemy użyć biblioteki z CDN za pośrednictwem oddzielnego tagu <script>
, jednocześnie wyraźnie deklarując ją jako zależność modułu w naszym projekcie.
webpack.config.js
externals: { react: 'React', 'react-dom': 'ReactDOM' }
Obsługa wielu instancji biblioteki
Wspaniale jest używać menedżera pakietów NPM w programowaniu frontonu do zarządzania bibliotekami i zależnościami innych firm. Czasami jednak możemy mieć wiele instancji tej samej biblioteki w różnych wersjach i nie współpracują one dobrze w jednym środowisku.
Może się tak zdarzyć na przykład z biblioteką React, w której możemy zainstalować React z NPM, a później inna wersja Reacta może stać się dostępna z dodatkowym pakietem lub wtyczką. Nasza struktura projektu może wyglądać następująco:
project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react
Komponenty pochodzące z react-plugin
React mają inną instancję Reacta niż pozostałe komponenty w projekcie. Teraz mamy dwie oddzielne kopie Reacta i mogą to być różne wersje. W naszej aplikacji ten scenariusz może zepsuć nasz globalny mutowalny DOM i możemy zobaczyć komunikaty o błędach w dzienniku konsoli internetowej. Rozwiązaniem tego problemu jest posiadanie tej samej wersji Reacta w całym projekcie. Możemy to rozwiązać za pomocą aliasów Webpack.
webpack.config.js
module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }
Gdy react-plugin
React próbuje wymagać Reacta, używa wersji w node_modules
projektu. Jeśli chcemy dowiedzieć się, której wersji Reacta używamy, możemy dodać console.log(React.version)
w kodzie źródłowym.
Skoncentruj się na rozwoju, a nie na konfiguracji pakietu internetowego
Ten post tylko zarysowuje powierzchnię mocy i użyteczności Webpacka.
Istnieje wiele innych programów ładujących i wtyczek Webpack, które pomogą Ci zoptymalizować i usprawnić tworzenie pakietów JavaScript.
Nawet jeśli jesteś początkującym, ten przewodnik daje solidne podstawy do rozpoczęcia korzystania z Webpack, co pozwoli Ci skupić się bardziej na rozwoju, a nie na konfiguracji bundlingu.