Next.js模板与默认组件 #

一、模板 template.tsx #

1.1 模板概念 #

模板与布局类似,都会包裹子组件,但模板在导航时会重新创建实例:

特性 布局 Layout 模板 Template
状态保持 保持 不保持
重新渲染
实例创建 一次 每次导航
性能 更好 稍差

1.2 基本用法 #

tsx
export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    return <div>{children}</div>
}

1.3 模板与布局的区别 #

布局示例

tsx
export default function Layout({
    children,
}: {
    children: React.ReactNode
}) {
    console.log('Layout rendered')
    return <div className="layout">{children}</div>
}

模板示例

tsx
export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    console.log('Template rendered')
    return <div className="template">{children}</div>
}

1.4 使用场景 #

进入/退出动画

tsx
'use client'

import { motion } from 'framer-motion'

export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <motion.div
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
            transition={{ duration: 0.3 }}
        >
            {children}
        </motion.div>
    )
}

页面访问统计

tsx
'use client'

import { useEffect } from 'react'
import { usePathname } from 'next/navigation'

export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    const pathname = usePathname()
    
    useEffect(() => {
        console.log('Page viewed:', pathname)
    }, [pathname])
    
    return <div>{children}</div>
}

页面加载效果

tsx
'use client'

import { useState, useEffect } from 'react'

export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    const [loading, setLoading] = useState(true)
    
    useEffect(() => {
        setLoading(false)
    }, [])
    
    if (loading) {
        return <div className="loading">加载中...</div>
    }
    
    return <div>{children}</div>
}

1.5 模板与布局组合 #

text
app/
├── layout.tsx
├── template.tsx
└── page.tsx

渲染顺序:

text
RootLayout
└── Template
    └── Page
tsx
export default function Layout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <div className="layout">
            <header>Header</header>
            {children}
        </div>
    )
}
tsx
export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <div className="template">
            {children}
        </div>
    )
}

二、默认组件 default.tsx #

2.1 默认组件概念 #

默认组件用于并行路由的备用UI,当插槽没有匹配的页面时显示:

text
app/
└── dashboard/
    ├── @team/
    │   └── page.tsx
    ├── @analytics/
    │   └── default.tsx    # 默认组件
    └── layout.tsx

2.2 基本用法 #

tsx
export default function Default() {
    return <div>暂无内容</div>
}

2.3 并行路由默认页 #

text
app/
└── dashboard/
    ├── @team/
    │   ├── page.tsx
    │   └── default.tsx
    ├── @analytics/
    │   ├── page.tsx
    │   └── default.tsx
    ├── layout.tsx
    └── page.tsx

布局

tsx
interface LayoutProps {
    team: React.ReactNode
    analytics: React.ReactNode
}

export default function DashboardLayout({
    team,
    analytics,
}: LayoutProps) {
    return (
        <div className="grid grid-cols-2 gap-4">
            <div>{team}</div>
            <div>{analytics}</div>
        </div>
    )
}

默认组件

tsx
export default function DefaultTeam() {
    return (
        <div className="p-4 text-gray-500">
            选择一个团队查看详情
        </div>
    )
}

2.4 使用场景 #

空状态显示

tsx
export default function Default() {
    return (
        <div className="flex flex-col items-center justify-center h-64 text-gray-400">
            <svg className="w-16 h-16 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
            </svg>
            <p>暂无数据</p>
        </div>
    )
}

加载占位

tsx
export default function Default() {
    return (
        <div className="animate-pulse">
            <div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
            <div className="h-4 bg-gray-200 rounded w-1/2"></div>
        </div>
    )
}

引导用户操作

tsx
export default function Default() {
    return (
        <div className="text-center py-8">
            <h3 className="text-lg font-medium mb-2">开始使用</h3>
            <p className="text-gray-500 mb-4">创建您的第一个项目</p>
            <button className="px-4 py-2 bg-blue-500 text-white rounded">
                创建项目
            </button>
        </div>
    )
}

三、综合示例 #

3.1 带动画的仪表盘 #

text
app/
└── dashboard/
    ├── layout.tsx
    ├── template.tsx
    ├── @sidebar/
    │   ├── page.tsx
    │   └── default.tsx
    ├── @content/
    │   ├── page.tsx
    │   └── default.tsx
    └── page.tsx

布局

tsx
interface LayoutProps {
    sidebar: React.ReactNode
    content: React.ReactNode
}

export default function DashboardLayout({ sidebar, content }: LayoutProps) {
    return (
        <div className="flex min-h-screen">
            <aside className="w-64 bg-gray-800 text-white">
                {sidebar}
            </aside>
            <main className="flex-1 p-6">
                {content}
            </main>
        </div>
    )
}

模板

tsx
'use client'

import { motion } from 'framer-motion'

export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ duration: 0.2 }}
        >
            {children}
        </motion.div>
    )
}

默认侧边栏

tsx
export default function DefaultSidebar() {
    return (
        <nav className="p-4">
            <ul className="space-y-2">
                <li><a href="/dashboard">概览</a></li>
                <li><a href="/dashboard/analytics">分析</a></li>
                <li><a href="/dashboard/settings">设置</a></li>
            </ul>
        </nav>
    )
}

默认内容

tsx
export default function DefaultContent() {
    return (
        <div className="flex items-center justify-center h-full">
            <p className="text-gray-500">选择一个菜单项查看详情</p>
        </div>
    )
}

3.2 页面切换动画 #

tsx
'use client'

import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'

export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    const pathname = usePathname()
    
    return (
        <AnimatePresence mode="wait">
            <motion.div
                key={pathname}
                initial={{ opacity: 0, x: 20 }}
                animate={{ opacity: 1, x: 0 }}
                exit={{ opacity: 0, x: -20 }}
                transition={{ duration: 0.3 }}
            >
                {children}
            </motion.div>
        </AnimatePresence>
    )
}

3.3 页面访问追踪 #

tsx
'use client'

import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'

export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    const pathname = usePathname()
    const searchParams = useSearchParams()
    
    useEffect(() => {
        const url = pathname + (searchParams.toString() ? `?${searchParams}` : '')
        
        if (typeof window !== 'undefined' && window.gtag) {
            window.gtag('config', 'GA_MEASUREMENT_ID', {
                page_path: url,
            })
        }
    }, [pathname, searchParams])
    
    return <>{children}</>
}

四、最佳实践 #

4.1 何时使用模板 #

  • 需要页面切换动画
  • 需要追踪页面访问
  • 需要每次导航时重置状态
  • 需要进入/退出效果

4.2 何时使用默认组件 #

  • 并行路由的备用UI
  • 空状态显示
  • 加载占位符
  • 引导用户操作

4.3 性能考虑 #

tsx
'use client'

import { lazy, Suspense } from 'react'

const HeavyAnimation = lazy(() => import('./HeavyAnimation'))

export default function Template({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <Suspense fallback={<>{children}</>}>
            <HeavyAnimation>
                {children}
            </HeavyAnimation>
        </Suspense>
    )
}

五、总结 #

模板与默认组件要点:

特性 模板 默认组件
文件 template.tsx default.tsx
用途 页面包裹 并行路由备用
状态 每次重新创建 -
场景 动画、追踪 空状态、占位

下一步,让我们学习元数据与SEO!

最后更新:2026-03-28