Доброго времени суток, друзья!
Основное назначение сборщиков модулей или бандлеров, таких как 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 .".
Запускаем повторную сборку.
Относительные пути имеют правильный вид и приложение работает. Круто.
На этом у меня все. Благодарю за внимание.
zhaparoff
Мантра «фигак-фигак и готово», которую исповедует parcel, конечно, заманчивая штука…
Но стоит потребоваться каким-то дополнительным настройкам, или в случае «обновил депенденси и все перестало билдаться», ты оказываешься практически бессилен, потому как нет ни доп. настроек, ни внятного мануала как это можно порешать/обойти. Так что для себя решил, что пасл подходит только для мелких, плюс-минус одноразовых проектов, с коротким жизненным циклом.
Одна фича, которая мне очень понравилась в нем — это то, что можно указать как точку входа не js/ts, а html; и при наличии соответствующих плагинов достаточно легко добиться того, чтобы он спаковал все (вообще ВСЕ, включая код, стили и даже картинки) в один большой html файл. В некоторых случаях это было очень удобно. Пока опять же, при обновлении депенденси, билд не сломался вообще.
Andy_Francev
Целиком и полностью поддерживаю!
Сам, прельстившись быстротой и простотой конфигурации, плотно подсел на него.
Начал верстать обычные лендосы. Вроде поначалу всё хорошо, но чем дальше я погружался в работу с ним, тем больше мне хотелось поскорее с него слезть.
У parcel v1 есть много ошибок, недоработок, которые уже никто не будет исправлять потому, что… скоро выйдет версия 2, в которой всё будет точно хорошо. Скоро – это уже год, и всё ещё не до конца работающая бета.
Вот примеры того, что в parcel v1 не работает, и работать не будет:
1) Parcel не прокидывает в pug функцию require. Считать, например, файл JSON вот так вот внутри файла .pug нельзя:
— const { title, cells, texts } = require('../../_data/data.json')
Я победил проблему написанием соответствующих хелперов, использующих ('fs').readFileSync.
2) Захотел поместить critical styles в head html? Получи ошибку, даже две: github.com/parcel-bundler/parcel/issues/2936
Править никто не будет. В конце ветки выложили патч. Он фиксит 1 ошибку, не до конца. Остаётся другая: github.com/parcel-bundler/parcel/issues/3008
Работать вроде можно, но…
3) Несколько шаблонов импортируют один и тот же scss файл через один и тот же js? Не работает! github.com/parcel-bundler/parcel/issues/2340
Ишью открыто 1 Dec 2018. Угадайте, кто нибудь из команды parcel что нибудь сделал?
Я нашёл обходной путь.
С этим вот всем я столкнулся при вёрстке обычных статических сайтов. Не многовато ли за «простоту настройки»?
luckinamargo
Я несколько лет назад собирала простые статичные сайты вроде лендингом gulp-ом. Все было отлично, никаких проблем (хотя сайты и впрямь были очень простые) Пробовали ли Вы его? :) Если да, то почему отказались в пользу Парсела? :)
zhaparoff
Gulp, grunt — это таск раннеры, позволяющие автоматизировать выполнение каких-то операций или их последовательностей. Сами по себе они ничего особо не делают, но при этом позволяют вызывать другие инструменты, начиная от простого копирования файлов, заканчивая бандлерами, упомянутыми ниже, последовательно, или параллельно, либо же комбинируя эти подходы.
Parcel, webpack, rollup — это в первую очередь бандлеры, которые позволяют производить преобразование исходных файлов (будь-то html, ts/js код, стили, картинки, и т.п.) в какой-то другой формат, попутно применяя преобразования, какие-то плагины, или делая еще 1000 вещей, которые могут взбрести в голову.