Живые части: «шоурум открыт» и калькулятор кухни
Страницы Eile готовятся один раз и отдаются из памяти мгновенно. Но кое-что должно меняться: открыт ли шоурум прямо сейчас, сколько будет стоить кухня длиной 3,4 метра. Для этого есть некэшируемые регионы и острова.
Как работает кэш
Когда посетитель впервые открывает страницу, Eile собирает её по шаблону и запоминает готовый HTML. Следующие посетители получают его из памяти за доли миллисекунды. Как только вы поправите страницу или шаблон, запомненное сбрасывается, и страница соберётся заново.
Это хорошо для всего, что меняется от правки к правке. А полоска «Шоурум открыт» меняется сама, по часам. Запомни её Eile в десять утра, она бы весь день показывала то, что было в десять.
Некэшируемый регион
Для таких мест есть регион: кусок страницы, который собирается заново на каждый запрос, а всё вокруг него по-прежнему берётся из памяти. Сделаем компонент Showroom:
<!-- @component
Открыт ли шоурум: считается заново на каждый запрос по московскому времени.
-->
<script>
let {
/** Открываемся, час */
opens = 10,
/** Закрываемся, час */
closes = 20,
} = $props();
const hour = Number(
new Intl.DateTimeFormat('ru', {
hour: 'numeric',
hourCycle: 'h23',
timeZone: 'Europe/Moscow',
}).format(new Date())
);
const open = hour >= opens && hour < closes;
</script>
<p class="now" class:open>
{#if open}
Шоурум открыт до {closes}:00, приходите посмотреть кухни вживую
{:else}
Шоурум закрыт, откроемся в {opens}:00
{/if}
</p>
<style>
.now {
margin: 0;
padding: 0.55rem 1.5rem;
background: #1f1e1c;
color: #f1efeb;
font-size: 0.875rem;
text-align: center;
}
.open {
background: #3d4733;
}
</style>new Date(): текущий момент.Intl.DateTimeFormatсtimeZone: 'Europe/Moscow'переводит его в час по Москве, где бы ни стоял сервер.class:open: добавляет классopen, когда шоурум открыт. Так у полоски меняется цвет.
Теперь поставим полоску в шапку как регион. Замените код Header:
<!-- @component
Шапка: открыт ли шоурум, название сайта и меню.
-->
<script>
import Nav from '$components/Nav';
import { site } from '$core';
import Uncached from '$core/Uncached';
</script>
<Uncached name="Showroom" props={{ opens: 10, closes: 20 }} />
<header class="header">
<a class="logo" href="/">{site().name}</a>
<Nav />
</header>
<style>
.header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
max-width: 48rem;
margin: 0 auto;
padding: 1.5rem;
}
.logo {
color: inherit;
font-size: 1.25rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
text-decoration: none;
}
</style><Uncached name="Showroom" …/> говорит: «этот компонент не запоминай, рисуй каждый раз». Параметры передаются в props.
Остров: калькулятор кухни
Регион собирается на сервере. А калькулятор должен отзываться на ползунок без перезагрузки, то есть работать в браузере. Для этого есть острова: компонент, который сервер рисует как обычно, а браузер потом оживляет. JavaScript попадает только на те страницы, где остров есть. Остальной сайт остаётся без единого скрипта.
Компонент Calc:
<!-- @component
Калькулятор: длина кухни в метрах и примерная цена. Считает прямо в браузере.
-->
<script>
let {
/** Цена за погонный метр, ₽ */
price = 0,
} = $props();
let length = $state(3);
const rub = new Intl.NumberFormat('ru');
</script>
<div class="calc">
<label for="length">Длина кухни: <b>{length.toLocaleString('ru')} м</b></label>
<input id="length" type="range" min="1.5" max="6" step="0.1" bind:value={length} />
<p>Примерно <b>{rub.format(Math.round(price * length))} ₽</b> без техники и сборки</p>
</div>
<style>
.calc {
display: grid;
gap: 0.6rem;
margin: 1.5rem 0;
padding: 1.25rem 1.4rem;
border-radius: 1.25rem;
background: #fff;
}
input {
width: 100%;
accent-color: #1f1e1c;
}
p {
margin: 0;
}
</style>let length = $state(3);: состояние, число, которое может меняться. Сначала кухня длиной три метра.bind:value={length}: связывает ползунок и переменную в обе стороны. Двигаете ползунок, меняетсяlength, а за ним всё, что от неё зависит.{rub.format(Math.round(price * length))}: цена пересчитывается при каждом движении ползунка.
Поставим остров на страницу кухни. Замените код шаблона Kitchen:
<script>
import Content from '$core/Content';
import Island from '$core/Island';
import Layout from '$components/Layout';
import { image } from '$core';
let { page } = $props();
const price = page.fields.price;
const plan = page.fields.plan;
const photo = page.fields.photo ? image(page.fields.photo) : null;
const rub = new Intl.NumberFormat('ru');
</script>
<Layout {page}>
{#if photo}
<img
class="photo"
src={photo.src}
srcset={photo.srcset}
sizes="(min-width: 48rem) 45rem, 100vw"
width={photo.width}
height={photo.height}
alt={photo.alt}
/>
{/if}
<h1>{page.title}</h1>
<dl class="facts">
{#if plan}
<div><dt>Планировка</dt><dd>{plan}</dd></div>
{/if}
{#if price}
<div><dt>Цена</dt><dd>от {rub.format(price)} ₽ за метр</dd></div>
{/if}
</dl>
{#if price}
<Island name="Calc" props={{ price }} />
{/if}
<Content of={page} />
</Layout>
<style>
.photo {
display: block;
border-radius: 1.25rem;
}
.facts {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 2.5rem;
margin: 0;
}
dt {
color: #6b6862;
font-size: 0.875rem;
}
dd {
margin: 0;
font-size: 1.25rem;
font-weight: 650;
}
</style><Island name="Calc" props={{ price }} />: цена уходит острову параметром. Острову недоступен $core: он живёт в браузере, где нет базы сайта. Всё нужное передавайте через props.
