Руководство по управлению зависимостями Webpack
Опубликовано: 2022-03-11Концепция модульности является неотъемлемой частью большинства современных языков программирования. Однако в JavaScript отсутствовал какой-либо формальный подход к модуляризации до появления последней версии ECMAScript ES6.
В Node.js, одной из самых популярных на сегодняшний день сред JavaScript, сборщики модулей позволяют загружать модули NPM в веб-браузеры, а компонентно-ориентированные библиотеки (такие как React) поощряют и облегчают модульность кода JavaScript.
Webpack — один из доступных сборщиков модулей, который обрабатывает код JavaScript, а также все статические ресурсы, такие как таблицы стилей, изображения и шрифты, в пакетный файл. Обработка может включать в себя все необходимые задачи для управления и оптимизации зависимостей кода, такие как компиляция, объединение, минимизация и сжатие.
Однако настройка Webpack и его зависимостей может быть сложной и не всегда простой процесс, особенно для новичков.
В этом сообщении блога представлены рекомендации с примерами того, как настроить Webpack для различных сценариев, и указаны наиболее распространенные ловушки, связанные с объединением зависимостей проекта с помощью Webpack.
В первой части этой записи блога объясняется, как упростить определение зависимостей в проекте. Далее мы обсудим и продемонстрируем конфигурацию для разделения кода многостраничных и одностраничных приложений. Наконец, мы обсудим, как настроить Webpack, если мы хотим включить сторонние библиотеки в наш проект.
Настройка псевдонимов и относительных путей
Относительные пути не связаны напрямую с зависимостями, но мы используем их при определении зависимостей. Если файловая структура проекта сложна, может быть сложно разрешить соответствующие пути к модулям. Одним из наиболее фундаментальных преимуществ конфигурации Webpack является то, что она помогает упростить определение относительных путей в проекте.
Допустим, у нас есть следующая структура проекта:
- Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js
Мы можем ссылаться на зависимости относительными путями к нужным нам файлам, и если мы хотим импортировать компоненты в контейнеры в нашем исходном коде, это выглядит следующим образом:
Home.js
Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';
Modal.js
import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';
Каждый раз, когда мы хотим импортировать скрипт или модуль, нам нужно знать расположение текущего каталога и найти относительный путь к тому, что мы хотим импортировать. Мы можем представить, как эта проблема может усложниться, если у нас есть большой проект с вложенной файловой структурой или мы хотим провести рефакторинг некоторых частей сложной структуры проекта.
Мы можем легко справиться с этой проблемой с помощью параметра resolve.alias
Webpack. Мы можем объявить так называемые псевдонимы — имя каталога или модуля с его расположением, и мы не полагаемся на относительные пути в исходном коде проекта.
webpack.config.js
resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }
Теперь в файле Modal.js
мы можем гораздо проще импортировать средство выбора даты:
import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';
Разделение кода
У нас могут быть сценарии, в которых нам нужно добавить сценарий в окончательный пакет, или разделить последний пакет, или мы хотим загрузить отдельные пакеты по требованию. Настройка нашего проекта и конфигурации Webpack для этих сценариев может оказаться непростой задачей.
В конфигурации Webpack параметр Entry
сообщает Webpack, где находится начальная точка для конечного пакета. Точка входа может иметь три различных типа данных: строка, массив или объект.
Если у нас есть одна отправная точка, мы можем использовать любой из этих форматов и получить тот же результат.
Если мы хотим добавить несколько файлов, и они не зависят друг от друга, мы можем использовать формат массива. Например, мы можем добавить analytics.js
в конец 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 ' } };
Управление несколькими точками входа
Допустим, у нас есть многостраничное приложение с несколькими файлами HTML, такими как index.html
и admin.html
. Мы можем сгенерировать несколько пакетов, используя точку входа в качестве типа объекта. Приведенная ниже конфигурация создает два пакета 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>
Оба пакета JavaScript могут использовать общие библиотеки и компоненты. Для этого мы можем использовать CommonsChunkPlugin
, который находит модули, встречающиеся в чанках с несколькими входами, и создает общий пакет, который можно кэшировать на нескольких страницах.
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] };
Теперь мы не должны забывать добавлять <script src="build/common.js"></script>
перед пакетными скриптами.
Включение ленивой загрузки
Webpack может разбивать статические ресурсы на более мелкие фрагменты, и этот подход более гибкий, чем стандартная конкатенация. Если у нас есть большое одностраничное приложение (SPA), простое объединение в один пакет — не лучший подход, потому что загрузка одного огромного пакета может быть медленной, а пользователям обычно не нужны все зависимости для каждого представления.
Ранее мы объяснили, как разделить приложение на несколько пакетов, объединить общие зависимости и извлечь выгоду из поведения кэширования браузера. Этот подход очень хорошо работает для многостраничных приложений, но не для одностраничных.
Для SPA мы должны предоставлять только те статические ресурсы, которые необходимы для отображения текущего представления. Маршрутизатор на стороне клиента в архитектуре SPA — идеальное место для разделения кода. Когда пользователь вводит маршрут, мы можем загрузить только те зависимости, которые необходимы для результирующего представления. В качестве альтернативы мы можем загружать зависимости, когда пользователь прокручивает страницу вниз.
Для этого мы можем использовать функции require.ensure
или System.import
, которые Webpack может определять статически. Webpack может генерировать отдельный пакет на основе этой точки разделения и вызывать его по запросу.
В этом примере у нас есть два контейнера React; представление администратора и представление панели инструментов.
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>; } }
Если пользователь вводит URL-адрес /dashboard
или /admin
, загружается только соответствующий требуемый пакет JavaScript. Ниже мы можем увидеть примеры с маршрутизатором на стороне клиента и без него.
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') );
Извлечение стилей в отдельные пакеты
В Webpack загрузчики, такие как style-loader
и css-loader
, предварительно обрабатывают таблицы стилей и встраивают их в выходной пакет JavaScript, но в некоторых случаях они могут вызвать Flash нестилизованного содержимого (FOUC).

Мы можем избежать FOUC с помощью ExtractTextWebpackPlugin
, который позволяет генерировать все стили в отдельные пакеты CSS вместо того, чтобы встраивать их в окончательный пакет 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') ] }
Работа со сторонними библиотеками и плагинами
Много раз нам приходится использовать сторонние библиотеки, различные плагины или дополнительные скрипты, потому что мы не хотим тратить время на разработку одних и тех же компонентов с нуля. Существует множество устаревших библиотек и подключаемых модулей, которые не поддерживаются активно, не понимают модули JavaScript и предполагают наличие глобальных зависимостей под предопределенными именами.
Ниже приведены несколько примеров с подключаемыми модулями jQuery с объяснением того, как правильно настроить Webpack, чтобы иметь возможность генерировать окончательный пакет.
Предоставитьплагин
Большинство сторонних плагинов полагаются на наличие определенных глобальных зависимостей. В случае jQuery подключаемые модули полагаются на определяемую переменную $
или jQuery
, и мы можем использовать подключаемые модули jQuery, вызывая $('div.content').pluginFunc()
в нашем коде.
Мы можем использовать плагин Webpack ProvidePlugin
для добавления var $ = require("jquery")
каждый раз, когда встречается глобальный идентификатор $
.
webpack.config.js
webpack.ProvidePlugin({ '$': 'jquery', })
Когда Webpack обрабатывает код, он ищет наличие $
и предоставляет ссылку на глобальные зависимости без импорта модуля, указанного функцией require
.
Импорт-загрузчик
Некоторые плагины jQuery предполагают $
в глобальном пространстве имен или полагаются на this
, что это объект window
. Для этого мы можем использовать imports-loader
, который вставляет глобальные переменные в модули.
example.js
$('div.content').pluginFunc();
Затем мы можем внедрить переменную $
в модуль, настроив imports-loader
:
require("imports?$=jquery!./example.js");
Это просто добавляет var $ = require("jquery");
в example.js
.
Во втором варианте использования:
webpack.config.js
module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }
Используя символ =>
(не путать со стрелочными функциями ES6), мы можем устанавливать произвольные переменные. Последнее значение переопределяет глобальную переменную this
, чтобы она указывала на объект window
. Это то же самое, что обернуть все содержимое файла с помощью (function () { ... }).call(window);
и вызов this
функции с window
в качестве аргумента.
Мы также можем потребовать библиотеки, использующие формат модуля CommonJS или AMD:
// CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });
Некоторые библиотеки и модули могут поддерживать разные форматы модулей.
В следующем примере у нас есть подключаемый модуль jQuery, который использует формат модуля AMD и CommonJS и имеет зависимость от 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" }] }
Мы можем выбрать, какой формат модуля мы хотим использовать для конкретной библиотеки. Если мы объявим define
равным false
, Webpack не будет анализировать модуль в формате модуля AMD, а если мы объявим переменные exports
равными false
, Webpack не будет анализировать модуль в формате модуля CommonJS.
Expose-загрузчик
Если нам нужно предоставить модуль глобальному контексту, мы можем использовать expose-loader
. Это может быть полезно, например, если у нас есть внешние скрипты, которые не являются частью конфигурации Webpack и полагаются на символ в глобальном пространстве имен, или мы используем подключаемые модули браузера, которым требуется доступ к символу в консоли браузера.
webpack.config.js
module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }
Библиотека jQuery теперь доступна в глобальном пространстве имен для других скриптов на веб-странице.
window.$ window.jQuery
Настройка внешних зависимостей
Если мы хотим включить модули из внешних скриптов, нам нужно определить их в конфигурации. В противном случае Webpack не сможет сгенерировать окончательный пакет.
Мы можем настроить внешние сценарии, используя опцию externals
в конфигурации Webpack. Например, мы можем использовать библиотеку из CDN через отдельный <script>
, при этом явно объявляя ее как зависимость модуля в нашем проекте.
webpack.config.js
externals: { react: 'React', 'react-dom': 'ReactDOM' }
Поддержка нескольких экземпляров библиотеки
Замечательно использовать диспетчер пакетов NPM во фронтенд-разработке для управления сторонними библиотеками и зависимостями. Однако иногда у нас может быть несколько экземпляров одной и той же библиотеки с разными версиями, и они плохо работают вместе в одной среде.
Это может произойти, например, с библиотекой React, где мы можем установить React из NPM, а позже может стать доступна другая версия React с дополнительным пакетом или плагином. Структура нашего проекта может выглядеть следующим образом:
project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react
Компоненты, поступающие из react-plugin
, имеют другой экземпляр React, чем остальные компоненты в проекте. Теперь у нас есть две отдельные копии React, и они могут быть разных версий. В нашем приложении этот сценарий может испортить нашу глобальную изменяемую модель DOM, и мы можем увидеть сообщения об ошибках в журнале веб-консоли. Решение этой проблемы — использовать одну и ту же версию React для всего проекта. Мы можем решить это с помощью псевдонимов Webpack.
webpack.config.js
module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }
Когда react-plugin
пытается потребовать React, он использует версию в node_modules
проекта. Если мы хотим узнать, какую версию React мы используем, мы можем добавить console.log(React.version)
в исходный код.
Сосредоточьтесь на разработке, а не на настройке Webpack
Этот пост лишь поверхностно описывает мощь и полезность Webpack.
Существует множество других загрузчиков и плагинов Webpack, которые помогут вам оптимизировать и упростить связывание JavaScript.
Даже если вы новичок, это руководство дает вам прочную основу для начала использования Webpack, что позволит вам больше сосредоточиться на разработке, а не на конфигурации пакета.