Spec-Zone.ru › webpack 4

Подмена

Компилятор webpack может понимать модули, написанные в формате ES2015, CommonJS или AMD. Однако некоторые сторонние библиотеки могут ожидать глобальных зависимостей (например, $ для jQuery). Библиотеки также могут создавать глобальные переменные, которые необходимо экспортировать. Эти «неправильные модули» — один из случаев, когда требуется подмена.

Не рекомендуется использовать глобальные переменные! Вся концепция webpack заключается в том, чтобы позволить более модульное развитие фронтенда. Это означает написание изолированных модулей, которые являются самодостаточными и не полагаются на скрытые зависимости (например, глобальные переменные). Пожалуйста, используйте эти возможности только в необходимых случаях.

Другой случай, когда подмена может быть полезной, — это когда вы хотите полифил функциональность браузера для поддержки большего количества пользователей. В этом случае вы можете загружать полифилы только в те браузеры, которым необходимы исправления (т. е. загружать их по требованию).

В следующей статье будут рассмотрены оба этих случая.

Для простоты этот гайд основан на примерах в Начальном руководстве. Убедитесь, что вы знакомы с настройкой, описанной там, прежде чем продолжать.

Подмена глобальных переменных

Давайте начнём с первого случая — подмены глобальных переменных. Прежде чем делать что-либо, давайте ещё раз посмотрим на наш проект:

проект

webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
|- /node_modules

Помните пакет lodash, который мы использовали? Для демонстрации предположим, что мы хотим предоставить его в качестве глобальной переменной для всего приложения. Для этого мы можем использовать ProvidePlugin.

Плагин ProvidePlugin делает пакет доступным как переменную в каждом модуле, скомпилированном с помощью webpack. Если webpack обнаруживает использование этой переменной, он включит указанный пакет в окончательный бандл. Давайте удалим оператор import для lodash и вместо этого предоставим его через плагин:

src/index.js

- import _ from 'lodash';
-
  function component() {
    const element = document.createElement('div');

-   // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

webpack.config.js

  const path = require('path');
+ const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
+   plugins: [
+     new webpack.ProvidePlugin({
+       _: 'lodash',
+     }),
+   ],
  };

По сути, мы сказали webpack...

Если вы встретите по крайней мере одну инстанцию переменной _, включите пакет lodash и предоставьте его модулям, которым он нужен.

Если мы запустим сборку, мы должны увидеть тот же результат:

...
    Asset    Size  Chunks                    Chunk Names
bundle.js  544 kB       0  [emitted]  [big]  main
...

Мы также можем использовать ProvidePlugin для экспорта единственного экспорта модуля, настроив его с «путем массива» (например, [module, child, ...children?]). Предположим, что мы хотим предоставить только метод join из lodash там, где он вызывается:

src/index.js

  function component() {
    const element = document.createElement('div');

-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
      new webpack.ProvidePlugin({
-       _: 'lodash',
+       join: ['lodash', 'join'],
      }),
    ],
  };

Это хорошо сочетается с устранением лишнего кода, так как остальная часть библиотеки lodash должна быть удалена.

Уточнённая подмена

Некоторые устаревшие модули полагаются на this быть объектом window. Давайте обновим наш index.js так, чтобы это было так:

  function component() {
    const element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');
+
+   // Assume we are in the context of `window`
+   this.alert('Hmmm, this probably isn\'t a great idea...')

    return element;
  }

  document.body.appendChild(component());

Это становится проблемой, когда модуль выполняется в контексте CommonJS, где this равно module.exports. В этом случае вы можете переопределить this с помощью imports-loader:

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
+   module: {
+     rules: [
+       {
+         test: require.resolve('index.js'),
+         use: 'imports-loader?this=>window',
+       },
+     ],
+   },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join'],
      }),
    ],
  };

Глобальные экспорты

Предположим, библиотека создаёт глобальную переменную, использование которой ожидается от её потребителей. Мы можем добавить небольшой модуль в наше окружение, чтобы продемонстрировать это:

проект

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
  |- /src
    |- index.js
+   |- globals.js
  |- /node_modules

src/globals.js

const file = 'blah.txt';
const helpers = {
  test: function() { console.log('test something'); },
  parse: function() { console.log('parse something'); },
};

Теперь, хотя вы вряд ли сделаете это в своём собственном исходном коде, вы можете столкнуться с устаревшей библиотекой, содержащей код, похожий на показанный выше. В этом случае мы можем использовать exports-loader, чтобы экспортировать эту глобальную переменную как обычный экспорт модуля. Например, чтобы экспортировать file как file и helpers.parse как parse:

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    module: {
      rules: [
        {
          test: require.resolve('index.js'),
          use: 'imports-loader?this=>window',
        },
+       {
+         test: require.resolve('globals.js'),
+         use: 'exports-loader?file,parse=helpers.parse',
+       },
      ],
    },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join'],
      }),
    ],
  };

Теперь в нашем скрипте входа (т. е. src/index.js) мы можем import { file, parse } from './globals.js'; и всё должно работать гладко.

Загрузка полифилов

Почти всё, о чём мы говорили до сих пор, касалось работы с устаревшими пакетами. Перейдём ко второй теме: полифилы.

Существует много способов загрузить полифилы. Например, чтобы включить babel-polyfill, мы можем просто:

npm install --save babel-polyfill

и import его, чтобы включить его в наш основной бандл:

src/index.js

+ import 'babel-polyfill';
+
  function component() {
    const element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

Обратите внимание, что мы не привязываем import к переменной. Это потому, что полифилы просто выполняются самостоятельно, до остального кода, позволяя нам затем считать, что определённая нативная функциональность существует.

Обратите внимание, что этот подход отдаёт приоритет корректности перед размером бандла. Для надёжности полифилы/подмены должны выполняться **до всего остального кода**, и, следовательно, либо должны загружаться синхронно, либо весь код приложения должен загружаться после загрузки всех полифилов/подмен. В сообществе существует много заблуждений, что современные браузеры «не нуждаются» в полифилах или что полифилы/подмены просто добавляют недостающие функции — на самом деле, они часто исправляют некорректные реализации даже в самых современных браузерах. Таким образом, наилучшей практикой остаётся безусловное и синхронное загрузка всех полифилов/подмен, несмотря на затраты на размер бандла, которые это влечёт.

Если вы считаете, что устранили эти проблемы и хотите рискнуть ошибками, вот один из способов сделать это: переместим наш import в новый файл и добавим полифил whatwg-fetch:

npm install --save whatwg-fetch

src/index.js

- import 'babel-polyfill';
-
  function component() {
    const element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());

проект

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
  |- /src
    |- index.js
    |- globals.js
+   |- polyfills.js
  |- /node_modules

src/polyfills.js

import 'babel-polyfill';
import 'whatwg-fetch';

webpack.config.js

  const path = require('path');
  const webpack = require('webpack');

  module.exports = {
-   entry: './src/index.js',
+   entry: {
+     polyfills: './src/polyfills.js',
+     index: './src/index.js',
+   },
    output: {
-     filename: 'bundle.js',
+     filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    module: {
      rules: [
        {
          test: require.resolve('index.js'),
          use: 'imports-loader?this=>window',
        },
        {
          test: require.resolve('globals.js'),
          use: 'exports-loader?file,parse=helpers.parse',
        },
      ],
    },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join'],
      }),
    ],
  };

С этим мы можем добавить логику условной загрузки нашего нового файла polyfills.bundle.js. Как вы принимаете это решение, зависит от используемых технологий и браузеров. Мы просто проведём простые тесты, чтобы определить, нужны ли наши полифилы:

dist/index.html

  <!doctype html>
  <html>
    <head>
      <title>Getting Started</title>
+     <script>
+       const modernBrowser = (
+         'fetch' in window &&
+         'assign' in Object
+       );
+
+       if ( !modernBrowser ) {
+         const scriptElement = document.createElement('script');
+
+         scriptElement.async = false;
+         scriptElement.src = '/polyfills.bundle.js';
+         document.head.appendChild(scriptElement);
+       }
+     </script>
    </head>
    <body>
      <script src="index.bundle.js"></script>
    </body>
  </html>

Теперь мы можем fetch данные в нашем скрипте входа:

src/index.js

  function component() {
    const element = document.createElement('div');

    element.innerHTML = join(['Hello', 'webpack'], ' ');

    return element;
  }

  document.body.appendChild(component());
+
+ fetch('https://jsonplaceholder.typicode.com/users')
+   .then(response => response.json())
+   .then(json => {
+     console.log('We retrieved some data! AND we\'re confident it will work on a variety of browser distributions.')
+     console.log(json)
+   })
+   .catch(error => console.error('Something went wrong when fetching this data: ', error))

Если мы запустим сборку, будет сгенерирован ещё один файл polyfills.bundle.js и всё должно работать гладко в браузере. Обратите внимание, что эту настройку можно улучшить, но она должна дать вам хорошее представление о том, как можно предоставить полифилы только тем пользователям, которым они действительно нужны.

Дополнительные оптимизации

Пакет babel-preset-env использует browserslist, чтобы транспилировать только то, что не поддерживается в вашей матрице браузеров. Этот пресет поставляется с опцией useBuiltIns, false по умолчанию, которая преобразует ваш глобальный импорт babel-polyfill в более подробную схему «функция по функции» import:

import 'core-js/modules/es7.string.pad-start';
import 'core-js/modules/es7.string.pad-end';
import 'core-js/modules/web.timers';
import 'core-js/modules/web.immediate';
import 'core-js/modules/web.dom.iterable';

Для получения дополнительной информации см. документацию по babel-preset-env.

Встроенные компоненты Node.js

Встроенные компоненты Node.js, такие как process, могут быть полифилены непосредственно из файла конфигурации без использования специальных лоадеров или плагинов. Для получения дополнительной информации и примеров см. страницу конфигурации node.

Другие утилиты

Есть несколько других инструментов, которые могут помочь при работе с устаревшими модулями.

Загрузчик script-loader оценивает код в глобальном контексте, аналогично включению с помощью тега script. В этом режиме все обычные библиотеки должны работать. require, module, и т. д. будут не определены.

При использовании script-loader, модуль добавляется в бандл в виде строки. Он не минифицируется webpack, поэтому используйте сжатую версию. Также нет devtool поддержки для библиотек, добавленных этим лоадером.

Когда нет версии модуля AMD/CommonJS, и вы хотите включить dist, вы можете пометить этот модуль в noParse. Это заставит webpack включить модуль без его разбора или разрешения require() и import заявлений. Этот приём также используется для повышения производительности сборки.

Любая функция, требующая AST, например ProvidePlugin, не будет работать.

Наконец, некоторые модули поддерживают несколько стилей модулей, например комбинацию AMD, CommonJS и устаревших. В большинстве таких случаев они сначала проверяют наличие define и затем используют странный код для экспорта свойств. В этих случаях может помочь принудительное использование пути CommonJS, установив define=>false с помощью imports-loader.

Дополнительное чтение

  • Вознаграждение пользователей современных браузеров скриптом
  • useBuiltIns в babel-preset-env

© JS Foundation and other contributors
Licensed under the Creative Commons Attribution License 4.0.
https://v4.webpack.js.org/guides/shimming

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API