Penyelesaian Penyewa Berasaskan Subdomain dan Laluan
Selesaikan penyewa daripada subdomain atau awalan laluan menggunakan penulisan semula middleware dan pengepala.
Penyelesaian Penyewa Berasaskan Subdomain dan Laluan ialah pelajaran Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan) percuma di CoddyKit. Ini ialah pelajaran 1 daripada 4. Sebanyak 3 pelajaran dalam laluan pembelajaran ini boleh dibaca sepenuhnya secara percuma — selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan praktikal dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan), dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan) merangkumi sejumlah 4 pelajaran.
Apakah Resolusi Penyewa?
Dalam aplikasi SaaS berbilang penyewa, setiap permintaan masuk mesti dipetakan kepada penyewa tertentu sebelum sebarang logik perniagaan dijalankan. Proses ini dipanggil resolusi penyewa.
Terdapat dua strategi utama:
- Berasaskan subdomain:
acme.app.com,globex.app.com— setiap penyewa memiliki subdomain. - Berasaskan laluan:
app.com/acme/dashboard,app.com/globex/dashboard— slug penyewa muncul sebagai segmen laluan pertama.
Perisian tengah Next.js 15 berjalan pada Persekitaran Masa Jalan Edge sebelum mana-mana halaman atau laluan API diproses, menjadikannya tempat yang ideal untuk mengekstrak, mengesahkan dan meneruskan konteks penyewa ke seluruh aplikasi.
Asas Perisian Tengah Next.js
Perisian tengah dalam Next.js 15 terletak dalam middleware.ts di akar projek. Ia mengeksport fungsi middleware lalai dan objek config pilihan dengan tatasusunan matcher.
Perkara utama:
- Berjalan pada setiap permintaan yang sepadan sebelum pengendali laluan.
- Boleh membaca pengepala permintaan, kuki dan URL.
- Boleh menulis semula URL permintaan (tidak kelihatan pada pelayar) atau mengalihkannya.
- Boleh melampirkan pengepala permintaan tersuai yang kemudiannya boleh dibaca oleh komponen halaman dan laluan API.
Fungsi perisian tengah menerima NextRequest dan mesti memulangkan NextResponse.
// middleware.ts — minimal skeleton
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest): NextResponse {
// Inspect request.nextUrl, request.headers, request.cookies …
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Mengekstrak Penyewa daripada Subdomain
Untuk resolusi berasaskan subdomain, huraikan request.nextUrl.hostname. Bagi hos seperti acme.app.com, pecahkan pada . dan ambil segmen pertama.
Kes tepi penting yang perlu dikendalikan:
localhost— tiada subdomain semasa pembangunan setempat; gunakan penyewa lalai atau parameter pertanyaan sebagai sandaran.www— awalan tapak pemasaran tidak boleh dianggap sebagai slug penyewa.- Alamat IP yang digunakan dalam persekitaran CI/Docker.
Selepas diekstrak, simpan slug dalam pengepala respons supaya setiap komponen hiliran boleh mengaksesnya tanpa menghuraikan semula hos.
// lib/tenant/resolveSubdomain.ts
export function resolveSubdomain(
hostname: string,
rootDomain: string // e.g. 'app.com'
): string | null {
// Strip port if present (e.g. localhost:3000)
const host = hostname.split(':')[0];
if (host === 'localhost' || host === '127.0.0.1') {
return null; // handled separately in dev
}
if (!host.endsWith(`.${rootDomain}`)) {
return null; // top-level domain — no tenant
}
const subdomain = host.slice(0, host.length - rootDomain.length - 1);
if (subdomain === 'www' || subdomain === '') return null;
return subdomain; // e.g. 'acme'
}Mengekstrak Penyewa daripada Awalan Laluan
Untuk resolusi berasaskan laluan, periksa request.nextUrl.pathname. URL seperti /acme/dashboard menghasilkan laluan yang segmen pertamanya ialah slug penyewa.
Perisian tengah mesti:
- Memecahkan nama laluan pada
/. - Mengesahkan segmen tidak kosong pertama terhadap set slug penyewa yang diketahui (atau carian pangkalan data di edge).
- Menulis semula URL kepada laluan tanpa maklumat penyewa, dengan membuang awalan supaya komponen halaman tidak perlu mengetahuinya.
Penulisan semula mengekalkan URL pelayar sambil menghalakan permintaan secara dalaman ke /dashboard (tanpa awalan slug).
// lib/tenant/resolvePath.ts
export function resolvePathTenant(
pathname: string
): { tenant: string | null; strippedPath: string } {
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) {
return { tenant: null, strippedPath: '/' };
}
// First segment is treated as the tenant slug.
// Validation against a real slug set happens in middleware.
const [tenantSlug, ...rest] = segments;
const strippedPath = '/' + rest.join('/');
return { tenant: tenantSlug, strippedPath: strippedPath || '/' };
}Menulis Semula URL dalam Perisian Tengah
Penulisan semula mengubah destinasi dalaman permintaan tanpa mengubah perkara yang dilihat pengguna dalam bar alamat pelayar. Inilah mekanisme yang menjadikan berbilang penyewa berasaskan laluan kelihatan telus.
Contohnya, apabila pengguna melawati app.com/acme/dashboard, perisian tengah menulis semula permintaan secara dalaman kepada app.com/dashboard, manakala slug penyewa acme diteruskan melalui pengepala permintaan tersuai.
Gunakan NextResponse.rewrite(url) dan lampirkan pengepala pada respons yang ditulis semula. Ambil perhatian bahawa dalam Next.js 15, anda mesti mengklon request.headers dan menetapkan pengepala penyewa pada permintaan baharu dalam penulisan semula, bukannya pada pengepala respons.
// middleware.ts — path-based rewrite example
import { NextRequest, NextResponse } from 'next/server';
import { resolvePathTenant } from './lib/tenant/resolvePath';
const ROOT_DOMAIN = process.env.NEXT_PUBLIC_ROOT_DOMAIN ?? 'app.com';
export function middleware(request: NextRequest) {
const { tenant, strippedPath } = resolvePathTenant(
request.nextUrl.pathname
);
if (!tenant) return NextResponse.next();
const url = request.nextUrl.clone();
url.pathname = strippedPath;
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant-slug', tenant);
return NextResponse.rewrite(url, {
request: { headers: requestHeaders },
});
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Perisian Tengah Bersatu: Sandaran Subdomain + Laluan
Perisian tengah yang sedia untuk produksi menggabungkan kedua-dua strategi: utamakan resolusi subdomain; gunakan resolusi berasaskan laluan sebagai sandaran apabila tiada subdomain ditemui (berguna untuk rakan kongsi label putih yang berkongsi domain atau untuk pembangunan setempat).
Turutan carian ialah:
- Semak
request.nextUrl.hostnameuntuk subdomain penyewa. - Jika tidak ditemui, semak segmen pertama nama laluan.
- Jika kedua-duanya tidak menghasilkan penyewa, teruskan permintaan tanpa perubahan (halaman pendaratan, laluan pengesahan dan sebagainya).
Selepas resolusi, slug penyewa diteruskan dalam pengepala permintaan x-tenant-slug supaya setiap Komponen Pelayan, Server Action dan Pengendali Laluan boleh membacanya tanpa mengulangi logik penghuraian.
// middleware.ts — unified resolver
import { NextRequest, NextResponse } from 'next/server';
import { resolveSubdomain } from './lib/tenant/resolveSubdomain';
import { resolvePathTenant } from './lib/tenant/resolvePath';
const ROOT_DOMAIN = process.env.NEXT_PUBLIC_ROOT_DOMAIN ?? 'app.com';
export function middleware(request: NextRequest) {
const { hostname, pathname } = request.nextUrl;
// 1. Try subdomain first
let tenantSlug = resolveSubdomain(hostname, ROOT_DOMAIN);
let rewritePath: string | null = null;
// 2. Fall back to path prefix
if (!tenantSlug) {
const result = resolvePathTenant(pathname);
tenantSlug = result.tenant;
rewritePath = result.strippedPath;
}
if (!tenantSlug) return NextResponse.next(); // public route
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant-slug', tenantSlug);
if (rewritePath) {
const url = request.nextUrl.clone();
url.pathname = rewritePath;
return NextResponse.rewrite(url, { request: { headers: requestHeaders } });
}
// Subdomain path — no URL rewrite needed, just forward the header
return NextResponse.next({ request: { headers: requestHeaders } });
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Membaca Konteks Penyewa dalam Komponen Pelayan
Setelah perisian tengah menetapkan pengepala x-tenant-slug, setiap Komponen Pelayan boleh membacanya dengan memanggil headers() daripada next/headers. Ini ialah API tak segerak Next.js 15 — sentiasa gunakan await padanya.
Amalan terbaik: bungkus akses pengepala dalam fungsi utiliti kecil (getTenantSlug) supaya setiap komponen mengimport pembantu yang sama dan bukannya menduplikasi rentetan kunci pengepala.
Slug penyewa kemudiannya digunakan untuk membuat pertanyaan pangkalan data bagi mendapatkan rekod penyewa penuh (id, pelan, tema, bendera ciri dan sebagainya) — biasanya dicache dengan unstable_cache atau cache() React untuk mengelakkan capaian DB berulang bagi setiap permintaan.
// lib/tenant/server.ts
import { headers } from 'next/headers';
import { cache } from 'react';
import { db } from '@/lib/db'; // your DB client
export async function getTenantSlug(): Promise<string> {
const headerStore = await headers();
const slug = headerStore.get('x-tenant-slug');
if (!slug) throw new Error('No tenant slug in request headers');
return slug;
}
// Cached per request — React deduplicates across the component tree
export const getTenant = cache(async () => {
const slug = await getTenantSlug();
const tenant = await db.tenant.findUnique({ where: { slug } });
if (!tenant) throw new Error(`Unknown tenant: ${slug}`);
return tenant;
});
// Usage inside any Server Component:
// const tenant = await getTenant();
// <ThemeProvider primary={tenant.brandColor}>...</ThemeProvider>Mengesahkan Penyewa di Edge
Menyelesaikan slug tenant daripada URL adalah mudah, tetapi Anda mungkin mahu mengesahkan bahawa slug tersebut benar-benar wujud sebelum membenarkan request diteruskan. Melakukannya dalam middleware mengelakkan ralat 404 jauh di dalam Komponen Pelayan.
Pilihan pengesahan Edge (middleware tidak boleh menggunakan API Node.js):
- KV store / Upstash Redis: dapatkan set slug sah yang telah diisi terlebih dahulu dengan satu panggilan HTTP.
- ORM yang serasi dengan Edge: Drizzle atau Prisma Accelerate boleh dijalankan pada Masa Jalan Edge.
- Senarai benarkan statik: hanya sesuai untuk senarai tenant yang kecil dan jarang berubah, serta dibenamkan dalam penggunaan.
Jika pengesahan gagal, redirect pengguna ke halaman 404 atau halaman tersuai tenant-tidak-ditemui, bukannya mengembalikan respons kosong.
// middleware.ts — Edge KV validation with Upstash Redis
import { NextRequest, NextResponse } from 'next/server';
import { Redis } from '@upstash/redis';
import { resolveSubdomain } from './lib/tenant/resolveSubdomain';
const redis = Redis.fromEnv();
const ROOT_DOMAIN = process.env.NEXT_PUBLIC_ROOT_DOMAIN ?? 'app.com';
export async function middleware(request: NextRequest) {
const { hostname } = request.nextUrl;
const tenantSlug = resolveSubdomain(hostname, ROOT_DOMAIN);
if (!tenantSlug) return NextResponse.next();
// 'tenants' is a Redis Set populated whenever a tenant is created/deleted
const exists = await redis.sismember('tenants', tenantSlug);
if (!exists) {
return NextResponse.redirect(
new URL('/tenant-not-found', request.url)
);
}
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant-slug', tenantSlug);
return NextResponse.next({ request: { headers: requestHeaders } });
}
export const config = { matcher: ['/((?!_next|favicon.ico).*)'] };Konteks Tenant dalam Tindakan Pelayan
Tindakan Pelayan berjalan dalam konteks request yang sama seperti Komponen Pelayan. Header x-tenant-slug yang disuntik oleh middleware juga tersedia melalui headers() di sini, menjadikannya amat mudah untuk mengehadkan semua mutasi data kepada tenant yang betul.
Peraturan keselamatan yang penting: jangan sesekali mempercayai slug tenant yang dihantar daripada klien (contohnya, medan borang tersembunyi). Sentiasa dapatkan tenant daripada header sebelah pelayan yang hanya boleh ditetapkan oleh middleware. Ini menghalang serangan penyamaran tenant apabila pengguna berniat jahat membina request dengan slug palsu dalam badan request.
'use server';
// app/[...]/actions.ts
import { headers } from 'next/headers';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth'; // your auth helper
export async function createProject(formData: FormData) {
// Derive tenant from middleware header — never from form data
const headerStore = await headers();
const tenantSlug = headerStore.get('x-tenant-slug');
if (!tenantSlug) throw new Error('Missing tenant context');
const session = await auth();
if (!session) throw new Error('Unauthenticated');
const name = formData.get('name') as string;
// Scope the insert to the resolved tenant
const tenant = await db.tenant.findUniqueOrThrow({
where: { slug: tenantSlug },
select: { id: true },
});
return db.project.create({
data: { name, tenantId: tenant.id, ownerId: session.user.id },
});
}Pembangunan Setempat Tanpa Subdomain Sebenar
Subdomain tidak berfungsi pada localhost secara lalai. Dua penyelesaian praktikal untuk pembangunan setempat:
- Pemetaan
/etc/hosts: Tambahkan entri seperti127.0.0.1 acme.localhost. Kebanyakan pelayar menyokong*.localhostsecara asli tanpa entri hosts dalam versi moden. - Pengesampingan parameter pertanyaan: Dalam pembangunan, middleware membaca parameter pertanyaan
?tenant=acmesebagai sandaran apabila tiada subdomain. Pengesampingan ini dilumpuhkan dalam pengeluaran melalui semakan pemboleh ubah persekitaran.
Pendekatan parameter pertanyaan paling pantas untuk iterasi — tiada konfigurasi sistem diperlukan — dan pautan demo mudah dikongsi dengan rakan sepasukan.
// middleware.ts — dev query-param tenant override
import { NextRequest, NextResponse } from 'next/server';
import { resolveSubdomain } from './lib/tenant/resolveSubdomain';
const ROOT_DOMAIN = process.env.NEXT_PUBLIC_ROOT_DOMAIN ?? 'app.com';
const IS_DEV = process.env.NODE_ENV === 'development';
export function middleware(request: NextRequest) {
const { hostname, searchParams } = request.nextUrl;
let tenantSlug = resolveSubdomain(hostname, ROOT_DOMAIN);
// Dev-only fallback: ?tenant=acme
if (!tenantSlug && IS_DEV) {
tenantSlug = searchParams.get('tenant');
}
if (!tenantSlug) return NextResponse.next();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant-slug', tenantSlug);
return NextResponse.next({ request: { headers: requestHeaders } });
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Menjana URL Berskop Tenant
Pautan dalam aplikasi mesti dijana dengan betul bergantung pada strategi penyelesaian yang digunakan. Menetapkan satu gaya secara hardcode akan merosakkan gaya yang satu lagi. Fungsi utiliti kecil boleh memusatkan logik ini.
Fungsi ini membaca pemboleh ubah persekitaran NEXT_PUBLIC_TENANT_STRATEGY ('subdomain' atau 'path') dan membina URL yang sepadan. Fungsi ini dipanggil dalam Komponen Pelayan semasa membina nilai <Link href> atau dalam utiliti klien yang menjana pautan perkongsian.
// lib/tenant/url.ts
const ROOT_DOMAIN = process.env.NEXT_PUBLIC_ROOT_DOMAIN ?? 'app.com';
const STRATEGY = (process.env.NEXT_PUBLIC_TENANT_STRATEGY ?? 'subdomain') as
| 'subdomain'
| 'path';
/**
* Build an absolute URL for a given tenant and path.
* @param slug - tenant slug, e.g. 'acme'
* @param path - app path, e.g. '/dashboard'
*/
export function tenantUrl(slug: string, path: string = '/'): string {
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
if (STRATEGY === 'subdomain') {
return `https://${slug}.${ROOT_DOMAIN}${normalizedPath}`;
}
// path-based: prepend slug as first segment
return `https://${ROOT_DOMAIN}/${slug}${normalizedPath}`;
}
// Example usage:
// tenantUrl('acme', '/dashboard') => 'https://acme.app.com/dashboard'
// tenantUrl('acme', '/dashboard') => 'https://app.com/acme/dashboard'Semakan Pengetahuan: Penyebaran Header Middleware
Pertimbangkan coretan middleware berikut yang menyelesaikan slug tenant dan perlu menghantarnya kepada Komponen Pelayan hiliran tanpa mengubah URL pelayar:
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-tenant-slug', tenantSlug);
return NextResponse.next({ request: { headers: requestHeaders } });Pernyataan manakah yang paling tepat menerangkan sebab header ditetapkan pada objek request di dalam NextResponse.next(), bukannya pada respons?
Rumusan: Penyelesaian Tenant Berasaskan Subdomain dan Laluan
Pelajaran ini merangkumi dua strategi utama untuk menyelesaikan konteks tenant dalam aplikasi berbilang tenant Next.js 15:
- Berasaskan subdomain: hurai
request.nextUrl.hostnamedalam middleware untuk mengekstrak slug (contohnya,acme.app.com→acme). Tiada penulisan semula URL diperlukan; hanya hantarkan slug dalam header request. - Berasaskan laluan: ekstrak segmen pertama nama laluan (contohnya,
/acme/dashboard→acme), tulis semula URL untuk membuang awalan, kemudian hantarkan slug dalam header request.
Prinsip utama yang perlu diingati:
- Gunakan
NextResponse.next({ request: { headers } })untuk menghantar header tersuai kepada Komponen Pelayan dan Tindakan Pelayan. - Sentiasa baca identiti tenant daripada header sebelah pelayan — jangan sesekali mempercayai medan borang atau parameter pertanyaan yang dibekalkan klien dalam pengeluaran.
- Sahkan kewujudan tenant di Edge (stor KV / ORM Edge) supaya kegagalan berlaku dengan pantas sebelum mencapai mana-mana pengendali laluan.
- Pusatkan penjanaan URL dalam utiliti
tenantUrl()yang dipacu oleh pemboleh ubah persekitaran supaya pertukaran strategi tidak memerlukan perubahan kod. - Untuk pembangunan setempat, subdomain
*.localhostatau sandaran parameter pertanyaan?tenant=menghapuskan keperluan untuk perubahan DNS atau/etc/hosts.
Pelajari TypeScript dengan tutor kecerdasan buatan — percuma
Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.
- Kursus
- 22
- Pelajaran
- 88
Soalan Lazim
Adakah pelajaran “Penyelesaian Penyewa Berasaskan Subdomain dan Laluan” percuma?
Ya — sebanyak 3 pelajaran dalam laluan pembelajaran Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan), termasuk “Penyelesaian Penyewa Berasaskan Subdomain dan Laluan”, boleh dibaca sepenuhnya secara percuma di web ini. Selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan interaktif dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Kursus Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan) merangkumi sejumlah 4 pelajaran.
Apakah yang akan saya pelajari dalam “Penyelesaian Penyewa Berasaskan Subdomain dan Laluan”?
Selesaikan penyewa daripada subdomain atau awalan laluan menggunakan penulisan semula middleware dan pengepala. Anda berlatih Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan) menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.
Adakah saya memerlukan pengalaman untuk memulakan Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan)?
Tiada pengalaman terdahulu diperlukan. Pembelajaran Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan) di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 1 daripada 4.
Berapa lamakah pelajaran “Penyelesaian Penyewa Berasaskan Subdomain dan Laluan” diambil?
Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.
Bolehkah saya menulis dan menjalankan kod dalam pelajaran Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan) ini?
Ya. Setiap pelajaran Tindanan Penuh Next.js 15 (Penghala Aplikasi + Tindakan Pelayan) menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.
Semua pelajaran dalam kursus ini
- Penyelesaian Penyewa Berasaskan Subdomain dan Laluan
- Corak Pengasingan Data Penyewa Tahap Baris
- Tema dan Bendera Ciri Mengikut Penyewa
- Pengukuran Penggunaan dan Penguatkuasaan Langganan