Доброго времени суток, друзья!

Основное назначение сборщиков модулей или бандлеров, таких как 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 .".

Запускаем повторную сборку.

Относительные пути имеют правильный вид и приложение работает. Круто.



На этом у меня все. Благодарю за внимание.