Серверные острова
На этой странице
Иногда одной области в остальном статичной страницы нужны свежие данные на каждый запрос — комментарии под закэшированной записью блога, наличие товара на карточке. Серверные острова оставляют страницу статичной и рендерят только эту область на сервере, когда страницу открывают.
1. Напишите остров
Остров — это модуль-фрагмент внутри src/islands/: обычный файл .js или .ts (не .ht.js, потому что острова — фрагменты, а не страницы). Идея та же, что и везде в sitelo: функция, возвращающая HTML.
export default async function comments({ props, request }) {
const comments = await fetchComments(props.postId)
return `<ul>${comments.map((c) => `<li>${c.text}</li>`).join('')}</ul>`
}Он получает { name, props, request } и должен вернуть строку HTML. Модули островов работают только на сервере — код без ссылок внутри src/ никогда не попадает в браузер.
2. Разместите его на странице
Импортируйте island() из sitelo/islands. Статическая сборка публикует запасной HTML; пропсы встраиваются в плейсхолдер, поэтому держите их небольшими и без секретов.
import { island } from 'sitelo/islands'
export default ({ params }) => `
<html>
<body>
<article>…статическое содержимое…</article>
${island('comments', { postId: params.slug }, '<p>Загрузка комментариев…</p>')}
<script type="module" src="/islands.js"></script>
</body>
</html>
`import { html, body, article, script } from 'javascript-to-html'
import { island } from 'sitelo/islands'
export default ({ params }) =>
html(
body(
article('…статическое содержимое…'),
island('comments', { postId: params.slug }, '<p>Загрузка комментариев…</p>'),
script({ type: 'module', src: '/islands.js' }),
),
)import { island } from 'sitelo/islands'
export default function Post({ params }) {
return (
<html>
<body>
<article>…статическое содержимое…</article>
{island('comments', { postId: params.slug }, '<p>Загрузка комментариев…</p>')}
<script type="module" src="/islands.js" />
</body>
</html>
)
}3. Добавьте клиентский загрузчик
Крошечный скрипт запрашивает каждый отрендеренный фрагмент и подставляет его. Он проходит через обычный конвейер ресурсов, так что достаточно простой точки входа src/islands.js:
import { mountIslands } from 'sitelo/islands/client'
mountIslands()В sitelo (dev) и sitelo preview это уже работает — оба отдают острова по адресу /_sitelo/islands/<name> из src/islands/. Preview загружает нативные модули .js / .mjs (как и хост на Node); острова на TypeScript поддерживаются в разработке через Vite.
Продакшн
Ваш статический хостинг продолжает отдавать страницы. Подключите небольшой обработчик там, где у вас выполняется серверный код — Node, serverless или edge-функция — и он отрендерит те же модули островов. Полный разбор с готовым Node-хостом и заготовками для Netlify и Vercel — в примере серверных островов.
// например, сервер Node или serverless/edge-функция
import { createIslandsHandler } from 'sitelo/islands/server'
const handleIslands = createIslandsHandler({
islands: {
comments: () => import('./src/islands/comments.js'),
},
})
// Web Request → Response | null (null = это не запрос острова)
export default { fetch: (request) => handleIslands(request) }С обычным http из Node или с express используйте createIslandsNodeHandler(options) — те же параметры, сигнатура (req, res, next). Все модули .js / .mjs внутри src/islands/ можно подключить автоматически через createIslandsFromDirectory. Если загрузчик обращается к другому источнику или пути, передайте mountIslands({ endpoint: 'https://api.example.com/islands' }) и согласуйте это с параметром endpoint обработчика.
Что считается стабильным
island()/mountIslands()/createIslandsHandler/createIslandsNodeHandler— публичный API, считается стабильнымsiteloиsitelo previewоба отдают/_sitelo/islandsизsrc/islands/createIslandsFromDirectory— одна и та же карта для Node-хостов и preview (нативные.js/.mjs/.cjs)- Заготовки хостов в пример серверных островов:
server.jsдля Node, функция Netlify + rewrite, serverless Vercel + rewrite - Пропсы остаются небольшими и без секретов (строка запроса GET) — так задумано; получайте секреты внутри острова, на сервере
- Пропсы приходят от клиента — проверяйте их или подписывайте через
SITELO_ISLANDS_SECRET
Стратегии загрузки
По умолчанию каждый остров загружается сразу вместе со страницей, так что страница с восемью островами делает восемь одновременных запросов во время первой отрисовки. Передайте when, чтобы отложить те, что не видны сразу.
// Загружается сразу вместе со страницей — по умолчанию.
island('cart', { id }, '<p>…</p>')
// Ждёт момента простоя.
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })
// Ждёт появления в области просмотра.
island('comments', { postId }, '<p>Загрузка комментариев…</p>', {
when: 'visible',
rootMargin: '400px', // начать загрузку на 400px раньше
})'load'(по умолчанию) — немедленно, вместе со всеми остальными островами'idle'— поrequestIdleCallback(с откатом на таймаут)'visible'— когда остров попадает в область просмотра, через IntersectionObserver
rootMargin применяется только к 'visible' и по умолчанию равен '200px'. Кроме того, острова завершаются по таймауту, а не крутятся вечно:
mountIslands({
timeout: 5000, // на остров; 0 отключает. По умолчанию 10000
rootMargin: '300px', // по умолчанию для островов с `when: 'visible'`
})mountIslands() завершается, как только «немедленные» острова улеглись — отложенные загружаются позже сами по себе и намеренно не ожидаются. Остров, который упал или не уложился в таймаут, сохраняет свой запасной HTML.
Пропсы — недоверенный ввод
Пропсы острова приходят от клиента. Они встроены в страницу, возвращаются с запросом, и кто угодно может изменить их по пути:
GET /_sitelo/islands/profile?props={"userId":"someone-else"}Относитесь к пропсам, которые получает ваш остров, точно как к параметру запроса: проверяйте их и никогда не используйте, чтобы достать данные, которые посетителю и так не положены.
Когда пропсы выбирают привилегированные данные, подписывайте их. Задайте секрет — и sitelo подпишет каждый плейсхолдер во время сборки, отклоняя всё остальное с кодом 403:
SITELO_ISLANDS_SECRET=$(openssl rand -hex 32) sitelo buildТу же переменную читают sitelo, sitelo preview и createIslandsHandler, так что разработка, preview и продакшн согласованы. Передайте тот же секрет своему продакшн-хосту. Предпочитаете задать его в коде?
import { configureIslands } from 'sitelo/islands'
configureIslands({ secret: process.env.MY_SECRET })Подписи — это HMAC-SHA256 от имени острова и его пропсов, поэтому подпись, выданная одному острову, не сработает для другого. Подпись доказывает, что пропсы пришли из вашей сборки — но не скрывает их, так что секретов в них по-прежнему быть не должно. Без секрета пропсы принимаются как есть, и их проверка целиком на вашем модуле острова.
Полезно знать
- Не развернули эндпоинт островов? Тогда просто останется запасной HTML — страницы деградируют мягко.
- Запросы идут методом
GETс пропсами в строке запроса, поэтому ответы кэшируемы — задайте параметрcacheControlобработчика, если хотите, чтобы CDN ненадолго придерживал фрагменты. - Во время загрузки плейсхолдер несёт
data-sitelo-island-state="loading"(затемloadedилиerror) — удобно для CSS.