"Разделяй и владей" е максима, която се приписва на Филип Македонски и следвана стриктно от Юлий Цезар и Наполеон Бонапард. Тя е валидна и в програмирането. Със сигурност ще чуете, че модулизиран код се поддържа лесно. Обратното -- трупане на всичкия код на едно място, често пъти се оприличава на "спагети".
Оригинално терминът "спагети код" идва от goto
инструкции в някои по-стари езици за програмиране. Навремето goto
е бил единственият начин да се върнеш на старо "парче" код и то да се изпълни наново. В провитен случай не е можело да бъдат реализирани цикли.
Модерните езици за програмиране обаче позволяват за извикване на функции и имат купища методологии за преизползване на код. Въпреки всичко, това не означава, че автоматично кодът ви ще е добре капсулован.
Един сайт (или програма) винаги се състои от множество функционалности. Възможно е те понякога да си взаимодействат, но принципно са разделени. Ако нямаме модули в кода си, а само хвърчащи функции, които се викат една друга -- пак се получават едни спагети. Просто вместо goto
викаме функция. Крайният резултат обаче е един и същ.
Правилното оформяне и модулизиране на код често пъти се подпомага от физическото му разделяне във файлове. При много сайтове ще видите заредени по няколко .CSS или .JS файла. Най-малкото -- някои идват от plugin-и.
От една страна това води до ползи по подрръжка на кода. От друга обаче се зареждат множество файлове на страницата ни. А всеки файл кара браузър да напрарви нова връзка към сървъра (игнорираме HTTP2 протокол). Зареждането на 10 файла по 10KB става по-бавно от зареждането на 1 файл от 100KB. И така излиза, че е най-добре всичко да бъде в един файл.
От няколко години вече е като стандарт статичните файлове в един сайт да минават някаква обработка. В следствие на нея може да се ползва по-модерен синтаксис. Той се преправя в код, който се поддържа от всички браузъри и позволява няколко файла да се събират в един.
Помощните средства за това за Task Runners и Bundlers. Двата типа инструменти може да се ползват по отделно или да се свържат.
В този урок ще говорим за bundlers (един по-контретен), а някой друг път може би и за task runners.
Някои от по-популярните са Browserify, Brunch, Parcel и Webpack.
По принцип основното предназначение на bundler-ите е да съберат няколко файла в един. Това само по себе си естествено е прекалено елементарно. Нещо подобно бях описал и в урока си GIT post-merge hook за комбиниране и minify-ване на JavaScript.
Модерните bundler-и правят не само това. Те включват и обработка на самия код, която може би е най-редно да се нарече transpile (транспилиране). Терминът е много близък до compile (компилиране) и разликата е наистина малка.
Транспилирането може да се смята за компилиране. То е по-скоро компилиране при специялни обстоятелства. Транспилирането е копмилиране, при което запазваме сходно ниво на абстракция. Така че "компилиране" винаги е правилен термин, но не винаги е най-точно описващия.
Източник: Compiling vs Transpiling
Това транспилиране може да позволи да ползваме нов синтаксис, който не се поддържа от всички браузъри. При транспилиране кодът се променя, така че да свърши същата работа, но със стария синтаксис.
Ползата е, че новите синтаксиси най-често са по-кратки и по-лесни за писане. От там -- по-лесни и за поддръжка.
Това го има и в предния ми пример, но зависи от външна услуга. В коментарите към урока се споменава, че не е перфектно решение. Bundler-ите често пъти имат plugin-и за минифициране на кода.
За който не е запознат -- това отново е свързано с промени по синтаксиса. Тук идеята не е поддръжка от стари браузъри, а намаляне на дължината. Примерно -- ако имам често-срещана променлива с много дълго име -- при минифициране тя може да се преименува просто на a
. Така ще спестим ценни байтове при зареждане на script-ове.
Колко ще спестите зависи от дължината на файла. Зависи също и от минификатора, който ще използвате (има твърде много). Но някакви средно стойности показват, че файловете намаляват с 30-40%. Това си е много добре!
Източник: A measurement study of JavaScript minification effectiveness
Стигаме и до момента, в който вече ще видим как да добавим webpack към проектите си.
Най-добре е това да се прави за нови проекти. Ако трябва да преправяме стари, към които да се добавя webpack може да се наложи голяма преработка. Ако има ентусиасти за подобно нещо -- чувствайте се свободни да опитате. Знайте обаче, че вероятно ще псувате доста.
Аз лично използвам webpack с няколко цели:
- транспилиране на SCSS към CSS
- транспилиране на ESNext към JS
- комбиниране на файловете в един (bundling)
- минифициране на двата крайни .CSS и .JS файла
Първо трябва да иточним, че webpack е разработен с Node.JS, така че първо трябва да си изтеглите и инсталирате него.
Идете на https://nodejs.org/en/ и го изтеглете. Най-добре да си вземете LTS (long-term support) версията, а не "Current". LTS е само малко по-стара, но много по-стабилна. Редовно с излизане на по-нов current се пуска и по-нов LTS, така че не се притеснявайте, че е outdated.
Инсталирайки си Node.JS през конзолата вече ще можете да викате и npm
команди.
Можете да ползвате и Command Prompt на Windows, но съветвам да си инсталирате CygWin или друг емулатор за *NIX команди.
От настройките на Windows 10 можете да си включите пълен Ubuntu Bash, който вкючва цялостна Linux Ubuntu конзола. Можете даже да си инсталирате програми с apt-get install
. За повече информация как да направите това можете да прочетете Ubuntu Bash on Windows 10 overview (setup, shortcomings, making it better and verdict);
Аз лично предпочитам bash конзолата, която идва при инсталиране на Git за Windows.
Някои мои колеги от работа пък харесват ConEmu.
Първо си създайте нова папка на локалния си сървър. Отворете конзола и се уверете, че сте навигирали до тази папка. Напишете npm init
за да инициализирате нов проект.
Ще започне процес, който ви пита за основна информация за прокта ви. Както ще въведете няма особено голямо значение. Само името да не съвпада с някой публичен проект (гледайте да е уникално).
Като резултат в папката ще се създаде нов файл с име package.json
с подобно съдържание:
{
"name": "webpack-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
След като вече имаме проект трябва да си инсталираме и така наречените "зависимости". Това са инструменти / библиотеки, чиято функционалност ще използваме в проекта си.
Има 2 основни типа зависимости:
- нужни за да върви крайния вариант на проекта ни
- нужни за разработка на проекта ни
Пример за втория тип са инструменти за транспилиране. След като е готов крайния код -- няма нужда да качваме тях на публичен сървър с останалия source code.
Напишете npm install webpack --save-dev
.
install
еnpm
подкомантада да инсталиране на нов модул.webpack
е името на модула, който искаме да инсталираме--save-dev
указва, че това е зависимост за разработка, а не за крайния проект сам со себе си
Към package.json
файла трябва да са се добавили следните редове:
"devDependencies": {
"webpack": "^4.29.6"
}
Нека в папката създадем следната файлова структура:
webpack-project
|-dist
|-src
| |-js
| | |-functions.js
| `-scss
| `-style.scss
|-index.html
Иначе казано:
- в папката на проекта да имаме
index.html
- да имаме и празна папка
dist
- да има още една папка с име
src
- в последната да има още 2 папки:
js
иscss
- в
js
да имаме файл с имеfunctions.js
- в папка
scss
да има файл с имеstyle.scss
В папка dist
ще бъдат записани файловете, които webpack ще генерира на базата на кодът ни от src
.
Съдържанието на този файл може да е просто:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>webpack test</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="dist/style.bundle.css" />
</head>
<body>
<script src="dist/functions.bundle.js"></script>
</body>
</html>
Не се притеснявайте, че все още не съществуват файлове dist/style.css
и dist/functions.bundle.js
и зареждане на файла ще покаже грешки.
Следващата стъпка е да се създадем webpack.config.js
файл. Там ще опишем настройките, така че webpack да свърши работата, която искаме.
Първото нещо, с което webpack
конфигурацията започва е Вход (Entry). Това е описано и в официалния сайт на webpack: https://webpack.js.org/concepts/.
Тук ще опишем от къде трябва да започне webpack при обработка на кода ни.
За да опишем път до файловете от src/js
и src/scss
обаче първо трябва да заредим още един модул: path
.
Напишете npm install path --save-dev
в конзолата.
Сега вече в webpack.config.js
можем да добавим следното съдържание:
const path = require('path');
module.exports = {
entry: [
path.resolve('./src/js/functions.js'),
path.resolve('./src/scss/style.scss')
]
};
module.exports
е синтаксис от ESNext, който прави публични описаните настройки, при зареждане от външен файл. Сега няма да задълбаваме точно какво значи това, а ще оставим темата за бъдещ урок.
В entry
подаваме масив с пътища до стартови файлове за webpack. Имаме 2 -- за JS и SCSS, които ще искаме да бъдат транспилирани.
Следващата стъпка при конфигуриране на webpack е Изход (Output). Тук описваме точно къде да бъдат записани новите файлове, след транспилиране на входните.
Нека променим съдържанието на webpack.config.js
на:
const path = require('path');
module.exports = {
entry: [
path.resolve('./src/js/functions.js'),
path.resolve('./src/scss/style.scss')
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'functions.bundle.js'
}
};
В output
описваме папката и името само на bundle-натия JS файл. Името на CSS файла ще зададем по-късно, когато заредим и plugin, който да обработва SCSS. webpack няма вградена поддръжка за SCSS, а само за JS bundling.
За да накараме webpack да транспилира кода ни трябва да извикаме команда. Тя обаче първо трябва да се опише в нашия package.json
файл.
В момента за scripts
вероятно имате:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
Изтрийте този "test"
и добавете:
"scripts": {
"dev": "webpack --mode=development"
}
scripts
от package.json
се пускат чрез: npm run <script_name>
. Където <script_name>
(без ъгловите скоби) е името на script-а, който сте дефинирали в package.json
. Там можете да имате няколко дефинирани script-а.
Това означава, че в момента npm run dev
ще ви изпълни webpack --mode=development
в контекста на Node.JS и проекта си.
Ключовото в предното обяснение е "в контекста на Node.JS и проекта си". Ако нямате webpack
инсталиран глобално, а само като dev-dependency
в проекта, ще получите грешка, ако се опитате да изпълните директно webpack
команда през конзолата.
Ако обаче имате глобално инсталиран webpack
-- това ще сработи.
Останалата част от комантада: --mode=development
указва на webpack, че в момента сме в среда на разработка, а не краен сървър, което подава различна информация към webpack plugin-ите, които ще инсталираме после. На база на средата може да минифицираме или не JS кода си.
Пуснете го и вероятно ще видите съобщение, че за да работи webpack трябва да се инсталира и webpack-cli
модул.
Наришете като отговор просто "yes" в конзолата и натиснете "Enter". Това ще добави webpack-cli
към dev dependencies
Като резултат трябва да видите новосъздаден файл с име functions.bundle.js
в папка dist
. Ако въведете някакъв код в style.scss
обаче ще получите грешка, че webpack не може да го прочете. Това е поради факта, който споменах по-рано, че ще ни трябва модул за SCSS.
Макар и webpack да може да bundle-ва JS файлове, не може по подразбиране да транспилира ESNext към JS. За целта също трябва да инсталираме модули.
Нека инсталираме:
npm install @babel/core @babel/preset-env babel-loader --save-dev
При инсталиране на зависимости можем да изрезим имената на няколко разделени с интервал.
Така по-горе инсталираме едновременно:
@babel/core
@babel/preset-env
babel-loader
Сега нека добавим следното в webpack.config.js
:
const path = require('path');
module.exports = {
entry: [
path.resolve('./src/js/functions.js'),
path.resolve('./src/scss/style.scss')
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'functions.bundle.js'
},
module: {
rules: [
{
exclude: /node_modules/,
test: /\.js$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
compact: true
}
}
]
}
};
За модулите задаваме rules
, което е масив с правила кой файл как да се обработва.
За exclude
задаваме webpack
да игнорира папката /node_modules/
, тъй като там има много файлове и може да забавят изпълнението.
За test
трябва с регулярен израз да опишем имена на файлове, които webpack
да зареди. В случая изразът, който сме описали е за файлове, чието име завършва с ".js".
Loader-ите за webpack са важна част, която позволяват на webpack да "прочете" съдържанието на файловете. Така той може да направи нужните обработки.
Като опции в момента задаваме babel да зареди стандартни настройки от модула @babel/preset-env
. Те зависят от средата (development или production). Тази среда е пряко свързана с dev
командата, която дефинирахме в package.json
. Спомнете си, че там в момента имахме --mode=development
.
Идва рен и на SCSS.
Първо нека инсталираме:
npm install mini-css-extract-plugin css-loader sass-loader node-sass --save-dev
По принцип само css-loader
ще позволи на webpack да чете CSS файлове. Допълнително са ни нужни и sass-loader
и node-sass
за да може той да обработи специфичния синтаксис на SASS и SCSS.
До тук webpack ще може да включи нашите стилове. Той ще ги превърне в JS и ще ги добави във functions.bundle.js
.
Този plugin позволява CSS-а ни да не иде в JS-а, а да си остане в отделни CSS файлове.
Но така ще имаме 1 файл за зареждане повече! Нали правим bundle за да намалим броя заявки към сървъра?
1 или 2 заявки само по себе си не е чак такъв проблем.
Аз обаче предпочитам хора с изключен JS на компютъра си да могат все пак да заредят CSS-а. Ако той е включен в functions.bundle.js
, тогава сайтът ни ще е гол HTML без никакви стилове. Това лично на мене не ми харесва.
Нека редактираме файла си, така че да имаме:
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: [
path.resolve('./src/js/functions.js'),
path.resolve('./src/scss/style.scss')
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'functions.bundle.js'
},
module: {
rules: [
{
exclude: /node_modules/,
test: /\.js$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
compact: true
}
},
{
exclude: /node_modules/,
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader'
},
{
loader: 'sass-loader'
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'style.bundle.css'
})
]
};
Като цяло настройките са аналогични. Отново изключваме node_modules
и задаваме с test
зареждане на .scss
файлове.
Този път обаче ни трябват няколко loader-а. Затова подаваме use
, който е масив от loader
-и.
Накрая обаче трябва да инстанцираме самия MiniCssExtractPlugin
plugin и да зададем име на файла, където ще записваме информацията.
Сега вече при изпълнение на npm run dev
ще генерираме dist/functions.bundle.js
и dist/style.bundle.css
.
За момента имаме само dev
script, който да транспилира кода. При празен src/js/functions.js
обаче, изходния dist/functions.bundle.js
е 5KB. Това е защото включва стартов код, който после ще ни трябва за зареждане на библиотеки.
Ще е добре да го оптимизираме.
Първото, което ще направим е да добавим в package.json
и следния script:
"build": "webpack --mode=production"
Имаме напредък -- файлът вече е 1 KB. Това е защото преди имахме множество коментари, които вече са напълно премахнати.
Нека оптимизираме кода още повече.
Нека инсталираме:
npm install uglifyjs-webpack-plugin --save-dev
След това да го заредим в webpack.config.js
:
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
entry: [
path.resolve('./src/js/functions.js'),
path.resolve('./src/scss/style.scss')
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'functions.bundle.js'
},
module: {
rules: [
{
exclude: /node_modules/,
test: /\.js$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
compact: true
}
},
{
exclude: /node_modules/,
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader'
},
{
loader: 'sass-loader'
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'style.bundle.css'
})
],
optimization: {
minimizer: [new UglifyJsPlugin({
test: /\.js$/
})]
}
};
Зареждаме const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
още в началото на файла. Накрая това добавяме и optimization
property към настройките, където опидваме минификатор.
Това няма да намали размера на текущия ни JS файл, тъй като нямаме собствен код. Но добявайки код във src/js/functions.js
той вече ще бъде минифициран и bundle файла ще остане възможно най-малък.
Всичко до момента щеше да е супер, ако не ни бавеше.
Сега трябва при всяка промяна, която правим, да пуснем поне dev
script-а, да изчакаме webpack да транспилира файловете и тогава вече да проверим в браузъра за състоянието на сайта. Това отнема страшно много време.
За това разработчиците на webpack са направили възможност той да върви постоянно. И когато засече промяна по файловете ни -- да ги транспилира възможно най-бързо самичък.
Неща добавим и следния script в package.json
:
"watch": "npm run dev -- -w"
Името е watch
, а това, което прави е npm run dev -- -w
.
Получава се една рекурсия -- npm run watch
ще изпъли npm run dev
. Обаче накрая имаме -- -w
.
Двете тирета означават, че информацията след тях се подава като допълнителни параметри на предния script. Така че npm run dev -- -w
се превежда до webpack --mode=development -w
.
Можехме да го въведем и така. С рекурсията обаче се уверяваме, че ако добавим още аргументи към dev
script-а, няма да има нужда да ги повтаряме и за watch
. Вместо това те ще си "захапят" автоматично.
Опитайте да добавите код към src/js/functions.js
файла. Примерно:
alert('JavaScript');
Още докато запишете файла и почти моментално webpack вече ще е готов с транспилирането. При което можете да заредите index.html страницата и да видите резултата.
Още нещо, с което можем да си оптимизираме работата е BrowserSync.
Това е Node.JS инструмент, който позволява без да refresh-вате сайтът си на ръка, автоматично да презарежда.
Особено удобно е, ако работите на 2 монитора. На единия си гледате сайта, а на другия си пишете CSS. Докато save-нете файла и виждате промяната, без дори да сменяте фокуса от редактора си на браузъра.
За целта трябва да инсталираме:
npm install browser-sync browser-sync-webpack-plugin --save-dev
После в webpack.config.js
трябва са заредим:
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
А към plugins да добавим:
new BrowserSyncPlugin({
host: 'localhost',
server: {
baseDir: ['.']
}
})
При извикване на watch трябва BrowserSync да тръгне автоматично.
Накрая трябва да имаме:
{
"name": "webpack-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack --mode=development",
"watch": "npm run dev -- -w",
"build": "webpack --mode=production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"babel-loader": "^8.0.5",
"browser-sync": "^2.26.3",
"browser-sync-webpack-plugin": "^2.2.2",
"css-loader": "^2.1.0",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.11.0",
"path": "^0.12.7",
"sass-loader": "^7.1.0",
"uglifyjs-webpack-plugin": "^2.1.2",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3"
}
}
const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
module.exports = {
entry: [
path.resolve('./src/js/functions.js'),
path.resolve('./src/scss/style.scss')
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'functions.bundle.js'
},
module: {
rules: [
{
exclude: /node_modules/,
test: /\.js$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
compact: true
}
},
{
exclude: /node_modules/,
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader'
},
{
loader: 'sass-loader'
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'style.bundle.css'
}),
new BrowserSyncPlugin({
host: 'localhost',
server: {
baseDir: ['.']
}
})
],
optimization: {
minimizer: [new UglifyJsPlugin({
test: /\.js$/
})]
}
};