Предыстория
Ни для кого не секрет, что с выходом Webpack 4 стратегия разделения кода сильно поменялась. Тут даже лучше сказать, что она была заново придумана, т.к. старый подход просто перестал работать, а новый не понятно как использовать.
Для тех, кто все еще не в курсе, плагина webpack.optimize.CommonsChunkPlugin больше нет. Совсем. Вместо этого предлагается в конфиге писать следующее:
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: "all"
}
}
// ...
}
Это должно работать как магия. Т.е. теперь не мы говорим webpack'у что сделать общим чанком, а он сам все сделает, да еще может даже и лучше нас.
И наступит счастье. Шутка. На самом деле нет...
Базовые приготовления
Вот пример из документации:
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Результатом сборки будут 3 файла: another.bundle.js, index.bundle.js, vendors~another~index.bundle.js
Hash: ac2ac6042ebb4f20ee54
Version: webpack 4.7.0
Time: 316ms
Asset Size Chunks Chunk Names
another.bundle.js 5.95 KiB another [emitted] another
index.bundle.js 5.89 KiB index [emitted] index
vendors~another~index.bundle.js 547 KiB vendors~another~index [emitted] vendors~another~index
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 489 bytes {vendors~another~index} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {vendors~another~index} [built]
[./src/another-module.js] 88 bytes {another} [built]
[./src/index.js] 86 bytes {index} [built]
+ 1 hidden module
Теперь, для того, чтобы запустить наши веб приложения, мы, в одном случае, должны подключить vendors~another~index.bundle.js и index.bundle.js, а во втором vendors~another~index.bundle.js и another.bundle.js.
В чем проблема?
Проблема в имени vendors~another~index.bundle.js. Пока у нас меньше трех точек входа, ничего страшного не происходит. Здесь все кажется логичным — бандл содержит npm модули (они же vendors) и общие модули для index и another. На каждую из страниц мы подключаем 2 файла и не имеем проблем.
Однако если у нас три и более точки входа, то новых бандлов (они же чанки) может быть куда больше и мы уже не знаем ни их количества, ни имен. Все становится еще веселее, если мы еще и css извлекаем в отдельные файлы. И это проблема.
Как решить эту проблему?
После завершения работы webpack у нас нет никаких файлов, которые содержали бы в себе информацию о том, какие именно бандлы на той или иной странице надо подключать. И в какой последовательности.
Однако в output'е мы можем найти вот такие строки:
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
На самом деле это почти то, что нам надо. Т.е. webpack прекрасно знает какие бандлы нужны для каждой точки входа, но почему-то сам не хочет этой информацией с нами делиться.
Манифест нам здесь не помогает. Да, мы знаем что такой (vendors~another~index.bundle.js) бандл есть. Мы знаем где он лежит. Но кому он нужен не знаем. Т.е. манифест бесполезен.
Тогда я решил что раз webpack знает нужную информацию, то ее возможно получится достать с помощью плагинов. Готовых я не нашел и решил написать свой. И, только ради демонстрации этого плагина, я и пишу эту статью.
import * as webpack from "webpack";
export interface IChunkDescription {
readonly id: string | number;
readonly name: string;
readonly files: string[];
}
export interface IEntrypointsPluginOptions {
readonly filename: string;
readonly replacer?: (key: string, value: any) => any;
readonly space?: string | number;
readonly filter?: (chunk: IChunkDescription) => boolean;
}
export default class EntrypointsPlugin {
private readonly options: IEntrypointsPluginOptions;
public constructor(options: IEntrypointsPluginOptions) {
this.options = Object.assign<IEntrypointsPluginOptions, IEntrypointsPluginOptions>({
filename: "entrypoints.json",
replacer: null,
space: null,
filter: null
}, options);
}
public apply(compiler: webpack.Compiler): void {
compiler.hooks.emit.tap("entrypoints", (compilation: webpack.compilation.Compilation) => {
let data = {};
let entrypoints = {};
const filter = this.options.filter;
const publicPath = compilation.compiler.options.output.publicPath;
for (let [key, value] of compilation.entrypoints.entries()) {
const chunks: IChunkDescription[] = value.chunks.map(data => {
const chunk: IChunkDescription = {
id: data.id,
name: data.name,
files: data.files
};
return filter == null || filter(chunk) ? chunk : null;
});
const files = ([] as string[]).concat(...chunks.filter(c => c != null)
.map(c => c.files.map(f => publicPath + f)));
const js = files.filter(f => /.js/.test(f) && !/.js.map/.test(f));
const css = files.filter(f => /.css/.test(f) && !/.css.map/.test(f));
let entrypoint = {};
if (js.length) entrypoint["js"] = js;
if (css.length) entrypoint["css"] = css;
data[key] = entrypoint;
}
const json = JSON.stringify(data, this.options.replacer, this.options.space);
compilation.assets[this.options.filename] = {
source: () => json,
size: () => json.length
};
});
}
}
В файле webpack.config.(ts|js) добавим новый плагин:
plugins: [
new EntrypointsPlugin({
filename: "entrypoints.json",
space: 2
})
]
и дождемся результата. Результатом будет файл entrypoints.json с вот таким содержанием:
{
"index": {
"js": ["vendors~another~index.bundle.js", "index.bundle.js"]
},
"another": {
"js": ["vendors~another~index.bundle.js", "another.bundle.js"]
}
}
Если используется extract-css, то кроме секции js будет еще и css.
Последнее, что нам остается, при формировании HTML страницы, это прочитать файл entrypoints.json, найти нужную точку входа, подключить js и css файлы из соответствующих списков.
Проблема решена
Как-то так.
Автор: Виталий Лещенко