Firebase Auth і Firestore у Next.js без плоскіх калекцыяў
Захавайце адзін экземпляр прыёмнікі Firebase, дазвольце калектарам аутанацыі выклікваць памятныя адзінакі, разместіце трэнуванні пад кожным пользователям, а таксама пераканайцеся ў правільнасці запісаў за дапамогою эмулятораў.
Стартэры, які успадкованы з Firebase + Next.js, частаўкаюць парой проблем: адна плоская колекцыя workouts, якая храніць дакументы кожнага абліквента, і блакі з логаванням, які пасля чаго вяртаюць undefined, таму ў вызываючых кодах ніколі не ствараецца усведамлення пра невыпанню запісу. Дэманстрацыі з адным входам маскуюць обе проблемы. Але рэальны трафік — няма.
Ядро апрантаку для аутэнтыкацыі і стежэння ў додатку для фізычных вучоў было перабудавана на Firebase JS SDK 12.17.1 і працавала з эмуляторамі Firebase, каб можна было пераканацца ў правільнасці запісоў з початку да канца. Нижэйшыя прыміткі сфокусаваны на якнайранейшам выправленні структуры.
Одна інстанцыя Firebase, а не пяць
Частая прычына проблем — вызыв initializeApp у верхней часты модуля, які імпортуецца колькама маршрутамі. Функцыя hot reload у Next.js плюс раздзелэнне пакетаў між серверам і кліентам можа запусціць гэты модуль два разы; другі вызыв тады паведамляе пра тое, што апрантак ужо існуе. Яго трэба захаваць:
import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_SENDER_ID!,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};const app = getApps().length ? getApp() : initializeApp(firebaseConfig);export const auth = getAuth(app);
export const db = getFirestore(app);
getApps().length ? getApp() : initializeApp(...) — гэта весь захад. Канфігурацыю трэба размешчаць у зменных аптэмпаратуры NEXT_PUBLIC_, а не кодаваць яе ў кодзе — не таму, што канфігурацыя Firebase для веба є секрэтной (за прыметамі яна адправляецца ў браузер), а таму, што окрэслыя проекты для разработкі і працы ў режыме продакшну не должны вымагаць правкі коду, каб перейсці з аднаго режыму ў іны.
У старэйшых нарадчыках у файл падкладаюць рэшткі "your-api-key". Сама по сабе гэта не ўтварае прычыны для абавянення; гэта проста звычка, якая ў канцэ нарабляе ситуацыю, калі практычная канфігурацыя для продакшну опускаецца у публічны репазітарый.
Аутэнтыкацыя, якая вяртае ўсё, што можа быць выкорыстана вызывачам
Выкорыстоўваць трэба такі формат. Зверніце увагу, чаго ён не робіць: ён не ловіць адзіны і не вяртае undefined.
import {
createUserWithEmailAndPassword,
type User,
} from "firebase/auth";
import { addDoc, collection, serverTimestamp } from "firebase/firestore";
import { auth, db } from "./firebase";
export async function registerUser(
email: string,
password: string,
): Promise<User> {
const cred = await createUserWithEmailAndPassword(auth, email, password);
return cred.user;
}
Старэйшыя варыяты абрабаткаюць вызов у формате try/catch, выводзяць паведамленне пра адміністрацыйныя наследжэння і вяртаючы undefined. Код интерфейсу пасля чаго чакае вызова registerUser(...) і прабуе з’ясаваць значэнне user.uid, якое можа не існаваць — таму некоректны пароль чы рэплікатываны адрес электронной пошты ніколі не стаюць чыстымі адміністрацыйнымі наследжэннямі; гэтыя проблемы пазнаюцца пазней у вачынку чытання атрыбута з undefined, далёкая ад рэальнай прычыны наследжэння.
Лепш выкорыстоўваць працэс адхілення. Вызовы Firebase для аутантыкацыі выклекаюць наследжэння з адзначаным типам (auth/email-already-in-use, auth/weak-password і т. п.). Спраўцаваць гэтыя коды так, каб у форме паказваўся зрозумелы тэкст. Нехай варыяты для обработкі дадзеных будуць чыстымі: альбо успех, альбо выкарыстанне наследжэння.
Структуруйце трэнування па корыстніку з самага пачатку
Гэтае змена мае наўбольшы значэнне па захад ад протатыпу. Старыя прыклады ствараюць плоскую колекцыю workouts з атрыбутам userId:
// what everyone copies — one collection for the whole app
addDoc(collection(db, "workouts"), { userId, ...workout });
Запыткі нашукання “майных трэнуванняў” скануюць калекцыю, якая расте разам з усім корпусам паўтаральнікаў, і правілы безпекі павінны фільтруваць за userId для кожной операцыі. Лепш выкарыстоўваць падколекцыю, каб трэнування кожнага паўтаральніка знаходзіліся ў сваёй сабе докумэнцы:
export type WorkoutInput = {
type: string;
durationMinutes: number;
caloriesBurned: number;
};
export async function addWorkoutSession(
userId: string,
workout: WorkoutInput,
): Promise<string> {
const ref = await addDoc(collection(db, "users", userId, "workouts"), {
...workout,
createdAt: serverTimestamp(),
});
return ref.id;
}
collection(db, “users”, userId, “workouts”) нацэлена на прыватную падколекцыю таго акаунта. Правілы безпекі можаць прагледзець request.auth.uid з сегментам шляху {uid} як пад час чытання, так і під час запісу, тады кожны запыт залишаецца ўнутрь докумэнцыў аднае паўтаральніцы.
Два моменты, які варта адгукнуць. Валідзіце serverTimestamp() заместо new Date(): гаджэты кліента (або зловольныя кліенты) запісваюць некоректныя часовыя пазнакі; serverTimestamp() — это спецыяльны показнік, які Firestore заполняе часам сервера пад час зберагчыка і які нельга падрабатваць. Якшы запісваць данні як WorkoutInput, а не як any, можна выявіць памылкі ў назвах полей, якія інакш выказваюцься толькі тады, калі графік тыхо не паказвае нічога.
Падтвердзіце, што данні дэйсна запішываюцца
Не давайце доверы коду Firebase, які ніколі не працаваў на эмуляторы — SDK будзе приймать вызовы, якія рэальныя правіла безпекі адхілілі б. Направьце SDK на локальныя эмуляторы і перадзейсцавайце весь процес: рэгістрацыю, запіс і чытанне дадзеных.
import { getAuth, connectAuthEmulator, createUserWithEmailAndPassword } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator, addDoc, getDocs, collection, serverTimestamp } from "firebase/firestore";
connectAuthEmulator(auth, "http://127.0.0.1:9099", { disableWarnings: true });
connectFirestoreEmulator(db, "127.0.0.1", 8080);const cred = await createUserWithEmailAndPassword(auth, email, "s3cret-pass");
const ref = await addDoc(
collection(db, "users", cred.user.uid, "workouts"),
{ type: "run", durationMinutes: 32, caloriesBurned: 410, createdAt: serverTimestamp() },
);
const snap = await getDocs(collection(db, "users", cred.user.uid, "workouts"));
Адзьённе таго жа пацеку через эмуляторны раннер даў конкрэтны ідентыфікатор аутанацыі, ідэнтыфікатор для запісу і дакумент для чытання, у якога пэрыярод createdAt быў рэальным часам зберагача Firestore (seconds/nanoseconds), а не спецыяльным значэнням — гэта ўбеджае, што сервер заполниў гэтая поле. Некоректныя пацекі або типы полей не працуюць на ноутбуку, а не пасля развяртання.
Уваға ўжо па абладнанні: Firebase CLI тепер выкарыстоўвае Java 21 або новейшую версію. Старэйшы JRE на чыстам ноутбуку зупініць эмуляторны раннер з памылкай версіі пры першай жа спробе запуску коду аплікацыі. Апдэйтуйте JDK, а потым спробавайце зноў.
Куды працаваць далей
Автанацыя плюс правільна працэўнае сфера для зберагання дадзеных — гэта аднакова падстава. Готавы продукт зазвычай налаштоввае функцыю onAuthStateChanged для керування станам интерфейса пасля абмовкі, перадае коды памылак автанацыі ў форму, а таксама прадстаўляе історыю змян за дапамогою getDocs, orderBy("createdAt", "desc") і параметра limit. Усі гэтыя функцыі ўсё ж такі залежаць ад двух ранніх рашэнняў: ініцыялізаваць Firebase аднойчы за захоўкам і размешчаць дакументы трэнування пад самым власнікам. Якщо працягнуць аднае з гэтых рашэнняў, то пазнейшыя функцыі будуць падвергнуты наследным нашкоджэнням.