Как стать автором
Обновить

Sapper и Svelte: быстрое руководство

Время на прочтение12 мин
Количество просмотров6.3K
Автор оригинала: Ovie Okeh

Замечание редактора английского оригинала: Это руководство по Svelte и Sapper было обновлено 2 июля 2021 года с учетом информации о SvelteKit, пришедшем на смену Sapper. Подробнее о SvelteKit рассказано в статье “Exploring SvelteKit, the newest Svelte-based framework.”

Исследовав фреймворк Svelte.js, можно убедиться, что на нем можно писать по-настоящему реактивные приложения, но при этом обходиться значительно меньшим количеством кода, чем при использовании других фронтенд-фреймворков. В то время, как можно создавать довольно сложные приложения при помощи одного только Svelte, код такого приложения быстро может прийти в беспорядок. Знакомьтесь с Sapper!

Здесь мы в общем виде рассмотрим Sapper, продемонстрируем, как с его помощью писать полнофункциональные, но при этом легковесные приложения, а также детально разберем приложение для рендеринга на стороне сервера.

Sapper мертв?

На конференции Svelte Summit в октябре 2020 года Рич Харрис, создатель Svelte и Sapper, в своей презентации “Futuristic Web Development” анонсировал, что он и его команда занимаются разработкой SvelteKit на замену Sapper.

В последующем посте Харрис объяснил, чем обусловлена такая смена. Во-первых, SvelteKit спроектирован так, чтобы в него было проще вкатываться, снизить бремя, связанное с технической поддержкой, а также обеспечить предсказуемую структуру проекта.

С более высокоуровневой точки зрения можно сказать, что SvelteKit был создан в ответ на подъем рабочего метода, именуемого «разработка на основе отдельных компонентов» (unbundled development). В таком случае сервер разработки не собирает пакет с приложением, а подает нужные модули по запросу. Благодаря этому, приложение запускается практически мгновенно, независимо от его размера.

Наконец, SvelteKit должен поддерживать всех основных бессерверных провайдеров и включать адаптерный API – так удается обеспечить его совместимость с теми платформами, на которые он официально не рассчитан.

Что такое Sapper?

Sapper – это фреймворк, используемый в паре со Svelte и помогающий создавать более крупные и сложные приложения быстро и эффективно.

В наши времена создать веб-приложение – это весьма сложное начинание, требующее подумать о разделении кода, управлении данными, оптимизации производительности, т.д. Отчасти это связано с тем, что сегодня есть в наличии мириады инструментов для фронтенд-разработки, но каждый из них характеризуется собственным уровнем сложности и кривой обучения.

Построить приложение должно быть не так сложно, правильно? Может ли это быть еще проще, чем сейчас? Сохранит ли разработчик рассудок, просто расставив галочки там, где нужно? Конечно же, сохранит – вопрос риторический!  

Начнем с названия: Sapper (сапер). Просто возьму и процитирую официальную документацию, в которой рассказывается, почему было выбрано именно такое название:

На войне солдаты, занимающиеся возведением мостов, ремонтом дорог, разминированием и демонтажем – все в боевых условиях – являются представителями саперных подразделений.

В веб-разработке риск обычно не так высок, как в инженерных войсках. Но и веб-разработчики действуют в по-своему враждебной среде, где устройствам не хватает питания, сетевые соединения работают плохо, а разработка фронтенда тесно сопряжена с характерными сложностями. Sapper, в сущности, делатель приложений для Svelte – ваш смелый и исправный союзник на этом фронте.

Хм, это совершенно логично.

Sapper (а вслед за ним и Svelte) специально задумывались как легковесные, производительные и логично устроенные, но, в то же время, предоставляющие достаточный объем возможностей, которые позволяли бы вам реализовывать ваши крутые идеи в виде веб-приложений.

В принципе, вот за какие вещи помогает отвечать Sapper, когда вы создаете веб-приложения на Svelte:

  • Маршрутизация

  • Серверный рендеринг

  • Автоматическое разделение кода

  • Оффлайновое управление (при помощи сервис-воркеров)

  • Высокоуровневое управление структурой проекта

Уверен, вы согласитесь: если управлять всем этим самостоятельно, это может быстро превратиться в рутину, отвлекающую вас от бизнес-логики

Но довольно слов – код убедительнее! Давайте разберем небольшое приложение для серверного рендеринга, в котором используется Svelte и Sapper.

Пример Sapper

Вместо того, чтобы рассказывать вам, с какой легкостью на Sapper можно писать приложения, давайте исследуем демо-приложение, которое получилось бы у вас, если бы вы решили возвести новый проект. Далее посмотрим, как это приложение работает за кулисами.

Для начала выполним следующую команду, чтобы сделать начальную загрузку нового проекта:

$ npx degit "sveltejs/sapper-template#rollup" my-app
$ cd my-app
$ npm install
$ npm run dev

Сделав так, вы получите «скелет» проекта, но в рамках данной статьи этого достаточно. На этом простом проекта сможем исследовать, как Sapper обрабатывает маршрутизацию и серверный рендеринг, и не будем слишком углубляться в детали.

Ныряем!

Структура проекта

Sapper – «разборчивый» (opinionated) фреймворк; это означает, что некоторые файлы и папки в нем являются обязательными, а сам каталог проекта должен быть структурирован определенным образом. Рассмотрим, как выглядит типичный проект Sapper, и что где должно находиться.

Входные точки

В каждом проекте Sapper есть три входные точки, а также файл src/template.html:

  1. src/client.js

  2. src/server.js

  3. src/service-worker.js (эта опциональна)

client.js

import * as sapper from '@sapper/app';
sapper.start({
target: document.querySelector('#sapper')
});

Это входная точка для приложения, отображаемого на клиенте. Это совсем простой файл, и все, что вам отсюда понадобится – это главный модуль Sapper из @sapper/app, из него мы вызовем метод start. Он принимает объект в качестве аргумента, а единственный обязательный ключ для него – это target.

Ключ target указывает, к какому узлу DOM будет подвешиваться приложение. Если ранее вы работали с React.js, то можете считать, что это аналог ReactDOM.render.

server.js

Нам нужен сервер, который выдавал бы наше приложение пользователю, не так ли? Поскольку это зона ответственности Node.js, у нас на выбор множество вариантов. Можно было бы использовать сервер Express.js, сервер Koa.js, сервер Polka, но здесь нужно придерживаться некоторых правил, а именно:

  1. Сервер должен выдавать содержимое папки /static. Sapper не волнует, как вы это сделаете. Просто выдавайте эту папку!

  2. Ваш серверный фреймворк обязан поддерживать то или иное промежуточное ПО (лично я не знаю таких, которые бы не поддерживали), а также должен использовать sapper.middleware(), импортированный из @sapper/server.

  3. Ваш сервер должен слушать process.env.PORT.

Всего три правила — неплохо, как по мне. Взглянем на src/server.js, сгенерированный для нас, чтобы изучить, каков он на практике.

service-worker.js

Если вам нужно освежить в памяти, что представляют собой сервис-воркеры, этот пост хорошо подойдет. Теперь: файл service-worker.js не является обязательным для сборки полнофункционального веб-приложения при помощи Sapper; он просто открывает вам доступ к таким возможностям, как оффлайновая поддержка, пуш-уведомления, фоновая синхронизация и т.д.

Поскольку для каждого приложения нужны специализированные сервис-воркеры, нет жестких и быстрых правил, как написать такой воркер. Если хотите, вы можете обойтись без этого файла, либо, все-таки, задействовать его, чтобы обеспечить пользователю более целостное и полное взаимодействие с приложением.

template.html

Это главная входная точка вашего приложения, через которую по мере необходимости внедряются все ваши компоненты, ссылки на стили, скрипты. Работа с ней, как правило, происходит по принципу «установил и забыл», кроме тех редких случаев, в которых нужно добавить модуль, связав его с сетью доставки контента (CDN) из вашего HTML.

Routes

Самый важный компонент любого приложения на Sapper. Именно здесь живет большая часть логики и содержимого. Следующий раздел посвящен более подробному изучению этого компонента.

Маршрутизация

Если вы правильно выполнили все команды из предыдущего раздела, то, перейдя по адресу http://localhost:3000, вы должны оказаться в простом веб-приложении с домашней страницей, страницей «о нас» и страницей блога. Пока все так просто.

Теперь давайте попробуем понять, как Sapper сможет соотнести этот URL с соответствующим файлом. В Sapper поддерживается два типа маршрутов: страничные и серверные.

Разбираемся дальше

Страничные маршруты

Когда вы переходите к странице – скажем, /about — Sapper отображает файл about.svelte, расположенный в каталоге src/routes. Таким образом, любой файл .svelte внутри этого каталога автоматически «отображается» на одноименный маршрут. Так, если у вас в каталоге src/routes есть файл под названием jumping.svelte, то переход к /jumping приведет к отображению этого файла.

Короче говоря, маршруты страниц – это файлы .svelte, находящиеся в каталоге src/routes. У этого подхода есть очень приятный побочный эффект: маршруты предсказуемы, и о них легко судить. Хотите новый маршрут? Создайте новый файл.svelte в каталоге src/routes – и вы молодец!

Что, если вам нужен вложенный маршрут, выглядящий примерно так: /projects/sapper/awesome? Все, что для этого нужно – создать по каталогу для каждого подмаршрута. Так, для предыдущего примера у нас получится примерно такая структура каталогов: src/routes/projects/sapper, после чего мы сможем разместить файл awesome.svelte внутри каталога /sapper.

Помня об этом, давайте посмотрим на наше приложение после начальной загрузки и перейдем в нем на страницу «о нас». Откуда, как как вам кажется, будет выводиться контент этой страницы? Что ж, давайте посмотрим на src/routes. И действительно, тут мы найдем файл about.svelte — все просто и предсказуемо!

Обратите внимание: index.svelte – это зарезервированный файл, который отображается, когда вы переходите по подмаршруту. Так, в нашем случае у нас есть маршрут /blogs, по которому можно свернуть на относящиеся к нему подмаршруты, например, /blogs/why-the-name.

Но обратите внимание: при переходе на /blogs в браузере отображается файл, тогда как сам /blogs – это каталог. Как выбрать, какой именно файл отобразить для такого маршрута?

Либо мы определим файл blog.svelte вне каталога /blogs, либо нам понадобится, чтобы файл index.svelte был размещен в каталоге /blogs – но не оба этих варианта сразу. Этот файл index.svelte будет отображен, когда вы посетите непосредственно /blogs.

Что по поводу URL с динамическими транслитерируемыми фрагментами (slugs)? В нашем примере было бы неосуществимо вручную создать все до единого посты для блога и сохранить их как файлы .svelte. Нам понадобится шаблон, используемый для отображения всех постов в блоге, независимо от транслитерируемого фрагмента.

Вновь взглянем на наш проект. Под src/routes/blogs у нас есть файл [slug].svelte. Что это по-вашему? Ух – а это и есть нужный нам шаблон. Таким образом, любой транслитерируемый фрагмент, идущий после /blogs, автоматически обрабатывается этим файлом, и у нас появляется возможность, например, выбирать все содержимое страницы в момент ее прикрепления, а затем отображать эту страницу в браузере.

Значит ли это, что любой файл или каталог под /routes автоматически отображается на URL? Да, но из этого правила есть исключение. Если поставить нижнее подчеркивание перед именем файла или каталога в качестве префикса, то Sapper не преобразует их в URL. Таким образом, вы легко можете класть в каталог с маршрутами вспомогательные файлы.  

Допустим, мы хотим, чтобы в каталоге helpers лежали все наши вспомогательные функции. У нас могла бы быть папка /routes/_helpers, и тогда любой файл, размещенный бы под /_helpers не считался бы маршрутом. Очень изящно, правда?

Серверные маршруты

В предыдущем разделе было показано, что можно иметь файл [slug].svelte, который помог бы нам сопоставить любой URL вот так: /blogs/<any_url>. Но как именно при этом обеспечивается отображение содержимого той страницы, что нас интересует?

Можно получить содержимое из статического файла или выполнить вызов к API для извлечения данных. Так или иначе, потребуется направить запрос к маршруту (или к конечной точке, если учитывать только API) для извлечения данных. Тут и вступают в дело серверные маршруты.

Из официальной документации: “Серверные маршруты – это модули, записанные в .js-файлах, экспортирующие функции, соответствующие HTTP-методам.”

Это всего лишь означает, что серверные маршруты являются конечными точками, которые можно вызывать для выполнения конкретных действий, например, сохранения данных, выборки данных, удаления данных, т.д. В принципе, это бэкенд вашего приложения, поэтому в одном проекте у вас сложено все, что вам нужно (но, если хотите, вы могли бы его разделить).

Вернемся к нашему предзагруженному проекту. Как нам выбрать содержимое каждого из постов блога внутри [slug].svelte? Ну, давайте откроем файл – и первый же фрагмент кода, который мы увидим, будет выглядеть так:

<script context="module">
  export async function preload({ params, query }) {
    // the `slug` parameter is available because
    // this file is called [slug].html
    const res = await this.fetch(`blog/${params.slug}.json`);
    const data = await res.json();
    if (res.status === 200) {
      return { post: data };
    } else {
      this.error(res.status, data.message);
    }
  }
</script>

Здесь мы видим всего лишь простую JS-функцию, выполняющую запрос GET и возвращающую данные, полученные в результате этого запроса. В качестве параметра она принимает объект, который затем деструктурируется в строке 2 для получения двух переменных: params и query.

Что содержится в params и query? Почему бы не добавить console.log() в начале функции, а затем не открыть в браузере пост из блога? Сделайте так – и увидите, как в консоли появится примерно следующий вывод:

{slug: "why-the-name"}slug: "why-the-name"__proto__: Object {}

Хм. Итак, если бы мы открыли пост “why-the-name” в строке 5, то наш запрос GET направится к blog/why-the-name.json, которую мы затем преобразуем в объект JSON в строке 6.

В строке 7 мы проверим, был ли наш запрос успешен, и, если так, возвратим его в строке  8, либо, иначе, вызовем специальный метод, называемый this.error, со статусом отклика и сообщением об ошибке.

Очень просто. Но где здесь сам серверный маршрут, и как он выглядит? Загляните в src/routes/blogs – и вы должны там увидеть файл [slug].json.js. Это и есть наш серверный маршрут. А вы заметили, что он называется точно так же, как и [slug].svelte? Именно таким образом Sapper отображает серверный маршрут на маршрут страницы. Поэтому, если вызвать this.fetch внутри файла example.svelte, то Sapper станет искать файл example.json.js, чтобы обработать запрос.

Теперь давайте расшифруем [slug].json.js, ведь сможем, да?

import posts from './_posts.js';
const lookup = new Map();
posts.forEach(post => {
lookup.set(post.slug, JSON.stringify(post));
});
export function get(req, res, next) {
// the slug parameter is available because
// this file is called [slug].json.js
const { slug } = req.params;
if (lookup.has(slug)) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(lookup.get(slug));
} else {
res.writeHead(404, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: Not found
}));
}
}

Самое интересное для нас в этом файле начинается со строки 8. В строках 3–6 просто выполняется подготовка данных, чтобы работать с ними в рамках маршрута. Помните, как мы делали запрос GET в нашем страничном маршруте: [slug].svelte? А это и есть серверный маршрут, обрабатывающий данный запрос.

Если вы знакомы с разными API из Express.js, то и этот код должен быть узнаваемым. Дело в том, что это всего лишь простой контроллер для конечной точки. Все, что он делает – берет транслитерированную часть, которую ему передали из объекта Request, ищет ее в нашем хранилище данных (в нашем случае, lookup), и возвращает ее в объекте  Response.

Если бы мы работали с базой данных, то строка 12 могла бы выглядеть примерно как Posts.find({ where: { slug } }). Идею вы уловили.

Серверные маршруты – это файлы, содержащие конечные точки, которые мы можем вызывать из наших страничных маршрутов. Итак, давайте быстро пробежимся по тому, что мы уже знаем:

  • Страничные маршруты – это файлы .svelte, расположенные в каталоге src/routes, контент из которого отображается в браузере.

  • Серверные маршруты – это .js-файлы, содержащие конечные точки API и отображаемые на конкретные страничные маршруты по имени.

  • Страничные маршруты могут вызывать конечные точки, определенные в серверных маршрутах – таким образом они могут выполнять конкретные действия, например, выборку данных.

  • Sapper превосходно продуман.

Рендеринг на стороне сервера

Рендеринг на стороне сервера (SSR) – огромная составляющая, делающая Sapper таким привлекательным. Если вы не знаете, что такое SSR, или почему он может вам понадобиться – в этой статье автор замечательно все это объясняет.

По умолчанию Sapper сначала отображает все ваши приложения на стороне сервера, а лишь потом прикрепляет динамические элементы на стороне клиента. Так вы получаете лучшее от обоих миров, не идя ни на какие компромиссы.

Но здесь есть и подножка: притом, что Sapper почти в совершенстве справляется с поддержкой сторонних модулей, есть среди модулей и такие, которым требуется доступ к объекту window, а, как известно, получить доступ к window со стороны сервера невозможно. Если просто импортировать такой модуль, то вся компиляция просто сорвется, и мир станет чуточку темнее.

Но не отчаивайтесь; от этого есть простое средство. Sapper позволяет импортировать модули динамически (хехе, и исходный размер пакета получается меньше), поэтому не обязательно импортировать модуль на самом верхнем уровне. А можно вместо этого поступить вот так:

<script>
  import { onMount } from 'svelte';
let MyComponent;
onMount(async () => {
const module = await import('my-non-ssr-component');
MyComponent = module.default;
});
</script>
<svelte:component this={MyComponent} foo="bar"/>

В строке 2 мы импортируем функцию onMount. Функция onMount встроена в Svelte и вызывается только в тот момент, когда элемент закрепляется на стороне клиента (можете считать ее эквивалентом componentDidMount из React).

Таким образом, когда мы только лишь импортируем наш проблемный модуль внутри функции onMount, этот модуль никогда не вызывается на сервере, и у нас не возникает проблем с недостающим объектом window. Вот! Ваш код компилируется успешно, и в мире опять все хорошо.

O, у этого подхода есть и еще один плюс: поскольку с этим компонентом применяется динамический импорт, реально приходится отправлять меньше кода на сторону клиента.

Заключение

Мы увидели, насколько понятно и просто работать с Sapper. Его система маршрутизации очень легко усваивается даже для абсолютных новичков, и получается API, через который не составляет труда управлять фронтендом. Рендеринг на стороне сервера также очень легко реализовать, т.д.

Есть еще множество возможностей, которых я здесь не затронул, в частности, заблаговременную загрузку, обработку ошибок, маршруты регулярных выражений. Единственный способ извлечь из всего этого пользу – опробовать эти возможности на практике.

Теги:
Хабы:
+4
Комментарии1

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия