Шаблон «Острова интерактивности» в последнее время на слуху: его применяют во многих фреймворках. К примеру, на нем основана вся архитектура недавно появившегося Fresh 1.0.
Но сегодня поговорим о фреймворке Astro, с которым веб-приложения создаются, как вам нужно. Хотите задействовать React? И немного Vue? Без проблем! Интегрировать компонент Svelte? Почему бы нет!
С Astro вы реализуете островную архитектуру: в приложения переносится только необходимый JavaScript, и ничего более.
Итак, приступим!
Начало работы с Astro
Проект с Astro подготовить очень просто, особенно для создания статического блога с одним интерактивным компонентом, то есть островом.
npm create astro@latest
Этой командой запустится пошаговое руководство. Ответьте на вопросы (просто укажите, что создаете блог), и сформируется все необходимое для его немедленного запуска.
Если возникнут проблемы, загляните в документацию.
Как создается блог
Автоматически, командой create
.
Используя шаблон Astro islands
, добавим в блог поле поиска в реальном времени и индексатор, для которого создадим настаиваемую интеграцию.
Конечный результат:
Акцент не на поиске, а на факте добавления статичному сайту интерактивного компонента.
Начнем с индексирования контента.
Создание первой интеграции Astro
Интеграции Astro подобны создаваемым для сайта плагинам, с помощью которых одному или нескольким этапам процесса сборки добавляется поведение.
Нам нужно индексировать генерируемый контент, поэтому интеграцию подключим в самом конце процесса сборки. Так проиндексируется каждый пост в блоге.
Для этого перейдем к файлу astro.config.mjs
в корне папки проекта, добавим функцию indexMySite
и установим пакет parse-md
для считывания титульных листов файлов Markdown:
function indexMySite() {
return {
name: "Index my site",
hooks: {
'astro:build:done': ({
pages,
routes
}) => {
let blogPages = routes.filter(r => {
return r.type == 'page' && r.component.indexOf(".md") != -1;
});
let myIndex = [];
const currentFiile = fileURLToPath(import.meta.url);
const __dirname = path.dirname(currentFiile);
blogPages.forEach(p => {
console.log("Reading...", p.component);
let mdContent = fs.readFileSync(__dirname + "/" + p.component);
let {
metadata
} = parseMD(mdContent.toString());
myIndex.push({
title: metadata.title,
description: metadata.description,
path: p.route
});
});
fs.writeFileSync('public/index.json', JSON.stringify(myIndex));
console.log(myIndex);
}
}
};
}
Этой функцией возвращается объект с обязательными свойствами name
(название интеграции) и hooks
(хуки, в нашем случае это astro:build:done
, чтобы код запускался сразу по завершении процесса сборки).
Другие хуки см. в документации.
Для ключа хука определим функцию, которой принимается массив pages
(страниц) и routes
(маршрутов). Последние используем для этой сборки, выполняя итеративный обход и учитывая только страницы блога (отфильтровываем все, что не имеет расширения .md
), а затем с помощью parse-md
получаем метаданные файла и добавляем в массив.
В конце создаем «индекс» поисковой системы, сохранив массив в JSON-файле общего каталога блога. Так он доступнее для островного компонента.
Функция готова. Убедимся в ее работоспособности на Astro: внутри того же файла переходим к функции defineConfig
и добавляем вызов новой функции внутри массива integrations
.
// https://astro.build/config
export default defineConfig({
site: 'https://example.com',
integrations: [mdx(), sitemap(), indexMySite()]
});
Добавление компонента search
Теперь сделаем поиск. С помощью Astro интегрируются компоненты разных фреймворков, мы будем использовать React.
Сначала добавим его поддержку: командой npx astro add react
установим React и добавим как интеграцию в файл astro.config.mjs
.
Затем создадим собственный компонент Search
в папке src/components
: для интерактивных компонентов специальная папка не нужна (в отличие от Fresh, где такая папка имеется — islands
).
Компонент очень прост: загружаем из общей папки файл index.json
и выполняем в нем поиск.
import { createRef, useEffect, useState} from "react"
let myIndex = null;
function ResultList({results}) {
if(results.length == 0) return ;
return (
<div id="results-list">
<ul>
{results.map( r => (
<li><a href={r.path}>{r.title}</a></li>
))}
</ul>
</div>
)
}
export default function Search() {
let inputRef = createRef()
const [results, setResults] = useState([])
useEffect(() => {
async function loadIndex() {
myIndex = await fetch('/index.json')
myIndex = await myIndex.json()
}
loadIndex()
}, [])
function doSearch() {
let q = inputRef.current.value
let matches = myIndex.filter( page => {
return page.title.indexOf(q) != -1
})
setResults(matches)
console.log(matches)
}
return (
<div>
<input placeholder="Search..." onChange={doSearch} ref={inputRef} />
<ResultList results={results} />
</div>
)
}
В этом файле мы определили два компонента: экспортируемый Search
для отображения поля ввода и выполнения поиска при вводе пользователем текста и ResultsList
, используемый из первого компонента для отображения списка результатов.
Теперь перейдем к файлу Header.astro
с кодом для заголовка блога и добавим готовый компонент:
---
import HeaderLink from './HeaderLink.astro';
import { SITE_TITLE } from '../config';
import Search from './Search';
---
<header>
<h2>
{SITE_TITLE}
</h2>
<nav>
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/about">About</HeaderLink>
<HeaderLink href="https://twitter.com/astrodotbuild" target="_blank">Twitter</HeaderLink>
<HeaderLink href="https://github.com/withastro/astro" target="_blank">GitHub</HeaderLink>
<Search />
</nav>
</header>
<style>
header {
margin: 0em 0 2em;
}
h2 {
margin: 0.5em 0;
}
</style>
Но здесь есть нюанс. Добавленный так (см. строку 17) компонент будет в Astro как любой другой: отобразится на сервере, но без добавления интерактивного кода на стороне клиента.
По умолчанию все компоненты в Astro отображаются на сервере. А этот нужно гидратировать на клиенте, сделав островом интерактивности.
Но как? Чтобы указать, когда гидратировать компонент, добавляются директивы client:
.
В нашем случае это client:load
, так как гидратация нужна сразу по завершении загрузки страницы:
---
import HeaderLink from './HeaderLink.astro';
import { SITE_TITLE } from '../config';
import Search from './Search';
---
<header>
<h2>
{SITE_TITLE}
</h2>
<nav>
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/about">About</HeaderLink>
<HeaderLink href="https://twitter.com/astrodotbuild" target="_blank">Twitter</HeaderLink>
<HeaderLink href="https://github.com/withastro/astro" target="_blank">GitHub</HeaderLink>
<Search client:load/>
</nav>
</header>
<style>
header {
margin: 0em 0 2em;
}
h2 {
margin: 0.5em 0;
}
</style>
Директива в строке 17 добавлена, и теперь компонент отобразится корректно, с кодом JavaScript внутри.
Мы отрисовали первый остров в Astro!
С островами все очень просто: из фреймворков клиенту передается только необходимый JavaScript, остальное остается на сервере.
Определить компонент как остров — так же просто, как передать директиву client:
при добавлении в HTML. Наконец, мы расширили поведение Astro, добавив настаиваемую интеграцию.
Читайте также:
- Почему все веб-сайты выглядят одинаково?
- Прощай, Adobe
- 9 лучших примеров макетов сайта и идей для веб-дизайна
Читайте нас в Telegram, VK и Дзен
Перевод статьи Fernando Doglio: Building Your First Island-based Project with Astro