Создаем первый «Astroвной» проект

Шаблон «Острова интерактивности» в последнее время на слуху: его применяют во многих фреймворках. К примеру, на нем основана вся архитектура недавно появившегося 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()]
});

Теперь сделаем поиск. С помощью 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, добавив настаиваемую интеграцию.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Fernando Doglio: Building Your First Island-based Project with Astro

Предыдущая статья5 тегов HTML, о которых вы могли не знать
Следующая статья7 правил ESLint, рекомендуемых для проектов TypeScript/React