Webpack: магия вне Хогвартса

Андрей Синицын
Lead Engineer @ Namaste Technologies

Если вам плохо видно

Откройте на вашем лэптопе / смартфоне:

darkmagic.andrey.space

Содержание

  • Что такое Webpack?
  • Терминология. Модуль, чанк, ассет, бандл
  • Как устроен процесс сборки бандла
  • Что получается на выходе у бандла?
  • Немного черной магии напоследок

Что такое Webpack?

  • Бандлер
  • Сборочный пайплайн

Терминология: модуль

  • Cамая маленькая сущность
  • Обычно инкапсулирует один файл
  • Основа для построения графа зависимостей
  • Именно к модулям применяются лоадеры Webpack

Пример модуля

sum.js
                
const sum = (a, b) => a + b

export default sum

                

multiply.js
                
const multiply = (a, b) => a * b

export default multiply
                
                

main.js (entrypoint)
                
import sum from './sum'
import multiply from './multiply'

console.log(sum(2, 4))
console.log(multiply(3, 5))
                
                

Webpack строит дерево зависимостей:

main.js sum.js multiply.js

Терминология: чанк

  • Cущность, включающая в себя несколько модулей
  • Инкапсулируемые модули связаны друг с другом
  • Граф зависимостей чанков - основа графа зависимостей ассетов
  • Всегда присутствует чанк-загрузчик, который предоставляет __webpack_require__
    (о загрузчике мы поговорим чуть позже)

Как Webpack видит проект сейчас

Чанк 0 main.js sum.js multiply.js

Терминология: ассет

  • Ассет - набор чанков, заключённых в одном файле

Добавляем слой вложенности

Ассет app.js Чанк-загрузчик Чанк 0 main.js sum.js multiply.js

Терминология: бандл

  • Бандл - результат сборки приложения
  • Бандл включает в себя ассеты, которые включают в себя чанки, состоящие из модулей
  • Граф зависимостей чанков и ассетов - основа графа зависимостей бандла

Результат сборки: простой

Бандл Ассет app.js Чанк-загрузчик Чанк 0 main.js sum.js multiply.js

Результат сборки: чуть посложнее

Бандл Ассет app.css Чанк с CSS-файлами Ассет app.js Чанк-загрузчик Чанк 0 main.js sum.js multiply.js Чанк 1 main.css Ассет news.js Чанк 3 landing.js component1.js app.css

Процесс сборки Webpack



Плагины могут выполняться во время любой стадии сборки; больше информации можно найти на bit.ly/webpack-hooks

Валидация конфигурации

Препроцессинг и построение графа зависимостей

Что на выходе у нашего бандла?

Один файл app.js, имеющий примерно такую структуру:

              
              (function(modules) {
                var installedModules = {}
                function __webpack_require__(moduleId) {
                  // код
                }
                return __webpack_require__(__webpack_require__.s = "./main.js")
              })({
                "./main.js": (function(module, exports, require) {
                  "use strict";
                  eval("содержимое main, обёрнутое в __webpack_require__")
                }),
                // то же самое для других файлов
              })
              
              

Что мы можем сказать, глядя на этот файл?

  • Это IIFE
  • Принимает в себя список модулей
  • Возвращает __webpack_require__ с moduleId главного модуля
  • Все модули представляют собой функцию, вызывающуюся со следующими параметрами
    • Модуль
    • Объект экспортов модуля
    • Экземпляр __webpack_require__
  • Все модули внутри представляют вызов eval, который мутирует переданный в изначальную функцию объект экспортов

Попробуем угадать, что делает __webpack_require__

Подсказка - все модули в JS - синглтоны

...ответ на загадку

  • Исходя из названия функции, она всегда будет возвращать экспорты
  • Сначала она будет сверяться с кэшем инициализированных модулей.
  • В случае, если кэш найден - возвращаем экспорты этого модуля
  • В противном случае пытаемся найти инициализатор модуля в списке переданных нам
  • Вызываем инициализатор с пустым объектом - будущим exports
  • Кэшируем модуль
  • Возвращаем экспорты модуля

Проверим себя


var installedModules = {};
function __webpack_require__(moduleId) {
  if(installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  var module = installedModules[moduleId] = {
    i: moduleId,
    l: false,
    exports: {}
  };
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  module.l = true;
  return module.exports;
}
            
            

__webpack_require__ всё же не так прост!

            
            (function (modules) {
              var installedModules = {};
              // define webpackRequire()
              __webpack_require__.m = modules;
              __webpack_require__.c = installedModules;
              __webpack_require__.p = ""; // publicPath из конфига
              __webpack_require__.d = () => {} // Геттер для Harmony-экспортов
              __webpack_require__.r = () => {} // Приводит объект экспортов из вызова require
                                               // к виду Harmony
                                               // __esModule = true 
                                               // Symbol.toStringTag = Module
              __webpack_require__.t = () => {} // Создаёт фейковый неймспейс-объект
              __webpack_require__.n = () => {} // функция getDefaultExport()
              __webpack_require__.o = () => {} // алиас Object.hasOwnProperty
                
            })({/* modules */})
            
            

...и к этому всему вы по умолчанию имеете доступ из вашего кода!

Просто обратитесь к __webpack_require__

Disclaimer: API официально не открыт, но уже очень давно кардинально не менялся

Но зачем всё это нужно?

Давайте займёмся магией вне Хогвартса
Вместе мы разберём, как можно менять исходный код приложения прямо из самого себя

Step 1: пишем простой плагин для Вебпака

Наш плагин будет приводить имена чанков в нашем приложении от номеров к именам файлов

            
              class CustomPlugin {
                apply(compiler) {
                  compiler.hooks.compilation.tap('CustomPlugin', c => {
                    // Установив хук на событие компиляции
                    // мы устанавливаем хук на подсобытие beforeModuleIds
                    c.hooks.beforeModuleIds.tap(
                      'CustomPlugin',
                      modules => renameModules(modules)
                    )
                  })
                }
              }
            
            

Step 1.2: пишем простой плагин для Вебпака

...переименовываем чанки!

            
              const renameModules = modules => modules.forEach(module => {
                module.id = path
                  .relative(module.context, module.resource)
                  .replace(/\\/g, '/')
              })
            
            

Step 2: первый урок Тёмных Искусств

...переходим в Visual Studio Code

Спасибо за внимание!

Осторожнее с магией вне Хогвартса ;)

Телеграм-канал, где можно обсудить доклад
bit.ly/jsnnwebpack
E-mail
me@andrey.space
Github
github.com/asn007