Дисклеймер
Я недавно начал разбираться с Webpack, поэтому некоторые вещи могут показаться странными. Если вы нашли ошибку, или ugly-код, то напишите об этом в комментариях, я буду благодарен.
Описание задачи
Я работаю над небольшим дополнением для сайта. Дополнение включает в себя 2 практически независимые части — личный кабинет и общедоступную часть. По сути это два независимых SPA.
После базовой настройки webpack встал вопрос, на который я попробую ответить в этой заметке:
Как настроить webpack 4 чтобы одновременно собирать 2 страницы так, чтобы для каждой подгружались только необходимые бандлы?
Потребности такие:
-
Личный кабинет — back
- vue, vue-router, vuex
- axios
- @riophae/vue-treeselect
- buefy
- Собственные скрипты
-
Общедоступная часть — front
- vue, vue-router, vuex
- axios
- @riophae/vue-treeselect
- Собственные скрипты
Конфиги с решением
И комментариями
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//const CopyWebpackPlugin = require('copy-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
// Общие переменные для нескольких конфигов
const PATHS = {
src_back: path.join(__dirname, '../src-back'),
src_front: path.join(__dirname, '../src-front'),
// Чанки для back. Вызываются в HtmlWebpackPlugin (в build и dev)
back_chunks : ['common_vendors', 'buefy', 'back' ],
front_chunks: ['common_vendors', 'front' ],
dist: path.join(__dirname, '../docs'),
assets: 'assets/',
}
module.exports = {
// точки входа
entry: {
back: PATHS.src_back,
front: PATHS.src_front,
},
// точки выхода
output: {
// Квадратные скобки означают, что берется файл с имеем точки входа https://youtu.be/JcKRovPhGo8?t=916
filename: `js/[name].js?v=[hash]`,
// папка назначения скомпилированных файлов https://nodejs.org/api/path.html#path_path_relative_from_to
path: PATHS.dist,
// Папка, которая отображается, может отличаться от реальной папки
publicPath: '/'
},
// Разбиваем на отдельные файлы
optimization: {
splitChunks: {
cacheGroups: {
common_vendors: {
test: /[\\/]node_modules[\\/](vue|vue-router|vuex|axios|@riophae[\\/]vue-treeselect)[\\/]/,
name: 'common_vendors', // имя чанка
chunks: 'initial',
enforce: true,
},
buefy: {
test: /[\\/]node_modules[\\/](buefy)[\\/]/,
name: 'buefy', // имя чанка
chunks: 'initial',
enforce: true,
},
}
}
},
resolve: {
// Порядок обработки файлов.
extensions: ['.js', '.vue', '.json'],
},
module: {
// Определяем порядок обработки разных типов файлов.
// Постпроцессоры, минификаторы и пр.
rules: [{
test: /\.js$/,
loader: "babel-loader",
// не включаем те файлы, которые содержет эта папка
exclude: "/node_modules/",
},
{
test: /\.vue$/,
loader: "vue-loader",
options: {
loader: {
// Определяем порядок обработки
scss: 'vue-style-loader!css-loader!sass-loader'
}
}
}, {
test: /\.(png|jpg|jpeg|gif|svg)$/,
loader: "file-loader",
options: {
name: '[name].[ext]'
}
}, {
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
loader: "file-loader",
options: {
name: '[name].[ext]'
}
},
{
test: /\.scss$/,
use: [
'style-loader',
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: true
}
},
{
loader: "postcss-loader",
options: {
sourceMap: true,
config: {
path: `./js/postcss.config.js`
}
}
},
{
loader: "sass-loader",
options: {
sourceMap: true
}
},
]
},
{
test: /\.less$/,
use: [
'style-loader',
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: true
}
},
{
loader: "postcss-loader",
options: {
sourceMap: true,
config: {
path: `./js/postcss.config.js`
}
}
},
{
loader: "less-loader",
options: {
sourceMap: true
}
},
]
},
{
test: /\.css$/,
use: [
"style-loader",
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: true
}
},
{
loader: "postcss-loader",
options: {
sourceMap: true,
config: {
path: `./js/postcss.config.js`
}
}
},
]
}
]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: `css/[name].css?h=[hash]`,
}),
//new CopyWebpackPlugin([]),
],
node: {
// Иначе как-то много кода лишнего добавляется. ХЗ:\
Buffer: false
},
// Чтобы переменные были доступны в других файлах конфигураций (dev, build)
externals: {
paths: PATHS
},
}
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const buildWebpackConfig = merge(baseWebpackConfig, {
mode: 'production',
resolve: {
alias: {
// Для build используем минифицированный файл библиотеки
'vue$': 'vue/dist/vue.min.js',
}
},
plugins: [
// Обработка шаблона точки входа "back"
new HtmlWebpackPlugin({
template: `${baseWebpackConfig.externals.paths.src_back}/index.html`,
filename: `${baseWebpackConfig.externals.paths.dist}/mx_static/reception_points-vueapp-backend.php`,
title: "mode_build", // Нужно для костыльного условия в шаблоне
inject: false,
chunks: baseWebpackConfig.externals.paths.back_chunks,
}),
// Обработка шаблона точки входа "front"
new HtmlWebpackPlugin({
template: `${baseWebpackConfig.externals.paths.src_front}/index.html`,
filename: `${baseWebpackConfig.externals.paths.dist}/mx_static/reception_points-vueapp-front.php`,
title: "mode_build", // Нужно для костыльного условия в шаблоне
inject: false,
chunks: baseWebpackConfig.externals.paths.front_chunks,
}),
]
});
module.exports = new Promise((resolve, reject) => {
resolve(buildWebpackConfig);
});
const webpack = require('webpack');
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const devWebpackConfig = merge(baseWebpackConfig, {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
resolve: {
alias: {
// Для dev используем не минифицированный файл библиотеки
'vue$': 'vue/dist/vue.js',
}
},
devServer: {
contentBase: baseWebpackConfig.externals.paths.dist,
// типа localhost (локальный домен настроен на Open Server)
host: 'mysite.test',
port: 8081,
// Выводить ошибки компиляции в браузер
overlay:{
warnings: true,
errors: true,
},
proxy: {
// Для проксирования AJAX запросов, иначе будет ошибка политики доступа
// С этой настройкой все запросы, содержащие '/api' будут переводиться с
// 'http://mysite.test:8081 на http://mysite.test
'/api': {
target: 'http://mysite.test',
}
},
},
plugins:[
new webpack.SourceMapDevToolPlugin({
filename: '[file].map'
}),
// Обработка шаблона точки входа "back"
new HtmlWebpackPlugin({
template: `${baseWebpackConfig.externals.paths.src_back}/index.html`,
filename: './index.html',
title: "mode_development", // Нужно для костыльного условия в шаблоне
inject: false,
chunks: baseWebpackConfig.externals.paths.back_chunks,
}),
// Обработка шаблона точки входа "front"
new HtmlWebpackPlugin({
template: `${baseWebpackConfig.externals.paths.src_front}/index.html`,
filename: './pp.html',
title: "mode_development", // Нужно для костыльного условия в шаблоне
inject: false,
chunks: baseWebpackConfig.externals.paths.front_chunks,
}),
]
});
module.exports = new Promise((resolve, reject) => {
resolve(devWebpackConfig);
});
module.exports = {
plugins: [
require('autoprefixer'),
require('css-mqpacker'),
require('cssnano')({
preset: [
'default', {
discardComments: {
removeAll: false,
},
//normalizeUrl: false
}
]
})
]
}
<% if (htmlWebpackPlugin.options.title == 'mode_development') { %>
<!--
Условие
if (htmlWebpackPlugin.options.title == 'mode_development') {
Я использую для того, чтобы отобразить все эти элементы при dev-режиме,
но скрыть при build, т.к. на проекте мне нужен только элемент
<div class="body_content">...</div>
-->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%= htmlWebpackPlugin.options.title || 'Empty title' %></title>
</head>
<body>
<% } %>
<div class="body_content">
<!-- Подгружаем CSS Файлы -->
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[css] %>">
<% } %>
<div id="app">
<buefy-navbar></buefy-navbar>
<router-view></router-view>
</div>
<!-- Подгружаем JS Файлы -->
<% for (var js in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[js] %>"></script>
<% } %>
<script src="https://use.fontawesome.com/a304bad23c.js"></script>
</div>
<% if (htmlWebpackPlugin.options.title == 'mode_development') { %>
</body>
</html>
<% } %>
{
"name": "server-test",
"version": "1.0.0",
"description": "test-descr",
"main": "index.js",
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"autoprefixer": "^9.6.1",
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.0.4",
"css-loader": "^3.2.0",
"css-mqpacker": "^7.0.0",
"cssnano": "^4.1.10",
"fibers": "^4.0.1",
"file-loader": "^4.2.0",
"fs": "0.0.1-security",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
"npm-git-install": "^0.3.0",
"path": "^0.12.7",
"postcss-loader": "^3.0.0",
"pug": "^2.0.4",
"pug-loader": "^2.4.0",
"sass": "^1.22.12",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0",
"vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1",
"webpack-merge": "^4.2.2"
},
"scripts": {
"dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js",
"build": "webpack --config ./build/webpack.build.conf.js"
},
"browserslist": [
"> 1%",
"last 3 version"
],
"author": "serg_x",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/vue-fontawesome": "^0.1.7",
"@riophae/vue-treeselect": "^0.4.0",
"axios": "^0.19.0",
"buefy": "^0.8.4",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.1"
}
}
Описание решения
В этом описании я буду оперировать тем кодом, который представлен выше в конфигах.
В разделе Конфиги с решением вы найдете полный код.
const PATHS = {
src_back: path.join(__dirname, '../src-back'),
src_front: path.join(__dirname, '../src-front'),
// Чанки для back. Вызываются в HtmlWebpackPlugin (в build и dev)
back_chunks : ['common_vendors', 'buefy', 'back' ],
front_chunks: ['common_vendors', 'front' ],
dist: path.join(__dirname, '../docs'),
assets: 'assets/',
}
Указываем две точки входа entry, и сделать динамическим имя для исхзодящих файлов output
// взято из webpack.base.conf.js
// Константа PATHS есть в спойлере в этой статье
entry: {
back: PATHS.src_back,
front: PATHS.src_front,
},
output: {
filename: `js/[name].js?v=[hash]`,
path: PATHS.dist,
publicPath: '/'
},
Вычленяем из output код для чанков common_vendors и buefy
- В
common_vendorsзаписываем то, что нужно везде. - В
buefyпопадают файлы библиотеки Buefy, которая нужная только вback.
Параметр test — Это регулярное выражение, которому должен соответствовать буть к файлу
optimization: {
splitChunks: {
cacheGroups: {
common_vendors: {
test: /[\\/]node_modules[\\/](vue|vue-router|vuex|axios|@riophae[\\/]vue-treeselect)[\\/]/,
name: 'common_vendors', // имя чанка
chunks: 'initial',
enforce: true,
},
buefy: {
test: /[\\/]node_modules[\\/](buefy)[\\/]/,
name: 'buefy',
chunks: 'initial',
enforce: true,
},
}
}
},
Добавить обработку двух шаблонов
Ключевой момент — настройка chunks. Тут мы определяем, какие чанки подгружаем в шаблон.
common_vendors и buefy мы определили на предыдущем этапе, а чанки back и front формируются какбы автоматически по имени точки входа, указанной в параметре entry
// взято из webpack.dev.conf.js и webpack.build.conf.js
// baseWebpackConfig (PATHS) есть в спойлере в этой статье
new HtmlWebpackPlugin({
template: `${baseWebpackConfig.externals.paths.src_back}/index.html`,
filename: './index.html',
title: "mode_development", // Нужно для костыльного условия в шаблоне
inject: false,
chunks: baseWebpackConfig.externals.paths.back_chunks, // ['common_vendors', 'buefy', 'back' ]
}),
// Обработка шаблона точки входа "front"
new HtmlWebpackPlugin({
template: `${baseWebpackConfig.externals.paths.src_front}/index.html`,
filename: './pp.html',
title: "mode_development",
inject: false,
chunks: baseWebpackConfig.externals.paths.front_chunks, // ['common_vendors', 'front' ]
}),
Инъекция файлов в HTML (pug/шмаг) шаблон
В предыдущем пункте мы указали настройку inject: false, поэтому должны сами подключить скрипты в шаблон.
В принципе, это можно сделать и вручную, но давайте не будем так делать:)
<!-- Так не делаем --> <script src="/js/common_vendors.js"></script> <script src="/js/buefy.js"></script> <script src="/js/back.js"></script>
В шаблоне мы можем вызвать конструкцию для подключения JS и CSS:
<!-- Подгружаем CSS Файлы -->
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[css] %>">
<% } %>
<div id="app">
...
</div>
<!-- Подгружаем JS Файлы -->
<% for (var js in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[js] %>"></script>
<% } %>
На этом все
Пожалуйста, напишите в комментариях, получилось ли у вас все настроить, и была ли понятна и корректна та информация, которую вы нашли в этой статье.
Комментарии (4)
Не писать ответ