[Разработка веб-сайтов, JavaScript, Программирование] Изучаем Parcel — альтернативу Webpack для небольших проектов

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Авг-2020 21:31


Доброго времени суток, друзья!
Основное назначение сборщиков модулей или бандлеров, таких как 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 .".
Запускаем повторную сборку.
Относительные пути имеют правильный вид и приложение работает. Круто.

На этом у меня все. Благодарю за внимание.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_programmirovanie (Программирование), #_javascript, #_webpack, #_parcel, #_bundler, #_bundle, #_sborschik (сборщик), #_bandler (бандлер), #_bandl (бандл), #_modul (модуль), #_paket (пакет), #_biblioteka (библиотека), #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_programmirovanie (
Программирование
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 19:32
Часовой пояс: UTC + 5