Building this site
Технические детали разработки сайта snov.digital
Техническая задача
Создать публично доступное "место", куда можно добавлять различный контент с адекватными временными затрами на соответствующий тип контента. Далее под понятием "сайт" будет подразумеваться данное публично доступное "место".
Типы контента
- записи блога/статьи с медиа и интерактивными вставками
- новости
- произвольный контент
Необходимые свойства сайта
- возможность представления и предоставления контента в виде статически сгенерированной страницы
- возможность добавлять новые типы конента
- добавление, редактирование, удаление экземпляра типа контента
- навигация между типами контента и между экзмемплярами типа контента
- адаптивное отображение контента на различных устройствах
- адаптивное отображение контента для разных пользователей
- интернализация
Выбор платформы и основных инструментов
Платформа: Веб
- охват аудитории
- необходимые для разработки и поддержки ресурсы
- возможности платформы
Язык: TypeScript
- самодокументирование
- стабильность при росте кодовой базы
- безопасный рефакторинг
Фреймворк: NextJS
- возможность генерации статического контента
- возможность использовать полноценный SSR при необходимости
- поддерживаемый, хорошо документированный, популярный фреймворк
Платорма разработки: GitHub
- удобство использование
- GitHub Actions
- наличие интеграций
Инфраструктура: Vercel
- возможность хостить статический контент
- полная поддержка приложений на NextJS
- интеграция с GitHub
Перманентное хранилище: Firebase Firestore
- адекватные бесплатные лимиты использования
- поддержка динамических реактивных данных
- интеграция с Firebase Authentication, Firebase Functions
Реализация
Сайт представляет собой набор статических страниц с встроенными статическими данными.
Статические данные — данные, которые являются перманентными в контексте рантайма приложения и являются одинаковыми для всех экземпляров приложения. Эти данные изменяются только при выпуске новой версии приложения. К такому виду данных относятся:
- набор цветов светлой/тёмной темы
- метаданные приложения (версия, хэш коммита)
- локализованные сообщения
- посты блога
- страница «Об авторе»
Хранилище
Статические данные должны постоянно храниться вне запущенного приложения и могут подвергаться изменениям извне приложения. Варианты хранилищ:
- непосредственный код приложения (inline)
- локальная файловая системы репозитория приложения
- сторонний сервис (база данных, например, Firebase Firestore)
По мере эволюции требований к взаимодействию с данными, выбор варианта используемого для данного типа данных хранилища может меняться.
Код приложения — самый простой вариант, в котором зачастую и зарождается новый тип статических данных. Вариант со сторонним сервисом является самым универсальным, но и самым дорогим.
Доступ к хранилищу
Когда пользователь заходит на сайт, он должен получить необходимые для отображения и работы с контентом сайта данные. В набор этих данных так же входят и статические данные, т.е. если статические данные хранятся в базе данных, то чтобы отобразить сайт тысяче пользователей нужно будет совершить тысячу чтений из базы данных.
Но ключевое свойство статических данных (перманентность и идентичность для всех экземпляров приложения) позволяет отвязать количество чтений из хранилища от количества запросов сайта.
NextJS предоставляет обширные возможности для генерации статических страниц, в том числе внедрение в каждую страницу данных, получаемых на этапе сборки приложения. Эти данные по своим свойствам полностью соответствуют определению статических данных.
Таким образом данные будут прочитаны только 1 (один) раз во время выпуска новой версии приложения (обновлении контента приложения). Такие свойства хранилища статических данных позволяют использовать внешнюю базу данных с минимальным потреблением её ресурсов, а следовательно финансовыми затратами. Из процесса получения данных при запросе клиентом полностью исчезает взаимодействие с хранилищем: даныне были запрошены и получены заранее и встроены в код приложения.
Для доступа к хранилищу при локальной разработки целесообразно использовать стратегию с кэшированием:
- Инвалидация кэша при начале разработки
- Загрузка данных из хранилища
- Кэширование данных
- Чтение данных из кэша при последующих запросах, переход к п. 2, если запрашиваемых данных нет в кэше.
- Внедрение данных в приложение функциональностью NextJS (getInitialProps, getStaticProps)
Процесс получения данных на этапе выпуска новой версии приложения:
- Чтение данных из Firebase Firestore
- Кэширование данных
- Внедрение данных в приложение функциональностью NextJS (getInitialProps, getStaticProps)
Кэш может храниться локально в файловой системе.
Типы статических данных
Статические данные могут быть как глобальными для всего приложения, так и уникальными для каждой страницы (далее — страничные данные).
Глобальные статические данные
Предоставление доступа к глобальным данным реализуется через расширение стандартных объектов NextJS: NextApp в pages/_app.tsx и NextDocument в pages/_document.tsx.
- Данные запрашиваются при помощи любого API в Document.getInitialProps:
Document.getInitialProps = async () => {const globalData = await fetchGlobalDataFromAnyAPI();return {globalData,};};
- Внедряются в 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]. Но такая форма доступа имеет проблемы с раскрытием деталей реализации.
- Создание провайдера и хука доступа к данным:
export function createGlobalDataContext<D>(initialData: D) {const ctx = createContextReact(initialData);const useCtx = () => useContext(ctx);return {Provider: ctx.Provider,useContext: useCtx,};}
- Создание 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;}}
- Объявление типа GlobalData, создание провайдера и хука доступа:
export type GlobalData = any;export const globalDataContext = createGlobalDataContext<GlobalData>({} as GlobalData);export const GlobalDataProvider = globalDataContext.Provider;export const useGlobalData = globalDataContext.useContext;
- Использование провайдера и 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>);}}
- Получение доступа к данным на любой странице:
export const JustAnotherPage = () => {const { someData } = useGlobalData();return <div>{someData}</div>;};
В результате любой компонент, используемый в приложении может полуить доступ к глобальным данным.
Страничные статические данные
Pierrot — это CMS-ish библиотека для Next.js, предоставляющая просто интерфейс для доступа к хранилищам данным и для их внедрения в приложение. Код в Глобальные статические данные
Локализованные страницы сайта имеют URL формата snov.digital/[[language]]/[[page]].
Сообщения хранится как key-value пары в виде полей объекта [[language]].
Список сообщений хранится в Firebase Firestore и внедряется в приложение при помощи Pierrot.
Добавление нового локализованного сообщения
Темизируемые примитивы:
- главная навигационная панель
- ссылки на разделы сайта
- главная страница
- заголовок сайта
- описание сайта
- футер
- текст футера
При этом каждый из примитивов может быть отображён поразному для разных устройств (разрешения экрана, плотности пикселей и т.д.)
Подход со статическими данными, хранящимися в Firestore и внедряемыми на этапе сборки, позволяет реализовать панель управления с функциональностью:
- изменение статических данных
- сборка новой версии приложения с изменёнными статическими данными
Так образом, можно управлять контентом сайта без необходимости работы непосредственно с кодом.