Webpack. Начало работы, часть 2 из 2
09.07.2022
Теги: Frontend • JavaScript • Node.js • Web-разработка • Конфигурация • Настройка • Установка • ШаблонСайта
Модули и загрузчики
6. Загрузка шрифтов
Давайте скачаем шрифт Roboto
, распакуем архив и положим ttf-файлы в директорию src/font/roboto
. Потом создадим файл src/css/roboto.css
и импортируем его в файле src/css/style.css
. И запустим сборку, чтобы посмотреть, что получилось.
@font-face { /* regular */
font-family: Roboto;
src: url('../font/roboto/Roboto-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-stretch: normal;
font-variant: normal;
}
@font-face { /* italic */
font-family: Roboto;
src: url('../font/roboto/Roboto-Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-stretch: normal;
font-variant: normal;
}
@font-face { /* bold */
font-family: Roboto;
src: url('../font/roboto/Roboto-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-stretch: normal;
font-variant: normal;
}
@font-face { /* bold italic */
font-family: Roboto;
src: url('../font/roboto/Roboto-BoldItalic.ttf') format('truetype');
font-weight: 700;
font-style: italic;
font-stretch: normal;
font-variant: normal;
}
@import url('roboto.css');
body {
margin: 0;
padding: 0;
background-color: #ccc;
box-sizing: border-box;
font-family: 'Roboto', sans-serif;
}
$ npm run build
Все отработало правильно, шрифт подключился, но ttf-файлы оказались в корне директории dist
, создавая беспорядок. Давайте добавим еще одну настройку, чтобы при сборке все ttf-файлы оказались в директории dist/font
. И еще одну настройку, чтобы файлы ресурсов, для которых не указана директория, сохранялись в dist/asset
.
const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { mode: 'development', entry: { main: path.resolve(__dirname, 'src/js/index.js'), }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', // NEW здесь будут файлы ресурсов, для которых не задан путь в настройках загрузчика assetModuleFilename: 'asset/[hash][ext][query]', clean: true, }, plugins: [ new HtmlWebpackPlugin({ title: 'Webpack config example', template: path.resolve(__dirname, 'src/index.html'), // файл шаблона filename: 'index.html', // выходной файл }), new CopyWebpackPlugin({ patterns: [ { from: path.resolve(__dirname, 'src/favicon.png'), to: path.resolve(__dirname, 'dist') }, ], }), ], module: { rules: [ { test: /\.txt$/i, // простой текст type: 'asset/source' }, { test: /\.(png|jpe?g|gif|webp)$/i, // изображение type: 'asset', parser: { dataUrlCondition: { maxSize: 4096 // ограничение 4kb } }, generator: { filename: 'img/[hash][ext][query]' // все изображения в dist/img } }, { test: /\.css$/, // файл css-стилей use: [ 'style-loader', 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: { 'postcss-preset-env': { browsers: 'last 3 versions', }, }, }, }, }, ], }, { test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов type: 'asset/resource', generator: { filename: 'font/[hash][ext][query]' // все шрифты в dist/font } } ], } }
$ npm run build
В принципе, никто не запрещает загрузить шрифты с использованием asset/inline
— в этом случае ttf-файлы будут представлены в файле dist/main.bundle.js
в виде base64 строки.
module.exports = { /* .......... */ module: { rules: [ /* .......... */ { test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов type: 'asset/inline' } ], } }
7. Загрузчик sass-loader
Загружает sass/scss файл и компилирует его в css. Давайте создадим директорию src/scss
и внутри нее — файлы base.scss
, _vars.scss
, _font.scss
.
// файл src/scss/base.scss @import 'vars'; @import 'font'; body { margin: 0; padding: 0; background-color: $background-color; box-sizing: border-box; font-family: $font-family; }
// файл src/scss/_vars.scss $background-color: #ccc; $font-family: 'Roboto', sans-serif;
// файл src/scss/_font.scss @font-face { font-family: Roboto; src: url('../font/roboto/Roboto-Regular.ttf') format('truetype'); font-weight: 400; font-style: normal; font-stretch: normal; font-variant: normal; }
Устанавим sass-loader
, пропишем в файле конфигурации, импортируем base.scss
в файле src/js/index.js
.
$ npm install sass-loader sass --save-dev
module.exports = { /* .......... */ module: { rules: [ /* .......... */ { test: /\.s?css$/, // файл css-стилей use: [ 'style-loader', 'css-loader', { loader: 'postcss-loader', options: { /* .......... */ }, }, 'sass-loader', ], }, /* .......... */ ], } }
import data from '../data/webpack.txt' import logo from '../img/logo.png' import icon from '../img/icon.png' // import '../css/style.css' import '../scss/base.scss' document.body.insertAdjacentHTML('beforeend', `<p>${data}</p>`) document.body.insertAdjacentHTML('afterbegin', `<img src="${logo}" alt="">`) document.body.insertAdjacentHTML('beforeend', `<img src="${icon}" alt="">`)
$ npm run build
8. Загрузчик babel-loader
Позволяет транспилировать js-код ES6+ в js-код ES5, чтобы этот код понимали старые браузеры. Создадим файл User.js
в директории src/js/class
и импортируем класс в файле src/js/index.js
.
class User { name = 'Аноним' hello() { console.log(`Привет, ${this.name}!`) } } export default User
import data from '../data/webpack.txt' import logo from '../img/logo.png' import icon from '../img/icon.png' // import '../css/style.css' import '../scss/base.scss' import User from './class/User.js' document.body.insertAdjacentHTML('beforeend', `<p>${data}</p>`) document.body.insertAdjacentHTML('afterbegin', `<img src="${logo}" alt="">`) document.body.insertAdjacentHTML('beforeend', `<img src="${icon}" alt="">`) const user = new User() user.hello()
Установим загрузчик и все необходимое для транспиляции js-кода, пропишем в файле конфигурации обработку js-файлов.
$ npm install babel-loader @babel/core @babel/preset-env --save-dev
module.exports = { /* .......... */ module: { rules: [ /* .......... */ { test: /\.js$/, // js-файлы, транспиляция exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ], } }
$ npm run build
9. Загрузчик ts-loader
Позволяет транспилировать ts-код в js-код ES5, чтобы этот код понимали старые браузеры. Создадим файл src/ts/index.ts
, который будет новой точкой входа. Изменим файл конфигурации, чтобы обрабатывать ts-файлы, добавим файл конфигурации tsconfig.json
и запустим сборку.
$ npm install ts-loader typescript --save-dev
import User from './class/User' const user: User = new User() user.hello()
class User { name = 'Аноним' hello(): void { console.log(`Привет, ${this.name}!`) } } export default User
module.exports = { mode: 'development', entry: { main: path.resolve(__dirname, 'src/ts/index.ts'), // NEW новая точка входа }, output: { /* .......... */ }, resolve: { extensions: ['.ts', '...'], // NEW добавляем поддержку ts-файлов }, plugins: [ /* .......... */ ], module: { rules: [ /* .......... */ { test: /\.ts$/, // NEW транспиляция ts-файлов exclude: /node_modules/, use: { loader: 'ts-loader', } } ], } }
{ "compilerOptions": { "target": "es5" } }
$ npm run build
Еще один вариант обработки ts-файлов — вместо ts-loader
использовать @babel/preset-typescript
.
$ npm install @babel/preset-typescript --save-dev
module.exports = { mode: 'development', entry: { main: path.resolve(__dirname, 'src/ts/index.ts'), // NEW новая точка входа }, output: { /* .......... */ }, resolve: { extensions: ['.ts', '...'], // NEW добавляем поддержку ts-файлов }, plugins: [ /* .......... */ ], module: { rules: [ /* .......... */ { test: /\.(ts|js)$/, // транспиляция js- и ts-файлов exclude: /node_modules/, use: { loader: 'babel-loader', options: { // NEW используем @babel/preset-typescript presets: ['@babel/preset-env', '@babel/preset-typescript'] } } }, ], } }
$ npm run build
10. Файл css-стилей
Сейчас стили добавляются загрузчиком style-loader
, который динамически вставляет тег <style>
внутрь <head>
. Но можно сохранить стили как css-файл и подключить его с помощью <link>
внутри <head>
.
$ npm install mini-css-extract-plugin --save-dev
/* .......... */ const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { /* .......... */ plugins: [ /* .......... */ new MiniCssExtractPlugin() // NEW плагин ], module: { rules: [ /* .......... */ { test: /\.s?css$/, // файл css-стилей use: [ MiniCssExtractPlugin.loader, // NEW загрузчик 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: { 'postcss-preset-env': { browsers: 'last 3 versions', }, }, }, }, }, 'sass-loader', ], }, /* .......... */ ], }, /* .......... */ }
Сервер разработки
Установим пакет webpack-dev-server
, зададим для него настройки в webpack.config.js
и добавим команду start
в файл package.json
.
$ npm install webpack-dev-server --save-dev
module.exports = { /* .......... */ devServer: { port: 9000, open: true, // открыть браузер } watchOptions: { ignored: /node_modules/, // не отслеживать node_modules poll: 1000, // проверять изменения каждую секунду }, }
poll
нет небходимости, но она позволяет решить проблемы с NFS, VirtualBox и Docker.
{ .......... "scripts": { "start": "webpack serve", // запустить сервер, выполянять сборку при изменении файлов "build": "webpack build", // выполнить сборку проекта "watch": "webpack watch" // выполянять сборку при изменении файлов }, .......... }
Если есть несколько точек входа, нужно добавить еще одну настройку, чтобы избежать проблем (см.документацию):
module.exports = { /* .......... */ optimization: { runtimeChunk: 'single', }, }
$ npm run start
При запуске сервера настройка watch
всегда имеет значение true
. Вебпак отслеживает файлы в директории проекта (за исключением node_modules
) и при изменении — выполняет пересборку. Но сохраняет файлы сборки не в директорию dist
, а в виртуальную директорию в оперативной памяти. То есть, в отличие от команд build
и watch
— директория dist
даже не создается. Посмотреть файлы сборки можно, если в браузере перейти по адресу
http://localhost:9000/webpack-dev-server
Впрочем, такое поведение можно изменить и сохранять файлы сборки в директории dist
— тогда у нас будет актуальная копия виртуальной директории.
module.exports = { /* .......... */ devServer: { port: 9000, open: true, // открыть браузер devMiddleware: { writeToDisk: true, // записывать файлы сборки на диск }, }, watchOptions: { ignored: /node_modules/, // не отслеживать node_modules poll: 1000, // проверять изменения каждую секунду }, }
Но здесь есть неприятный сюрприз — старые файлы сборки не удаляются, опция output.clean
не работает, так что лучше использовать плагин CleanWebpackPlugin
. Разработчики вебпак знают об этом баге, но не спешат его исправлять.
Режим сборки
Может быть development
или production
, до сих пор мы использовали только development
. В режиме production
вебпак удаляет комментарии и минифицирует код. Давайте будем передавать командам в файле package.json
переменную окружения NODE_DEV
, а в файле webpack.config.js
проверять значение этой переменной. В Windows и Linux переменные окружения устанавливаются по разному, так что установим пакет cross-env
.
$ npm install cross-env --save-dev
{ .......... "scripts": { "start": "webpack serve", "build": "webpack build", "watch": "webpack watch", "build:prod": "cross-env NODE_ENV=production webpack" }, .......... }
NODE_ENV
можно и с помощью опции командной строки --node-env
.
const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const buildMode = process.env.NODE_ENV === 'production' ? 'production' : 'development' const outputDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist' const sourceMap = process.env.NODE_ENV === 'production' ? 'nosources-source-map' : 'eval-source-map' const loaderCSS = process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader' const config = { mode: buildMode, // NEW режим сборки devtool: sourceMap, // NEW карта кода entry: { main: path.resolve(__dirname, 'src/ts/index.ts'), }, output: { path: path.resolve(__dirname, outputDir), // NEW точка выхода filename: '[name].[contenthash].js', // здесь будут файлы ресурсов, для которых не задан путь в настройках загрузчика assetModuleFilename: 'asset/[hash][ext][query]', clean: true }, resolve: { extensions: ['.ts', '...'], }, plugins: [ new HtmlWebpackPlugin({ title: 'Webpack config example', template: path.resolve(__dirname, 'src/index.html'), // файл шаблона filename: 'index.html', // выходной файл }), new CopyWebpackPlugin({ patterns: [ { from: path.resolve(__dirname, 'src/favicon.png'), to: path.resolve(__dirname, outputDir) // NEW куда копировать }, ], }), // NEW еще один плагин для production ], module: { rules: [ { test: /\.txt$/i, // простой текст type: 'asset/source' }, { test: /\.(png|jpe?g|gif|webp)$/i, // изображение type: 'asset', parser: { dataUrlCondition: { maxSize: 4096 // ограничение 4kb } }, generator: { filename: 'img/[hash][ext][query]' // все изображения в dist/img или build/img } }, { test: /\.s?css$/, // файл css-стилей use: [ loaderCSS, // NEW style или link в head 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: { 'postcss-preset-env': { browsers: 'last 3 versions', }, }, }, }, }, 'sass-loader', ], }, { test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов type: 'asset/resource', generator: { filename: 'font/[hash][ext][query]' // все шрифты в dist/font или build/font } }, { test: /\.(ts|js)$/, // js и ts файлы, транспиляция exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-typescript'] } } }, ], }, devServer: { port: 9000, open: true, // открыть браузер }, watchOptions: { ignored: /node_modules/, // не отслеживать node_modules poll: 1000, // проверять изменения каждую секунду }, } if (process.env.NODE_ENV === 'production') { config.plugins.push( new MiniCssExtractPlugin({filename: '[name].[contenthash].css'}) ) } module.exports = config
Сборка для production
у нас теперь будет в директории build
, можно запустить Live Server (см. здесь), чтобы проверить работу сборки (нужно изменить настройки, потому что по умолчанию Live Server обслуживает dist
). Но не следует запускать dev-сервер вебпак для обслуживания этой директории — нужно помнить, что он обслуживает виртуальную директорию.
Сейчас отличий при сборке в режиме development
и production
немного, но это учебный проект. В реальной жизни отличий может быть очень много, и код в webpack.config.js
может быть весьма запутанным. Но можно создать файлы webpack.development.js
и webpack.production.js
и передавать имя файла конфигурации в командной строке.
{ .......... "scripts": { "build": "webpack build --config webpack.development.js", "build:prod": "webpack build --config webpack.production.js" }, .......... }
Но тут тоже есть подводные камни — многие опции не отличаются для development
и production
. Так что при изменении такой опции — изменять нужно сразу в двух местах. Но есть решение и на этот случай — вынести такие опции в отдельный файл webpack.common.js
и объединять файлы конфигурации.
$ npm install webpack-merge --save-dev
// файл webpack.config.js const { merge } = require('webpack-merge') const commonConfig = require('./webpack.common.js') module.exports = (env, argv) => { const buildMode = process.env.NODE_ENV === 'production' ? 'production' : 'development' const modeConfig = require(`./webpack.${buildMode}.js`) return merge(commonConfig, modeConfig) }
// файл webpack.common.js const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { entry: { main: path.resolve(__dirname, 'src/ts/index.ts'), }, resolve: { extensions: ['.ts', '...'], }, plugins: [ new HtmlWebpackPlugin({ title: 'Webpack config example', template: path.resolve(__dirname, 'src/index.html'), // файл шаблона filename: 'index.html', // выходной файл }), ], module: { rules: [ { test: /\.txt$/i, // простой текст type: 'asset/source' }, { test: /\.(png|jpe?g|gif|webp)$/i, // изображение type: 'asset', parser: { dataUrlCondition: { maxSize: 4096 // ограничение 4kb } }, generator: { filename: 'img/[hash][ext][query]' // все изображения в dist/img или build/img } }, { test: /\.(woff2?|eot|ttf|otf)$/i, // файлы шрифтов type: 'asset/resource', generator: { filename: 'font/[hash][ext][query]' // все шрифты в dist/font или build/font } }, { test: /\.(ts|js)$/, // js и ts файлы, транспиляция exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-typescript'] } } }, ], }, }
// файл webpack.development.js const path = require('path') const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { mode: 'development', devtool: 'eval-source-map', output: { path: path.resolve(__dirname, 'dist'), // директория dist filename: '[name].[contenthash].js', assetModuleFilename: 'asset/[hash][ext][query]', clean: true }, plugins: [ new CopyWebpackPlugin({ patterns: [ { from: path.resolve(__dirname, 'src/favicon.png'), to: path.resolve(__dirname, 'dist') // директория dist }, ], }), ], module: { rules: [ { test: /\.s?css$/, // файл css-стилей use: [ 'style-loader', // <style> внутри <head> 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: { 'postcss-preset-env': { browsers: 'last 3 versions', }, }, }, }, }, 'sass-loader', ], }, ] }, devServer: { port: 9000, open: true, // открыть браузер }, watchOptions: { ignored: /node_modules/, // не отслеживать node_modules poll: 1000, // проверять изменения каждую секунду }, }
// файл webpack.production.js const path = require('path') const CopyWebpackPlugin = require('copy-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { mode: 'production', devtool: 'nosources-source-map', output: { path: path.resolve(__dirname, 'build'), // директория build filename: '[name].[contenthash].js', assetModuleFilename: 'asset/[hash][ext][query]', clean: true }, plugins: [ new CopyWebpackPlugin({ patterns: [ { from: path.resolve(__dirname, 'src/favicon.png'), to: path.resolve(__dirname, 'build') // директория build }, ], }), new MiniCssExtractPlugin({ // <link> внутри <head> filename: '[name].[contenthash].css' }) ], module: { rules: [ { test: /\.s?css$/, // файл css-стилей use: [ MiniCssExtractPlugin.loader, // <link> внутри <head> 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: { 'postcss-preset-env': { browsers: 'last 3 versions', }, }, }, }, }, 'sass-loader', ], }, ] }, }
Вебпак будет по-прежнему искать файл конфигурации по умолчанию webpack.config.js
, но поскольку мы устанавливаем значение NODE_ENV
— будет объединение конфигурации из webpack.common.js
с конфигурацией из webpack.development.js
или webpack.production.js
.
Исходные коды здесь.
- Webpack. Начало работы, часть 1 из 2
- Расширение «ESLint» для VS Code, часть 2 из 2
- Расширение «ESLint» для VS Code, часть 1 из 2
- Расширение «Prettier — Code formatter» для VS Code
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
Поиск: Frontend • JavaScript • Node.js • Web-разработка • Конфигурация • Настройка • Установка • Шаблон сайта • Webpack • Сборка