Building this site

Building this site

programming
web
development

Технические детали разработки сайта snov.digital

Техническая задача

Создать публично доступное "место", куда можно добавлять различный контент с адекватными временными затрами на соответствующий тип контента. Далее под понятием "сайт" будет подразумеваться данное публично доступное "место".

Типы контента

  • записи блога/статьи с медиа и интерактивными вставками
  • новости
  • произвольный контент

Необходимые свойства сайта

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

Выбор платформы и основных инструментов

Платформа: Веб

  • охват аудитории
  • необходимые для разработки и поддержки ресурсы
  • возможности платформы

Язык: TypeScript

  • самодокументирование
  • стабильность при росте кодовой базы
  • безопасный рефакторинг

Фреймворк: NextJS

  • возможность генерации статического контента
  • возможность использовать полноценный SSR при необходимости
  • поддерживаемый, хорошо документированный, популярный фреймворк

Платорма разработки: GitHub

  • удобство использование
  • GitHub Actions
  • наличие интеграций

Инфраструктура: Vercel

  • возможность хостить статический контент
  • полная поддержка приложений на NextJS
  • интеграция с GitHub

Перманентное хранилище: Firebase Firestore

  • адекватные бесплатные лимиты использования
  • поддержка динамических реактивных данных
  • интеграция с Firebase Authentication, Firebase Functions

Реализация

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

Статические данные

Статические данные — данные, которые являются перманентными в контексте рантайма приложения и являются одинаковыми для всех экземпляров приложения. Эти данные изменяются только при выпуске новой версии приложения. К такому виду данных относятся:

  • набор цветов светлой/тёмной темы
  • метаданные приложения (версия, хэш коммита)
  • локализованные сообщения
  • посты блога
  • страница «Об авторе»

Хранилище

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

  1. непосредственный код приложения (inline)
  2. локальная файловая системы репозитория приложения
  3. сторонний сервис (база данных, например, Firebase Firestore)

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

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

Доступ к хранилищу

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

Но ключевое свойство статических данных (перманентность и идентичность для всех экземпляров приложения) позволяет отвязать количество чтений из хранилища от количества запросов сайта.

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

Таким образом данные будут прочитаны только 1 (один) раз во время выпуска новой версии приложения (обновлении контента приложения). Такие свойства хранилища статических данных позволяют использовать внешнюю базу данных с минимальным потреблением её ресурсов, а следовательно финансовыми затратами. Из процесса получения данных при запросе клиентом полностью исчезает взаимодействие с хранилищем: даныне были запрошены и получены заранее и встроены в код приложения.

Для доступа к хранилищу при локальной разработки целесообразно использовать стратегию с кэшированием:

  1. Инвалидация кэша при начале разработки
  2. Загрузка данных из хранилища
  3. Кэширование данных
  4. Чтение данных из кэша при последующих запросах, переход к п. 2, если запрашиваемых данных нет в кэше.
  5. Внедрение данных в приложение функциональностью NextJS (getInitialProps, getStaticProps)

Процесс получения данных на этапе выпуска новой версии приложения:

  1. Чтение данных из Firebase Firestore
  2. Кэширование данных
  3. Внедрение данных в приложение функциональностью NextJS (getInitialProps, getStaticProps)

Кэш может храниться локально в файловой системе.

Типы статических данных

Статические данные могут быть как глобальными для всего приложения, так и уникальными для каждой страницы (далее — страничные данные).

Глобальные статические данные

Предоставление доступа к глобальным данным реализуется через расширение стандартных объектов NextJS: NextApp в pages/_app.tsx и NextDocument в pages/_document.tsx.

  1. Данные запрашиваются при помощи любого API в Document.getInitialProps:
Document.getInitialProps = async () => {
const globalData = await fetchGlobalDataFromAnyAPI();
return {
globalData,
};
};
  1. Внедряются в HTML в Document.render
const GLOBAL_DATA_KEY = 'GLOBAL_DATA';
type Props<T> = {
globalData: T;
};
class Document extends NextDocument<Props> {
render() {
const stringifiedGlobalData = JSON.stringify(this.props.globalData);
return (
<Html>
<Head>{'...'}</Head>
<body>
<script dangerouslySetInnerHTML={{ __html: `window.${GLOBAL_DATA_KEY}=${stringifiedGlobalData}` }}></script>
<Main />
<NextScript />
</body>
</Html>
);
}
}

После этого этапа глобальные данные уже становятся доступны в приложении через window[GLOBAL_DATA_KEY]. Но такая форма доступа имеет проблемы с раскрытием деталей реализации.

  1. Создание провайдера и хука доступа к данным:
export function createGlobalDataContext<D>(initialData: D) {
const ctx = createContextReact(initialData);
const useCtx = () => useContext(ctx);
return {
Provider: ctx.Provider,
useContext: useCtx,
};
}
  1. Создание hydrate:

hydrate предназначена для того, чтобы получить актуальные данные для приложения в зависимости от среды (браузер или node.js): мы гарантируем, что данные находятся в window[GLOBAL_DATA_KEY],если код выполняется в браузере; если код выполняется в node.js, мы имеем прямой доступ к globalData через props, пришедшие в NextApp.

function hydrate<D>(props: { globalData: D }) {
if (typeof window !== 'undefined') {
return (window as any)[GLOBAL_DATA_KEY];
} else {
return props.globalData;
}
}
  1. Объявление типа GlobalData, создание провайдера и хука доступа:
export type GlobalData = any;
export const globalDataContext = createGlobalDataContext<GlobalData>({} as GlobalData);
export const GlobalDataProvider = globalDataContext.Provider;
export const useGlobalData = globalDataContext.useContext;
  1. Использование провайдера и hydrate в NextApp:
type Props = {
globalData: GlobalData;
};
export default class App extends NextApp<Props> {
// globalData будет гарантированно содержать данные после выполнения конструктора
globalData!: GlobalData;
constructor(props: Props) {
super(props);
this.globalData = hydrate(props);
}
render() {
const { Component, pageProps } = this.props;
return (
<GlobalDataProvider value={this.globalData}>
<Component {...pageProps} />
</GlobalDataProvider>
);
}
}
  1. Получение доступа к данным на любой странице:
export const JustAnotherPage = () => {
const { someData } = useGlobalData();
return <div>{someData}</div>;
};

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

Страничные статические данные

Pierrot

Pierrot — это CMS-ish библиотека для Next.js, предоставляющая просто интерфейс для доступа к хранилищам данным и для их внедрения в приложение. Код в Глобальные статические данные

Интернализация

Локализованные страницы сайта имеют URL формата snov.digital/[[language]]/[[page]].

Сообщения хранится как key-value пары в виде полей объекта [[language]].

Список сообщений хранится в Firebase Firestore и внедряется в приложение при помощи Pierrot.

Добавление нового локализованного сообщения

Визуальные темы

Темизируемые примитивы:

  • главная навигационная панель
    • ссылки на разделы сайта
  • главная страница
    • заголовок сайта
    • описание сайта
  • футер
    • текст футера

При этом каждый из примитивов может быть отображён поразному для разных устройств (разрешения экрана, плотности пикселей и т.д.)

Панель управления

Подход со статическими данными, хранящимися в Firestore и внедряемыми на этапе сборки, позволяет реализовать панель управления с функциональностью:

  • изменение статических данных
  • сборка новой версии приложения с изменёнными статическими данными

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