Точки входа (entry)

Точка входа (entry) определяет модуль, с которого Webpack начинает построение графа зависимостей. Именно с этого файла начинается анализ импортов, подключение зависимостей, обработка loader-ами и формирование итогового бандла.

Во время сборки Webpack:

  1. Загружает файл точки входа.
  2. Анализирует все import и require.
  3. Рекурсивно обходит зависимости.
  4. Формирует граф модулей.
  5. Создаёт один или несколько выходных файлов.

Без точки входа Webpack не знает, с какого файла начинать обработку проекта.


Простейшая конфигурация точки входа

module.exports = {
    entry: './src/index.js'
};

В данном случае:

  • ./src/index.js — главный файл приложения;
  • именно он становится корнем графа зависимостей;
  • все подключённые из него модули попадут в сборку.

Структура проекта:

project/
├── src/
│   ├── index.js
│   ├── app.js
│   └── utils.js
└── webpack.config.js

Файл index.js:

import './app';
import './utils';

Webpack начинает с index.js, затем подключает app.js и utils.js.


Значение entry по умолчанию

Если свойство entry не указано, Webpack использует значение:

./src/index.js

То есть такая конфигурация:

module.exports = {};

эквивалентна:

module.exports = {
    entry: './src/index.js'
};

Если файла не существует, сборка завершится ошибкой.


Типы значений entry

Свойство entry поддерживает несколько форматов:

  • строка;
  • массив;
  • объект;
  • функция;
  • асинхронная функция.

Строковое значение

Наиболее простой вариант.

module.exports = {
    entry: './src/main.js'
};

Используется в небольших приложениях с единой точкой запуска.


Массив точек входа

Webpack позволяет объединять несколько файлов в одну стартовую точку.

module.exports = {
    entry: [
        './src/polyfills.js',
        './src/index.js'
    ]
};

Порядок имеет значение.

Сначала будет обработан:

./src/polyfills.js

а затем:

./src/index.js

Частое применение:

  • подключение polyfill;
  • глобальные настройки;
  • предварительная инициализация окружения;
  • runtime-конфигурация.

Пример:

entry: [
    'core-js/stable',
    'regenerator-runtime/runtime',
    './src/index.js'
]

Объект точек входа

Наиболее распространённый вариант для средних и крупных проектов.

module.exports = {
    entry: {
        app: './src/app.js',
        admin: './src/admin.js'
    }
};

Webpack создаст отдельный бандл для каждой точки входа.

При использовании:

output: {
    filename: '[name].bundle.js'
}

результат:

dist/
├── app.bundle.js
└── admin.bundle.js

Независимые приложения

Несколько entry используются, когда в одном проекте существует несколько независимых интерфейсов.

Пример:

src/
├── public/
│   └── index.js
├── admin/
│   └── index.js
└── landing/
    └── index.js

Конфигурация:

module.exports = {
    entry: {
        public: './src/public/index.js',
        admin: './src/admin/index.js',
        landing: './src/landing/index.js'
    }
};

Каждая часть проекта будет собираться отдельно.


Использование с HTML

Многостраничные приложения часто используют несколько HTML-файлов.

Например:

dist/
├── index.html
├── admin.html
├── public.bundle.js
└── admin.bundle.js

В index.html:

<script src="public.bundle.js"></script>

В admin.html:

<script src="admin.bundle.js"></script>

Расширенная форма объекта entry

Каждая точка входа может быть описана объектом.

module.exports = {
    entry: {
        app: {
            import: './src/app.js'
        }
    }
};

Поле import содержит основной файл точки входа.


Свойство dependOn

Позволяет переиспользовать общие зависимости между точками входа.

Пример:

module.exports = {
    entry: {
        shared: ['lodash'],

        app: {
            import: './src/app.js',
            dependOn: 'shared'
        },

        admin: {
            import: './src/admin.js',
            dependOn: 'shared'
        }
    }
};

Webpack вынесет lodash в отдельный общий бандл.

Результат:

dist/
├── shared.js
├── app.js
└── admin.js

Это уменьшает размер итоговых файлов.


Свойство filename

Позволяет задать имя файла для конкретной точки входа.

module.exports = {
    entry: {
        app: {
            import: './src/app.js',
            filename: 'js/app.bundle.js'
        }
    }
};

Результат:

dist/
└── js/
    └── app.bundle.js

Свойство runtime

Определяет runtime chunk для entry.

module.exports = {
    entry: {
        app: {
            import: './src/app.js',
            runtime: 'runtime'
        }
    }
};

Webpack вынесет служебный runtime-код в отдельный файл.


Свойство layer

Используется совместно с системой layers.

module.exports = {
    entry: {
        app: {
            import: './src/app.js',
            layer: 'main'
        }
    }
};

Позволяет группировать модули по логическим слоям.


Свойство chunkLoading

Управляет механизмом загрузки chunk-файлов.

module.exports = {
    entry: {
        app: {
            import: './src/app.js',
            chunkLoading: 'jsonp'
        }
    }
};

Варианты зависят от окружения:

  • jsonp
  • import
  • require
  • async-node

Динамическая точка входа

Webpack поддерживает функцию в качестве entry.

module.exports = {
    entry: () => './src/app.js'
};

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


Генерация entry на основе файловой системы

Пример автоматического поиска страниц:

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

const pagesDir = './src/pages';

const entries = {};

fs.readdirSync(pagesDir).forEach(file => {
    const name = path.parse(file).name;

    entries[name] = `${pagesDir}/${file}`;
});

module.exports = {
    entry: entries
};

Если структура:

src/pages/
├── home.js
├── catalog.js
└── profile.js

Webpack автоматически создаст:

{
    home: './src/pages/home.js',
    catalog: './src/pages/catalog.js',
    profile: './src/pages/profile.js'
}

Асинхронная точка входа

Webpack поддерживает Promise.

module.exports = {
    entry: async () => {
        return {
            app: './src/app.js'
        };
    }
};

Это полезно при:

  • чтении конфигурации из API;
  • генерации entry во время сборки;
  • интеграции с CMS;
  • построении сложных build-сценариев.

Entry и граф зависимостей

Точка входа становится вершиной графа модулей.

Пример:

index.js
├── app.js
│   ├── api.js
│   └── ui.js
└── utils.js

Webpack анализирует все связи между файлами.

Даже если модуль подключён глубоко внутри дерева:

import helper from './helpers/math';

он всё равно попадёт в граф зависимостей.


Несколько entry и дублирование кода

При использовании нескольких точек входа может возникать дублирование.

Пример:

entry: {
    app: './src/app.js',
    admin: './src/admin.js'
}

Если оба файла импортируют:

import _ from 'lodash';

то lodash может попасть в оба бандла.


Решение через SplitChunksPlugin

Webpack умеет автоматически выделять общий код.

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
};

Webpack создаст общий chunk:

vendors.js

вместо дублирования библиотек.


Entry и code splitting

Точки входа работают совместно с динамическими импортами.

Пример:

import('./dashboard');

Webpack создаст отдельный chunk для dashboard.

Главный entry-файл:

entry: './src/index.js'

загрузит дополнительный chunk только при необходимости.


Entry и runtime

Каждая entry имеет собственный runtime.

Runtime содержит:

  • таблицу модулей;
  • механизм загрузки chunk;
  • кеш модулей;
  • служебную инфраструктуру Webpack.

При множественных entry runtime может дублироваться.

Оптимизация:

optimization: {
    runtimeChunk: 'single'
}

Webpack создаст:

runtime.js

общий для всех entry.


Entry в SPA

Для одностраничных приложений обычно используется один entry.

entry: './src/index.js'

Пример структуры React-приложения:

src/
├── index.js
├── App.js
├── routes/
└── components/

Вся система начинается с index.js.


Entry в MPA

Многостраничные приложения используют несколько entry.

Пример интернет-магазина:

entry: {
    home: './src/home.js',
    product: './src/product.js',
    cart: './src/cart.js',
    checkout: './src/checkout.js'
}

Каждая страница получает собственный JavaScript-бандл.


Entry и HtmlWebpackPlugin

При множественных entry обычно используется несколько экземпляров плагина.

plugins: [
    new HtmlWebpackPlugin({
        filename: 'index.html',
        chunks: ['home']
    }),

    new HtmlWebpackPlugin({
        filename: 'admin.html',
        chunks: ['admin']
    })
]

Плагин автоматически подключает нужные бандлы.


Entry и HMR

Webpack Dev Server использует точки входа для внедрения Hot Module Replacement.

Во время разработки Webpack может автоматически добавлять служебные runtime-модули к entry.

Пример внутреннего преобразования:

entry: [
    'webpack/hot/dev-server',
    './src/index.js'
]

Entry и target

Точка входа зависит от целевого окружения.

Для браузера:

target: 'web'

Для Node.js:

target: 'node'

В Node.js-сборке entry может выглядеть так:

entry: './server/index.js'

Entry для библиотек

Webpack может собирать библиотеки.

module.exports = {
    entry: './src/library.js',

    output: {
        library: 'MyLibrary',
        libraryTarget: 'umd'
    }
};

Точка входа становится публичным API библиотеки.


Entry и tree shaking

Webpack анализирует зависимости начиная с entry-файла.

Если модуль:

  • не используется;
  • не импортируется;
  • не имеет side effects;

то он может быть исключён из сборки.

Пример:

// utils.js
export function sum() {}
export function multiply() {}
// index.js
import { sum } from './utils';

Функция multiply может не попасть в production-бандл.


Ошибки при настройке entry

Неверный путь

entry: './src/main.js'

Если файл отсутствует:

Module not found

Абсолютные пути

Иногда используются абсолютные пути:

const path = require('path');

entry: path.resolve(__dirname, 'src/index.js')

Это уменьшает вероятность ошибок.


Конфликт имён

Проблема:

entry: {
    app: './src/app.js'
}

и:

output: {
    filename: 'bundle.js'
}

Несколько entry будут пытаться записать один файл.

Правильный вариант:

output: {
    filename: '[name].js'
}

Практический пример конфигурации

const path = require('path');

module.exports = {
    mode: 'production',

    entry: {
        main: {
            import: './src/main.js',
            dependOn: 'vendor'
        },

        admin: {
            import: './src/admin.js',
            dependOn: 'vendor'
        },

        vendor: ['lodash']
    },

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[contenthash].js',
        clean: true
    },

    optimization: {
        runtimeChunk: 'single',

        splitChunks: {
            chunks: 'all'
        }
    }
};

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

dist/
├── runtime.js
├── vendor.js
├── main.a1b2c3.js
└── admin.d4e5f6.js

Такая конфигурация:

  • разделяет runtime;
  • выносит общие библиотеки;
  • уменьшает дублирование;
  • оптимизирует кеширование;
  • поддерживает независимые части приложения.