1191 字
6 分钟
服务端渲染 SSR 实战:Next.js 与 Nuxt.js 深度解析
服务端渲染 SSR 实战:Next.js 与 Nuxt.js 深度解析
🌐 前言:服务端渲染(SSR)是解决 SPA 首屏加载慢、SEO 不友好的有效方案。Next.js 和 Nuxt.js 是目前最流行的 SSR 框架,这篇文章将深入讲解它们的原理和实战技巧。
一、为什么需要 SSR
1.1 SPA 的问题
首屏加载慢:
用户请求 -> 下载 HTML -> 下载 JS -> 执行 JS -> 渲染页面(白屏时间 = 下载 + 执行 JS 的时间)SEO 不友好:
- 搜索引擎爬虫可能无法执行 JavaScript
- 页面内容为空,影响收录和排名
1.2 SSR 的优势
更快的首屏:
用户请求 -> 服务端渲染 -> 返回完整 HTML -> 展示页面(用户可以立即看到内容)更好的 SEO:
- 搜索引擎直接抓取渲染好的 HTML
- 内容完整,有利于排名
1.3 渲染模式对比
| 模式 | 首屏 | SEO | 交互 | 适用场景 |
|---|---|---|---|---|
| CSR | 慢 | 差 | 好 | 后台管理系统 |
| SSR | 快 | 好 | 好 | 内容网站、电商 |
| SSG | 极快 | 好 | 一般 | 博客、文档站 |
| ISR | 快 | 好 | 好 | 新闻、大流量站点 |
二、Next.js 实战
2.1 项目结构
my-app/├── app/ # App Router (Next.js 13+)│ ├── layout.tsx│ ├── page.tsx│ └── loading.tsx├── pages/ # Pages Router (传统)│ ├── index.tsx│ └── api/├── public/├── components/└── next.config.js2.2 数据获取
服务端组件(推荐):
async function getData() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } // ISR: 60秒重新验证 }); return res.json();}
export default async function Page() { const posts = await getData();
return ( <main> <h1>博客文章</h1> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article> ))} </main> );}客户端组件:
'use client';
import { useState, useEffect } from 'react';
export default function Counter() { const [count, setCount] = useState(0);
useEffect(() => { // 客户端逻辑 }, []);
return ( <button onClick={() => setCount(c => c + 1)}> Count: {count} </button> );}2.3 路由和导航
export default function RootLayout({ children}: { children: React.ReactNode}) { return ( <html lang="zh"> <body> <nav> <Link href="/">首页</Link> <Link href="/about">关于</Link> </nav> {children} </body> </html> );}2.4 API 路由
import { NextResponse } from 'next/server';
export async function GET() { const users = await db.users.findMany(); return NextResponse.json(users);}
export async function POST(request: Request) { const body = await request.json(); const user = await db.users.create(body); return NextResponse.json(user, { status: 201 });}2.5 中间件
import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) { // 认证检查 const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); }
return NextResponse.next();}
export const config = { matcher: ['/dashboard/:path*']};三、Nuxt.js 实战
3.1 项目结构
nuxt-app/├── components/ # Vue 组件├── composables/ # 组合式函数├── layouts/ # 布局├── middleware/ # 中间件├── pages/ # 页面(自动路由)├── public/ # 静态文件├── server/ # 服务端代码│ ├── api/ # API 路由│ └── middleware/ # 服务端中间件└── nuxt.config.ts3.2 页面和数据获取
<script setup>// 服务端获取数据const { data: posts } = await useFetch('/api/posts');
// 客户端获取数据const { data: users } = await useFetch('/api/users', { server: false // 只在客户端获取});
// ISRconst { data: articles } = await useFetch('/api/articles', { key: 'articles', initialCache: false});</script>
<template> <div> <h1>文章列表</h1> <article v-for="post in posts" :key="post.id"> <h2>{{ post.title }}</h2> <p>{{ post.excerpt }}</p> </article> </div></template>3.3 服务端 API
export default defineEventHandler(async (event) => { const posts = await prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
return posts;});
// server/api/posts.post.tsexport default defineEventHandler(async (event) => { const body = await readBody(event);
const post = await prisma.post.create({ data: body });
return post;});3.4 组合式函数
export const useAuth = () => { const user = useState('user', () => null);
const login = async (credentials) => { const { data } = await useFetch('/api/auth/login', { method: 'POST', body: credentials });
user.value = data.value; };
const logout = async () => { await useFetch('/api/auth/logout', { method: 'POST' }); user.value = null; };
return { user, login, logout, isLoggedIn: computed(() => !!user.value) };};四、性能优化
4.1 图片优化
Next.js:
import Image from 'next/image';
export default function Gallery() { return ( <Image src="/photo.jpg" alt="照片" width={800} height={600} priority // 优先加载 placeholder="blur" blurDataURL="data:image/jpeg;base64,..." /> );}Nuxt.js:
<template> <NuxtImg src="/photo.jpg" alt="照片" width="800" height="600" loading="lazy" placeholder /></template>4.2 代码分割
// Next.js 动态导入import dynamic from 'next/dynamic';
const HeavyComponent = dynamic( () => import('../components/HeavyComponent'), { loading: () => <p>Loading...</p> });
// Nuxt.js 动态导入const HeavyComponent = defineAsyncComponent(() => import('~/components/HeavyComponent.vue'));4.3 缓存策略
// Next.js 缓存配置export async function getStaticProps() { const data = await fetchData();
return { props: { data }, revalidate: 60, // ISR: 60秒重新生成 notFound: !data };}
// Nuxt.js 缓存const { data } = await useFetch('/api/data', { key: 'unique-key', server: true, default: () => ({})});五、部署
5.1 Vercel(推荐)
# Next.jsnpm i -g vercelvercel
# Nuxt.jsnpm i -g vercelvercel5.2 Node.js 服务器
{ "scripts": { "build": "next build", "start": "next start" }}5.3 Docker
FROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run buildEXPOSE 3000CMD ["npm", "start"]六、总结
Next.js vs Nuxt.js:
| 特性 | Next.js | Nuxt.js |
|---|---|---|
| 框架 | React | Vue |
| 路由 | App Router / Pages | 文件系统自动路由 |
| 数据获取 | Server Components / getXProps | useFetch / 自动导入 |
| 生态 | 更大 | 活跃 |
| 学习曲线 | 中等 | 较低 |
选择建议:
- React 团队选 Next.js
- Vue 团队选 Nuxt.js
- 新项目两者都是优秀选择
SSR 不再是可选项,而是现代 Web 应用的标配。掌握 Next.js 或 Nuxt.js,将帮助你构建更快、更友好的 Web 应用!
服务端渲染 SSR 实战:Next.js 与 Nuxt.js 深度解析
https://www.oferry.com/posts/a86/