middlewarevalidatorservernextnextclientMiddleware ermöglicht es Ihnen, das Verhalten von mit createServerFn erstellten Server-Funktionen mit Dingen wie gemeinsamer Validierung, Kontext und vielem mehr anzupassen. Middleware kann sogar von anderer Middleware abhängen, um eine Kette von Operationen zu erstellen, die hierarchisch und in der Reihenfolge ausgeführt werden.
Middleware wird mit der Funktion createMiddleware definiert. Diese Funktion gibt ein Middleware-Objekt zurück, das verwendet werden kann, um die Middleware mit Methoden wie middleware, validator, server und client weiter anzupassen.
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).server(
async ({ next, data }) => {
console.log('Request received:', data)
const result = await next()
console.log('Response processed:', result)
return result
},
)
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).server(
async ({ next, data }) => {
console.log('Request received:', data)
const result = await next()
console.log('Response processed:', result)
return result
},
)
Sobald Sie Ihre Middleware definiert haben, können Sie sie in Kombination mit der Funktion createServerFn verwenden, um das Verhalten Ihrer Server-Funktionen anzupassen.
import { createServerFn } from '@tanstack/react-start'
import { loggingMiddleware } from './middleware'
const fn = createServerFn()
.middleware([loggingMiddleware])
.handler(async () => {
// ...
})
import { createServerFn } from '@tanstack/react-start'
import { loggingMiddleware } from './middleware'
const fn = createServerFn()
.middleware([loggingMiddleware])
.handler(async () => {
// ...
})
Mehrere Methoden stehen zur Verfügung, um die Middleware anzupassen. Wenn Sie (hoffentlich) TypeScript verwenden, wird die Reihenfolge dieser Methoden durch das Typsystem erzwungen, um maximale Inferenz und Typsicherheit zu gewährleisten.
Die Methode middleware wird verwendet, um Middleware-Abhängigkeiten in die Kette einzufügen, die **vor** der aktuellen Middleware ausgeführt wird. Rufen Sie einfach die Methode middleware mit einem Array von Middleware-Objekten auf.
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).middleware([
authMiddleware,
loggingMiddleware,
])
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).middleware([
authMiddleware,
loggingMiddleware,
])
Typsichere Kontext- und Payload-Validierung werden ebenfalls von übergeordneten Middleware geerbt!
Die Methode validator wird verwendet, um das Datenobjekt zu modifizieren, bevor es an diese Middleware, verschachtelte Middleware und letztendlich die Server-Funktion übergeben wird. Diese Methode sollte eine Funktion erhalten, die das Datenobjekt annimmt und ein validiertes (und optional modifiziertes) Datenobjekt zurückgibt. Es ist üblich, eine Validierungsbibliothek wie zod dafür zu verwenden. Hier ist ein Beispiel
import { createMiddleware } from '@tanstack/react-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware({ type: 'function' })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
import { createMiddleware } from '@tanstack/react-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware({ type: 'function' })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
Die Methode server wird verwendet, um **serverseitige** Logik zu definieren, die die Middleware sowohl vor als auch nach jeder verschachtelten Middleware und letztendlich einer Server-Funktion ausführt. Diese Methode erhält ein Objekt mit den folgenden Eigenschaften
Die Funktion next wird verwendet, um die nächste Middleware in der Kette auszuführen. **Sie müssen das Ergebnis der Ihnen übergebenen Funktion next awaiten und zurückgeben (oder direkt zurückgeben),** damit die Kette weiter ausgeführt wird.
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('Request received')
const result = await next()
console.log('Response processed')
return result
},
)
import { createMiddleware } from '@tanstack/react-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('Request received')
const result = await next()
console.log('Response processed')
return result
},
)
Die Funktion next kann optional mit einem Objekt aufgerufen werden, das eine context-Eigenschaft mit einem Objektwert hat. Alle Eigenschaften, die Sie an diesen context-Wert übergeben, werden in den übergeordneten context gemischt und der nächsten Middleware bereitgestellt.
import { createMiddleware } from '@tanstack/react-start'
const awesomeMiddleware = createMiddleware({ type: 'function' }).server(
({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
},
)
const loggingMiddleware = createMiddleware({ type: 'function' })
.middleware([awesomeMiddleware])
.server(async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
})
import { createMiddleware } from '@tanstack/react-start'
const awesomeMiddleware = createMiddleware({ type: 'function' }).server(
({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
},
)
const loggingMiddleware = createMiddleware({ type: 'function' })
.middleware([awesomeMiddleware])
.server(async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
})
Obwohl Server-Funktionen hauptsächlich serverseitig gebundene Operationen sind, gibt es immer noch viele clientseitige Logiken rund um die ausgehende RPC-Anfrage vom Client. Das bedeutet, dass wir auch clientseitige Logiken in Middleware definieren können, die auf der Client-Seite um jede verschachtelte Middleware und letztendlich die RPC-Funktion und ihre Antwort an den Client ausgeführt wird.
Standardmäßig wird die Middleware-Validierung nur auf dem Server durchgeführt, um die Größe des Client-Bundles gering zu halten. Sie können jedoch auch Daten auf der Client-Seite validieren, indem Sie die Option validateClient: true an die Funktion createMiddleware übergeben. Dies führt dazu, dass die Daten auf der Client-Seite validiert werden, bevor sie an den Server gesendet werden, was möglicherweise eine Hin- und Rückreise spart.
Warum kann ich kein anderes Validierungsschema für den Client übergeben?
Das clientseitige Validierungsschema wird vom serverseitigen Schema abgeleitet. Das liegt daran, dass das clientseitige Validierungsschema verwendet wird, um die Daten zu validieren, bevor sie an den Server gesendet werden. Wenn das clientseitige Schema vom serverseitigen Schema abweicht, würde der Server unerwartete Daten erhalten, was zu unerwartetem Verhalten führen könnte.
import { createMiddleware } from '@tanstack/react-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const workspaceMiddleware = createMiddleware({ validateClient: true })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
import { createMiddleware } from '@tanstack/react-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const workspaceMiddleware = createMiddleware({ validateClient: true })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
Client-Middleware-Logik wird mit der Methode client auf einem Middleware-Objekt definiert. Diese Methode wird verwendet, um clientseitige Logik zu definieren, die die Middleware sowohl vor als auch nach jeder verschachtelten Middleware und letztendlich der clientseitigen RPC-Funktion (oder der serverseitigen Funktion, wenn Sie SSR durchführen oder diese Funktion von einer anderen Server-Funktion aufrufen) ausführt.
Clientseitige Middleware-Logik teilt sich einen Großteil derselben API wie die mit der Methode server erstellte Logik, wird aber auf der Client-Seite ausgeführt. Dies beinhaltet
Ähnlich wie die Funktion server erhält sie auch ein Objekt mit den folgenden Eigenschaften
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
console.log('Request sent')
const result = await next()
console.log('Response received')
return result
},
)
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
console.log('Request sent')
const result = await next()
console.log('Response received')
return result
},
)
Client-Kontext wird standardmäßig NICHT an den Server gesendet, da dies unbeabsichtigt große Payloads an den Server senden könnte. Wenn Sie Client-Kontext an den Server senden möchten, müssen Sie die Funktion next mit einer sendContext-Eigenschaft und einem Objekt aufrufen, um Daten an den Server zu übertragen. Alle Eigenschaften, die an sendContext übergeben werden, werden zusammen mit den Daten gemischt, serialisiert und an den Server gesendet und sind im normalen Kontextobjekt jeder verschachtelten Server-Middleware verfügbar.
const requestLogger = createMiddleware({ type: 'function' })
.client(async ({ next, context }) => {
return next({
sendContext: {
// Send the workspace ID to the server
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Woah! We have the workspace ID from the client!
console.log('Workspace ID:', context.workspaceId)
return next()
})
const requestLogger = createMiddleware({ type: 'function' })
.client(async ({ next, context }) => {
return next({
sendContext: {
// Send the workspace ID to the server
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Woah! We have the workspace ID from the client!
console.log('Workspace ID:', context.workspaceId)
return next()
})
Sie haben vielleicht bemerkt, dass im obigen Beispiel, obwohl clientseitig gesendeter Kontext typsicher ist, keine Laufzeitvalidierung erforderlich ist. Wenn Sie dynamische, benutzergenerierte Daten über den Kontext übergeben, kann dies ein Sicherheitsproblem darstellen. **Wenn Sie dynamische Daten vom Client über den Kontext an den Server senden, sollten Sie diese in der serverseitigen Middleware validieren, bevor Sie sie verwenden.** Hier ist ein Beispiel
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const requestLogger = createMiddleware({ type: 'function' })
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Validate the workspace ID before using it
const workspaceId = zodValidator(z.number()).parse(context.workspaceId)
console.log('Workspace ID:', workspaceId)
return next()
})
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const requestLogger = createMiddleware({ type: 'function' })
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Validate the workspace ID before using it
const workspaceId = zodValidator(z.number()).parse(context.workspaceId)
console.log('Workspace ID:', workspaceId)
return next()
})
Ähnlich wie beim Senden von Client-Kontext an den Server können Sie auch Server-Kontext an den Client senden, indem Sie die Funktion next mit einer sendContext-Eigenschaft und einem Objekt aufrufen, um Daten an den Client zu übertragen. Alle Eigenschaften, die an sendContext übergeben werden, werden zusammen mit der Antwort gemischt, serialisiert und an den Client gesendet und sind im normalen Kontextobjekt jeder verschachtelten Client-Middleware verfügbar. Das zurückgegebene Objekt des Aufrufs von next in client enthält den vom Server an den Client gesendeten Kontext und ist typsicher. Middleware kann den vom Server an den Client gesendeten Kontext aus vorherigen Middleware-Ketten ableiten, die über die Funktion middleware verknüpft sind.
Warnung
Der Rückgabetyp von next in client kann nur aus Middleware abgeleitet werden, die in der aktuellen Middleware-Kette bekannt ist. Daher ist der genaueste Rückgabetyp von next in der Middleware am Ende der Middleware-Kette.
const serverTimer = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
},
)
const requestLogger = createMiddleware({ type: 'function' })
.middleware([serverTimer])
.client(async ({ next }) => {
const result = await next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
return result
})
const serverTimer = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
},
)
const requestLogger = createMiddleware({ type: 'function' })
.middleware([serverTimer])
.client(async ({ next }) => {
const result = await next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
return result
})
Middleware, die die Methode server verwendet, wird im gleichen Kontext wie Server-Funktionen ausgeführt. Sie können daher exakt dieselben Server-Funktions-Kontext-Dienstprogramme verwenden, um Anfrage-Header, Statuscodes usw. zu lesen und zu modifizieren.
Middleware, die die Methode client verwendet, wird in einem **völlig anderen clientseitigen Kontext** als Server-Funktionen ausgeführt. Sie können daher nicht dieselben Dienstprogramme verwenden, um die Anfrage zu lesen und zu modifizieren. Sie können die Anfrage jedoch modifizieren, indem Sie zusätzliche Eigenschaften beim Aufruf der Funktion next zurückgeben. Derzeit unterstützte Eigenschaften sind
Hier ist ein Beispiel für das Hinzufügen eines Authorization-Headers zu jeder Anfrage, die diese Middleware verwendet
import { getToken } from 'my-auth-library'
const authMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
},
)
import { getToken } from 'my-auth-library'
const authMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
},
)
Middleware kann auf zwei verschiedene Arten verwendet werden
Globale Middleware wird automatisch für jede Server-Funktion in Ihrer Anwendung ausgeführt. Dies ist nützlich für Funktionen wie Authentifizierung, Protokollierung und Überwachung, die für alle Anfragen gelten sollen.
Um globale Middleware zu verwenden, erstellen Sie eine Datei global-middleware.ts in Ihrem Projekt (typischerweise unter app/global-middleware.ts). Diese Datei wird in Client- und Serverumgebungen ausgeführt und ist der Ort, an dem Sie globale Middleware registrieren.
So registrieren Sie globale Middleware
// app/global-middleware.ts
import { registerGlobalMiddleware } from '@tanstack/react-start'
import { authMiddleware } from './middleware'
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// app/global-middleware.ts
import { registerGlobalMiddleware } from '@tanstack/react-start'
import { authMiddleware } from './middleware'
registerGlobalMiddleware({
middleware: [authMiddleware],
})
Typen globaler Middleware sind von Natur aus von Server-Funktionen **getrennt**. Das bedeutet, dass, wenn eine globale Middleware zusätzliche Kontexte für Server-Funktionen oder andere serverfunktionsspezifische Middleware bereitstellt, die Typen nicht automatisch an die Server-Funktion oder andere serverfunktionsspezifische Middleware weitergegeben werden.
// app/global-middleware.ts
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// app/global-middleware.ts
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// authMiddleware.ts
const authMiddleware = createMiddleware({ type: 'function' }).server(
({ next, context }) => {
console.log(context.user) // <-- This will not be typed!
// ...
},
)
// authMiddleware.ts
const authMiddleware = createMiddleware({ type: 'function' }).server(
({ next, context }) => {
console.log(context.user) // <-- This will not be typed!
// ...
},
)
Um dies zu lösen, fügen Sie die globale Middleware, auf die Sie verweisen möchten, dem Middleware-Array der Server-Funktion hinzu. **Die globale Middleware wird dedupliziert zu einem einzigen Eintrag (der globale Instanz), und Ihre Server-Funktion erhält die korrekten Typen.**
Hier ist ein Beispiel, wie das funktioniert
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user)
// ...
})
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user)
// ...
})
Middleware wird Abhängigkeits-zuerst ausgeführt, beginnend mit globaler Middleware, gefolgt von Server-Funktions-Middleware. Das folgende Beispiel wird in dieser Reihenfolge protokollieren
const globalMiddleware1 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware1')
return next()
},
)
const globalMiddleware2 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware2')
return next()
},
)
registerGlobalMiddleware({
middleware: [globalMiddleware1, globalMiddleware2],
})
const a = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware({ type: 'function' })
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware({ type: 'function' })
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware({ type: 'function' })
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
const globalMiddleware1 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware1')
return next()
},
)
const globalMiddleware2 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware2')
return next()
},
)
registerGlobalMiddleware({
middleware: [globalMiddleware1, globalMiddleware2],
})
const a = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware({ type: 'function' })
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware({ type: 'function' })
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware({ type: 'function' })
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
Middleware-Funktionalität wird basierend auf der Umgebung für jedes erzeugte Bundle per Tree Shaking entfernt.
Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.