При сборке современных JavaScript-приложений часто возникает
необходимость подключать библиотеки, изначально не предназначенные для
модульной системы ES Modules или CommonJS. Особенно это характерно для
старых браузерных библиотек, глобальных плагинов и legacy-кода,
завязанного на глобальные переменные window,
global, self или this.
Webpack предоставляет механизм shim-модулей, позволяющий:
Shim в контексте Webpack — это прослойка совместимости между немодульным кодом и системой модулей Webpack.
Многие старые библиотеки создавались до появления модульных стандартов. Они предполагают наличие зависимостей в глобальной области видимости.
Пример типичной legacy-библиотеки:
// plugin.js
(function () {
window.myPlugin = function () {
console.log(jQuery.fn.jquery);
};
})();
Проблемы такого подхода:
jQuery;import;export;В результате при сборке возникает ошибка:
ReferenceError: jQuery is not defined
Для решения подобных задач используются:
ProvidePluginimports-loaderexports-loaderexpose-loaderProvidePlugin автоматически подставляет импорт модуля
при обнаружении определённого идентификатора в коде.
Это позволяет:
import вручную;Когда Webpack встречает указанный идентификатор, он автоматически внедряет импорт.
Пример:
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
Теперь при использовании:
$('.menu').hide();
Webpack автоматически преобразует код примерно в:
var $ = require('jquery');
$('.menu').hide();
npm install jquery
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
]
};
$('.app').fadeIn();
Импорт больше не требуется.
new webpack.ProvidePlugin({
_: 'lodash',
axios: 'axios',
Vue: ['vue/dist/vue.esm.js', 'default']
})
Для ES-модулей иногда требуется указывать:
['module', 'default']
Пример:
Vue: ['vue/dist/vue.esm.js', 'default']
Webpack импортирует:
import Vue from 'vue/dist/vue.esm.js'
До React 17 JSX требовал наличия React в области
видимости.
Без импорта:
export default function App() {
return <div>Hello</div>;
}
Возникала ошибка:
React is not defined
Решение:
new webpack.ProvidePlugin({
React: 'react'
})
Webpack автоматически внедрял импорт React.
new webpack.ProvidePlugin({
Vue: ['vue/dist/vue.esm.js', 'default']
})
После этого:
new Vue({
el: '#app'
});
может работать без import Vue.
_new webpack.ProvidePlugin({
_: 'lodash'
})
Использование:
const result = _.uniq([1, 1, 2, 3]);
В браузере объект process отсутствует.
Некоторые библиотеки Node.js ожидают его наличие:
if (process.env.NODE_ENV === 'production') {
// ...
}
Webpack может автоматически подставить polyfill:
new webpack.ProvidePlugin({
process: 'process/browser'
})
Некоторые пакеты используют Buffer.
Для браузера требуется polyfill:
npm install buffer
Настройка:
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer']
})
Использование:
const data = Buffer.from('hello');
import $ from 'jquery';
Преимущества:
Преимущества:
Недостатки:
Старые jQuery-проекты:
$('.button').click(...)
new Vue(...)
process.env
Buffer
global
Когда переписывание тысяч импортов невозможно.
В современных приложениях предпочтительнее явный импорт:
import axios from 'axios';
Скрытые импорты осложняют:
Для библиотек особенно важны явные зависимости.
Shim-модуль — это промежуточный модуль, адаптирующий несовместимый код.
Пример:
// jquery-shim.js
import $ from 'jquery';
window.$ = $;
window.jQuery = $;
export default $;
import './jquery-shim';
import './legacy-plugin';
Теперь старый плагин получает глобальный jQuery.
(function () {
function LegacyLib() {}
window.LegacyLib = LegacyLib;
})();
// legacy-shim.js
import './legacy-lib';
export default window.LegacyLib;
import LegacyLib from './legacy-shim';
const lib = new LegacyLib();
Позволяет внедрять зависимости внутрь legacy-модулей.
npm install imports-loader --save-dev
module.exports = {
module: {
rules: [
{
test: require.resolve('./legacy.js'),
loader: 'imports-loader',
options: {
imports: [
'default jquery $'
]
}
}
]
}
};
Webpack преобразует:
(function () {
console.log($);
})();
в:
import $ from 'jquery';
(function () {
console.log($);
})();
Позволяет экспортировать значения из legacy-скриптов.
var MyLibrary = {
version: '1.0'
};
{
test: require.resolve('./legacy.js'),
loader: 'exports-loader',
options: {
exports: 'default MyLibrary'
}
}
import MyLibrary from './legacy';
console.log(MyLibrary.version);
Делает модуль глобальной переменной.
npm install expose-loader --save-dev
{
test: require.resolve('jquery'),
loader: 'expose-loader',
options: {
exposes: ['$', 'jQuery']
}
}
После сборки:
window.$
window.jQuery
становятся доступны глобально.
ProvidePlugin может ухудшать tree shaking.
Причина:
new webpack.ProvidePlugin({
_: 'lodash'
})
При неаккуратной конфигурации может подключиться весь lodash.
import debounce from 'lodash/debounce';
Некоторые модули выполняют код при импорте:
console.log('module loaded');
Автоматическая подстановка может вызывать:
TypeScript не знает о скрытых глобальных переменных.
Пример:
$('.app')
Ошибка:
Cannot find name '$'
npm install @types/jquery --save-dev
declare const $: JQueryStatic;
ESLint также не знает о скрытых переменных.
Ошибка:
'$' is not defined
module.exports = {
globals: {
$: 'readonly',
jQuery: 'readonly'
}
};
Webpack 4 автоматически подключал polyfill для Node.js API.
Webpack 5 прекратил это поведение.
Теперь необходимо явно подключать:
bufferprocessstreamcryptopathresolve: {
fallback: {
buffer: require.resolve('buffer/'),
process: require.resolve('process/browser')
}
}
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser'
})
]
Иногда используется:
window.$ = require('jquery');
Недостатки:
import $ from 'jquery';
if (!window.$) {
window.$ = $;
}
Код становится менее очевидным:
$('.menu')
Невозможно понять источник $.
При большом количестве shim-модулей:
Тестовая среда может не содержать:
window.$
process
Buffer
Требуется отдельная настройка.
Подключение:
Устранение ошибок:
is not defined
Переход от:
$('.app')
к:
import $ from 'jquery';
После полной миграции:
npm install jquery process buffer
npm install expose-loader imports-loader exports-loader --save-dev
const webpack = require('webpack');
module.exports = {
resolve: {
fallback: {
buffer: require.resolve('buffer/'),
process: require.resolve('process/browser')
}
},
module: {
rules: [
{
test: require.resolve('jquery'),
loader: 'expose-loader',
options: {
exposes: ['$', 'jQuery']
}
}
]
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Buffer: ['buffer', 'Buffer'],
process: 'process/browser'
})
]
};
Для современных приложений:
import;Shim-модули оправданы:
window.app = {}
window.utils = {}
window.API = {}
new webpack.ProvidePlugin({
_: 'lodash',
axios: 'axios',
React: 'react',
Vue: 'vue',
moment: 'moment'
})
Такая архитектура затрудняет: