
Ich habe an Next.js-Anwendungen gearbeitet, die auf über 100.000 monatlich aktive Nutzer skaliert wurden, und an Landing Pages mit Millionen monatlicher Besucher. In diesem Artikel teile ich alle Lektionen, die ich durch unzählige Iterationen gelernt habe.
Egal, ob Sie Teil eines kleinen Teams von ein, zwei oder drei Entwicklern sind oder an einem riesigen Next.js-Projekt mit mehreren Teams an derselben Codebasis arbeiten, es ist entscheidend, das Fundament Ihrer App von Anfang an richtig zu legen. Selbst wenn Sie an einem bestehenden Projekt arbeiten, werden Sie einige hart erarbeitete Erkenntnisse entdecken, die Sie noch heute auf Ihre App anwenden können.
Ich nehme Sie an die Hand und zeige Ihnen alle Schritte und Pakete, die Sie einrichten sollten, und erkläre Ihnen, warum sie nützlich sind, damit Ihre App reibungslos skaliert.
Beginnen Sie mit der Erstellung eines neuen Next.js-Projekts.
npx create-next-app@latest
Es könnte Sie fragen, ob es die neueste create-next-app Version installieren darf, drücken Sie einfach Ja.
Need to install the following packages:
create-next-app@15.0.0
Ok to proceed? (y)
Konfigurieren Sie Ihr Projekt anschließend, indem Sie bei allem mit Ja antworten (TypeScript, Tailwind, App Router).
✔ **What is your project named?** … reactsquad-production
✔ **Would you like to use** **TypeScript****?** … No / Yes
Yes
✔ **Would you like to use** **ESLint****?** … No / Yes
Yes
✔ **Would you like to use** **Tailwind CSS****?** … No / Yes
Yes
✔ **Would you like to use** **`src/` directory****?** … No / Yes
Yes
✔ **Would you like to use** **App Router****? (recommended)** … No / Yes
Yes
✔ **Would you like to customize the default** **import alias** **(@/*)?** … No / No
Wechseln Sie dann in das Verzeichnis Ihres Projekts und öffnen Sie es in Ihrem bevorzugten Editor.
$ cd nextjs-for-production
~/dev/nextjs-for-production (main) 🤯
$ cursor .
Sie möchten überprüfen, ob Ihre Einrichtung funktioniert hat.
npm run dev aus, um den Entwicklungsserver zu starten.http://localhost:3000 , um Ihre Anwendung anzuzeigen.Ihr Projekt hat TypeScript bereits konfiguriert, aber Sie möchten auch einen expliziten Befehl zu Ihrer package.json das alle Ihre Dateien auf Typfehler überprüft.
"type-check": "tsc -b"
Diesen Befehl werden Sie später zusammen mit anderen automatisierten statischen Analyseprüfungen verwenden.
Wenn Sie mit einem großen Team an einem Projekt zusammenarbeiten, ist es wichtig, die Art und Weise, wie jeder Code schreibt, zu vereinheitlichen. Besprechen Sie mit Ihrem Team Entscheidungen wie die Verwendung von Semikolons, Anführungszeichenstilen und Tabs gegenüber Leerzeichen.
Verwenden Sie dann Tools, um Ihren Styleguide durchzusetzen und Code automatisch zu formatieren.
Dafür gibt es zwei Tools: Prettier und ESLint.
Prettier ist ein meinungsstarker Code-Formatter, der Stil-Diskussionen während Code-Reviews eliminiert.
Installieren Sie Prettier zusammen mit seinem Tailwind-Plugin.
npm install --save-dev prettier prettier-plugin-tailwindcss
Erstellen Sie eine prettier.config.js Datei mit Ihren bevorzugten Regeln.
module.exports = {
arrowParens: 'avoid',
bracketSameLine: false,
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxSingleQuote: false,
plugins: ['prettier-plugin-tailwindcss'],
printWidth: 80,
proseWrap: 'always',
quoteProps: 'as-needed',
requirePragma: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
useTabs: false,
};
Fügen Sie ein Formatierungsskript zu Ihrer package.json.
"format": "prettier --write .",
Führen Sie den Formatter aus, um Ihren Stil anzuwenden.
$ npm run format
> nextjs-for-production@0.1.0 format
> prettier --write .
next.config.mjs 4ms (unchanged)
package-lock.json 54ms (unchanged)
package.json 1ms (unchanged)
postcss.config.mjs 2ms (unchanged)
README.md 20ms (unchanged)
src/app/globals.css 17ms (unchanged)
src/app/layout.tsx 30ms (unchanged)
src/app/page.tsx 11ms (unchanged)
tailwind.config.ts 2ms (unchanged)
tsconfig.json 2ms (unchanged)
Ihre Dateien sind jetzt „prettier“, aber Sie möchten auch ESLint verwenden.
ESLint kann Ihren Code sowohl auf stilistische als auch auf logische Probleme scannen. Installieren Sie ESLint und seine Plugins, wie unicorn, playwright und import sort.
npm install --save-dev @typescript-eslint/parser eslint-plugin-unicorn eslint-plugin-import eslint-plugin-playwright eslint-config-prettier eslint-plugin-prettier eslint-plugin-simple-import-sort
Aktualisieren Sie Ihre .eslintrc.json.
{
"extends": [
"next/core-web-vitals",
"plugin:unicorn/recommended",
"plugin:import/recommended",
"plugin:playwright/recommended",
"plugin:prettier/recommended"
],
"plugins": ["simple-import-sort"],
"rules": {
"simple-import-sort/exports": "error",
"simple-import-sort/imports": "error",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-reduce": "off",
"unicorn/prevent-abbreviations": [
"error",
{
"allowList": {
"e2e": true
},
"replacements": {
"props": false,
"ref": false,
"params": false
}
}
]
},
"overrides": [
{
"files": ["*.js"],
"rules": {
"unicorn/prefer-module": "off"
}
}
]
}
Die Plugins dieses Tutorials bieten verschiedene Funktionen. Für detaillierte Beschreibungen besuchen Sie die jeweiligen GitHub-Seiten.
Kurz gesagt, setzen sie Codierungsstandards durch, organisieren Importe und stellen die korrekte Verwendung moderner JavaScript-Funktionen sicher. Da ESLint und Prettier in Konflikt geraten können, sorgt diese Einrichtung dafür, dass sie reibungslos zusammenarbeiten. Die Plugins helfen auch, Fehler zu vermeiden und Stile konsistent zu halten, insbesondere bei Tools wie Vitest und Playwright.
Fügen Sie ein Linting-Skript zu Ihrer package.json.
"lint:fix": "next lint --fix",
Führen Sie es aus, um alle Dateien gemäß Ihren neuen Regeln zu formatieren.
$ npm run lint:fix
> nextjs-for-production@0.1.0 lint:fix
> next lint --fix
✔ No ESLint warnings or errors
Wenn Sie eine TypeScript-Versionswarnung erhalten, können Sie diese ignorieren.
Hinweis: Zum Zeitpunkt der Erstellung dieses Artikels ist ESLint 9 verfügbar, aber dieses Tutorial verwendet ESLint 8, da viele Plugins noch nicht die neueste Version unterstützen.
Bei der Zusammenarbeit in einem großen Team ist es auch hilfreich, konsistente Commit-Nachrichten durchzusetzen, um die Projekthistorie klar zu halten. Durch die Wahl der richtigen Standards können Sie die Generierung von Changelogs und Releases mit korrekter semantischer Versionierung automatisieren.
Installieren Commitlint und die notwendigen Konfigurationen. Dazu gehört Husky, das bei der Verwaltung von Git-Hooks hilft.
npm install --save-dev @commitlint/cli@latest @commitlint/config-conventional@latest husky@latest
Initialisieren Sie Husky in Ihrem Projekt, um die Basiskonfiguration einzurichten.
npx husky-init && npm install
Fügen Sie Hooks hinzu, um Linting und Typüberprüfung vor jedem Commit zu automatisieren und Ihren Commit-Nachrichten-Workflow anzupassen.
npx husky add .husky/pre-commit 'npm run lint && npm run type-check'
npx husky add .husky/prepare-commit-msg 'exec < /dev/tty && npx cz --hook || true'
Der pre-commit -Hook läuft nach git commit, aber bevor die Commit-Nachricht finalisiert wird, und führt Linting und Typüberprüfung für Ihren Code aus.
Der prepare-commit-msg -Hook läuft, nachdem git commit initiiert wurde, aber bevor der Commit-Nachrichten-Editor geöffnet wird. Er führt die commitizen -CLI aus, damit Sie konventionelle Commit-Nachrichten erstellen können. Sie werden gleich lernen, wie Sie diesen Hook verwenden.
Entfernen Sie die Zeile, in der steht npm test aus .husky/_/pre-commit.
Stellen Sie sicher, dass diese Skripte ausführbar sind.
chmod a+x .husky/pre-commit
chmod a+x .husky/prepare-commit-msg
Hier steht chmod für „change mode“. Dieser Befehl ermöglicht es Ihnen, die Zugriffsrechte oder Modi einer Datei oder eines Verzeichnisses in Unix- und Unix-ähnlichen Betriebssystemen zu ändern. Das Argument a+x fügt Ausführungsrechte für alle Benutzer hinzu.
Installieren Sie Commitizen, das eine CLI zum Erstellen konventioneller Commit-Nachrichten bereitstellt.
npm install --save-dev commitizen cz-conventional-changelog
Konfigurieren Sie Commitizen in Ihrer package.json , um den Conventional Changelog Standard zu verwenden.
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
Der Conventional Changelog Standard ist eine Spezifikation, um Commit-Nachrichten eine für Menschen und Maschinen lesbare Bedeutung zu verleihen. Er wurde entwickelt, um die Erstellung von Changelogs und Releases basierend auf der Git-Historie Ihres Projekts zu automatisieren.
Ein zukünftiger Artikel wird diesen Standard detailliert erläutern. Er ist in diesem Tutorial enthalten, weil es wichtig ist, ihn von Anfang an richtig zu machen. Sie können ihn nutzen und davon profitieren, ohne ein tiefgehendes Verständnis zu benötigen.
Erstellen Sie Ihre commitlint.config.cjs Datei mit Regeln, die den Anforderungen Ihres Teams entsprechen. Diese Konfiguration gewährleistet, dass Ihre Commit-Nachrichten konsistent und relevant für die vorgenommenen Änderungen sind.
const config = {
extends: ['@commitlint/config-conventional'],
rules: {
'references-empty': [1, 'never'],
'footer-max-line-length': [0, 'always'],
'body-max-line-length': [0, 'always'],
},
};
module.exports = config;
Führen Sie den folgenden Befehl aus, um Ihre Commit-Nachrichten mithilfe einer geführten CLI zu erstellen.
$ git add --all
$ npx cz
cz-cli@4.3.0, cz-conventional-changelog@3.3.0
? Select the type of change that you're
committing: (Use arrow keys)
❯ feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the
meaning of the code (white-space, formatting,
missing semi-colons, etc)
Der Befehl cz stellt eine Reihe von Fragen und schreibt dann Ihre Commit-Nachricht für Sie.
? Select the type of change that you\\'re committing: feat: A new feature
? What is the scope of this change (e.g. component or file name): (press enter to skip)
? Write a short, imperative tense description of the change (max 63 chars):
(61) set up TS type checks, ESLint, Prettier, Commitlint and Husky
? Provide a longer description of the change: (press enter to skip)
Sets up package.json script to do TS type checks. Sets up Prettier and ESLint with good rules. Configures Husky and sets up Commitizen using the conventional commit standard.
? Are there any breaking changes? No
? Does this change affect any open issues? No
[main eb69ccd] feat(set up static analysis checks): set up TS type checks, ESLint, Prettier, Commitlint and Husky
7 files changed, 3108 insertions(+), 159 deletions(-)
create mode 100755 .husky/pre-commit
create mode 100755 .husky/prepare-commit-msg
create mode 100644 commitlint.config.cjs
Während Sie alle Fragen beantworten, führen die Husky-Hooks automatisch TypeScript-Typüberprüfungen und Linting aus.
Es gibt üblicherweise zwei gängige Methoden, Ihren Code zu organisieren: Gruppierung nach Typ oder Gruppierung nach Funktion.
Gruppierung nach Typ sieht so aus:
.
├── components
│ ├── todos
│ └── user
├── reducers
│ ├── todos
│ └── user
└── tests
├── todos
└── user
Und Gruppierung nach Funktion sieht so aus:
.
├── todos
│ ├── component
│ ├── reducer
│ └── test
└── user
├── component
├── reducer
└── test
Das Gruppieren von Dateien nach Funktion in einem Projekt organisiert alle zugehörigen Komponenten, Reducer und Tests zusammen, was die Verwaltung und Änderung jeder Funktion erleichtert. Dies bietet folgende Vorteile:
Hier ist ein konkreteres Beispiel.
src/
├── app/
│ ├── ...
│ ├── (group)/
│ │ ├── about/
│ │ │ └── page.tsx
│ │ ├── settings/
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── dashboard/
│ │ ├── page.tsx
│ │ └── layout.tsx
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── ...
│ └── header/
│ ├── header-component.tsx
│ ├── header-component.test.ts
│ └── header.module.css
├── features/
│ ├── ...
│ ├── todos/
│ │ ├── ...
│ │ ├── todos-component.tsx
│ │ ├── todos-component.test.ts
│ │ ├── todos-container.ts
│ │ ├── todos-reducer.ts
│ │ ├── todos-reducer.test.ts
│ │ └── todos-styles.ts
│ └── user/
│ ├── ...
│ ├── user-reducer.ts
│ └── user-reducer.test.ts
├── hocs/
│ ├── ...
│ └── with-layout.tsx
├── hooks/
├── redux/
│ ├── root-reducer.ts
│ ├── root-saga.ts
│ └── store.ts
├── ...
└── middleware.ts
Dieses Beispiel zeigt Ihnen, wie Sie Ihr src/ Verzeichnis. Im Allgemeinen wird alles nach Funktionen gruppiert. Testdateien befinden sich neben ihren jeweiligen Implementierungsdateien. Alles, was von mehreren Funktionen gemeinsam genutzt wird, wird jedoch in allgemeinen Ordnern gruppiert, wie z.B. components/ oder HOCs/ oder hooks/. Gemeinsame Einstellungen für das Zustandsmanagement – in diesem Beispiel für Redux – befinden sich in einem redux/ Ordner.
Manche Leute gruppieren auch gerne nach Funktionen innerhalb des app/ Ordners. So würde das aussehen.
src/
├── app/
│ ├── ...
│ ├── (group)/
│ ├── dashboard/
│ │ ├── components/
│ │ │ ├── dashboard-header.tsx
│ │ │ ├── dashboard-header.test.ts
│ │ │ ├── dashboard-widgets.tsx
│ │ │ └── dashboard-widgets.test.ts
│ │ ├── services/
│ │ │ ├── fetch-data.ts
│ │ │ ├── fetch-data.test.ts
│ │ │ ├── auth-service.ts
│ │ │ └── auth-service.test.ts
│ │ ├── page.tsx
│ │ └── layout.ts
│ ├── layout.tsx
│ └── page.tsx
├── ...
└── middleware.ts
In diesem Tutorial werden Sie Dateien auf die erste Weise nach Funktionen gruppieren.
Wenn Sie Fehler vermeiden und Regressionen verhindern möchten, müssen Sie Tests schreiben. Vitest ist eine gute Wahl, da es die gleiche API wie das beliebteste Framework hat, nämlich Jest, aber es läuft schneller.
Installieren Sie Vitest.
npm install -D vitest
Und konfigurieren Sie einen Testbefehl in Ihrer package.json.
"test": "vitest --reporter=verbose",
Erstellen Sie dann eine example.test.ts -Datei und schreiben Sie einen kurzen Test, um zu überprüfen, ob Vitest funktioniert.
import { describe, expect, test } from 'vitest';
describe('example', () => {
test('given a passing test: passes', () => {
expect(1).toStrictEqual(1);
});
});
Er sollte bestehen.
$ npm test
> nextjs-for-production@0.1.0 test
> vitest --reporter=verbose
DEV v2.0.5 /Users/jan/dev/nextjs-for-production
✓ src/example.test.ts (1)
✓ example (1)
✓ given a passing test: passes
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 14:06:24
Duration 203ms (transform 20ms, setup 0ms, collect 17ms, tests 1ms, environment 0ms, prepare 56ms)
PASS Waiting for file changes...
press h to show help, press q to quit
Wenn Sie lernen möchten, wie Sie bessere Tests schreiben können, lesen Sie diesen Artikel , der Ihnen 12 zeitlose Testprinzipien vermittelt, die jeder erfahrene Entwickler kennen sollte.
Sie möchten auch Tests für Ihre React-Komponenten schreiben. Dafür können Sie die React Testing Libraryverwenden, oft abgekürzt als RTL.
npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event @types/react @types/react-dom happy-dom @vitejs/plugin-react vite-tsconfig-paths
Erstellen Sie dann eine vitest.config.ts Datei.
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
server: {
port: 3000,
},
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./src/tests/setup-test-environment.ts'],
include: ['./src/**/*.{spec,test}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
watch: {
ignored: [
String.raw`.*\\/node_modules\\/.*`,
String.raw`.*\\/build\\/.*`,
String.raw`.*\\/postgres-data\\/.*`,
],
},
coverage: {
reporter: ['text', 'json', 'html'],
},
},
});
Es richtet den Entwicklungsserver ein, gibt Testparameter an, wie zum Beispiel, dass die Umgebung happy-dom und Dateipfade ist, und definiert Formate für Abdeckungsberichte.
Und erstellen Sie eine Datei namens src/tests/setup-test-environment.ts um Ihre Testumgebung einzurichten.
import '@testing-library/jest-dom/vitest';
// See <https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment>.
// @ts-ignore
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
Die erste Zeile importiert zusätzliche Assertions, die die integrierten Assertions von Vitest erweitern, wodurch Sie DOM-Knoten einfacher testen können. Zum Beispiel können Sie prüfen, ob ein Element sichtbar ist, einen bestimmten Textinhalt hat oder spezifische Attribute enthält.
Das globale Umgebungs-Flag stellt sicher, dass Tests, die Zustandsaktualisierungen beinhalten, wie erwartet ohne Timing-Probleme funktionieren.
Als Nächstes möchten Sie eine benutzerdefinierte Render-Methode einrichten src/tests/react-test-utils.tsx.
/* eslint-disable import/export */
import type { RenderOptions } from '@testing-library/react';
import { render } from '@testing-library/react';
import type { ReactElement } from 'react';
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'queries'>,
) =>
render(ui, {
wrapper: ({ children }) => <>{children}</>,
...options,
});
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };
export { default as userEvent } from '@testing-library/user-event';
Sie können dies verwenden, um Ihren Code in Provider zu hüllen, zum Beispiel bei der Verwendung von Redux, oder um Layouts und Styling hinzuzufügen.
Stellen Sie sicher, dass Ihre Konfiguration funktioniert hat, indem Sie einen Test für eine React-Komponente schreiben.
import { describe, expect, test } from 'vitest';
import { render, screen } from '@/tests/react-test-utils';
function MyReactComponent() {
return <div>My React Component</div>;
}
describe('MyReactComponent', () => {
test('given no props: renders a text', () => {
render(<MyReactComponent />);
expect(screen.getByText('My React Component')).toBeInTheDocument();
});
});
Sie importieren render und screen aus Ihrer Test-Utils-Datei, anstatt direkt von RTL.
toBeInTheDocument() ist eine dieser speziellen Assertions, die Sie zuvor in Ihrer Testumgebungs-Setup-Datei konfiguriert haben.
Sie sollte auch bestehen.
✓ src/features/example.test.tsx (1)
✓ MyReactComponent (1)
✓ given no props: renders a text
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 09:33:32
Duration 402ms (transform 24ms, setup 55ms, collect 87ms, tests 9ms, environment 80ms, prepare 45ms)
PASS Waiting for file changes...
press h to show help, press q to quit
Kommen wir nun zum Styling. Die beiden wichtigsten Aspekte, auf die es ankommt, sind Barrierefreiheit und Wartbarkeit. Eine der beliebtesten Bibliotheken, die beide dieser Aspekte perfekt umsetzt, ist Shadcn. Sie verwendet Tailwind für ein optimiertes Style-Management und Radix für verbesserte Barrierefreiheit.
Initialisieren Sie Shadcn in Ihrem Projekt.
$ npx shadcn@latest init
✔ **Which** **style** **would you like to use?** › New York
✔ **Which color would you like to use as** **base color****?** › Slate
✔ **Would you like to use** **CSS variables** **for colors?** … no / yes
✔ Writing components.json...
✔ Initializing project...
✔ Installing dependencies...
Success! Project initialization completed. You may now add components.
Wenn Sie nun eine Karte oder eine andere Komponente benötigen, können Sie diese einfach zu Ihrem Projekt hinzufügen, mithilfe der Shadcn-Befehlszeilenschnittstelle.
npx shadcn@latest add card
Wenn Ihre App skaliert, möchten Sie sie in mehrere Sprachen übersetzen, damit sie mehr Nutzer erreicht.
Und die nachträgliche Implementierung von Internationalisierung – oder i18n – in einem späteren Zyklus einer App kann mühsam sein, da Sie alle hartkodierten Zeichenketten finden und sie durch Aufrufe von Übersetzungsfunktionen ersetzen müssen.
Installieren Sie Negotiator und einen Locale-Matcher.
npm install negotiator @formatjs/intl-localematcher
Das @formatjs/intl-localematcher -Paket wählt die beste Sprache für den Inhalt Ihrer App basierend auf den Sprachpräferenzen eines Nutzers aus. Das negotiator -Paket hilft Ihrer App herauszufinden, welche Art von Inhalt (z. B. Sprache oder Format) der Browser Ihres Nutzers am besten verarbeiten kann, basierend auf den Informationen, die der Browser sendet.
Sie müssen auch die TypeScript-Typen für das negotiator-Paket installieren.
npm install --save-dev @types/negotiator
Erstellen Sie dann Ihre i18n -Konfiguration in einer neuen Datei unter src/features/internationalization/i18n-config.ts.
export const i18n = {
defaultLocale: 'en-US',
locales: ['en-US'],
} as const;
export type Locale = (typeof i18n)['locales'][number];
Verwenden Sie diese i18n -Konfiguration in Ihrer localization-middleware unter src/features/internationalization/localization-middleware.ts.
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { type NextRequest, NextResponse } from 'next/server';
import { i18n } from './i18n-config';
function getLocale(request: NextRequest) {
const headers = {
'accept-language': request.headers.get('accept-language') ?? '',
};
const languages = new Negotiator({ headers }).languages();
return match(languages, i18n.locales, i18n.defaultLocale);
}
export function localizationMiddleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = i18n.locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
if (pathnameHasLocale) {
return;
}
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
Der Zweck dieser Middleware ist es, Ihre Nutzer automatisch zu erkennen und zur passenden Sprachversion der Website umzuleiten, basierend auf ihren Browser-Spracheinstellungen.
Erstellen Sie dann eine Middleware-Datei src/middleware.ts im Stammverzeichnis Ihres Projekts und verwenden Sie dort Ihre Lokalisierungs-Middleware.
import { NextRequest } from 'next/server';
import { localizationMiddleware } from './features/internationalization/localization-middleware';
// Matcher ignoring `/_next/` and `/api/` and svg files.
export const config = { matcher: ['/((?!api|_next|.*.svg$).*)'] };
export function middleware(request: NextRequest) {
return localizationMiddleware(request);
}
Es ist Zeit, Ihre Übersetzungen hinzuzufügen. Erstellen Sie ein json Wörterbuch für Ihre englischen Übersetzungen unter src/features/internationalization/dictionaries/en-us.json.
{
"counter": {
"decrement": "Decrement",
"increment": "Increment"
},
"landing": {
"welcome": "Welcome"
}
}
Erstellen Sie dann eine Datei src/features/internationalization/get-dictionaries.ts für Ihre getDictionary Funktion.
import "server-only";
import type { Locale } from "./i18n-config";
// We enumerate all dictionaries here for better linting and typescript support.
// We also get the default import for cleaner types.
const dictionaries = {
"en-US": () => import("./dictionaries/en-US.json").then((module) => module.default),
};
export const getDictionary = async (locale: Locale) =>
dictionaries[locale]?.() ?? dictionaries["en-US"]();
Sie nimmt ein Gebietsschema entgegen und gibt dann das entsprechende Wörterbuch zurück.
Wenn Sie einen Hook hinzufügen möchten, mit dem Ihre Benutzer ihre Sprache auswählen können, könnte das so aussehen.
import { usePathname } from 'next/navigation';
import { Locale } from './i18n-config';
export function useSwitchLocaleHref() {
const pathName = usePathname();
const getSwitchLocaleHref = (locale: Locale) => {
if (!pathName) return '/';
const segments = pathName.split('/');
segments[1] = locale;
return segments.join('/');
};
return getSwitchLocaleHref;
}
Und dann können Sie diesen Hook in einer Komponente verwenden und dessen Rückgabewert in einem <Link /> wie diesem.
<Link href={getSwitchLocaleHref(locale)}>{locale}</Link>
Ändern Sie Ihre URL-Struktur, um i18n zu unterstützen. Erstellen Sie einen neuen Ordner [lang] in Ihrem app/ Ordner, der ein dynamisches Segment für die Sprache erstellt.
Verschieben Sie Ihre page.tsx und layout.tsx Dateien in diesen Ordner und passen Sie das Layout an, um die korrekte Sprache im <html /> Tag einzustellen.
import '../globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { Locale } from '@/features/internationalization/i18n-config';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: "Jan Hesters' Next.js for production tutorial",
description: 'Brought to you by ReactSquad.io',
};
export default function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: { lang: Locale };
}>) {
return (
<html lang={params.lang}>
<body className={inter.className}>{children}</body>
</html>
);
}
Zukünftig können Sie die getDictionary-Funktion in jeder Serverkomponente verwenden.
import { getDictionary } from '@/features/internationalization/get-dictionaries';
import { Locale } from '@/features/internationalization/i18n-config';
import { CounterComponent } from './counter-component';
export default async function IndexPage({
params: { lang },
}: {
params: { lang: Locale };
}) {
const dictionary = await getDictionary(lang);
return (
<div>
<p>Current locale: {lang}</p>
<p>This text is rendered on the server: {dictionary.landing.welcome}</p>
<CounterComponent dictionary={dictionary.counter} />
</div>
);
}
Für Client-Komponenten sollten Sie stattdessen das entsprechende Dictionary übergeben.
'use client';
import { useState } from 'react';
import type { getDictionary } from '@/features/internationalization/get-dictionaries';
export function CounterComponent({
dictionary,
}: {
dictionary: Awaited<ReturnType<typeof getDictionary>>['counter'];
}) {
const [count, setCount] = useState(0);
return (
<p>
This component is rendered on the client:
<button onClick={() => setCount(n => n - 1)}>
{dictionary.decrement}
</button>
{count}
<button onClick={() => setCount(n => n + 1)}>
{dictionary.increment}
</button>
</p>
);
}
Dieses Tutorial wird Postgres als Datenbank verwenden, da es sich vielfach bewährt hat. Sie werden jedoch das Prisma ORM verwenden, um die Datenbankschicht zu abstrahieren. Dies gibt Ihnen die Flexibilität, eine Vielzahl von Datenbanken zu verwenden, und vereinfacht die API, die Sie für die Interaktion damit verwenden.
npm install prisma --save-dev
Sie müssen auch den Prisma-Client installieren.
npm install @prisma/client
Prisma initialisieren.
npx prisma init
Dadurch wird automatisch Ihre prisma/schema.prisma Datei.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
// Uses connection pooling
url = env("DATABASE_URL")
}
model UserProfile {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
name String @default("")
acceptedTermsAndConditions Boolean @default(false)
}
Fügen Sie einUserProfile Modell hinzu, das eine E-Mail und einen Namen sowie einen booleschen Wert enthält, der angibt, ob die Nutzungsbedingungen akzeptiert wurden.
Wenn Sie npx prisma initausgeführt haben, benennen Sie Ihre .env Datei um in .env.local, andernfalls erstellen Sie sie und stellen Sie sicher, dass sie die Anmeldeinformationen für Ihre Prisma-Datenbank enthält.
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Erstellen Sie nun eine src/lib/prisma.ts Datei, die Ihre Prisma-Client-Verbindung enthalten wird.
import { PrismaClient } from '@prisma/client';
declare global {
var __database__: PrismaClient;
}
let prisma: PrismaClient;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.__database__) {
global.__database__ = new PrismaClient();
}
prisma = global.__database__;
}
export default prisma;
Die Neuzuweisung stellt sicher, dass in einer Nicht-Produktionsumgebung (z. B. während der Entwicklung oder beim Testen) nur eine Instanz des Prisma-Clients erstellt und in der gesamten Anwendung wiederverwendet wird. Dieser Ansatz verhindert den Mehraufwand, jedes Mal, wenn ein Modul, das Datenbankzugriff benötigt, importiert wird, neue Verbindungen zur Datenbank wiederholt zu initialisieren.
Ändern Sie Ihre package.json , um die folgenden Hilfsbefehle für Prisma aufzunehmen.
"prisma:deploy": "npx prisma migrate deploy && npx prisma generate",
"prisma:migrate": "npx prisma migrate dev --name",
"prisma:push": "npx prisma db push && npx prisma generate",
"prisma:reset-dev": "run-s prisma:wipe prisma:seed dev",
"prisma:seed": "tsx ./prisma/seed.ts",
"prisma:setup": "prisma generate && prisma migrate deploy && prisma db push",
"prisma:studio": "npx prisma studio",
"prisma:wipe": "npx prisma migrate reset --force && npx prisma db push",
Einige dieser Befehle verwenden den run-s, tsx und dotenv Pakete, die Sie installieren müssen.
npm install --save-dev npm-run-all tsx dotenv
Hier ist eine Erläuterung der einzelnen Prisma-Befehle:
npm run prisma:setup
Wenn Ihr Prisma Ihre .env.local-Datei nicht erkennt, setzen Sie Ihre Umgebungsvariable manuell in Ihrem Terminal. Auf einem Mac kann dies mit dem Befehl export erfolgen.
export DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Erstellen Sie eine prisma/seed.ts -Datei. Sie können sie verwenden, um Ihre Datenbank mit Daten für die Entwicklung zu befüllen.
import { exit } from 'node:process';
import { PrismaClient } from '@prisma/client';
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
const prisma = new PrismaClient();
const prettyPrint = (object: any) =>
console.log(JSON.stringify(object, undefined, 2));
async function seed() {
const user = await prisma.userProfile.create({
data: {
email: 'jan@reactsquad.io',
name: 'Jan Hesters',
acceptedTermsAndConditions: true,
},
});
console.log('========= 🌱 result of seed: =========');
prettyPrint({ user });
}
seed()
.then(async () => {
await prisma.$disconnect();
})
// eslint-disable-next-line unicorn/prefer-top-level-await
.catch(async error => {
console.error(error);
await prisma.$disconnect();
exit(1);
});
Wenn Sie es ausführen, wird es Ihren Benutzer erstellen.
$ npm run prisma:seed
> reactsquad-production@0.1.0 prisma:seed
> tsx ./prisma/seed.ts
========= 🌱 result of seed: =========
{
"user": {
"id": "clzekb5sp0000ock9gsp72p33",
"createdAt": "2024-08-03T20:04:27.289Z",
"updatedAt": "2024-08-03T20:04:27.289Z",
"email": "jan@reactsquad.io",
"name": "Jan Hesters",
"acceptedTermsAndConditions": true
}
}
In Ihren Server-Komponenten können Sie prisma verwenden, um Daten abzurufen.
import prisma from '@/lib/prisma';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
export default async function Dashboard() {
const user = await prisma.userProfile.findUnique({
where: { email: 'jan@reactsquad.io' },
});
return (
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>User Profile</Card.Title>
</CardHeader>
<CardContent>
{user ? (
<ul>
<li>Name: {user.name}</li>
<li>Email: {user.email}</li>
</ul>
) : (
<p>User not found.</p>
)}
</CardContent>
</Card>
);
}
Es ist eine gute Idee, Ihre Datenbankaufrufe mithilfe von Fassaden zu abstrahieren. Eine Fassade ist ein Entwurfsmuster, bei dem Sie eine vereinfachte Schnittstelle zu einem komplexen Subsystem bereitstellen.
Erstellen Sie eine Datei, die alle Fassaden enthält, die sich auf Ihr Benutzerprofilmodell beziehen features/user-profile/user-profile-model.ts.
import { UserProfile } from '@prisma/client';
import prisma from '@/lib/prisma';
export async function retrieveUserProfileFromDatabaseByEmail(
email: UserProfile['email'],
) {
return await prisma.userProfile.findUnique({ where: { email } });
}
Es gibt zwei Hauptgründe für die Verwendung von Fassaden.
Verwenden Sie dann Ihre Fassade in Ihrer Serverkomponente.
import { retrieveUserProfileFromDatabaseByEmail } from '@/features/user-profiles/user-profiles-model';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
export default async function Dashboard() {
const user =
await retrieveUserProfileFromDatabaseByEmail('jan@reactsquad.io');
return (
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>User Profile</Card.Title>
</CardHeader>
<CardContent>
{user ? (
<ul>
<li>Name: {user.name}</li>
<li>Email: {user.email}</li>
</ul>
) : (
<p>User not found.</p>
)}
</CardContent>
</Card>
);
}
Sie können Vercel Postgres für Ihr Produktions-Deployment verwenden. Sie bieten eine leicht verständliche Anleitung, die Sie in der Vercel-Dokumentation nachlesen. Aber hier sind die Schritte kurz zusammengefasst, zu Ihrer Bequemlichkeit.
Um eine Datenbank in Ihrem Vercel-Projekt einzurichten, gehen Sie wie folgt vor:
Zum Erstellen einer neuen Datenbank:
sample_postgres_db (oder einen Namen Ihrer Wahl) in das Feld 'Store Name' ein. Stellen Sie sicher, dass der Name nur alphanumerische Zeichen, „_“ oder „-“ enthält und 32 Zeichen nicht überschreitet.Anschließend müssen Sie POSTGRES_URL_NON_POOLING zu der Datenquelle in Ihrem Prisma-Schema hinzufügen.
datasource db {
provider = "postgresql"
// Uses connection pooling
url = env("DATABASE_URL")
// Uses direct connection, ⚠️ make sure to keep this to `POSTGRES_URL_NON_POOLING`
// or you'll have dangling databases from migrations
directUrl = env("POSTGRES_URL_NON_POOLING")
}
Vercel verwendet Verbindungspooler, die einen Pool von Datenbankverbindungen verwalten, der von verschiedenen Teilen einer Anwendung wiederverwendet werden kann, anstatt für jede Datenbankanfrage eine neue Verbindung herzustellen. Die directUrl -Eigenschaft wird verwendet, um sicherzustellen, dass Operationen, die direkten Datenbankzugriff erfordern, wie z. B. Migrationen, den Verbindungspooler für eine zuverlässige Ausführung umgehen können.
Sie können die Umgebungsvariablen für Ihre Vercel-Datenbank abrufen, indem Sie sie von Vercel ziehen.
vercel env pull .env
Man sollte auch E2E-Tests verwenden, da sie das größte Vertrauen geben, dass die App wie beabsichtigt funktioniert. E2E-Tests werden viel zu oft übersprungen, aber man sollte es sich wirklich zur Gewohnheit machen, sie zu schreiben. Die Vorteile potenzieren sich.
Initialisieren Playwright.
$ npm init playwright@latest
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Where to put your end-to-end tests? · playwright
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
Wenn Sie Playwright zum ersten Mal verwenden, können Sie einen Blick in den test-examples/ Ordner werfen, den das Initialisierungsskript erstellt, und ihn dann löschen, da Sie ihn nicht benötigen werden.
Ändern Sie den webServer Schlüssel in Ihrer playwright.config.ts Datei.
webServer: {
command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev',
port: 3000,
},
Fügen Sie zwei Skripte für Ihre E2E-Tests zu Ihrer package.json hinzu.
"test:e2e": "npx playwright test",
"test:e2e:ui": "npx playwright test --ui",
Das erste führt Ihren Playwright-Test im Headless-Modus aus, während das zweite Ihren Test im UI-Modus ausführt, was Ihnen Time-Travel-Debugging, Watch-Modus und mehr bietet.
import { expect, test } from '@playwright/test';
test.describe('landing page', () => {
test('given any user: shows the test user', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Jan Hesters')).toBeVisible();
await expect(page.getByText('jan@reactsquad.io')).toBeVisible();
});
});
Führen Sie Ihre Tests aus, um zu überprüfen, ob Ihr Playwright-Setup funktioniert hat.
$ npm run test:e2e
> reactsquad-production@0.1.0 test:e2e
> npx playwright test
Running 3 tests using 3 workers
3 passed (3.9s)
Es wurden drei Tests ausgeführt, da Playwright standardmäßig so konfiguriert ist, dass es in Chrome, Safari und Firefox läuft.
Es ist bewährte Praxis, Ihre App mit CI/CD zu betreiben. CI/CD steht für Continuous Delivery und Continuous Deployment.
Fügen Sie ein Secret für Ihre Datenbank-URL zu Ihrer Repository-Einstellungen in GitHub.
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/testdb"
Erstellen Sie dann Ihre GitHub Actions YAML-Konfiguration in einer Datei namens .github/workflows/pull-request.yml für eine umfassende CI/CD-Pipeline, die Linting, Typüberprüfung, Tests und mehr umfasst.
name: Pull Request
on: [pull_request]
jobs:
lint:
name: ⬣ ESLint
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🔬 Lint
run: npm run lint
type-check:
name: ʦ TypeScript
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🔎 Type check
run: npm run type-check --if-present
commitlint:
name: ⚙️ commitlint
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⚙️ commitlint
uses: wagoid/commitlint-github-action@v4
vitest:
name: ⚡ Vitest
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
ports:
- 5432:5432
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🛠 Setup Database
run: npm run prisma:wipe
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: ⚡ Run vitest
run: npm run test -- --coverage
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
playwright:
name: 🎭 Playwright
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
ports:
- 5432:5432
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🌐 Install Playwright Browsers
run: npx playwright install --with-deps
- name: 🛠 Setup Database
run: npm run prisma:wipe
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: 🎭 Playwright Run
run: npx playwright test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: 📸 Playwright Screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Jedes Mal, wenn Sie nun einen Pull Request für Ihre App erstellen, führt er automatisch Ihre Tests aus, um die Funktionalität sicherzustellen, führt TypeScript-Typüberprüfungen durch und lintet den Code, damit alle Code mit der gleichen Formatierung beisteuern.
Interessiert an der Einstellung erfahrener Next.js-Entwickler für Ihr Startup? ReactSquad ist die richtige Adresse. Buchen Sie einen Anruf und finden Sie innerhalb von 48 Stunden einen passenden Experten.