Введение

Важное замечание

Sapper ещё в ранней стадии разработки, и некоторые вещи могут поменяться, когда мы дойдём до релиза 1.0. Этот документ всё ещё дорабатывается. Если у вас появятся вопросы, обратитесь за помощью в русскоязычный Telegram-канал.

Прочтите руководство по миграции для помощи при обновлении с более старых версий.

Что такое Sapper?

Sapper — это фреймворк для создания невероятно производительных web-приложений. Прямо сейчас вы смотрите на одно из них! Вот два на наших основных принципа:

  • Каждая страница вашего приложения является компонентом Svelte
  • Вы создаёте новые страницы путём добавления компонентов директорию src/routes вашего проекта. Они будут рендериться на сервере, так что время загрузки приложения для пользователем будет максимально быстрым, а уже затем клиентское приложение возьмёт на себя бразды правления.

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

Чтобы понять это руководство, знать Svelte не обязательно, но желательно. Svelte — это фреймворк, который компилирует ваши компоненты в высокооптимизированный ванильный JavaScript. Прочитайте вводную статью в блоге и учебник, чтобы узнать о нём побольше.

Откуда такое название?

В армии есть содаты, которые занимаются разминированием — сапёры. В американской армии тоже есть sappers, но их сфера деятельности намного шире — кроме разминирования, они ещё в боевых условиях строят мосты, ремонтируют дороги и проводят сносы.

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

Сравнение с Next.js

Next.js — это фреймворк для React от Zeit и он является источником вдохновения для Sapper. Однако между ними есть несколько заметных отличий:

  • Sapper работает на Svelte, а не на React, поэтому он быстрее и приложения получаются меньше по размеру
  • Вместо маски маршрута мы используем описание параметров маршрута в именах файлов (см. Раздел Маршруты ниже)
  • Серверные маршруты создаются точно так же, как и маршруты обычных страниц в директории src/routes. Например, это позволяет очень просто добавить точку входа для JSON API, такую же как есть на этой странице (попробуйте — /docs.json)
  • Ссылки — это обычные элементы <a>, а не специальные компоненты вроде <Link>. Это означает, например, что эта ссылка, прекрасно работает с маршрутизатором, даже не смотря на то, что она изначально располагается в импортированном документе с markdown разметкой.

Начало работы

Самый простой способ начать создавать приложение Sapper — скопировать к себе на компьютер репеозиторий шаблона sapper-template при помощи утилиты degit:

npx degit "sveltejs/sapper-template#rollup" my-app
# или: npx degit "sveltejs/sapper-template#webpack" my-app
cd my-app
npm install
npm run dev

Это создаст новый проект в каталоге my-app, установит его зависимости и запустит сервер на localhost:3000. Попробуйте поредактировать файлы, чтобы увидеть, насколько всё просто работает — быть может вам вообще не понадобиться читать оставшуюся часть этого руководства!

Структура приложения

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

Если вы загляните в внутрь шаблона sapper-template, вы увидите несколько файлов, которые Sapper ожидает там найти:

├ package.json
├ src
│ ├ routes
│ │ ├ # тут ваши маршруты
│ │ ├ _error.svelte
│ │ └ index.svelte
│ ├ client.js
│ ├ server.js
│ ├ service-worker.js
│ └ template.html
├ static
│ ├ # тут ваши картинки и прочая статика
└ rollup.config.js / webpack.config.js

При первом запуске Sapper создаст дополнительный каталог __sapper__, содержащий сгенерированные файлы.

Вы заметите несколько дополнительных файлов и каталог cypress, которые относятся к процессу тестирования — но в данный момент нам не нужно заострять на этом внимания.

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

package.json

Файл package.json содержит зависимости вашего приложения и определяет ряд скриптов:

  • npm run dev — запустить приложение в режиме разработки и следить за изменениями в исходных файлах
  • npm run build — собрать приложение для продакшена
  • npm run export — сгенерировать статическую версию приложениия, если это возможно (см. Экспортирование)
  • npm start — запустить приложение для продакшена, если оно уже было собрано до этого
  • npm test — запустить тесты (см. Тестирование)

src

Тут содержатся три точки входа вашего приложения — src/client.js, src/server.js и (необязательно) src/service-worker.js — вместе с файлом src/template.html.

src/client.js

Здесь обязательно нужно импортировать и вызвать функцию start из сгенерированного модуля @sapper/app:

import * as sapper from '@sapper/app';

sapper.start({
	target: document.querySelector('#sapper')
});

Для большинства случаев это весь код модуля, но вы можете дополнительно написать здесь любой код под ваши нужды. Смотрите раздел API клиента для получения дополнительной информации о функциях, которые вы можете импортировать.

src/server.js

Это обычное Express приложение (можно взять Polka или ещё какой-либо сервер), с тремя обязательными требованиями:

  • оно должно сервить содержимое папки static, используя, например, sirv
  • оно должен вызвать app.use(sapper.middleware()) в том месте, где sapper импортируется из @sapper/server
  • оно должно 'висеть' на порту, указанном в process.env.PORT

В остальном, вы можете написать сервер так, как вам нравится.

src/service-worker.js

Сервис-воркеры действуют как прокси-серверы, которые дают вам детальный контроль над тем, как реагировать на сетевые запросы. Например, когда браузер запрашивает /козлики.jpg, сервис-воркер может вернуть файл, который он уже ранее закешировал, или он может передать запрос на сервер, или он может даже ответить чем-то совершенно другим, например, картинкой оленей.

Помимо прочего, они позволяют создавать приложения, работающие в автономном режиме.

Поскольку каждому приложению требуется особое поведение сервис-воркеров (одним надо всё отдавать из кэша, другим кеш нужен только при отсутствии подключения), Sapper никак не ограничивает поведение сервис-воркеров. Вы сами пишете его логику в service-worker.js. Вы можете импортировать любой из следующих объектов из @sapper/service-worker:

  • files — массив файлов, найденных в директории static
  • shell — JavaScript код для клиента, сгенерированный сборщиком(Rollup или webpack)
  • routes — массив объектов { pattern: RegExp }, которые вы можете использовать, чтобы определить относится ли к Sapper запрошенная страница
  • timestamp — время, когда был создан сервис-воркер(полезно для создания уникальных имён кэшей)

src/template.html

Этот файл является шаблоном для ответов с сервера. В процессе сборки Sapper будет внедрять контент, заменяющий следующие метки:

  • %sapper.base% — элемент <base> (см. Базовые URL)
  • %sapper.styles% — необходимый CSS для запрашиваемой страницы
  • %sapper.head% — HTML представление специфичного для данной страницы содержимого элемента <head>, вроде элемента <title>
  • %sapper.html% — HTML представление содержимого отрендеренной страницы
  • %sapper.scripts% — элементы <script> для клиентской части приложения

src/routes

Это основа вашего приложения — страницы и маршруты сервера. Подробнее вы узнаете в разделе Маршруты.

static

Это место для размещения любых файлов, которые использует ваше приложение — шрифты, изображения и так далее. Например, static/favicon.png будет доступна как /favicon.png.

Sapper не будет сервить эти файлы. Обычно для этого используют sirv или serve-static. Но он будет сканировать содержимое папки static, чтобы вы могли легко сгенерировать манифест кэша для поддержки автономной работы(см. service-worker.js).

rollup.config.js / webpack.config.js

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

Маршруты

Как мы уже видели, в Sapper есть два типа маршрутов — маршруты страниц и маршруты сервера.

Страницы

Страницы — это компоненты Svelte, описанные в файлах .svelte. Когда пользователь впервые посещает приложение, ему будет предоставлена сгенерирования на сервере версия запрошенного маршрута, а также некоторый JavaScript, который выполняет 'гидратацию' страницы и инициализирует маршрутизатор на стороне клиента. С этого момента навигация на другие страницы будет полностью выполняться на стороне клиента обеспечивая очень быстрое перемещение, что типично для клиентских приложений.

Имя файла определяет маршрут. Например, src/routes/index.svelte — корневой файл вашего сайта:

<!-- src/routes/index.svelte -->
<svelte:head>
	<title>Добро пожаловать!</title>
</svelte:head>

<h1>Приветствую вас на моём сайте!</h1>

Файл с именем src/routes/about.svelte или src/routes/about/index.svelte будет соответствовать маршруту /about:

<!-- src/routes/about.svelte -->
<svelte:head>
	<title>О сайте</title>
</svelte:head>

<h1>Информация о сайте</h1>
<p>Это самый лучший сайт!</p>

Динамические параметры задаются при помощи квадратных скобок [...]. Например, таким образом можно сделать страницу, отображающую статью из блога:

<!-- src/routes/blog/[slug].svelte -->
<script context="module">
	// необязательная функция preload принимает объект
	// `{ path, params, query }` и превращает его в
	// данные, которые надо отрисовать на странице
	export async function preload(page, session) {
		// у нас есть доступ к параметру `slug`, потому что
		// файл называется [slug].html
		const { slug } = page.params;

		// `this.fetch` — это обертка для `fetch`, которая
		// позволяет выполнять легитимные запросы
		// как с сервера, так и с клиента
		const res = await this.fetch(`blog/${slug}.json`);
		const article = await res.json();

		return { article };
	}
</script>

<script>
	export let article;
</script>

<svelte:head>
	<title>{article.title}</title>
</svelte:head>

<h1>{article.title}</h1>

<div class='content'>
	{@html article.html}
</div>

Подробнее о функциях preload и this.fetch мы узнаем в разделе Предзагрузка

Маршруты сервера

Серверные маршруты — это модули, написанные в файлах .js, которые экспортируют функции, соответствующие HTTP методам. Каждая функция получает в качестве аргументов объекты HTTP request и response, а также функцию next. Это полезно для создания JSON API. Например, вот как бы вы могли бы создать эндпоинт для обслуживания страницы блога выше:

// routes/blog/[slug].json.js
import db from './_database.js'; // нижнее подчёркивание, говорит Sapper, что это не маршрут

export async function get(req, res, next) {
	// у нас есть доступ к параметру `slug`, потому что
	// файл называется [slug].json.js
	const { slug } = req.params;

	const article = await db.get(slug);

	if (article !== null) {
		res.setHeader('Content-Type', 'application/json');
		res.end(JSON.stringify(article));
	} else {
		next();
	}
}

delete — зарезервированное слово в JavaScript. Для обработки запросов DELETE экспортируйте функцию с именем del.

Правила именования файлов

Существует три простых правила именования файлов, которые определяют ваши маршруты:

  • Файл с именем src/routes/about.svelte соответствует маршруту /about. Файл с именем src/routes/blog/[slug].svelte соответствует маршруту /blog/:slug, и в этом случае params.slug доступен для preload
  • Файл src/routes/index.svelte соответствует корню вашего сайта. src/routes/about/index.svelte обрабатывается так же, как src/routes/about.svelte.
  • Файлы и каталоги начинающиеся с нижнего подчёркивания не создают маршруты. Это позволяет объединять вспомогательные модули и компоненты с маршрутами, которые зависят от них — например, у вас может быть файл с именем src/routes/_helpers/datetime.js, но маршрут /_helpers/datetime не будет создан

Страница с ошибкой

В дополнение к обычным страницам есть специальная страница, которую Sapper ожидает найти по пути — src/routes/_error.svelte. Она будет показана, если возникнет ошибка при отображении запрошенной страницы.

В шаблоне будет доступен объект error и код HTTP статуса в status.

Регулярные выражения в маршрутах

Вы можете использовать регулярные выражения для указания параметров маршрута, поместив их в скобки после имени параметра.

Например, src/routes/items/[id([0-9]+)].svelte будет соответствовать только числовым идентификаторам — /items/123, а маршрут /items/xyz не пройдёт.

Из-за технических особенностей, в регулярных выражениях невозможно использовать следующие символы: /, \, ?, :, ( and ).

API клиента

Модуль @sapper/app, который генерирует Sapper на основе вашего струкутры приложения, содержит функции для управления Sapper из кода и реагирования на события.

start({ target })

  • target — елемент, в который будут отрисовываться страницы

Настраивает маршрутизатор и запускает приложение — отлавливает клики по элементам <a>, взаимодействует с history API, отображает и обновляет компоненты Svelte.

Возвращает объект Promise, который выполняется, когда загруженная страница закончит 'гидратацию'.

import * as sapper from '@sapper/app';

sapper.start({
	target: document.querySelector('#sapper')
}).then(() => {
	console.log('клиентское приложение запустилось');
});

goto(href, options?)

  • href — страница, на которую надо перейти
  • options — может включать свойство replaceState, которое определяет, использовать ли history.pushState (по умолчанию) или history.replaceState. Не обязательно.

Перемещает по заданному в href маршруту. Если пунктом назначения является маршрут Sapper, то Sapper перехватит и отработает перемещение, в ином случае страница будет просто перезагружена с новым href. Иначе говоря, это ничем не отличается от поведения, когда пользователь просто кликает по ссылке с таким-же href атрибутом.

Возвращает объект Promise, который разрешается, когда перемещение будет завершено.

prefetch(href)

  • href — страница для упреждающей загрузки

Выполняет упреждающую загрузку указанной страницы, что означает: а) обеспечение полной загрузки кода для страницы и б) вызов метода preload страницы с соответствующими параметрами. Это поведение, аналогично случаю, когда пользователь тапает или в проводит курсором над элементом <a> с установленным атрибутом rel=prefetch.

Возвращает объект Promise, который разрешается, когда упреждающая загрузка будет завершена.

prefetchRoutes(routes?)

  • routes — массив строк, маршрутов для упреждающей загрузки. Не обязательно.

Выполняет упреждающую загрузку кода для маршрутов, которые ещё не были загружены до этого. Обычно вызывается после завершения sapper.start(), чтобы ускорить последующую навигацию (это реализует букву 'L' в PRPL шаблоне). Вы можете указать маршруты по любому подходящему пути, например, /about (для src/routes/about.svelte) или /blog/* (для src/routes/blog/[slug].svelte). В отличие от prefetch, это не вызовет функцию preload для каждой из загружаемых страниц. Вызов функции без аргументов приведёт к тому, что будут загружены все маршруты.

Возвращает объект Promise, который разрешается, когда упреждающая загрузка всех маршрутов будет завершена.

Предзагрузка

Как мы видели в разделе Маршруты, компоненты страниц верхнего уровня могут иметь функцию preload, которая будет загружать некоторые данные, от которых зависит страница. Она похожа на getInitialProps в Next.js или asyncData в Nuxt.js.

<script context="module">
	export async function preload(page, session) {
		const { slug } = page.params;

		const res = await this.fetch(`blog/${slug}.json`);
		const article = await res.json();

		return { article };
	}
</script>

Она помещается в блоке <script context="module">, потому что он не является частью экземпляров компонентов; вместо этого он выполняется до создания компонента, что позволяет избежать морганий компонента во время выборки данных. Подробнее в Учебнике

Аргументы

В функцию preload передаётся два аргумента — page и session.

page является объектом { host, path, params, query }, где host и path— это соответственно часть хоста и путь из URL, params выводится из URL и имени файла маршрута, а query является объектом значений из строки запроса..

Для примера рассмотрим знакомую страницу src/routes/blog/[slug].svelte. Предположим, к ней обратились по а URL-адресу вида /blog/some-post?foo=bar&baz, тогда мы получим следующие данные:

  • page.path === '/blog/some-post'
  • page.params.slug === 'some-post'
  • page.query.foo === 'bar'
  • page.query.baz === true

session генерируется на сервере путём передачи параметра session в sapper.middleware (TODO это требует дополнительной документации. Возможно будет раздел API сервера?)

Возвращаемое значение

Если вы вернёте промис из preload, страница не будет отображаться, пока промис не исполнится. Но вы также можете вернуть и простой объект.

Когда Sapper ренедерит страницу на сервере, он пытается сериализовать полученное значение (используя devalue) и помещает его на страницу, поэтому клиентской части нет необходимости повторно вызывать preload при инициализации. Сериализация выдаст ошибку, если значение включает функции или пользовательские классы (но можно использовать циклические и повторяющиеся ссылки, а также встроенные модули, типа Date,Map, Set и RegExp).

Контекст

Внутри функции preload у вас есть доступ к трём методам ...

  • this.fetch(url, options)
  • this.error(statusCode, error)
  • this.redirect(statusCode, location)

this.fetch

В браузерах вы можете использовать fetch для выполнения AJAX запросов, например, для получения данных с ваших серверных маршрутов. На сервере это несколько сложнее — вы можете делать HTTP-запросы, но нужно прописать origin, и у вас нет доступа к файлам cookie. Это означает, что невозможно запрашивать данные, основанные на сеансе пользователя, например, требующих входа в систему.

Чтобы исправить это, Sapper предлагает функцию this.fetch, которая работает одинаково как на сервере, так и на клиенте:

<script context="module">
	export async function preload() {
		const res = await this.fetch(`secret-data.json`, {
			credentials: 'include'
		});

		// ...
	}
</script>

Обратите внимание, что вам нужно будет использовать какую-либо прослойку для управления сессиями в вашем app/server.js, чтобы обрабатывать сеансы пользователей или делать что-либо, связанное с аутентификацией. Например express-session.

this.error

Если пользователь перейдёт на /blog/some-invalid-slug, хотелось бы ему показать страницу с ошибкой '404 — страница не найдена'. И мы можем сделать это с помощью this.error:

<script context="module">
	export async function preload({ params, query }) {
		const { slug } = params;

		const res = await this.fetch(`blog/${slug}.json`);

		if (res.status === 200) {
			const article = await res.json();
			return { article };
		}

		this.error(404, 'Страница не найдена');
	}
</script>

Аналогичным образом обрабатываются и другие коды ошибок, с которыми вы можете столкнуться.

this.redirect

Вы можете прервать отрисовку и перенаправить пользователя в другое место с помощью this.redirect:

<script context="module">
	export async function preload(page, session) {
		const { user } = session;

		if (!user) {
			return this.redirect(302, 'login');
		}

		return { user };
	}
</script>

Макеты

До сих пор мы рассматривали страницы как полностью автономные компоненты — при переходе между страницами существующий компонент уничтожался, а новый занимал его место.

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

Чтобы создать компонент макета, который будет применяться к каждой странице приложения, создайте файл с именем src/routes/_layout.svelte. По умолчанию компонент макета (такой же будет использовать Sapper, если не будет этого файла) выглядит следующим образом...

<slot></slot>

...но мы можем добавить любую разметку, стили и поведение, которые мы хотим. Например, давайте добавим панель навигациии:

<!-- src/routes/_layout.svelte -->
<nav>
	<a href=".">Главная</a>
	<a href="about">О сайте</a>
	<a href="settings">Настройки</a>
</nav>

<slot></slot>

Если мы создадим страницы для /, /about и /settings...

<!-- src/routes/index.svelte -->
<h1>Главная</h1>
<!-- src/routes/about.svelte -->
<h1>О сайте</h1>
<!-- src/routes/settings.svelte -->
<h1>Настройки</h1>

...навигация всегда будет видна, и переход между тремя страницами приведёт только к замене содержимого элемента <h1>.

Вложенные маршруты

Предположим, что у нас не просто одна страница /settings, а есть и вложенные страницы, вроде /settings/profile и /settings/notifications с общим подменю (для реального примера см. github.com/settings).

Мы можем создать макет, который применяется только к страницам, расположенным ниже /settings (при этом останется и корневой макет с навигацией):

<!-- src/routes/settings/_layout.svelte -->
<h1>Настройки</h1>

<div class="submenu">
	<a href="settings/profile">Профиль</a>
	<a href="settings/notifications">Уведомления</a>
</div>

<slot></slot>

Компонентам макета передаётся свойство segment, которое может быть полезно, например, для стилизации:

+<script>
+    export let segment;
+</script>
+
<div class="submenu">
-    <a href="settings/profile">Профиль</a>
-    <a href="settings/notifications">Уведомления</a>
+    <a
+        class:selected={segment === "profile"}
+        href="settings/profile"
+    >Профиль</a>
+
+    <a
+        class:selected={segment === "notifications"}
+        href="settings/notifications"
+    >Уведомления</a>
</div>

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

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

Создание компонента совместимого с SSR

Sapper хорошо работает с большинством сторонних библиотек, с которыми вы можете столкнуться. Однако иногда сторонняя библиотека поставляется собранной с прицелом на работу сразу с несколькими различными загрузчиками модулей. Иногда такой подход создаёт зависимость от объекта window, например, может проверяться на существование свойство window.global.

Поскольку в серверной среде, такой как Sapper, нет window, действие простого импорта такого модуля может привести к сбою импорта и завершить работу сервера Sapper с ошибкой:

ReferenceError: window is not defined

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

<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"/>

Хранилища

Значения page и session, передаваемые в функции preload, а так же preloading, доступны компонентам как хранилища.

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

<script>
	import { stores } from '@sapper/app';
	const { preloading, page, session } = stores();
</script>
  • preloading булевое значение только для чтения, показывающее идет ли еще процесс загрузки после перехода
  • page содержит объект {path, params, query}, только для чтения. Аналогичен объекту, передаваемому функции preload.
  • session содержит любые данные сессии, которые были оставлены на сервере. Это доступное для записи хранилище, то есть вы можете обновить его новыми данными (например, после входа пользователя в систему), затем приложение будет перерисовано.

Обновление данных сессии

На сервере можно заполнить данными session, передав соответствующий параметр в sapper.middleware:

// src/server.js
express() // или Polka, или похожий фреймворк
	.use(
		serve('static'),
		authenticationMiddleware(),
		sapper.middleware({
			session: (req, res) => ({
				user: req.user
			})
		})
	)
	.listen(process.env.PORT);

Данные сессии должны быть сериализуемыми(используется devalue) — никаких функций или пользовательских классов, только встроенные в JavaScript типы данных

Упреждающая загрузка

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

Для динамических маршрутов, вроде нашего примера src/routes/blog/[slug].svelte, этого бывает недостаточно. Чтобы отобразить сообщение в блоге, нам нужно получить для него данные, и мы не сможем этого сделать, пока не узнаем значение для slug. В худшем случае, это приведёт к задержке отображения страницы, так как браузер будет ожидать получения запрошенных данных от сервера.

rel=prefetch

Мы можем свести задержку к минимуму, предварительно запросив нужные данные. Добавление атрибута rel=prefetch в ссылку...

<a rel=prefetch href='blog/what-is-sapper'>Что такое Sapper?</a>

...заставит Sapper запустить preload функцию потенциально следующей страницы, как только пользователь наведёт курсор мыши на ссылку (на десктопе) или тапнет по ней (на мобильном устройстве), вместо того, чтобы ожидать события click, которое приведёт к переходу на эту страницу. Как правило, это даст нам дополнительные пару сотен миллисекунд, что как раз является разницей между быстрым UI, и тем который ощущается как тормозной.

rel=prefetch это идиома Sapper, а не стандартный атрибут для элементов <a>

Сборка

До сих пор мы использовали sapper dev для сборки нашего приложения и запуска сервера для разработки. Но когда дело доходит до продакшена, нам нужна автономная оптимизированная сборка.

sapper build

Эта команда упаковывает ваше приложение в директорию __sapper__/build. (Вы можете указать любую папку, а также изменить некоторые другие параметры — для получения дополнительной информации выполните sapper build --help.)

На выходе мы получаем Node приложение, которое вы можете запустить из корня проекта командой:

node __sapper__/build

Экспортирование

Есть немалая часть сайтов, которые по сути своей статичны, то есть им для работы не нужен сервер Express. Вместо этого они могут распространяться в виде статических файлов, что позволяет развёртывать их практически на любом хостинге (вроде Netlify или GitHub Pages). Статические сайты, как правило, дешевле в эксплуатации и имеют непервзойденную производительность.

Sapper позволяет вам экспортировать сайт в статические файлы с помощью одной простой команды sapper export. Кстати, вы прямо сейчас смотрите на экспортированный сайт!

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

sapper export

В директории вашего проекта Sapper выполните следующую команду:

# npx позволяет использовать локально установленные зависимости
npx sapper export

Будет создана директория __sapper__/export с готовой сборкой вашего сайта. Вы можете сразу запустить его таким образом:

npx serve __sapper__/export

Перейдите в браузере на адрес localhost:5000 и убедитесь, что ваш сайт работает корректно.

Вы также можете добавить скрипт в свой файл package.json...

{
	"scripts": {
		...
		"export": "sapper export"
	}
}

...что позволит экспортировать ваше приложение командой npm run export.

Как это работает

Когда вы запускаете sapper export, Sapper сначала создаёт рабочую версию вашего приложения, как происходит при запуске sapper build, и копирует содержимое вашей папки static в место назначения. Затем он запускает сервер и 'заходит' на главную страницу получившегося сайта. Оттуда он следует по всем найденным ссылкам из элементов <a> и сохраняет любые данные, предоставляемые приложением.

По этой причине любые страницы, которые необходимы в экспортированом сайте, должны быть доступны с помощью элементов <a>. Кроме того, любые серверные или иные нестраничные маршруты должны запрашиваться в preload, а не в onMount или ещё где-то.

Когда экспортировать не нужно

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

Обратите внимание, что вы всё ещё можете экспортировать приложения с динамическими маршрутами, как в нашем примере src/routes/blog/[slug].html. Команда sapper export будет обрабатывать this.fetch запросы внутри функций preload, поэтому данные, поступающие из src/routes/blog/[slug].json.js тоже будут сохранены.

Конфликты маршрутов

Поскольку sapper export создаёт отражение всех маршрутов в виде файлового дерева, то невозможно иметь два серверных маршрута, где возникает ситуация, что директория и файл в одном и том же месте будут иметь одинаковое имя. Например, src/routes/foo/index.js и src/routes/foo/bar.js будут пытаться создать файлы export/foo и export/foo/bar, что приведёт к ошибке.

Решение состоит в том, чтобы переименовать один из маршрутов и избежать подобного конфликта — например так: src/routes/foo-bar.js. Не забудьте, что при этом придётся подправить код приложения в том месте, где он берёт данные /foo/bar, указав новый маршрут /foo-bar.

Для маршрутов страниц этой проблемы не возникает, поскольку мы создаём файл export/foo/index.html вместо export/foo.

Развёртывание

Приложения Sapper запускаются везде, где поддерживается работа Node 8 или выше.

Развёртывание в Now

Этот раздел описывает работу только с Now 1, а не с Now 2

Мы легко можем развернуть приложение на платформе Now:

npm install -g now
now

Эти команды загрузят исходный код в Now, после чего он самостоятельно выполнит npm run build и npm start и даст вам URL, по которому будет располагаться развёрнутое приложение.

Для других хостингов вам скорее всего нужно будет выполнять npm run build вручную.

Развёртывание сервис-воркеров

Sapper обеспечивает уникальность файла сервис-воркера(service-worker.js), путём добавления временной метки в исходный код, которая рассчитывается с использованием функции Date.now ().

В окружениях, где приложение разворачивается на нескольких физических серверах (например, Now), следует использовать одинаковую временную метку для сервис-воркеров на всех инстансах. В противном случае пользователи могут столкнуться с проблемами, когда сервис-воркер будет неожиданно обновляться, потому что приложение обращается сначала к серверу 1, затем к серверу 2, а метка времени на них будет различаться.

Чтобы переопределить метку времени Sapper, вы можете использовать переменную среды (например, SAPPER_TIMESTAMP), а затем изменить service-worker.js подобным образом:

const timestamp = process.env.SAPPER_TIMESTAMP; // вместо `import { timestamp }`

const ASSETS = `cache${timestamp}`;

export default {
	/* ... */
	plugins: [
		/* ... */
		replace({
			/* ... */
			'process.env.SAPPER_TIMESTAMP': process.env.SAPPER_TIMESTAMP || Date.now()
		})
	]
}

Затем вы можете определить эту переменную при запуске, например:

SAPPER_TIMESTAMP=$(date +%s%3N) npm run build

При развертовании на Now, вы можете передать эту переменную непосредственно в Now:

now -e SAPPER_TIMESTAMP=$(date +%s%3N)

Безопасность

По умолчанию Sapper не добавляет в приложение никаких http-заголовков, касающихся безопасности, но вы можете добавить их самостоятельно, используя прослойку, например Helmet.

Политики защиты контента(CSP)

Sapper генерирует встроенные в страницу элементы <script>, которые могут не выполняться, если заголовки Политики защиты контента/Content Security Policy (CSP) запрещают выполнение таких скриптов (unsafe-inline).

Чтобы обойти это, Sapper может встроить nonce, который может быть сконфигурирован для генерации нужных CSP заголовков. Вот пример использования Express и Helmet:

// server.js
import uuidv4 from 'uuid/v4';
import helmet from 'helmet';

app.use((req, res, next) => {
	res.locals.nonce = uuidv4();
	next();
});
app.use(helmet({
	contentSecurityPolicy: {
		directives: {
			scriptSrc: [
				"'self'",
				(req, res) => `'nonce-${res.locals.nonce}'`
			]
		}
	}
}));
app.use(sapper.middleware());

Использование res.locals.nonce подобным образом предусмотрено документацией Helmet по CSP.

Базовые URL

Обычно точка входа в приложение Sapper находится в /. Но в некоторых случаях вашему приложению требуется быть доступным по другому базовому пути — например, если Sapper контролирует только часть вашего домена, или у вас несколько приложений Sapper, которые живут рядом друг с другом.

Это можно сделать таким образом:

// app/server.js

express() // или Polka, или иной фреймворк
	.use(
		'/my-base-path', // <!-- добавьте эту строку
		compression({ threshold: 0 }),
		serve('static'),
		sapper.middleware()
	)
	.listen(process.env.PORT);

Sapper правильно настроит маршруты как на стороне сервера, так и на стороне клиента для работы с указанным базовым путём.

Если вы экспортируете своё приложение, то нужно указать по какому пути искать корень сайта:

sapper export --basepath my-base-path

Тестирование

Вы можете использовать любые фреймворки и библиотеки для тестирования по своему вкусу. В sapper-template по умолчанию используется Cypress.

Запуск тестов

npm test

Будет запущен сервер и открыт Cypress. Вы можете (и должны!) добавлять свои собственные тесты в cypress/integration/spec.js. Для получения дополнительной информации обратитесь к документации.

Отладка

Заниматься отладкой серверного кода особенно просто при помощи ndb. Установите его глобально ...

npm install -g ndb

...потом запустите приложение Sapper:

ndb npm run dev

Предполагается, что скрипт npm run dev запускает sapper dev. Вы также можете запустить Sapper через npx таким образом — ndb npx sapper dev.

Обратите внимание, что в терминале может не быть никакого вывода, пока запускается ndb.