Введение

Сегодня в мире разработки веб-приложений нам очень часто приходится прибегать к помощи сборщиков. И хоть на текущий момент существует большой выбор инструментов сборки приложений, значительная доля написанных проектов использует Webpack в качестве сборщика. И бывает, что стандартный функционал Webpack не покрывает наши потребности, а плагинов, которые это могут сделать, нет в открытом доступе. Тогда мы приходим к выводу, что нужно писать свой плагин. Эта статья посвящена необходимой базе, которая вам понадобится, чтобы понять, как устроены плагины для Webpack и как начать их писать.

Что же такое Webpack

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

Про что статья

Эта статья посвящена созданию собственного плагина для Webpack. Мы рассмотрим, как устроены плагины, разберем два самых важные объекта при разработке плагинов, хуки этих объектов и виды этих хуков, а также шаг за шагом разберем процесс разработки плагина на примере. Независимо от того, хотите ли вы оптимизировать сборку, внедрить специфические требования вашего проекта или просто лучше понять, как работает Webpack изнутри, написание собственного плагина станет отличным способом углубить ваши знания и навыки. Давайте начнем путешествие в мир Webpack-плагинов, которое позволит вам раскрыть весь потенциал этого инструмента!

Структура класса, основные методы и инстансы

Webpack плагин это всего лишь функция или класс. На примере поговорим и рассмотрим вариант использования класса. Класс плагина должен иметь метод apply. Этот метод вызывается один раз компилятором Webpack при установке плагина. Метод apply получает ссылку на компилятор Webpack первым параметром, который в свою очередь предоставляет доступ к хукам компилятора. Плагин структурирован следующим образом:

class MyPlugin {
    apply(compiler) {
        console.log('Ссылка на компилятор webpack', compiler)
    }
}

Compiler и Compilation

Среди двух наиболее важных объектов при разработке плагинов есть такие как: compiler и compilation.

class MyPlugin {
    apply(compiler) {
        compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
            console.log('Создан новый объект компиляции:', compilation);
            compilation.hooks.buildModule.tap('MyPlugin', (module) => {
                console.log('Собираемый модуль:', module.resource);
            });
        });
    }
}

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

  • Compiler относятся к этапу, когда Webpack начинает сборку. Это происходит до того, как создается объект компиляции (compilation object). На этом этапе вы можете взаимодействовать с процессом компиляции — например, изменять или проверять параметры конфигурации, менять настройки Webpack конфига, например взаимодействовать с плагинами и тд. Compiler хуки доступны в плагинах и позволяют выполнять такие действия, как настройка контекста сборки или изменение параметров, прежде чем начнётся сама сборка.

  • Compilation ссылается на более поздний этап, который начинается после того, как объект компиляции был создан. Используя compilation хуки вы можете выполнять действия с модулями, трансформировать их, добавлять новые файлы или изменять существующие.

Объект компиляции.

В Webpack объект компиляции (или Compilation Object) представляет собой центральный элемент в процессе сборки. Он создаётся для каждого входного файла (entry point) в процессе сборки и содержит всю информацию о текущем состоянии сборки, включая модули, зависимости и ресурсы. Получить его можно в различных хуках, например в compilation:

class MyPlugin {
    apply(compiler) {
        // Используем хук compilation для доступа к объекту компиляции
        compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
			// `compilation` здесь - это объект компиляции
            console.log('Создан новый объект компиляции:', compilation);
        });
    }
}

Хуки

Хуки делятся на два типа, это синхронные и асинхронные. По названию думаю уже понятно, что синхронные хуки блокируют основной поток и Webpack ждет пока такой хук выполнится, чтобы продолжить работу. При регистрации синхронного хука используется метод tap. В то время как асинхронные хуки выполняются параллельно основному потоку, но требуют вызова callback (если вы используете tapAsync для регистрации хука) или возвращения промиса (если используете tapPromise).

class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      'MyPlugin',
      (compilation, callback) => {
       // Что-то асинхронное
        setTimeout(function () {
          callback(); // Обязательно вызвать callback в конце
        }, 1000);
      }
    );
  }
}

Пишем свой плагин

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

Для начала создадим папку, перейдем в нее, инициализируем проект и установим необходимые нам зависимости.

mkdir webpack-module-example
cd webpack-module-example
npm init
npm i webpack webpack-cli esbuild-loader --save-dev

Следующим шагом создадим собственно файл с нашим плагином, index.ts для входной точки вебпака и простой Webpack конфиг с одним правилом.

// myPlugin.js
class MyPlugin {
    apply(compiler) {
        compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
            compilation.hooks.buildModule.tap('MyPlugin', (module) => {
                console.log('Собираемый модуль:', module.resource);
            });
        });
    }
}
module.exports = { MyPlugin };
// index.ts
console.log('Hello');
// webpack.config.js
const path = require('path');
const { MyPlugin } = require('./myPlugin.js');
module.exports = {
    mode: 'production',
    entry: path.resolve(__dirname, './index.ts'),
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'bundle.js',
    },
    module: {
        rules: [
            {
                test: /\.(ts|js)x?$/,
                exclude: /node_modules/,
                loader: 'esbuild-loader',
                options: {
                    target: 'es2015',
                },
            },
        ],
    },
    resolve: {
        extensions: ['.tsx', '.jsx', '.ts', '.js'],
    },
    plugins: [new MyPlugin()],
};

И последним шагом добавим скрипт на запуск вебпака в наш package.json.

// package.json
{
    "name": "webpack-module-example",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "build": "webpack --config webpack.config.js"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "esbuild-loader": "^4.2.2",
        "webpack": "^5.94.0",
        "webpack-cli": "^5.1.4"
    }
}

Результат

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

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

Например, те, кто знаком с плагином Module Federation для Webpack, позволяющим организовывать микро-фронтенды, сталкивались с тем, что при создании инстанса плагина, ему нужно передавать все статичные адреса на каждый модуль:

new ModuleFederationPlugin({
    name: 'app',
    filename: 'remoteEntry.js',
    remotes: {	
	    app2: 'app2@http://localhost:3002/remoteEntry.js',
	    app3: 'app3@http://localhost:3003/remoteEntry.js',
	    app4: 'app4@http://localhost:3004/remoteEntry.js',
	    app5: 'app5@http://localhost:3005/remoteEntry.js',
	    app6: 'app6@http://localhost:3006/remoteEntry.js',
	},
    exposes: {
        './MyComponent': './src/MyComponent',
    },
    shared: [
        'react',
        'react-dom',
    ],
})

А если вы хотите использовать динамические модули, то туда попадают промисы, которые еще больше увеличивают сложность создания инстанса плагина и затрудняют его читаемость:

new ModuleFederationPlugin({
    name: 'host',
    remotes: {
        app1: `promise new Promise(resolve => {
            const urlParams = new URLSearchParams(window.location.search)
            const version = urlParams.get('app1VersionParam')
            const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
            const script = document.createElement('script')
            script.src = remoteUrlWithVersion
            script.onload = () => {
                const proxy = {
                    get: (request) => window.app1.get(request),
                    init: (arg) => {
	                    try {
	                        return window.app1.init(arg)
	                    } catch(e) {
	                        console.log('remote container already initialized')
	                    }
                    }
                }
                resolve(proxy)
            }
            document.head.appendChild(script);
        })`
    }
})

Можно, конечно, использовать функцию для создания таких промисов, и значение поля remotes в вашем конфиге будет содержать только вызов одной функции с названиями модулей в аргументах. Однако это уже другая тема. В рамках этой статьи я бы предложил выделить это в отдельный плагин, который сможет работать поверх плагина Module Federation, содержать в себе всю логику, принимать все необходимые ему аргументы, возможно, выполнять какую-то логику до запроса ваших модулей и вызывать плагин Module Federation.

Кроме того, плагин Module Federation не предоставляет типизацию для удалённых модулей, и в этом случае тоже может помочь самописный плагин, который будет выгружать декларации для удаленных модулей, используемых в вашем пакете. Осталось только генерировать их на этапе сборки вашего проекта и куда-то складывать, но это уже другая тема.

Заключение

В заключение, мы рассмотрели структуру плагинов, основные объекты, используемые при их разработке, хуки этих объектов и их виды. Мы также создали простой плагин в качестве наглядного примера. Надеюсь, эта статья поможет вам лучше разобраться в основных аспектах плагинов для Webpack.

Изучить все хуки можно в документации: Compiler и Compilation.

Комментарии (0)