文章目录
正文
1. 服务端渲染(SSR)基础概念
1.1 什么是服务端渲染(SSR)
服务端渲染是指在服务器端将Vue组件渲染成HTML字符串,然后直接发送到浏览器,最后在浏览器端将静态HTML"激活"为完全可交互的应用程序。
1.2 SSR 与 CSR 对比
特性 | 服务端渲染 (SSR) | 客户端渲染 (CSR) |
---|---|---|
首屏加载 | 更快 | 较慢 |
SEO | 更好 | 较差 |
服务器负载 | 较高 | 较低 |
开发复杂度 | 较高 | 较低 |
动态交互 | 需要激活 | 直接可用 |
缓存策略 | 较复杂 | 简单 |
1.3 SSR 的适用场景
- 内容展示类网站(博客、新闻、电商)
- 对搜索引擎优化(SEO)要求高的应用
- 对首屏加载性能敏感的应用
- 需要在各种环境下保持一致体验的应用
2. Vue SSR 实现原理
2.1 基本架构
// server.js
const express = require('express')
const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')
const app = express()
app.get('/', async (req, res) => {
const vueApp = createSSRApp({
data: () => ({ message: 'Hello SSR!' }),
template: '<div>{{ message }}</div>'
})
try {
const appContent = await renderToString(vueApp)
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Demo</title>
</head>
<body>
<div id="app">${appContent}</div>
<script src="/client.js"></script>
</body>
</html>
`
res.end(html)
} catch (error) {
console.error(error)
res.status(500).end('Internal Server Error')
}
})
app.listen(3000, () => {
console.log('Server running at http://localhost:3000')
})
2.2 Vue SSR 双端代码
// entry-client.js
import { createSSRApp } from 'vue'
import App from './App.vue'
const app = createSSRApp(App)
app.mount('#app')
// entry-server.js
import { createSSRApp } from 'vue'
import App from './App.vue'
export function render() {
const app = createSSRApp(App)
return app
}
2.3 Hydration 过程
客户端激活(Hydration)是指浏览器接收服务端渲染的HTML后,Vue接管页面并使其具有交互性的过程。
// 激活过程示意
import { createSSRApp } from 'vue'
import App from './App.vue'
// 服务器已经渲染了HTML,现在我们只需要"激活"它
const app = createSSRApp(App)
// 使用与服务器相同的数据,以避免数据不匹配警告
window.__INITIAL_STATE__ && app.provide('initialState', window.__INITIAL_STATE__)
// 挂载到已有的DOM上,而不是替换它
app.mount('#app')
3. Nuxt.js 基础
3.1 Nuxt.js 简介
Nuxt.js 是一个基于Vue.js的通用应用框架,它预设了利用Vue.js开发服务端渲染应用的各种配置,提供了自动代码分割、服务端渲染、强大的路由系统等特性。
3.2 创建 Nuxt 项目
# 使用npx方式创建(推荐)
npx nuxi init my-nuxt-app
# 安装依赖
cd my-nuxt-app
npm install
# 启动开发服务
npm run dev
3.3 目录结构
my-nuxt-app/
├── .nuxt/ # 构建目录(自动生成)
├── assets/ # 未编译的静态资源
├── components/ # Vue组件目录
├── composables/ # 组合式函数
├── content/ # 内容目录(Markdown等)
├── layouts/ # 布局组件
├── middleware/ # 中间件
├── pages/ # 页面组件(自动生成路由)
├── plugins/ # 插件目录
├── public/ # 静态资源目录
├── server/ # 服务端目录
├── app.vue # 应用入口组件
├── nuxt.config.ts # Nuxt配置文件
└── package.json # 项目配置
4. Nuxt.js 核心功能
4.1 自动路由系统
Nuxt.js 会根据 pages
目录的文件结构自动生成 Vue Router 配置。
pages/
├── index.vue # 对应路由 /
├── about.vue # 对应路由 /about
├── users/
│ ├── index.vue # 对应路由 /users
│ └── [id].vue # 对应路由 /users/:id
└── posts/
├── [id].vue # 对应路由 /posts/:id
└── [id]/comments.vue # 对应路由 /posts/:id/comments
<!-- pages/users/[id].vue -->
<template>
<div>
<h1>用户详情</h1>
<p>用户ID: {{ $route.params.id }}</p>
<p>或者使用组合式API: {{ route.params.id }}</p>
</div>
</template>
<script setup>
const route = useRoute()
</script>
4.2 数据获取方法
Nuxt.js 提供多种方式获取数据:
<!-- 1. useAsyncData -->
<script setup>
const { data: posts } = await useAsyncData('posts', () => {
return $fetch('/api/posts')
})
</script>
<!-- 2. useFetch (更简洁的API) -->
<script setup>
const { data: users } = await useFetch('/api/users')
</script>
<!-- 3. 使用服务端API处理程序 -->
<script setup>
const { data } = await useFetch('/api/custom')
</script>
// server/api/custom.ts
export default defineEventHandler(async (event) => {
// 在服务器端执行
return {
message: 'This comes from the server'
}
})
4.3 组件自动导入
Nuxt.js 自动导入 components
目录中的组件,无需手动导入:
components/
├── AppHeader.vue
├── AppFooter.vue
└── UI/
├── Button.vue
└── Card.vue
<!-- 在任何页面或组件中直接使用 -->
<template>
<div>
<AppHeader />
<UI-Card>
<UI-Button>点击我</UI-Button>
</UI-Card>
<AppFooter />
</div>
</template>
<!-- 无需导入语句 -->
<script setup>
// 组件已自动导入
</script>
4.4 布局系统
<!-- layouts/default.vue -->
<template>
<div>
<header>导航栏</header>
<main>
<slot /> <!-- 页面内容插槽 -->
</main>
<footer>页脚</footer>
</div>
</template>
<!-- layouts/custom.vue -->
<template>
<div class="custom-layout">
<slot />
</div>
</template>
<!-- 在页面中使用布局 -->
<template>
<div>页面内容</div>
</template>
<script setup>
// 方法1:definePageMeta
definePageMeta({
layout: 'custom'
})
// 方法2:动态切换
const layout = ref('default')
function changeLayout() {
layout.value = 'custom'
}
</script>
5. Nuxt.js 高级特性
5.1 中间件
// middleware/auth.ts (路由中间件)
export default defineNuxtRouteMiddleware((to, from) => {
const user = useUser() // 假设存在的用户状态
if (!user.value && to.path !== '/login') {
return navigateTo('/login')
}
})
<!-- 在页面中使用中间件 -->
<script setup>
definePageMeta({
middleware: ['auth']
})
</script>
5.2 服务端API路由
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
// 处理GET请求
return [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]
})
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
// 读取请求体
const body = await readBody(event)
// 处理POST请求
return {
id: 3,
name: body.name,
created: true
}
})
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
// 获取路由参数
const id = getRouterParam(event, 'id')
// 获取查询参数
const query = getQuery(event)
return {
id,
name: `User ${id}`,
detail: query.detail === 'true'
}
})
5.3 状态管理
// composables/useCounter.ts
export const useCounter = () => {
// 创建全局状态
const count = useState('counter', () => 0)
// 方法
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
increment,
decrement
}
}
<!-- 使用共享状态 -->
<template>
<div>
<p>Count: {{ counter.count }}</p>
<button @click="counter.increment">+</button>
<button @click="counter.decrement">-</button>
</div>
</template>
<script setup>
const counter = useCounter()
</script>
5.4 环境变量与运行时配置
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 服务器端可用的私有密钥
apiSecret: process.env.API_SECRET,
// 客户端也可用的公共密钥
public: {
apiBase: process.env.API_BASE || 'https://api.example.com'
}
}
})
// 在组件中使用
const config = useRuntimeConfig()
console.log(config.public.apiBase) // 客户端和服务器都可用
console.log(config.apiSecret) // 仅在服务器端可用
6. 性能优化与部署
6.1 Nuxt.js 性能优化
// nuxt.config.ts
export default defineNuxtConfig({
// 启用/禁用渲染模式
ssr: true,
// 自定义渲染模式
routeRules: {
// 静态页面
'/': { static: true },
// 仅客户端渲染页面
'/admin/**': { ssr: false },
// ISR (增量静态再生)
'/blog/**': { swr: 60 } // 缓存60秒
},
// 启用Vue检查器
vite: {
vue: {
compilerOptions: {
isCustomElement: (tag) => tag.includes('custom-')
}
}
},
// 实验性能分析
experimental: {
payloadExtraction: true
}
})
6.2 图片优化
<template>
<div>
<!-- 自动优化图片 -->
<NuxtImg
src="/images/hero.jpg"
width="800"
height="400"
format="webp"
quality="80"
loading="lazy"
/>
<!-- 响应式图片 -->
<NuxtPicture
src="/images/hero.jpg"
width="800"
height="400"
alt="Hero image"
:sizes="[320, 640, 768, 1024]"
format="webp"
/>
</div>
</template>
6.3 部署选项
6.3.1 静态网站生成 (SSG)
# 生成静态网站
npx nuxi generate
# 生成的文件在 .output/public 目录中
# 可部署到任何静态托管服务如Netlify, Vercel, GitHub Pages等
6.3.2 服务器端部署
# 构建生产版本
npx nuxi build
# 启动生产服务器
node .output/server/index.mjs
6.3.3 Docker 部署
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
# 使用更轻量的node镜像
FROM node:18-alpine
WORKDIR /app
COPY --from=0 /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
7. 实际应用案例
7.1 全栈博客应用
<!-- pages/blog/[slug].vue -->
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<div v-html="post.content"></div>
<div class="comments">
<h3>评论 ({{ comments.length }})</h3>
<CommentForm @submit="addComment" />
<CommentList :comments="comments" />
</div>
</article>
</template>
<script setup>
// 页面参数
const route = useRoute()
const { slug } = route.params
// 获取文章数据
const { data: post } = await useFetch(`/api/posts/${slug}`)
// 获取评论
const { data: comments } = await useFetch(`/api/posts/${slug}/comments`)
// 添加评论
async function addComment(comment) {
await $fetch(`/api/posts/${slug}/comments`, {
method: 'POST',
body: comment
})
// 刷新评论列表
await refreshNuxtData('comments')
}
</script>
// server/api/posts/[slug]/index.get.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
const post = await prisma.post.findUnique({
where: { slug },
include: { author: true }
})
if (!post) {
throw createError({
statusCode: 404,
message: `Post with slug "${slug}" not found`
})
}
return post
})
7.2 电子商务产品展示
<!-- pages/products/[id].vue -->
<template>
<div v-if="product" class="product-page">
<div class="product-gallery">
<NuxtPicture
v-for="(image, index) in product.images"
:key="index"
:src="image.src"
:alt="image.alt"
width="600"
height="400"
format="webp"
/>
</div>
<div class="product-info">
<h1>{{ product.name }}</h1>
<p class="price">${{ product.price.toFixed(2) }}</p>
<div v-html="product.description"></div>
<div class="actions">
<button @click="addToCart" class="add-to-cart">
加入购物车
</button>
</div>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const toast = useToast() // 假设有一个通知组件
// 获取商品数据
const { data: product, error } = await useFetch(`/api/products/${route.params.id}`)
// 处理错误
if (error.value) {
throw createError({
statusCode: error.value.statusCode || 500,
message: error.value.message || '获取产品信息失败'
})
}
// 加入购物车
const { addItem } = useCart() // 假设的购物车组合式函数
function addToCart() {
addItem({
id: product.value.id,
name: product.value.name,
price: product.value.price,
quantity: 1
})
toast.success('已添加到购物车')
}
// SEO元信息
useHead({
title: product.value?.name,
meta: [
{ name: 'description', content: product.value?.summary || '' },
{ property: 'og:title', content: product.value?.name },
{ property: 'og:image', content: product.value?.images[0]?.src || '' }
]
})
</script>
结语
感谢您的阅读!期待您的一键三连!欢迎指正!