|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
Next.js 作为 React 生态中最流行的服务端渲染框架之一,以其强大的路由系统而著称。从早期的页面路由到最新的 App Router,Next.js 的路由系统一直在不断演进,以满足现代 Web 应用日益复杂的需求。本文将深入探讨 Next.js 路由系统的个性化配置,从基础原理到高级实践,帮助开发者打造符合现代 Web 应用需求的定制化路由解决方案。
路由是 Web 应用的骨架,它不仅决定了应用的 URL 结构,还影响着用户体验、代码组织方式、性能优化等多个方面。通过掌握 Next.js 路由配置的艺术,开发者可以构建出更加灵活、高效且易于维护的 Web 应用。
Next.js 路由基础
文件系统路由原理
Next.js 采用了基于文件系统的路由机制,这是其核心特性之一。在这种机制下,文件和目录结构直接映射到应用的 URL 路径,使得路由管理变得直观且易于维护。
在 Next.js 13 之前,主要使用的是 Pages Router,其路由规则如下:
• pages/index.js→/
• pages/about.js→/about
• pages/blog/[slug].js→/blog/:slug
而随着 Next.js 13 的发布,引入了全新的 App Router,基于 React Server Components,提供了更强大的路由功能:
• app/page.js→/
• app/about/page.js→/about
• app/blog/[slug]/page.js→/blog/:slug
路由类型对比
Next.js 提供了两种路由系统:传统的 Pages Router 和现代的 App Router。
Pages Router的特点:
• 简单直观,易于上手
• 支持静态生成(SSG)和服务端渲染(SSR)
• 通过getStaticProps、getServerSideProps等函数获取数据
App Router的特点:
• 基于 React Server Components
• 支持嵌套路由、布局和模板
• 更细粒度的渲染控制(Streaming SSR)
• 服务器组件和客户端组件的分离
下面是一个基础的 App Router 结构示例:
- // app/layout.js - 根布局
- export default function RootLayout({ children }) {
- return (
- <html lang="en">
- <body>{children}</body>
- </html>
- );
- }
- // app/page.js - 首页
- export default function HomePage() {
- return <h1>Welcome to Next.js!</h1>;
- }
- // app/about/page.js - 关于页面
- export default function AboutPage() {
- return <h1>About Us</h1>;
- }
复制代码
基础路由配置
静态路由
静态路由是最简单的路由形式,直接对应文件系统中的文件。
在 App Router 中:
- // app/products/page.js
- export default function ProductsPage() {
- return (
- <div>
- <h1>Our Products</h1>
- <p>Check out our amazing product lineup!</p>
- </div>
- );
- }
复制代码
这将创建一个/products路径,访问该路径将显示ProductsPage组件。
动态路由
动态路由允许我们创建包含参数的 URL,例如产品详情页、用户个人资料页等。
在 App Router 中,动态路由通过方括号[]定义:
- // app/products/[id]/page.js
- export default function ProductPage({ params }) {
- return (
- <div>
- <h1>Product Details</h1>
- <p>Viewing product with ID: {params.id}</p>
- </div>
- );
- }
复制代码
这将匹配/products/1、/products/abc等路径,并将id参数传递给组件。
嵌套路由
嵌套路由允许我们在 URL 结构中创建层次关系,同时保持组件的嵌套结构。
在 App Router 中,嵌套路由通过创建嵌套文件夹实现:
- // app/dashboard/layout.js
- export default function DashboardLayout({ children }) {
- return (
- <div>
- <nav>Dashboard Navigation</nav>
- {children}
- </div>
- );
- }
- // app/dashboard/page.js
- export default function DashboardHome() {
- return <h1>Dashboard Home</h1>;
- }
- // app/dashboard/settings/page.js
- export default function DashboardSettings() {
- return <h1>Dashboard Settings</h1>;
- }
复制代码
这样,/dashboard将显示DashboardHome组件,而/dashboard/settings将显示DashboardSettings组件,两者都会被包裹在DashboardLayout中。
路由组
路由组允许我们在不影响 URL 路径的情况下组织文件。通过将文件夹放在括号中,如(marketing),可以创建逻辑分组而不添加到 URL 中。
- // app/(marketing)/about/page.js
- export default function AboutPage() {
- return <h1>About Us</h1>;
- }
- // app/(marketing)/contact/page.js
- export default function ContactPage() {
- return <h1>Contact Us</h1>;
- }
复制代码
这些页面将分别对应/about和/contact路径,(marketing)文件夹名不会出现在 URL 中。
中间件与路由拦截
路由中间件基础
Next.js 中间件允许我们在请求完成之前运行代码,这对于实现路由守卫、重定向、身份验证等功能非常有用。
创建中间件文件middleware.js(或.ts)在项目根目录或与app目录同级:
- // middleware.js
- import { NextResponse } from 'next/server';
- import { nextAuth } from './lib/auth';
- export async function middleware(request) {
- const { pathname } = request.nextUrl;
-
- // 检查用户是否已认证
- const isAuthenticated = await nextAuth.isAuthenticated(request);
-
- // 如果访问受保护的路由但未认证,重定向到登录页
- if (pathname.startsWith('/dashboard') && !isAuthenticated) {
- return NextResponse.redirect(new URL('/login', request.url));
- }
-
- return NextResponse.next();
- }
- // 配置中间件匹配的路径
- export const config = {
- matcher: ['/dashboard/:path*', '/profile/:path*'],
- };
复制代码
高级中间件模式
中间件可以用于实现更复杂的路由拦截和修改逻辑:
- // middleware.js
- import { NextResponse } from 'next/server';
- export async function middleware(request) {
- const { pathname, searchParams } = request.nextUrl;
- const token = request.cookies.get('token')?.value;
-
- // A/B 测试路由分发
- if (pathname === '/home') {
- const variant = Math.random() > 0.5 ? 'a' : 'b';
- return NextResponse.redirect(new URL(`/home-${variant}`, request.url));
- }
-
- // 地理位置重定向
- const country = request.geo?.country;
- if (pathname === '/news' && country === 'CN') {
- return NextResponse.redirect(new URL('/news/china', request.url));
- }
-
- // API 速率限制
- if (pathname.startsWith('/api/')) {
- const clientIP = request.ip;
- const rateLimitResult = await checkRateLimit(clientIP);
-
- if (!rateLimitResult.allowed) {
- return new NextResponse('Too many requests', { status: 429 });
- }
- }
-
- // 添加自定义响应头
- const response = NextResponse.next();
- response.headers.set('X-Custom-Header', 'Custom Value');
-
- return response;
- }
- // 辅助函数:检查速率限制
- async function checkRateLimit(ip) {
- // 实现速率限制逻辑
- return { allowed: true };
- }
- export const config = {
- matcher: [
- '/((?!_next/static|_next/image|favicon.ico).*)',
- ],
- };
复制代码
基于角色的访问控制
中间件还可以用于实现基于角色的访问控制(RBAC):
- // middleware.js
- import { NextResponse } from 'next/server';
- import { verifyToken } from './lib/auth';
- export async function middleware(request) {
- const { pathname } = request.nextUrl;
- const token = request.cookies.get('auth_token')?.value;
-
- // 不需要认证的路径
- const publicPaths = ['/login', '/register', '/'];
- if (publicPaths.includes(pathname)) {
- return NextResponse.next();
- }
-
- // 验证令牌
- if (!token) {
- return NextResponse.redirect(new URL('/login', request.url));
- }
-
- const user = await verifyToken(token);
-
- if (!user) {
- return NextResponse.redirect(new URL('/login', request.url));
- }
-
- // 基于角色的路由保护
- const roleBasedPaths = {
- '/admin': ['admin'],
- '/dashboard': ['user', 'admin'],
- '/settings': ['user', 'admin'],
- };
-
- for (const [path, allowedRoles] of Object.entries(roleBasedPaths)) {
- if (pathname.startsWith(path) && !allowedRoles.includes(user.role)) {
- return NextResponse.redirect(new URL('/unauthorized', request.url));
- }
- }
-
- // 将用户信息添加到请求头中,以便在路由中使用
- const requestHeaders = new Headers(request.headers);
- requestHeaders.set('x-user-id', user.id);
- requestHeaders.set('x-user-role', user.role);
-
- return NextResponse.next({
- request: {
- headers: requestHeaders,
- },
- });
- }
- export const config = {
- matcher: [
- '/((?!_next/static|_next/image|favicon.ico|api/auth).*)',
- ],
- };
复制代码
高级路由模式
并行路由
并行路由允许在同一布局中同时渲染多个独立的页面,这对于创建复杂的应用界面(如仪表板)非常有用。
在 App Router 中,并行路由通过命名插槽实现:
- // app/dashboard/@analytics/page.js
- export default function Analytics() {
- return <div>Analytics Panel</div>;
- }
- // app/dashboard/@team/page.js
- export default function Team() {
- return <div>Team Panel</div>;
- }
- // app/dashboard/layout.js
- export default function DashboardLayout({
- children,
- analytics,
- team,
- }) {
- return (
- <div className="dashboard">
- <aside>{children}</aside>
- <div className="main-content">
- <section className="analytics">{analytics}</section>
- <section className="team">{team}</section>
- </div>
- </div>
- );
- }
复制代码
这样,当访问/dashboard时,将同时渲染默认的children、Analytics和Team组件。我们还可以通过路由如/dashboard/@analytics单独访问某个插槽。
拦截路由
拦截路由允许我们在当前页面的上下文中显示路由内容,而不离开当前页面。这对于模态框、抽屉等 UI 模式非常有用。
- // app/photos/[id]/page.js
- export default function PhotoPage({ params }) {
- return <div>Viewing photo {params.id}</div>;
- }
- // app/@modal/(..)photos/[id]/page.js
- export default function PhotoModal({ params }) {
- return (
- <div className="modal">
- <h2>Photo Modal</h2>
- <p>Viewing photo {params.id} in a modal</p>
- </div>
- );
- }
- // app/layout.js
- import { Suspense } from 'react';
- export default function RootLayout({ children, modal }) {
- return (
- <html>
- <body>
- {children}
- <Suspense fallback={null}>{modal}</Suspense>
- </body>
- </html>
- );
- }
复制代码
通过这种方式,当用户点击照片链接时,可以在模态框中显示照片详情,而不离开当前页面。
条件路由
条件路由允许我们根据特定条件(如用户设备、地理位置、时间等)提供不同的路由体验。
- // app/page.js
- import { redirect } from 'next/navigation';
- import { isMobileDevice } from '@/lib/device';
- export default async function Home() {
- const isMobile = await isMobileDevice();
-
- if (isMobile) {
- redirect('/mobile-home');
- }
-
- return <div>Desktop Home Page</div>;
- }
- // app/mobile-home/page.js
- export default function MobileHome() {
- return <div>Mobile Home Page</div>;
- }
复制代码
动态路由与数据获取
动态路由通常需要根据路由参数获取数据。在 App Router 中,我们可以使用 React Server Components 直接在组件中获取数据:
- // app/blog/[slug]/page.js
- async function getBlogPost(slug) {
- const res = await fetch(`https://api.example.com/blog/${slug}`, {
- cache: 'force-cache', // 默认行为,静态生成
- // 或者使用 { cache: 'no-store' } 进行动态渲染
- });
-
- if (!res.ok) {
- throw new Error('Failed to fetch blog post');
- }
-
- return res.json();
- }
- export default async function BlogPostPage({ params }) {
- const post = await getBlogPost(params.slug);
-
- return (
- <article>
- <h1>{post.title}</h1>
- <div dangerouslySetInnerHTML={{ __html: post.content }} />
- </article>
- );
- }
复制代码
对于更复杂的数据获取场景,我们可以使用generateStaticParams函数预构建静态页面:
- // app/blog/[slug]/page.js
- async function getBlogSlugs() {
- const res = await fetch('https://api.example.com/blog/slugs');
- const slugs = await res.json();
- return slugs;
- }
- export async function generateStaticParams() {
- const slugs = await getBlogSlugs();
- return slugs.map((slug) => ({ slug }));
- }
- // ...其余代码与上面相同
复制代码
路由优化与性能
代码分割与懒加载
Next.js 默认会对每个页面进行代码分割,但我们还可以进一步优化,通过动态导入实现组件级别的懒加载:
- // app/components/heavy-component.js
- export default function HeavyComponent() {
- return <div>This is a heavy component with lots of dependencies</div>;
- }
- // app/page.js
- import dynamic from 'next/dynamic';
- // 动态导入 HeavyComponent,不进行 SSR
- const HeavyComponent = dynamic(
- () => import('../components/heavy-component'),
- { ssr: false }
- );
- // 带有加载状态的动态导入
- const HeavyComponentWithLoading = dynamic(
- () => import('../components/heavy-component'),
- {
- loading: () => <p>Loading...</p>,
- ssr: false
- }
- );
- export default function HomePage() {
- return (
- <div>
- <h1>Welcome to the Home Page</h1>
- <HeavyComponent />
- <HeavyComponentWithLoading />
- </div>
- );
- }
复制代码
路由预加载
Next.js 提供了<Link>组件的prefetch属性,可以在后台预加载页面资源,提高导航速度:
- // app/components/navigation.js
- import Link from 'next/link';
- export default function Navigation() {
- return (
- <nav>
- <Link href="/" prefetch={true}>Home</Link>
- <Link href="/about" prefetch={true}>About</Link>
- <Link href="/contact" prefetch={true}>Contact</Link>
- </nav>
- );
- }
复制代码
对于更精细的控制,我们可以使用router.prefetch方法:
- // app/components/product-card.js
- 'use client';
- import { useRouter } from 'next/navigation';
- import { useEffect } from 'react';
- export default function ProductCard({ product }) {
- const router = useRouter();
-
- useEffect(() => {
- // 当鼠标悬停在卡片上时预加载产品详情页
- router.prefetch(`/products/${product.id}`);
- }, [product.id, router]);
-
- return (
- <div
- onClick={() => router.push(`/products/${product.id}`)}
- style={{ cursor: 'pointer' }}
- >
- <h3>{product.name}</h3>
- <p>{product.description}</p>
- </div>
- );
- }
复制代码
缓存策略优化
在 App Router 中,我们可以通过fetchAPI 的cache选项精细控制数据缓存策略:
- // app/products/page.js
- async function getProducts() {
- // 使用默认缓存(静态生成)
- const res = await fetch('https://api.example.com/products');
- return res.json();
- }
- async function getRealTimeData() {
- // 不缓存,每次请求都重新获取
- const res = await fetch('https://api.example.com/real-time-data', {
- cache: 'no-store',
- });
- return res.json();
- }
- async function getRevalidatedData() {
- // 缓存数据,但最多 60 秒后重新验证
- const res = await fetch('https://api.example.com/revalidated-data', {
- next: { revalidate: 60 },
- });
- return res.json();
- }
- export default async function ProductsPage() {
- const products = await getProducts();
- const realTimeData = await getRealTimeData();
- const revalidatedData = await getRevalidatedData();
-
- return (
- <div>
- <h1>Products</h1>
- {/* 渲染产品列表 */}
- </div>
- );
- }
复制代码
路由级性能优化
我们可以通过路由配置实现更细粒度的性能优化:
- // app/shop/[category]/[product]/page.js
- import { unstable_cache } from 'next/cache';
- async function getProduct(category, product) {
- // 使用不稳定缓存 API 缓存数据获取函数
- const getProductData = unstable_cache(
- async () => {
- const res = await fetch(`https://api.example.com/shop/${category}/${product}`);
- return res.json();
- },
- [`product-${category}-${product}`],
- {
- revalidate: 3600, // 1 小时后重新验证
- tags: [`product-${product}`], // 用于按需重新验证
- }
- );
-
- return getProductData();
- }
- export default async function ProductPage({ params }) {
- const product = await getProduct(params.category, params.product);
-
- return (
- <div>
- <h1>{product.name}</h1>
- <p>{product.description}</p>
- <p>Price: ${product.price}</p>
- </div>
- );
- }
- // 按需重新验证的 API 路由
- // app/api/revalidate/route.js
- import { revalidateTag } from 'next/cache';
- export async function POST(request) {
- const { tag } = await request.json();
-
- revalidateTag(tag);
-
- return Response.json({ revalidated: true, now: Date.now() });
- }
复制代码
自定义路由配置
next.config.js 路由配置
通过next.config.js文件,我们可以实现更高级的路由配置:
- // next.config.js
- module.exports = {
- // 自定义页面扩展名
- pageExtensions: ['jsx', 'js', 'tsx', 'ts'],
-
- // 路径别名
- webpack: (config) => {
- config.resolve.alias = {
- ...config.resolve.alias,
- '@components': './components',
- '@lib': './lib',
- };
- return config;
- },
-
- // 重写和重定向
- async rewrites() {
- return [
- {
- source: '/dashboard',
- destination: '/dashboard/overview',
- },
- {
- source: '/blog/:slug',
- destination: '/articles/:slug',
- },
- ];
- },
-
- async redirects() {
- return [
- {
- source: '/old-home',
- destination: '/',
- permanent: true,
- },
- {
- source: '/admin/:path*',
- destination: '/dashboard/:path*',
- permanent: false,
- },
- ];
- },
-
- // 头部配置
- async headers() {
- return [
- {
- source: '/(.*)',
- headers: [
- {
- key: 'X-Frame-Options',
- value: 'DENY',
- },
- {
- key: 'X-Content-Type-Options',
- value: 'nosniff',
- },
- ],
- },
- ];
- },
- };
复制代码
自定义路由处理
我们可以创建自定义路由处理程序来处理特定的路由模式:
- // app/api/route.js
- import { NextResponse } from 'next/server';
- export async function GET(request) {
- const { searchParams } = new URL(request.url);
- const id = searchParams.get('id');
-
- const data = await fetchData(id);
-
- return NextResponse.json(data);
- }
- export async function POST(request) {
- const body = await request.json();
-
- const result = await createData(body);
-
- return NextResponse.json(result, { status: 201 });
- }
复制代码
国际化路由配置
Next.js 提供了内置的国际化(i18n)支持,我们可以配置基于子路径或域名的多语言路由:
- // next.config.js
- module.exports = {
- i18n: {
- locales: ['en', 'fr', 'de'],
- defaultLocale: 'en',
- domains: [
- {
- domain: 'example.com',
- defaultLocale: 'en',
- },
- {
- domain: 'example.fr',
- defaultLocale: 'fr',
- },
- {
- domain: 'example.de',
- defaultLocale: 'de',
- },
- ],
- },
- };
复制代码
然后,我们可以创建基于语言的路由结构:
- // app/[locale]/layout.js
- import { NextIntlClientProvider } from 'next-intl';
- import { getMessages } from 'next-intl/server';
- import { notFound } from 'next/navigation';
- const locales = ['en', 'fr', 'de'];
- export default async function LocaleLayout({
- children,
- params: { locale }
- }) {
- // 验证是否提供了有效的 locale
- if (!locales.includes(locale as any)) notFound();
-
- // 提供所有消息,以便在客户端组件中使用
- const messages = await getMessages();
-
- return (
- <NextIntlClientProvider messages={messages}>
- {children}
- </NextIntlClientProvider>
- );
- }
- // app/[locale]/page.js
- export default function HomePage() {
- return (
- <div>
- <h1>Welcome to the Home Page</h1>
- {/* 本地化内容 */}
- </div>
- );
- }
复制代码
动态路由生成
对于需要动态生成大量路由的场景,我们可以使用generateStaticParams结合外部数据源:
- // app/docs/[...slug]/page.js
- async function getAllDocPaths() {
- const res = await fetch('https://api.example.com/docs/paths');
- const paths = await res.json();
- return paths;
- }
- export async function generateStaticParams() {
- const paths = await getAllDocPaths();
-
- return paths.map((path) => ({
- slug: path.split('/'),
- }));
- }
- export default async function DocPage({ params }) {
- const { slug } = params;
- const path = slug.join('/');
-
- const res = await fetch(`https://api.example.com/docs/${path}`);
- const doc = await res.json();
-
- return (
- <div>
- <h1>{doc.title}</h1>
- <div dangerouslySetInnerHTML={{ __html: doc.content }} />
- </div>
- );
- }
复制代码
实战案例
构建多租户 SaaS 应用路由
让我们构建一个具有多租户支持的 SaaS 应用路由系统:
- // middleware.js
- import { NextResponse } from 'next/server';
- export async function middleware(request) {
- const { pathname, host } = request.nextUrl;
-
- // 从主机名提取租户信息
- const subdomain = host.split('.')[0];
-
- // 跳过静态资源和 API 路由
- if (
- pathname.startsWith('/_next') ||
- pathname.startsWith('/api') ||
- pathname.startsWith('/static')
- ) {
- return NextResponse.next();
- }
-
- // 租户特定的重定向
- if (pathname === '/') {
- return NextResponse.redirect(new URL(`/dashboard`, request.url));
- }
-
- // 添加租户信息到请求头
- const requestHeaders = new Headers(request.headers);
- requestHeaders.set('x-tenant', subdomain);
-
- return NextResponse.next({
- request: {
- headers: requestHeaders,
- },
- });
- }
- export const config = {
- matcher: [
- '/((?!_next/static|_next/image|favicon.ico|api/auth).*)',
- ],
- };
复制代码- // app/layout.js
- import './globals.css';
- export const metadata = {
- title: 'Multi-tenant SaaS Application',
- description: 'A demo of multi-tenant routing in Next.js',
- };
- export default function RootLayout({ children }) {
- return (
- <html lang="en">
- <body>{children}</body>
- </html>
- );
- }
复制代码- // app/dashboard/layout.js
- import { Suspense } from 'react';
- import TenantProvider from '@/components/TenantProvider';
- export default function DashboardLayout({
- children,
- analytics,
- settings,
- }) {
- return (
- <TenantProvider>
- <div className="dashboard">
- <aside>
- <nav>
- {/* 租户特定的导航 */}
- </nav>
- </aside>
- <main>
- <Suspense fallback={<div>Loading...</div>}>
- {children}
- </Suspense>
- </main>
- <div className="panels">
- <Suspense fallback={<div>Loading analytics...</div>}>
- {analytics}
- </Suspense>
- <Suspense fallback={<div>Loading settings...</div>}>
- {settings}
- </Suspense>
- </div>
- </div>
- </TenantProvider>
- );
- }
复制代码- // app/components/TenantProvider.js
- 'use client';
- import { createContext, useContext, useEffect, useState } from 'react';
- const TenantContext = createContext();
- export function TenantProvider({ children }) {
- const [tenant, setTenant] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- async function loadTenant() {
- try {
- // 从 API 获取租户信息
- const res = await fetch('/api/tenant');
- const data = await res.json();
- setTenant(data);
- } catch (error) {
- console.error('Failed to load tenant:', error);
- } finally {
- setLoading(false);
- }
- }
-
- loadTenant();
- }, []);
-
- if (loading) {
- return <div>Loading tenant information...</div>;
- }
-
- return (
- <TenantContext.Provider value={{ tenant }}>
- {children}
- </TenantContext.Provider>
- );
- }
- export function useTenant() {
- return useContext(TenantContext);
- }
复制代码- // app/api/tenant/route.js
- import { headers } from 'next/headers';
- export async function GET() {
- const headersList = headers();
- const tenantSubdomain = headersList.get('x-tenant');
-
- // 根据子域名获取租户信息
- // 这里可以是数据库查询或其他数据源
- const tenant = await getTenantBySubdomain(tenantSubdomain);
-
- if (!tenant) {
- return Response.json({ error: 'Tenant not found' }, { status: 404 });
- }
-
- return Response.json(tenant);
- }
- async function getTenantBySubdomain(subdomain) {
- // 模拟数据库查询
- const tenants = {
- 'client1': { id: 1, name: 'Client 1', theme: 'light' },
- 'client2': { id: 2, name: 'Client 2', theme: 'dark' },
- };
-
- return tenants[subdomain] || null;
- }
复制代码
构建电子商务网站路由
让我们构建一个具有复杂路由需求的电子商务网站:
- // middleware.js
- import { NextResponse } from 'next/server';
- export async function middleware(request) {
- const { pathname, searchParams } = request.nextUrl;
-
- // 从查询参数获取优惠码
- const couponCode = searchParams.get('coupon');
-
- if (couponCode) {
- // 验证优惠码
- const isValid = await validateCoupon(couponCode);
-
- if (isValid) {
- // 将优惠码存储在 cookie 中
- const response = NextResponse.next();
- response.cookies.set('coupon_code', couponCode, {
- maxAge: 60 * 60 * 24, // 1 天
- httpOnly: true,
- });
- return response;
- }
- }
-
- // 产品类别重定向
- if (pathname.startsWith('/products/')) {
- const category = pathname.split('/')[2];
-
- // 检查类别是否存在
- const categoryExists = await checkCategoryExists(category);
-
- if (!categoryExists) {
- return NextResponse.redirect(new URL('/products', request.url));
- }
- }
-
- return NextResponse.next();
- }
- async function validateCoupon(code) {
- // 实现优惠码验证逻辑
- return true;
- }
- async function checkCategoryExists(category) {
- // 实现类别检查逻辑
- return true;
- }
- export const config = {
- matcher: [
- '/((?!_next/static|_next/image|favicon.ico).*)',
- ],
- };
复制代码- // app/products/page.js
- import ProductGrid from '@/components/ProductGrid';
- import Filters from '@/components/Filters';
- import SortOptions from '@/components/SortOptions';
- async function getProducts({ searchParams }) {
- const { category, sort, minPrice, maxPrice } = searchParams;
-
- const params = new URLSearchParams();
- if (category) params.append('category', category);
- if (sort) params.append('sort', sort);
- if (minPrice) params.append('minPrice', minPrice);
- if (maxPrice) params.append('maxPrice', maxPrice);
-
- const res = await fetch(`https://api.example.com/products?${params}`);
- return res.json();
- }
- async function getCategories() {
- const res = await fetch('https://api.example.com/categories');
- return res.json();
- }
- export default async function ProductsPage({ searchParams }) {
- const products = await getProducts({ searchParams });
- const categories = await getCategories();
-
- return (
- <div className="products-page">
- <h1>Our Products</h1>
-
- <div className="filters-sidebar">
- <Filters categories={categories} />
- </div>
-
- <div className="products-content">
- <SortOptions />
- <ProductGrid products={products} />
- </div>
- </div>
- );
- }
复制代码- // app/products/[category]/page.js
- import ProductGrid from '@/components/ProductGrid';
- import Filters from '@/components/Filters';
- import SortOptions from '@/components/SortOptions';
- async function getProductsByCategory(category, { searchParams }) {
- const { sort, minPrice, maxPrice } = searchParams;
-
- const params = new URLSearchParams();
- params.append('category', category);
- if (sort) params.append('sort', sort);
- if (minPrice) params.append('minPrice', minPrice);
- if (maxPrice) params.append('maxPrice', maxPrice);
-
- const res = await fetch(`https://api.example.com/products?${params}`);
- return res.json();
- }
- async function getCategoryDetails(category) {
- const res = await fetch(`https://api.example.com/categories/${category}`);
- return res.json();
- }
- export async function generateStaticParams() {
- const res = await fetch('https://api.example.com/categories');
- const categories = await res.json();
-
- return categories.map((category) => ({
- category: category.slug,
- }));
- }
- export default async function CategoryPage({ params, searchParams }) {
- const { category } = params;
- const products = await getProductsByCategory(category, { searchParams });
- const categoryDetails = await getCategoryDetails(category);
-
- return (
- <div className="category-page">
- <h1>{categoryDetails.name}</h1>
- <p>{categoryDetails.description}</p>
-
- <div className="filters-sidebar">
- <Filters />
- </div>
-
- <div className="products-content">
- <SortOptions />
- <ProductGrid products={products} />
- </div>
- </div>
- );
- }
复制代码- // app/products/[category]/[slug]/page.js
- import { notFound } from 'next/navigation';
- import ProductGallery from '@/components/ProductGallery';
- import ProductInfo from '@/components/ProductInfo';
- import RelatedProducts from '@/components/RelatedProducts';
- import AddToCartButton from '@/components/AddToCartButton';
- async function getProduct(category, slug) {
- const res = await fetch(`https://api.example.com/products/${slug}`, {
- next: { revalidate: 3600 }, // 1 小时后重新验证
- });
-
- if (!res.ok) {
- return null;
- }
-
- return res.json();
- }
- async function getRelatedProducts(productId) {
- const res = await fetch(`https://api.example.com/products/${productId}/related`);
- return res.json();
- }
- export async function generateStaticParams() {
- const res = await fetch('https://api.example.com/products');
- const products = await res.json();
-
- return products.map((product) => ({
- category: product.category.slug,
- slug: product.slug,
- }));
- }
- export default async function ProductPage({ params }) {
- const { category, slug } = params;
- const product = await getProduct(category, slug);
-
- if (!product) {
- notFound();
- }
-
- const relatedProducts = await getRelatedProducts(product.id);
-
- return (
- <div className="product-page">
- <div className="product-main">
- <ProductGallery images={product.images} />
- <div className="product-details">
- <ProductInfo product={product} />
- <AddToCartButton product={product} />
- </div>
- </div>
-
- <div className="related-products">
- <h2>You might also like</h2>
- <RelatedProducts products={relatedProducts} />
- </div>
- </div>
- );
- }
复制代码
最佳实践与注意事项
路由组织最佳实践
1. 保持路由结构简单直观:遵循 Next.js 的约定优于配置原则,尽量使用文件系统路由。
2. 合理使用路由组:使用路由组(folder-name)组织相关页面,而不影响 URL 结构。
3. 避免过度嵌套:过深的路由嵌套会使应用变得复杂,尽量保持路由层次扁平。
4. 使用布局减少代码重复:将共享的 UI 元素(如导航、页脚)放在布局组件中。
保持路由结构简单直观:遵循 Next.js 的约定优于配置原则,尽量使用文件系统路由。
合理使用路由组:使用路由组(folder-name)组织相关页面,而不影响 URL 结构。
避免过度嵌套:过深的路由嵌套会使应用变得复杂,尽量保持路由层次扁平。
使用布局减少代码重复:将共享的 UI 元素(如导航、页脚)放在布局组件中。
- // 良好的路由组织示例
- app/
- ├── (auth)/
- │ ├── login/
- │ │ └── page.js
- │ └── register/
- │ └── page.js
- ├── (dashboard)/
- │ ├── layout.js
- │ ├── page.js
- │ ├── analytics/
- │ │ └── page.js
- │ └── settings/
- │ └── page.js
- ├── (marketing)/
- │ ├── about/
- │ │ └── page.js
- │ └── contact/
- │ └── page.js
- ├── api/
- │ └── auth/
- │ └── route.js
- ├── layout.js
- └── page.js
复制代码
性能优化最佳实践
1. 使用静态生成:对于不经常变化的内容,使用静态生成(SSG)提高性能。
2. 实现增量静态再生成:对于需要定期更新的内容,使用 ISR。
3. 懒加载非关键组件:使用next/dynamic懒加载大型组件或非首屏组件。
4. 优化数据获取:在服务器组件中获取数据,减少客户端 JavaScript 包大小。
5. 使用预加载:对于用户可能访问的页面,使用<Link prefetch>或router.prefetch()预加载。
使用静态生成:对于不经常变化的内容,使用静态生成(SSG)提高性能。
实现增量静态再生成:对于需要定期更新的内容,使用 ISR。
懒加载非关键组件:使用next/dynamic懒加载大型组件或非首屏组件。
优化数据获取:在服务器组件中获取数据,减少客户端 JavaScript 包大小。
使用预加载:对于用户可能访问的页面,使用<Link prefetch>或router.prefetch()预加载。
- // 性能优化示例
- import dynamic from 'next/dynamic';
- import Link from 'next/link';
- // 懒加载大型组件
- const HeavyChart = dynamic(
- () => import('../components/HeavyChart'),
- {
- loading: () => <div>Loading chart...</div>,
- ssr: false
- }
- );
- // 预加载页面
- function Navigation() {
- return (
- <nav>
- <Link href="/dashboard" prefetch={true}>Dashboard</Link>
- <Link href="/analytics" prefetch={true}>Analytics</Link>
- </nav>
- );
- }
- // 服务器组件中获取数据
- async function BlogPage() {
- const posts = await getBlogPosts(); // 在服务器上获取数据
-
- return (
- <div>
- <h1>Blog Posts</h1>
- {posts.map(post => (
- <div key={post.id}>
- <h2>{post.title}</h2>
- <p>{post.excerpt}</p>
- </div>
- ))}
- </div>
- );
- }
复制代码
安全性最佳实践
1. 验证路由参数:始终验证动态路由参数,防止恶意输入。
2. 实现适当的认证和授权:使用中间件保护敏感路由。
3. 避免在 URL 中暴露敏感信息:不要将敏感数据(如 ID、令牌)放在 URL 中。
4. 使用 HTTPS:确保所有路由都通过 HTTPS 提供服务。
验证路由参数:始终验证动态路由参数,防止恶意输入。
实现适当的认证和授权:使用中间件保护敏感路由。
避免在 URL 中暴露敏感信息:不要将敏感数据(如 ID、令牌)放在 URL 中。
使用 HTTPS:确保所有路由都通过 HTTPS 提供服务。
- // 安全路由参数验证示例
- export default function UserProfilePage({ params }) {
- const { userId } = params;
-
- // 验证 userId 是否为有效的 UUID
- if (!isValidUUID(userId)) {
- notFound();
- }
-
- // 获取用户数据
- const user = getUserById(userId);
-
- if (!user) {
- notFound();
- }
-
- // 检查当前用户是否有权查看此用户资料
- if (!canViewUserProfile(user)) {
- redirect('/unauthorized');
- }
-
- return <UserProfile user={user} />;
- }
- // 中间件认证示例
- export async function middleware(request) {
- const token = request.cookies.get('auth_token')?.value;
-
- // 检查令牌是否存在
- if (!token) {
- return NextResponse.redirect(new URL('/login', request.url));
- }
-
- // 验证令牌
- const user = await verifyToken(token);
-
- if (!user) {
- // 删除无效令牌
- const response = NextResponse.redirect(new URL('/login', request.url));
- response.cookies.delete('auth_token');
- return response;
- }
-
- // 将用户信息添加到请求头
- const requestHeaders = new Headers(request.headers);
- requestHeaders.set('x-user-id', user.id);
- requestHeaders.set('x-user-role', user.role);
-
- return NextResponse.next({
- request: {
- headers: requestHeaders,
- },
- });
- }
复制代码
可访问性最佳实践
1. 使用语义 HTML:确保路由结构使用适当的 HTML5 语义元素。
2. 提供导航辅助功能:包括跳过导航链接、面包屑导航等。
3. 管理焦点:页面导航时适当管理键盘焦点。
4. 提供有意义的页面标题:每个页面应有描述性的标题。
使用语义 HTML:确保路由结构使用适当的 HTML5 语义元素。
提供导航辅助功能:包括跳过导航链接、面包屑导航等。
管理焦点:页面导航时适当管理键盘焦点。
提供有意义的页面标题:每个页面应有描述性的标题。
- // 可访问性路由示例
- // app/layout.js
- export default function RootLayout({ children }) {
- return (
- <html lang="en">
- <body>
- <a href="#main-content" className="skip-link">
- Skip to main content
- </a>
- <nav aria-label="Main navigation">
- {/* 导航内容 */}
- </nav>
- <main id="main-content" tabIndex={-1}>
- {children}
- </main>
- <footer aria-label="Site footer">
- {/* 页脚内容 */}
- </footer>
- </body>
- </html>
- );
- }
- // app/products/[slug]/page.js
- export async function generateMetadata({ params }) {
- const product = await getProduct(params.slug);
-
- return {
- title: `${product.name} - Our Store`,
- description: product.description,
- };
- }
- export default function ProductPage({ params }) {
- const product = await getProduct(params.slug);
-
- return (
- <div>
- <nav aria-label="Breadcrumb">
- <ol>
- <li><a href="/">Home</a></li>
- <li><a href="/products">Products</a></li>
- <li><a href={`/products/${product.category}`}>{product.category}</a></li>
- <li>{product.name}</li>
- </ol>
- </nav>
-
- <h1>{product.name}</h1>
- {/* 产品内容 */}
- </div>
- );
- }
复制代码
总结与展望
Next.js 的路由系统已经从简单的文件系统路由发展成为功能强大且灵活的框架,能够满足现代 Web 应用的复杂需求。通过本文的探索,我们了解了 Next.js 路由的基础原理、高级配置和最佳实践。
从基础的静态和动态路由,到高级的并行路由、拦截路由和条件路由,Next.js 提供了丰富的工具来构建定制化的路由解决方案。结合中间件、性能优化技术和安全最佳实践,我们可以创建出既强大又可靠的 Web 应用。
随着 Next.js 的不断发展,我们可以期待更多创新的路由功能和改进。React Server Components、Suspense 和 Streaming SSR 等技术将继续塑造 Next.js 路由的未来,为开发者提供更强大的工具来构建下一代 Web 应用。
通过掌握 Next.js 路由配置的艺术,开发者可以充分发挥框架的潜力,创建出用户体验优秀、性能卓越且易于维护的现代化 Web 应用。 |
|