Как использовать Firebase в динамических веб-приложениях с SSR (обработкой на стороне сервера)

Если вы работали с Firebase JS SDK или другими клиентскими SDK Firebase, то, вероятно, знакомы с интерфейсом FirebaseApp и знаете, как с его помощью настраивать экземпляры приложений. Чтобы упростить выполнение аналогичных операций на стороне сервера, Firebase предоставляет FirebaseServerApp.

FirebaseServerApp – это вариант FirebaseApp для использования в средах с серверной отрисовкой (SSR). В нем есть инструменты для продолжения сеансов Firebase, которые охватывают разделение рендеринга на стороне клиента (CSR) и рендеринга на стороне сервера. Эти инструменты и стратегии могут помочь улучшить динамические веб-приложения, созданные с помощью Firebase и развернутые в средах Google, таких как Firebase App Hosting.

Используя FirebaseServerApp, вы можете:

  • Выполнять код на стороне сервера в контексте пользователя, в отличие от Firebase Admin SDK, у которого есть полные права администратора.
  • Включите использование Проверки приложений в средах SSR.
  • Продолжить сеанс Firebase Auth, созданный в клиенте.

Жизненный цикл FirebaseServerApp

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

Удаление экземпляров FirebaseServerApp

Иногда бывает сложно определить, когда нужно вызывать deleteApp для экземпляра FirebaseServerApp, особенно если вы параллельно выполняете много асинхронных операций. Поле releaseOnDeref в FirebaseServerAppSettings упрощает эту задачу. Если вы назначите releaseOnDeref ссылку на объект со сроком действия области запроса (например, объект заголовков запроса SSR), FirebaseServerApp уменьшит количество ссылок, когда фреймворк вернет объект заголовка. Это автоматически очистит ваш экземпляр FirebaseServerApp.

Пример использования releaseOnDeref:

/// Next.js
import { headers } from 'next/headers'
import { FirebaseServerAppSettings, initializeServerApp} from "firebase/app";

export default async function Page() {
  const headersObj = await headers();
  let appSettings: FirebaseServerAppSettings = {};
  appSettings.releaseOnDeref = headersObj;
  const serverApp = initializeServerApp(firebaseConfig, appSettings);
  ...
}

Возобновление сеансов с аутентификацией, созданных на клиенте

Когда экземпляр FirebaseServerApp инициализируется с помощью токена идентификатора Auth, он позволяет связывать сеансы аутентифицированных пользователей между средами клиентской и серверной отрисовки. Экземпляры Firebase Auth SDK, инициализированные с помощью объекта FirebaseServerApp, который содержит токен идентификатора Auth, будут пытаться выполнить вход пользователя при инициализации без необходимости вызова методов входа приложением.

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

/// Next.js
import { initializeServerApp } from "firebase/app";
import { getAuth } from "firebase/auth";

// Replace the following with your app's
// Firebase project configuration
const firebaseConfig = {
  // ...
};

const firebaseServerAppSettings = {
  authIdToken: token  // See "Pass client tokens to the server side
                      // rendering phase" for an example on how transmit
                      // the token from the client and the server.
}

const serverApp =
  initializeServerApp(firebaseConfig,
                      firebaseServerAppSettings);
const serverAuth = getAuth(serverApp);

// FirebaseServerApp and Auth will now attempt
// to sign in the current user based on provided
// authIdToken.

Как использовать проверку приложений в средах SSR

Принудительное применение Проверки приложений зависит от экземпляра SDK Проверки приложений, который Firebase SDK используют для внутреннего вызова getToken. Полученный токен включается в запросы ко всем сервисам Firebase, что позволяет серверной части проверять приложение.

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

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

/// Next.js
import { initializeServerApp } from "firebase/app";

// Replace the following with your app's
// Firebase project configuration
const firebaseConfig = {
  // ...
};

const firebaseServerAppSettings = {
  appCheckToken: token // See "Pass client tokens to the server side
                       // rendering phase" for an example on how transmit
                       // the token from the client and the server.
}

const serverApp =
  initializeServerApp(firebaseConfig,
                      firebaseServerAppSettings);

// The App Check token will now be appended to all Firebase service requests.

Передайте токены клиента на этап отрисовки на стороне сервера

Чтобы передавать аутентифицированные токены Auth ID (и токены Проверки приложений) от клиента на этап отрисовки на стороне сервера, используйте сервис-воркер. Этот подход включает перехват запросов на получение, которые запускают SSR, и добавление токенов в заголовки запросов.

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

Используйте Firestore в средах SSR.

При создании веб-приложений с отрисовкой на стороне сервера (SSR) часто требуется передавать данные между сервером и клиентом, чтобы оптимизировать производительность и удобство работы пользователей. Firestore SDK предоставляет инструменты сериализации, которые позволяют делать снимки и собирать данные определенных типов на сервере и передавать их напрямую в компоненты на стороне клиента. Этот процесс устраняет избыточные выборки, позволяя клиенту гидратировать состояние, используя данные, предварительно полученные на этапе SSR. Кроме того, вы можете перейти от этих сериализованных состояний к прослушивателям в реальном времени, гарантируя, что ваше приложение останется синхронизированным с базой данных.

В этом разделе описывается, как повторно использовать данные, полученные на этапе серверной отрисовки (SSR), в клиентских компонентах.

Сериализация типов данных

Некоторые типы данных Firestore предоставляют метод toJSON для преобразования данных в сериализуемый формат. К ним относятся экземпляры объектов, таких как Bytes, GeoPoint, Timestamp и VectorValue.

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

import {
  Bytes
} from 'firebase/firestore';

const BYTES_DATA = new Uint8Array([0, 1, 2, 3, 4, 5]);
const bytes = Bytes.fromUint8Array(BYTES_DATA);
const bytesJSON = bytes.toJSON();

Десериализация типов данных

Типы данных Firestore включают статический метод fromJSON для преобразования сериализованных данных в рабочий тип данных Firestore.

Например, следующий код десериализует тип данных Bytes:

import {
  Bytes
} from 'firebase/firestore';

// Assuming the same `bytesJSON` variable from the previous example.
const deserializedBytes = Bytes.fromJSON(bytesJSON);

Сериализация и десериализация снимков Firestore

Как и в случае с типами данных Firestore, вы можете сериализовать экземпляры DocumentSnapshot и QuerySnapshot с помощью toJSON. Однако для десериализации необходимо использовать отдельные функции documentSnapshotFromJSON и querySnapshotFromJSON вместо статического метода fromJSON.

Например, результаты querySnapshot операции query можно сериализовать с помощью метода toJSON:

import {
  collection,
  getDocs,
  query,
  querySnapshotFromJSON
} from 'firebase/firestore';
// Assuming a configured instance of Firestore in the variable `firestore`.
const queryRef = query(collection(firestore, QUERY_PATH));
const querySnapshot = await getDocs(queryRef);
const querySnapshotJson = querySnapshot.toJSON();

Затем эти данные можно десериализовать:

import {
  querySnapshotFromJSON
} from 'firebase/firestore';

// deserializedSnapshot is an object of type QuerySnapshot:

const deserializedSnapshot =
  querySnapshotFromJSON(firestore, querySnapshotJson);

Прослушиватели с сериализованными снимками

Хотя данные, запрашиваемые на этапе SSR, ценны для первоначального рендеринга CSR, вам все равно может потребоваться отслеживать сервис Firestore на предмет обновлений этой информации в реальном времени.

Если вашему приложению требуются такие обновления в реальном времени, вы можете использовать функцию onSnapshotResume, чтобы инициализировать SnapshotListener Firestore с сериализованными данными Snapshot. Пример:

const observer = {
  next: (qs) => {
    console.log("onSnapshot invoked: ", qs.data());
  },
  error: (e) => {
    console.log("error callback invoked: ", e.toString());
  }
};
const unsubscribe = onSnapshotResume(firestore, querySnapshotJson, observer);