Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

"Разделяй и владей" е максима, която се приписва на Филип Македонски и следвана стриктно от Юлий Цезар и Наполеон Бонапард. Тя е валидна и в програмирането. Със сигурност ще чуете, че модулизиран код се поддържа лесно. Обратното -- трупане на всичкия код на едно място, често пъти се оприличава на "спагети".

goto = функции

Оригинално терминът "спагети код" идва от goto инструкции в някои по-стари езици за програмиране. Навремето goto е бил единственият начин да се върнеш на старо "парче" код и то да се изпълни наново. В провитен случай не е можело да бъдат реализирани цикли.

Модерните езици за програмиране обаче позволяват за извикване на функции и имат купища методологии за преизползване на код. Въпреки всичко, това не означава, че автоматично кодът ви ще е добре капсулован.

Един сайт (или програма) винаги се състои от множество функционалности. Възможно е те понякога да си взаимодействат, но принципно са разделени. Ако нямаме модули в кода си, а само хвърчащи функции, които се викат една друга -- пак се получават едни спагети. Просто вместо goto викаме функция. Крайният резултат обаче е един и същ.

"Физическо" разделяне на кода

Правилното оформяне и модулизиране на код често пъти се подпомага от физическото му разделяне във файлове. При много сайтове ще видите заредени по няколко .CSS или .JS файла. Най-малкото -- някои идват от plugin-и.

От една страна това води до ползи по подрръжка на кода. От друга обаче се зареждат множество файлове на страницата ни. А всеки файл кара браузър да напрарви нова връзка към сървъра (игнорираме HTTP2 протокол). Зареждането на 10 файла по 10KB става по-бавно от зареждането на 1 файл от 100KB. И така излиза, че е най-добре всичко да бъде в един файл.

Как да се справим тогава, с поддръжката на този огромен файл?

От няколко години вече е като стандарт статичните файлове в един сайт да минават някаква обработка. В следствие на нея може да се ползва по-модерен синтаксис. Той се преправя в код, който се поддържа от всички браузъри и позволява няколко файла да се събират в един.

Помощните средства за това за Task Runners и Bundlers. Двата типа инструменти може да се ползват по отделно или да се свържат.

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 може да се наложи голяма преработка. Ако има ентусиасти за подобно нещо -- чувствайте се свободни да опитате. Знайте обаче, че вероятно ще псувате доста.

Аз лично използвам webpack с няколко цели:

  1. транспилиране на SCSS към CSS
  2. транспилиране на ESNext към JS
  3. комбиниране на файловете в един (bundling)
  4. минифициране на двата крайни .CSS и .JS файла

Инициализиране на проект и инсталиране на webpack

Node.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 команди.

*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

Първо си създайте нова папка на локалния си сървър. Отворете конзола и се уверете, че сте навигирали до тази папка. Напишете 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"
}
Зависимости (dependencies)

След като вече имаме проект трябва да си инсталираме и така наречените "зависимости". Това са инструменти / библиотеки, чиято функционалност ще използваме в проекта си.

Има 2 основни типа зависимости:

  1. нужни за да върви крайния вариант на проекта ни
  2. нужни за разработка на проекта ни

Пример за втория тип са инструменти за транспилиране. След като е готов крайния код -- няма нужда да качваме тях на публичен сървър с останалия source code.

Инсталиране на webpack dev dependency

Напишете 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.

Код за index.html

Съдържанието на този файл може да е просто:

<!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

Следващата стъпка е да се създадем webpack.config.js файл. Там ще опишем настройките, така че webpack да свърши работата, която искаме.

Първото нещо, с което webpack конфигурацията започва е Вход (Entry). Това е описано и в официалния сайт на webpack: https://webpack.js.org/concepts/.

Entry

Тук ще опишем от къде трябва да започне 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, които ще искаме да бъдат транспилирани.

Output

Следващата стъпка при конфигуриране на 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

За да накараме 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 и проекта си.

Защо не пишем направо webpack --mode=development в конзолата, а npm run deb?

Ключовото в предното обяснение е "в контекста на Node.JS и проекта си". Ако нямате webpack инсталиран глобално, а само като dev-dependency в проекта, ще получите грешка, ако се опитате да изпълните директно webpack команда през конзолата.

Ако обаче имате глобално инсталиран webpack -- това ще сработи.

Останалата част от комантада: --mode=development указва на webpack, че в момента сме в среда на разработка, а не краен сървър, което подава различна информация към webpack plugin-ите, които ще инсталираме после. На база на средата може да минифицираме или не JS кода си.

npm run dev

Пуснете го и вероятно ще видите съобщение, че за да работи webpack трябва да се инсталира и webpack-cli модул.

Наришете като отговор просто "yes" в конзолата и натиснете "Enter". Това ще добави webpack-cli към dev dependencies

Като резултат трябва да видите новосъздаден файл с име functions.bundle.js в папка dist. Ако въведете някакъв код в style.scss обаче ще получите грешка, че webpack не може да го прочете. Това е поради факта, който споменах по-рано, че ще ни трябва модул за SCSS.

webpack модули

Макар и webpack да може да bundle-ва JS файлове, не може по подразбиране да транспилира ESNext към JS. За целта също трябва да инсталираме модули.

Нека инсталираме:

npm install @babel/core @babel/preset-env babel-loader --save-dev

При инсталиране на зависимости можем да изрезим имената на няколко разделени с интервал.

Така по-горе инсталираме едновременно:

  • @babel/core
  • @babel/preset-env
  • babel-loader
Зареждане на babel модул в webpack

Сега нека добавим следното в 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 модул в webpack

Идва рен и на 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.

Защо ни е mini-css-extract-plugin

Този plugin позволява CSS-а ни да не иде в JS-а, а да си остане в отделни CSS файлове.

Но така ще имаме 1 файл за зареждане повече! Нали правим bundle за да намалим броя заявки към сървъра?

1 или 2 заявки само по себе си не е чак такъв проблем.

Аз обаче предпочитам хора с изключен JS на компютъра си да могат все пак да заредят CSS-а. Ако той е включен в functions.bundle.js, тогава сайтът ни ще е гол HTML без никакви стилове. Това лично на мене не ми харесва.

Добавяне на SCSS модула в webpack.config.js

Нека редактираме файла си, така че да имаме:

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.

development и production

За момента имаме само 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 файла ще остане възможно най-малък.

webpack watch

Всичко до момента щеше да е супер, ако не ни бавеше.

Сега трябва при всяка промяна, която правим, да пуснем поне 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

Още нещо, с което можем да си оптимизираме работата е 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 да тръгне автоматично.

Резултат

Накрая трябва да имаме:

package.json

{
  "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"
  }
}

webpack.config.js

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$/
        })]
    }
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment