https://habr.com/ru/post/516704/- Разработка веб-сайтов
- JavaScript
- Программирование
Доброго времени суток, друзья!
Основное назначение сборщиков модулей или бандлеров, таких как Webpack или Parcel, состоит в том, чтобы обеспечить включение всех модулей, необходимых для работы приложения, в правильном порядке в один минифицированный (если речь идет о сборке для продакшна) скрипт, который подключается в index.html.
На самом деле сборщики, как правило, умеют оптимизировать не только JS, но и HTML, CSS-файлы, могут преобразовывать Less, Sass в CSS, TypeScript, React и Vue (JSX) в JavaScript, работать с изображениями, аудио, видео и другими форматами данных, а также предоставляют дополнительные возможности, такие как: создание карты (используемых) ресурсов или источников (source map), визуальное представление размера всего бандла и его отдельных частей (модулей, библиотек), разделение кода на части (chunks), в том числе, в целях переиспользования (например, библиотеки, которые используются в нескольких модулях, выносятся в отдельный файл и загружаются лишь раз), умная загрузка пакетов из npm (например, загрузка только русской локализации из moment.js), всевозможные плагины для решения специфичных задач и т.п.
В этом отношении лидерство, безусловно, принадлежит Вебпаку. Однако, что если мы разрабатываем проект, в котором большая часть функционала, предоставляемого этим замечательным инструментом, не нужна? Существуют ли альтернативы данной технологии, более простые в освоении и использовании? Для меня ответом на этот вопрос стал
Parcel. К слову, если вы заинтересованы в изучении Вебпака, рекомендую к просмотру
это видео. Мой файл с настройками Вебпака по данному туториалу находится
здесь.
С вашего позволения, я не буду пересказывать документацию своими словами, тем паче, что она доступна на русском языке, а сосредоточусь на практической составляющей, а именно: мы с помощью шаблонных строк и динамического импорта создадим SPA, состоящее из трех страниц, на JavaScript, стилизуем приложение с помощью CSS, напишем простую функцию на TypeScript, импортируем ее в приложение, стилизуем контейнер для результатов данной функции с помощью Sass, и соберем приложение посредством Parcel в обоих режимах (разработка и продакшн).
Код проекта находится
здесь.
Если вам это интересно, прошу следовать за мной.
Приложение
Готовы? Тогда поехали.
Создаем директорию parcel-tutorial.
Заходим в нее и инициализируем проект с помощью
npm init -y
.
Создаем файл index.html. Мы будем использовать один из стандартных шаблонов Bootstrap — Cover:
<head>
...
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
</head>
<!-- Bootstrap class -->
<body class="text-center">
<! -- Main script -->
<script src="index.js"></script>
</body>
Заходим на
официальный сайт Bootstrap, переходим в раздел Examples, находим Cover в Custom components, нажимаем Ctrl+U (Cmd+U) для просмотра кода страницы.
Создаем директорию src, а в ней еще две папки — js и css.
В директории js создаем следующие файлы: header.js, footer.js, home.js, projects.js и contact.js. Это модули или, если угодно, компоненты нашего приложения: шапка, подвал, содержимое главной и других страниц.
В директории css создаем файл style.css.
На данный момент структура проекта выглядит следующим образом:
-- parcel-tutorial
-- src
-- css
-- style.css
-- js
-- contact.js
-- footer.js
-- header.js
-- home.js
-- projects.js
-- index.html
-- index.js
-- package.json
Возвращаемся к Bootstrap.
Копипастим код страницы в сооветствующие модули с небольшими изменениями.
header.js:
export default `
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">Parcel Tutorial</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" name="home">Home</a>
<a class="nav-link" name="projects">Projects</a>
<a class="nav-link" name="contact">Contact</a>
</nav>
</div>
</header>
`.trim()
Обратите внимание, что в ссылках мы поменяли href на name.
footer.js:
export default `
<footer class="mastfoot mt-auto">
<div class="inner">
<p>© 2020. All rights reserved.</p>
</div>
</footer>
`.trim()
home.js:
export default `
<h1 class="cover-heading">Home page</h1>
<p class="lead">Home page content</p>
`.trim()
projects.js:
export default `
<h1 class="cover-heading">Projects page</h1>
<p class="lead">Projects page content</p>
`.trim()
contact.js:
export default `
<h1 class="cover-heading">Contact page</h1>
<p class="lead">Contact page content</p>
`.trim()
Не забываем скопировать стили из cover.css в style.css.
Открываем index.js.
Импортируем шапку, подвал сайта и стили:
import header from './src/js/header.js'
import footer from './src/js/footer.js'
import './src/css/style.css'
Содержимое главной и других страниц будет загружаться динамически при клике по ссылке, поэтому создаем такой объект:
const pages = {
home: import('./src/js/home.js'),
projects: import('./src/js/projects.js'),
contact: import('./src/js/contact.js')
}
Название свойства данного объекта — соответствующая (запрашиваемая пользователем) страница.
Генерируем начальную страницу, используя импортированные ранее компоненты шапки и подвала сайта:
// Bootstrap classes
document.body.innerHTML = `
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
${header}
<main role="main" class="inner cover"></main>
${footer}
</div>
`.trim()
Содержимое страниц будет выводиться в элемент main, поэтому определяем его:
const mainEl = document.querySelector('main')
Создаем функцию рендеринга страниц:
const renderPage = async name => {
const template = await pages[name]
mainEl.innerHTML = template.default
}
Нам нужно дождаться загрузки соответствующего модуля, поэтому мы используем async/await (ключевое слово await приостанавливает выполнение функции). Функция принимает название запрашиваемой страницы (name) и использует его для доступа к соответствующему свойству объекта pages (pages[name]). Затем мы вставляем полученный шаблон в mainEl. В действительности, await возвращает объект Module, внутри которого содержится шаблон. Поэтому при вставке шаблона в качестве разметки в mainEl неоходимо обратиться к свойству default объекта Module (модули экспортируются по умолчанию), в противном случае, мы получим ошибку — невозможно конвертировать объект в HTML.
Рендерим главную страницу:
renderPage('home')
Активная ссылка, соответствующая текущей странице, имеет класс active. Нам нужно переключать классы при рендеринге новой страницы. Реализуем вспомогательную функцию:
const toggleClass = (activeLink, currentLink) => {
if (activeLink === currentLink) {
return;
} else {
activeLink.classList.remove('active')
currentLink.classList.add('active')
}
}
Функция принимает два аргумента — ссылку с классом active (activeLink) и ссылку, по которой кликнули (currentLink). Если указанные ссылки совпадают, ничего не делаем. Иначе, меняем классы.
Наконец, нам нужно добавить обработчик клика по ссылке. Реализуем еще одну вспомогательную функцию:
const initClickHandlers = () => {
const navEl = document.querySelector('nav')
navEl.addEventListener('click', ev => {
if (ev.target.tagName === 'A') {
const activeLink = navEl.querySelector('.active')
const currentLink = ev.target
toggleClass(activeLink, currentLink)
renderPage(currentLink.name)
}
})
}
В данной функции мы сначала находим элемент nav. Затем через делегирование обрабатываем клики по ссылкам: если целевым элементом является тег A, получаем активную ссылку (ссылку с классом active), текущую ссылку (ссылку, по которой кликнули), меняем классы и рендерим страницу. В качестве аргумента renderPage передается значение атрибута name текущей ссылки.
Мы почти закончили с приложением. Однако, прежде чем переходить к сборке проекта с помощью Parcel, необходимо отметить следующее: на сегодняшний день поддержка динамических модулей по данным
Can I use составляет 90%. Это много, но мы не готовы терять 10% пользователей. Поэтому наш код нуждается в преобразовании в менее современный синтаксис. Для транспиляции используется Babel. Нам нужно подключить два дополнительных модуля:
import "core-js/stable";
import "regenerator-runtime/runtime";
Обратите внимание, что мы не устанавливаем эти модули с помощью npm.
Также давайте сразу реализуем функцию на TypeScript, что-нибудь очень простое, например, функцию сложения двух чисел.
Создаем в директории js файл index.ts следующего содержания:
export const sum = (a: number, b: number): number => a + b
Единственное отличие от JavaScript, кроме расширения файла (.ts), заключается в том, что мы явно указываем типы принимаемых и возвращаемого функцией значений — в данном случае, number (число). На самом деле, мы могли бы ограничиться определением типа возвращаемого значения, TypeScript достаточно умный для того, чтобы понять: если возвращаемое значение является числом, то и передаваемые аргументы должны быть числами. Неважно.
Импортируем эту функцию в index.js:
import { sum } from './src/js/index.ts'
И вызываем ее с аргументами 1 и 2 в renderPage:
const renderPage = async name => {
// ...
mainEl.insertAdjacentHTML('beforeend', `
<output>Result of 1 + 2 -> <span>${sum(1, 2)}<span></output>
`)
}
Стилизуем контейнер с результатом функции с помощью Sass. В папке css создаем файл style.scss следующего содержания:
$color: #8e8e8e;
output {
color: $color;
border: 1px solid $color;
border-radius: 4px;
padding: .5rem;
user-select: none;
transition: transform .2s;
& span {
color: #eee;
}
&:hover {
transform: scale(1.1);
}
}
Импортируем данные стили в index.js:
import './src/css/style.scss'
Снова обратите внимание, что мы не устанавливаем TypeScript и Sass с помощью npm.
С приложением закончили. Переходим к Parcel.
Parcel
Для глобальной установки Parcel необходимо выполнить команду
npm i parcel-bundler -g
в терминале.
Открываем package.json и настраиваем запуск Парсела в режимах разработки и продакшна:
"scripts": {
"dev": "parcel index.html --no-source-maps --open",
"pro": "parcel build index.html --no-source-maps --no-cache"
},
Команда
npm run dev
запускает сборку проекта для разработки, а команда
npm run pro
— для продакшна. Но что означают все эти флаги? И почему мы не устанавливали Babel, TypeScript и Sass через npm?
Дело в том, что Парсел автоматически устанавливает все зависимости при обнаружении их импорта или использования в приложении. Например, если Парсел видит импорт стилей из файла с расширением .scss, он устанавливает Sass.
Теперь о командах и флагах.
Для сборки проекта в режиме разработки используется команда
parcel index.html
, где index.html — это входная точка приложения, т.е. файл, в котором имеется ссылка на основной скрипт или скрипты. Также данная команда запускает локальный сервер на localhost:1234.
Флаг
--no-source-maps
означает, что нам не нужны карты ресурсов.
Флаг
--open
указывает Парселу открыть index.html после сборки в браузере на локальном сервере.
Для сборки проекта в режиме продакшна используется команда
parcel build index.html
. Такая сборка предполагает минификацию JS, CSS и HTML-файлов.
Флаг
--no-cache
означает отключение кэширования ресурсов. Кэширование обеспечивает высокую скорость сборки и пересборки проекта в режиме реального времени. Это актуально при разработке, но не актуально при сборке готового продукта. Кроме того, это усложняет задачу создания офлайн-приложения с помощью сервис-воркера.
Еще один момент: сгенерированные файлы Парсел по умолчанию помещает в папку dist, которая создается при отсутствии. Проблема в том, что при повторной сборке старые файлы не удаляются. Для удаления таких файлов нужен специальный плагин, например,
parcel-plugin-clean-easy.
Устанавливаем данный плагин с помощью
npm i parcel-plugin-clean-easy -D
и добавляем в package.json следующее:
"parcelCleanPaths": [
"dist",
".cache"
]
parcelCleanPaths — это директории, подлежащие удалению при повторной сборке.
Теперь Парсел полностью настроен. Открываем терминал, набираем
npm run dev
, нажимаем enter.
Парсел собирает проект в режиме разработки, запускает локальный сервер и открывает приложение в браузере. Отлично.
Теперь попробуем собрать проект для продакшна.
Выполняем команду
npm run pro
.
Запускаем приложение в браузере.
Упс, кажется, что-то пошло не так.
Заглянем в сгенерированный index.html. Что мы там видим? Подсказка: обратите внимание на пути в тегах link и script. Не знаю точно, с чем это связано, но Парсел приводит относительные ссылки к виду "/path-to-file", а браузер не читает такие ссылки.
Для того, чтобы решить эту проблему, необходимо добавить в скрипт «pro» флаг "--public-url .".
Запускаем повторную сборку.
Относительные пути имеют правильный вид и приложение работает. Круто.
На этом у меня все. Благодарю за внимание.