Przewodnik po zarządzaniu zależnościami pakietów internetowych

Opublikowany: 2022-03-11

Koncepcja 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.

Pakiet internetowy: samouczek dla początkujących

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.

Powiązane: Utrzymuj kontrolę: przewodnik po pakietach internetowych i reakcjach, Pt. 1