🔥 SSR服务端渲染实战技巧 - 从零到一构建高性能全栈应用

🎯 学习目标:掌握Vue3 SSR核心技术,学会使用Nuxt3构建高性能全栈应用,解决首屏渲染和SEO优化问题

📊 难度等级 :高级

🏷️ 技术标签#SSR #Vue3 #Nuxt3 #服务端渲染 #性能优化

⏱️ 阅读时间:约9分钟


🌟 引言

在现代前端开发中,你是否遇到过这样的困扰:

  • 首屏渲染慢:SPA应用首次加载白屏时间过长,用户体验差
  • SEO优化困难:搜索引擎无法抓取动态生成的内容,影响网站排名
  • 配置复杂:SSR环境搭建繁琐,开发和生产环境不一致
  • 数据同步问题:服务端和客户端数据状态难以保持一致

今天分享5个SSR服务端渲染的核心技巧,让你的全栈应用性能更强、SEO更友好!


💡 核心技巧详解

1. Vue3 SSR环境快速搭建:零配置启动

🔍 应用场景

需要快速搭建Vue3 SSR开发环境,避免复杂的webpack配置

❌ 常见问题

传统手动配置SSR环境复杂且容易出错

javascript 复制代码
// ❌ 传统复杂配置
const express = require('express');
const { createSSRApp } = require('vue');
const { renderToString } = require('@vue/server-renderer');
// 需要大量配置代码...

✅ 推荐方案

使用Nuxt3快速搭建SSR环境

bash 复制代码
# ✅ 使用Nuxt3快速创建项目
npx nuxi@latest init my-ssr-app
cd my-ssr-app
pnpm install
pnpm dev
javascript 复制代码
/**
 * Nuxt3配置文件
 * @description 简洁的SSR配置
 */
// nuxt.config.ts
export default defineNuxtConfig({
  // ✅ 开启SSR
  ssr: true,
  
  // 性能优化配置
  nitro: {
    compressPublicAssets: true,
  },
  
  // CSS框架配置
  css: ['~/assets/css/main.css'],
  
  // 模块配置
  modules: [
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss'
  ]
});

💡 核心要点

  • 零配置启动:Nuxt3提供开箱即用的SSR环境
  • 自动路由:基于文件系统的自动路由生成
  • 内置优化:自动代码分割和性能优化

🎯 实际应用

创建一个简单的SSR页面

vue 复制代码
<!-- pages/index.vue -->
<template>
  <div class="home-page">
    <h1>{{ title }}</h1>
    <p>当前时间:{{ serverTime }}</p>
    <UserList :users="users" />
  </div>
</template>

<script setup>
/**
 * 首页组件
 * @description 展示服务端渲染的数据
 */```javascript
// 服务端数据获取 - 使用useFetch避免重复请求
const { data: users } = await useFetch('/api/users');

// 响应式数据
const title = ref('SSR应用首页');
const serverTime = new Date().toLocaleString();

// SEO优化
useSeoMeta({
  title: 'SSR应用首页',
  description: '高性能服务端渲染应用'
});
</script>

2. 数据预取策略:解决服务端客户端数据同步

🔍 应用场景

需要在服务端预先获取数据,确保首屏渲染完整内容

❌ 常见问题

客户端激活时数据不一致,导致页面闪烁

javascript 复制代码
// ❌ 客户端才获取数据
export default {
  async mounted() {
    // 这会导致首屏空白
    this.users = await fetchUsers();
  }
}

✅ 推荐方案

使用Nuxt3的数据获取API

javascript 复制代码
/**
 * 服务端数据预取
 * @description 在服务端预先获取数据
 * @returns {Promise} 用户数据
 */
// composables/useUsers.js
export const useUsers = () => {
  return useFetch('/api/users', {
    // 缓存策略
    key: 'users',
    // 服务端和客户端都执行
    server: true,
    // 默认值
    default: () => [],
    // 数据转换
    transform: (data) => {
      return data.map(user => ({
        ...user,
        avatar: user.avatar || '/default-avatar.png'
      }));
    }
  });
};
vue 复制代码
<!-- pages/users.vue -->
<template>
  <div class="users-page">
    <div v-if="pending" class="loading">
      加载中...
    </div>
    <div v-else>
      <UserCard 
        v-for="user in users" 
        :key="user.id" 
        :user="user" 
      />
    </div>
  </div>
</template>

<script setup>
/**
 * 用户列表页面
 * @description 展示预取的用户数据
 */
// 使用数据预取
const { data: users, pending, error } = await useUsers();

// 错误处理
if (error.value) {
  throw createError({
    statusCode: 500,
    statusMessage: '用户数据获取失败'
  });
}
</script>

💡 核心要点

  • 数据一致性:服务端和客户端使用相同数据
  • 缓存优化:合理使用缓存减少重复请求
  • 错误处理:优雅处理数据获取失败

🎯 实际应用

实现带分页的数据预取

javascript 复制代码
/**
 * 分页数据预取
 * @description 支持分页的用户数据获取
 * @param {number} page - 页码
 * @param {number} limit - 每页数量
 * @returns {Promise} 分页数据
 */
export const useUsersPagination = (page = 1, limit = 10) => {
  const route = useRoute();
  
  return useLazyFetch('/api/users', {
    query: {
      page: computed(() => route.query.page || page),
      limit
    },
    key: 'users-pagination',
    server: true
  });
};

3. 客户端激活优化:避免水合不匹配

🔍 应用场景

确保服务端渲染的HTML与客户端激活时的状态完全一致

❌ 常见问题

水合不匹配导致控制台警告和页面重新渲染

vue 复制代码
<!-- ❌ 会导致水合不匹配 -->
<template>
  <div>
    <!-- 服务端和客户端时间不一致 -->
    <p>当前时间:{{ new Date().toLocaleString() }}</p>
    <!-- 随机数在服务端和客户端不同 -->
    <p>随机数:{{ Math.random() }}</p>
  </div>
</template>

✅ 推荐方案

使用客户端专用组件和状态管理

vue 复制代码
<!-- components/ClientOnly.vue -->
<template>
  <div class="user-info">
    <h2>用户信息</h2>
    <!-- 服务端安全的内容 -->
    <p>用户名:{{ user.name }}</p>
    
    <!-- 客户端专用内容 -->
    <ClientOnly>
      <div class="client-features">
        <p>本地时间:{{ localTime }}</p>
        <p>浏览器:{{ browserInfo }}</p>
        <button @click="handleClientAction">
          客户端功能
        </button>
      </div>
      <template #fallback>
        <div class="loading-placeholder">
          加载客户端功能...
        </div>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup>
/**
 * 用户信息组件
 * @description 展示用户信息,包含客户端专用功能
 */
// 服务端安全的数据 - 使用useFetch确保SSR兼容
const { data: user } = await useFetch('/api/user/profile');

// 客户端专用数据
const localTime = ref('');
const browserInfo = ref('');

/**
 * 客户端专用操作
 * @description 只在客户端执行的功能
 */
const handleClientAction = () => {
  // 客户端专用逻辑
  console.log('客户端功能执行');
};

// 客户端挂载后执行
onMounted(() => {
  localTime.value = new Date().toLocaleString();
  browserInfo.value = navigator.userAgent;
});
</script>
javascript 复制代码
/**
 * 状态管理优化
 * @description 避免服务端客户端状态不一致
 */
// stores/app.js
export const useAppStore = defineStore('app', () => {
  // 服务端安全的状态
  const user = ref(null);
  const theme = ref('light');
  
  // 客户端专用状态
  const isClient = process.client;
  const clientFeatures = ref({
    geolocation: false,
    notification: false
  });
  
  /**
   * 初始化客户端功能
   * @description 只在客户端执行的初始化
   */
  const initClientFeatures = () => {
    if (!isClient) return;
    
    clientFeatures.value = {
      geolocation: 'geolocation' in navigator,
      notification: 'Notification' in window
    };
  };
  
  return {
    user,
    theme,
    clientFeatures,
    initClientFeatures
  };
});

💡 核心要点

  • ClientOnly组件:将客户端专用内容包装
  • 状态分离:区分服务端和客户端状态
  • 渐进增强:先显示基础内容,再增强功能

🎯 实际应用

实现主题切换功能

vue 复制代码
<!-- components/ThemeToggle.vue -->
<template>
  <div class="theme-toggle">
    <ClientOnly>
      <button 
        @click="toggleTheme"
        :class="['theme-btn', currentTheme]"
      >
        {{ currentTheme === 'dark' ? '🌙' : '☀️' }}
        {{ currentTheme === 'dark' ? '深色' : '浅色' }}
      </button>
      <template #fallback>
        <div class="theme-placeholder">🎨</div>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup>
/**
 * 主题切换组件
 * @description 客户端专用的主题切换功能
 */
const colorMode = useColorMode();

const currentTheme = computed(() => colorMode.value);

/**
 * 切换主题
 * @description 在深色和浅色主题间切换
 */
const toggleTheme = () => {
  colorMode.preference = currentTheme.value === 'dark' ? 'light' : 'dark';
};
</script>

4. API路由设计:统一的全栈数据接口

🔍 应用场景

需要为SSR应用提供统一的API接口,支持服务端和客户端调用

❌ 常见问题

API接口设计不统一,服务端和客户端调用方式不一致

javascript 复制代码
// ❌ 分散的API处理
app.get('/users', (req, res) => {
  // 简单的路由处理
  res.json(users);
});

✅ 推荐方案

使用Nuxt3的server API

javascript 复制代码
/**
 * 用户API路由
 * @description 统一的用户数据接口
 * @param {Object} event - 请求事件对象
 * @returns {Promise} 用户数据
 */
// server/api/users/index.get.js
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const { page = 1, limit = 10, search = '' } = query;
  
  try {
    // 数据库查询逻辑
    const users = await getUsersFromDB({
      page: parseInt(page),
      limit: parseInt(limit),
      search
    });
    
    return {
      success: true,
      data: users,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total: users.length
      }
    };
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '获取用户数据失败'
    });
  }
});

/**
 * 创建用户API
 * @description 创建新用户
 * @param {Object} event - 请求事件对象
 * @returns {Promise} 创建结果
 */
// server/api/users/index.post.js
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  
  // 数据验证
  const validatedData = await validateUserData(body);
  
  try {
    const newUser = await createUser(validatedData);
    
    return {
      success: true,
      data: newUser,
      message: '用户创建成功'
    };
  } catch (error) {
    throw createError({
      statusCode: 400,
      statusMessage: error.message
    });
  }
});
javascript 复制代码
/**
 * 数据库操作封装
 * @description 统一的数据库操作方法
 */
// server/utils/database.js
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

/**
 * 获取用户列表
 * @param {Object} options - 查询选项
 * @returns {Promise} 用户列表
 */
export const getUsersFromDB = async ({ page, limit, search }) => {
  const skip = (page - 1) * limit;
  
  const where = search ? {
    OR: [
      { name: { contains: search } },
      { email: { contains: search } }
    ]
  } : {};
  
  return await prisma.user.findMany({
    where,
    skip,
    take: limit,
    orderBy: { createdAt: 'desc' }
  });
};

/**
 * 创建用户
 * @param {Object} userData - 用户数据
 * @returns {Promise} 创建的用户
 */
export const createUser = async (userData) => {
  return await prisma.user.create({
    data: userData
  });
};

💡 核心要点

  • RESTful设计:遵循REST API设计规范
  • 统一响应格式:标准化API响应结构
  • 错误处理:完善的错误处理机制

🎯 实际应用

实现用户管理的完整API

javascript 复制代码
/**
 * 用户详情API
 * @description 获取单个用户详情
 * @param {Object} event - 请求事件对象
 * @returns {Promise} 用户详情
 */
// server/api/users/[id].get.js
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  
  if (!id || isNaN(parseInt(id))) {
    throw createError({
      statusCode: 400,
      statusMessage: '无效的用户ID'
    });
  }
  
  try {
    const user = await prisma.user.findUnique({
      where: { id: parseInt(id) },
      include: {
        posts: true,
        profile: true
      }
    });
    
    if (!user) {
      throw createError({
        statusCode: 404,
        statusMessage: '用户不存在'
      });
    }
    
    return {
      success: true,
      data: user
    };
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '获取用户详情失败'
    });
  }
});

5. 性能优化策略:缓存与预加载

🔍 应用场景

优化SSR应用的性能,减少服务器负载和响应时间

❌ 常见问题

每次请求都重新渲染,服务器压力大,响应慢

javascript 复制代码
// ❌ 没有缓存的渲染
app.get('*', async (req, res) => {
  // 每次都重新渲染
  const html = await renderToString(app);
  res.send(html);
});

✅ 推荐方案

实现多层缓存策略

javascript 复制代码
/**
 * 页面缓存配置
 * @description 配置不同页面的缓存策略
 */
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    // 路由缓存规则
    routeRules: {
      // 首页预渲染
      '/': { prerender: true },
      
      // 产品页面ISR
      '/products/**': { 
        isr: true,
        headers: { 'cache-control': 's-maxage=60' }
      },
      
      // API缓存
      '/api/**': { 
        cors: true,
        headers: { 'cache-control': 'max-age=300' }
      },
      
      // 管理页面SPA模式
      '/admin/**': { ssr: false }
    },
    
    // 静态资源缓存
    compressPublicAssets: true
  }
});
javascript 复制代码
/**
 * Redis缓存实现
 * @description 使用Redis缓存页面和数据
 */
// server/utils/cache.js
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

/**
 * 缓存页面内容
 * @param {string} key - 缓存键
 * @param {string} content - 页面内容
 * @param {number} ttl - 过期时间(秒)
 */
export const cachePageContent = async (key, content, ttl = 300) => {
  await redis.setex(`page:${key}`, ttl, content);
};

/**
 * 获取缓存的页面内容
 * @param {string} key - 缓存键
 * @returns {Promise<string|null>} 缓存内容
 */
export const getCachedPageContent = async (key) => {
  return await redis.get(`page:${key}`);
};

/**
 * 缓存API数据
 * @param {string} key - 缓存键
 * @param {Object} data - 数据对象
 * @param {number} ttl - 过期时间(秒)
 */
export const cacheApiData = async (key, data, ttl = 600) => {
  await redis.setex(`api:${key}`, ttl, JSON.stringify(data));
};
javascript 复制代码
/**
 * 智能预加载实现
 * @description 基于用户行为的智能预加载
 */
// composables/usePreload.js
export const usePreload = () => {
  const router = useRouter();
  
  /**
   * 预加载页面
   * @param {string} path - 页面路径
   */
  const preloadPage = async (path) => {
    try {
      await navigateTo(path, { 
        replace: false,
        external: false
      });
    } catch (error) {
      console.warn('预加载失败:', error);
    }
  };
  
  /**
   * 智能预加载策略
   * @description 根据用户行为预加载可能访问的页面
   */
  const setupIntelligentPreload = () => {
    // 鼠标悬停预加载
    const handleMouseEnter = (event) => {
      const link = event.target.closest('a[href]');
      if (link && link.href.startsWith(window.location.origin)) {
        const path = new URL(link.href).pathname;
        preloadPage(path);
      }
    };
    
    // 视口内链接预加载
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const link = entry.target;
          const path = new URL(link.href).pathname;
          preloadPage(path);
        }
      });
    });
    
    // 监听所有链接
    document.querySelectorAll('a[href]').forEach(link => {
      link.addEventListener('mouseenter', handleMouseEnter);
      observer.observe(link);
    });
  };
  
  return {
    preloadPage,
    setupIntelligentPreload
  };
};

💡 核心要点

  • 分层缓存:页面缓存、数据缓存、CDN缓存
  • 智能预加载:基于用户行为预测和预加载
  • 缓存策略:不同内容采用不同缓存时间

🎯 实际应用

实现完整的缓存中间件

javascript 复制代码
/**
 * 缓存中间件
 * @description 自动缓存页面和API响应
 */
// server/middleware/cache.js
export default defineEventHandler(async (event) => {
  const url = getRequestURL(event);
  const method = getMethod(event);
  
  // 只缓存GET请求
  if (method !== 'GET') return;
  
  const cacheKey = `${url.pathname}${url.search}`;
  
  // 尝试获取缓存
  const cached = await getCachedPageContent(cacheKey);
  if (cached) {
    setHeader(event, 'X-Cache', 'HIT');
    return cached;
  }
  
  // 继续处理请求
  const response = await $fetch(url.pathname + url.search);
  
  // 缓存响应
  if (response) {
    await cachePageContent(cacheKey, response, 300);
    setHeader(event, 'X-Cache', 'MISS');
  }
  
  return response;
});

📊 技巧对比总结

技巧 使用场景 优势 注意事项
Nuxt3快速搭建 新项目启动 零配置、开箱即用 学习Nuxt3约定
数据预取策略 首屏数据展示 避免白屏、SEO友好 合理使用缓存
客户端激活优化 交互功能实现 避免水合错误 区分服务端客户端
API路由设计 全栈数据接口 统一规范、易维护 完善错误处理
性能优化策略 高并发场景 减少服务器压力 缓存失效策略

🚨 常见问题及解决方案

在SSR开发过程中,开发者经常遇到一些棘手的问题。以下是最常见的问题及其解决方案:

问题1:水合不匹配 (Hydration Mismatch)

🔍 问题描述

控制台出现警告:Hydration node mismatch,页面出现闪烁或重新渲染

❌ 常见原因

vue 复制代码
<!-- ❌ 导致水合不匹配的代码 -->
<template>
  <div>
    <!-- 服务端和客户端时间不一致 -->
    <p>当前时间:{{ new Date().toLocaleString() }}</p>
    
    <!-- 随机ID在两端不同 -->
    <div :id="`item-${Math.random()}`">内容</div>
    
    <!-- 条件渲染基于客户端API -->
    <div v-if="window.innerWidth > 768">
      桌面版内容
    </div>
  </div>
</template>

✅ 解决方案

vue 复制代码
<!-- ✅ 正确的处理方式 -->
<template>
  <div>
    <!-- 使用服务端安全的数据 -->
    <p>服务器时间:{{ serverTime }}</p>
    
    <!-- 使用稳定的ID生成策略 -->
    <div :id="`item-${stableId}`">内容</div>
    
    <!-- 客户端专用内容使用ClientOnly -->
    <ClientOnly>
      <div v-if="isDesktop">桌面版内容</div>
      <template #fallback>
        <div>加载中...</div>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup>
/**
 * 避免水合不匹配的组件
 * @description 确保服务端和客户端渲染一致
 */
// 服务端安全的时间
const serverTime = new Date().toISOString();

// 稳定的ID生成
const stableId = computed(() => `item-${route.path.replace(/\//g, '-')}`);

// 客户端检测
const isDesktop = ref(false);

onMounted(() => {
  isDesktop.value = window.innerWidth > 768;
});
</script>

问题2:数据获取时机错误

🔍 问题描述

数据在客户端才获取,导致首屏空白或SEO问题

❌ 常见错误

vue 复制代码
<!-- ❌ 错误的数据获取时机 -->
<script setup>
const users = ref([]);

// 只在客户端执行,SSR时没有数据
onMounted(async () => {
  const response = await fetch('/api/users');
  users.value = await response.json();
});
</script>

✅ 解决方案

vue 复制代码
<!-- ✅ 正确的数据获取方式 -->
<script setup>
/**
 * 正确的SSR数据获取
 * @description 确保服务端和客户端都能获取数据
 */
// 使用useFetch进行SSR兼容的数据获取
const { data: users, pending, error, refresh } = await useFetch('/api/users', {
  // 服务端和客户端都执行
  server: true,
  // 缓存键,避免重复请求
  key: 'users-list',
  // 默认值
  default: () => [],
  // 错误处理
  onResponseError({ response }) {
    console.error('获取用户数据失败:', response.status);
  }
});

// 错误处理
if (error.value) {
  throw createError({
    statusCode: 500,
    statusMessage: '用户数据加载失败'
  });
}
</script>

问题3:环境变量在客户端泄露

🔍 问题描述

敏感的环境变量在客户端代码中暴露

❌ 安全风险

javascript 复制代码
// ❌ 危险:敏感信息暴露到客户端
const config = {
  apiKey: process.env.SECRET_API_KEY, // 会暴露到客户端
  dbUrl: process.env.DATABASE_URL     // 会暴露到客户端
};

✅ 解决方案

javascript 复制代码
/**
 * 安全的环境变量处理
 * @description 区分服务端和客户端环境变量
 */
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // 服务端专用(私有)
    secretApiKey: process.env.SECRET_API_KEY,
    databaseUrl: process.env.DATABASE_URL,
    
    // 客户端可访问(公开)
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
      appName: process.env.NUXT_PUBLIC_APP_NAME || 'My App'
    }
  }
});

// 在服务端API中使用
// server/api/users.get.js
export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();
  
  // 只在服务端可用
  const apiKey = config.secretApiKey;
  
  // 安全的API调用
  const response = await fetch('https://api.example.com/users', {
    headers: {
      'Authorization': `Bearer ${apiKey}`
    }
  });
  
  return await response.json();
});

// 在客户端组件中使用
// pages/index.vue
const config = useRuntimeConfig();
// 只能访问public配置
const apiBase = config.public.apiBase;

问题4:内存泄漏和性能问题

🔍 问题描述

SSR应用在高并发下出现内存泄漏或性能下降

❌ 常见问题

javascript 复制代码
// ❌ 可能导致内存泄漏的代码
let globalCache = new Map(); // 全局缓存无限增长

export default defineEventHandler(async (event) => {
  // 没有清理机制的缓存
  const key = getQuery(event).id;
  if (!globalCache.has(key)) {
    const data = await fetchData(key);
    globalCache.set(key, data); // 永不清理
  }
  return globalCache.get(key);
});

✅ 解决方案

javascript 复制代码
/**
 * 内存安全的缓存实现
 * @description 带有TTL和大小限制的缓存
 */
// server/utils/safeCache.js
class SafeCache {
  constructor(maxSize = 1000, ttl = 300000) { // 5分钟TTL
    this.cache = new Map();
    this.timers = new Map();
    this.maxSize = maxSize;
    this.ttl = ttl;
  }
  
  /**
   * 设置缓存项
   * @param {string} key - 缓存键
   * @param {any} value - 缓存值
   */
  set(key, value) {
    // 检查缓存大小限制
    if (this.cache.size >= this.maxSize) {
      this.evictOldest();
    }
    
    // 清除旧的定时器
    if (this.timers.has(key)) {
      clearTimeout(this.timers.get(key));
    }
    
    // 设置新值和定时器
    this.cache.set(key, value);
    const timer = setTimeout(() => {
      this.delete(key);
    }, this.ttl);
    
    this.timers.set(key, timer);
  }
  
  /**
   * 获取缓存项
   * @param {string} key - 缓存键
   * @returns {any} 缓存值
   */
  get(key) {
    return this.cache.get(key);
  }
  
  /**
   * 删除缓存项
   * @param {string} key - 缓存键
   */
  delete(key) {
    this.cache.delete(key);
    if (this.timers.has(key)) {
      clearTimeout(this.timers.get(key));
      this.timers.delete(key);
    }
  }
  
  /**
   * 清除最旧的缓存项
   */
  evictOldest() {
    const firstKey = this.cache.keys().next().value;
    if (firstKey) {
      this.delete(firstKey);
    }
  }
}

// 创建全局安全缓存实例
const safeCache = new SafeCache();

export { safeCache };

// 使用安全缓存
// server/api/users/[id].get.js
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  const cacheKey = `user-${id}`;
  
  // 尝试从缓存获取
  let user = safeCache.get(cacheKey);
  
  if (!user) {
    // 从数据库获取
    user = await getUserById(id);
    // 安全地缓存结果
    safeCache.set(cacheKey, user);
  }
  
  return user;
});

问题5:SEO优化不完整

🔍 问题描述

虽然使用了SSR,但SEO效果仍然不理想

❌ 常见遗漏

vue 复制代码
<!-- ❌ SEO信息不完整 -->
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
  </div>
</template>

<script setup>
// 只设置了基本标题
useHead({
  title: '我的页面'
});
</script>

✅ 完整的SEO优化

vue 复制代码
<!-- ✅ 完整的SEO优化 -->
<template>
  <div>
    <h1>{{ article.title }}</h1>
    <p>{{ article.excerpt }}</p>
    <img :src="article.image" :alt="article.title" />
  </div>
</template>

<script setup>
/**
 * 完整的SEO优化示例
 * @description 包含所有必要的SEO元素
 */
const { data: article } = await useFetch('/api/articles/1');

// 完整的SEO配置
useSeoMeta({
  // 基本信息
  title: article.value.title,
  description: article.value.excerpt,
  keywords: article.value.tags.join(', '),
  
  // Open Graph
  ogTitle: article.value.title,
  ogDescription: article.value.excerpt,
  ogImage: article.value.image,
  ogUrl: `https://mysite.com/articles/${article.value.id}`,
  ogType: 'article',
  
  // Twitter Card
  twitterCard: 'summary_large_image',
  twitterTitle: article.value.title,
  twitterDescription: article.value.excerpt,
  twitterImage: article.value.image,
  
  // 文章特定
  articleAuthor: article.value.author,
  articlePublishedTime: article.value.publishedAt,
  articleModifiedTime: article.value.updatedAt,
  articleTag: article.value.tags
});

// 结构化数据
useJsonld({
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: article.value.title,
  description: article.value.excerpt,
  image: article.value.image,
  author: {
    '@type': 'Person',
    name: article.value.author
  },
  publisher: {
    '@type': 'Organization',
    name: 'My Site',
    logo: {
      '@type': 'ImageObject',
      url: 'https://mysite.com/logo.png'
    }
  },
  datePublished: article.value.publishedAt,
  dateModified: article.value.updatedAt
});
</script>

问题6:开发和生产环境不一致

🔍 问题描述

开发环境正常,生产环境出现问题

✅ 解决方案

javascript 复制代码
/**
 * 环境一致性配置
 * @description 确保开发和生产环境行为一致
 */
// nuxt.config.ts
export default defineNuxtConfig({
  // 开发配置
  devtools: { enabled: process.env.NODE_ENV === 'development' },
  
  // 构建配置
  nitro: {
    // 预渲染配置
    prerender: {
      routes: ['/sitemap.xml', '/robots.txt']
    },
    
    // 压缩配置
    compressPublicAssets: true,
    
    // 环境变量验证
    experimental: {
      wasm: true
    }
  },
  
  // 运行时配置验证
  hooks: {
    'ready': () => {
      // 验证必要的环境变量
      const requiredEnvVars = ['DATABASE_URL', 'SECRET_KEY'];
      const missing = requiredEnvVars.filter(key => !process.env[key]);
      
      if (missing.length > 0) {
        throw new Error(`缺少必要的环境变量: ${missing.join(', ')}`);
      }
    }
  }
});

// 环境检测工具
// utils/environment.js
export const isDevelopment = process.env.NODE_ENV === 'development';
export const isProduction = process.env.NODE_ENV === 'production';
export const isServer = process.server;
export const isClient = process.client;

/**
 * 环境安全的日志记录
 * @param {string} message - 日志消息
 * @param {any} data - 日志数据
 */
export const safeLog = (message, data = null) => {
  if (isDevelopment) {
    console.log(message, data);
  } else if (isProduction && isServer) {
    // 生产环境只在服务端记录
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
};

🎯 实战应用建议

最佳实践

  1. 项目初始化:优先选择Nuxt3,享受零配置的开发体验
  2. 数据获取:合理使用useFetch和useLazyFetch,避免重复请求
  3. 客户端功能:使用ClientOnly包装客户端专用功能
  4. API设计:遵循RESTful规范,统一响应格式
  5. 性能优化:实施多层缓存策略,智能预加载关键页面

性能考虑

  • 首屏时间:通过SSR和数据预取优化首屏加载速度
  • SEO优化:确保关键内容在服务端渲染,提升搜索引擎友好度
  • 缓存策略:根据内容更新频率设置合理的缓存时间
  • 错误处理:完善的错误边界和降级方案

💡 总结

这5个SSR服务端渲染技巧在现代全栈开发中至关重要,掌握它们能让你的应用:

  1. 快速启动:使用Nuxt3零配置搭建SSR环境
  2. 数据一致:通过预取策略确保服务端客户端数据同步
  3. 稳定激活:避免水合不匹配,提供流畅的用户体验
  4. 统一接口:设计规范的API路由,支持全栈开发
  5. 高性能:实施缓存和预加载策略,优化应用性能

希望这些技巧能帮助你在SSR开发中构建更高性能、更SEO友好的全栈应用!


🚀 完整项目示例:SSR博客系统

为了帮助你更好地理解SSR的实际应用,这里提供一个完整的博客系统示例,包含文章列表、详情页、用户认证等功能。

项目结构

bash 复制代码
my-ssr-blog/
├── nuxt.config.ts              # Nuxt配置
├── package.json                # 依赖管理
├── server/                     # 服务端代码
│   ├── api/                    # API路由
│   │   ├── articles/           # 文章相关API
│   │   │   ├── index.get.js    # 获取文章列表
│   │   │   ├── index.post.js   # 创建文章
│   │   │   └── [id].get.js     # 获取文章详情
│   │   ├── auth/               # 认证相关API
│   │   │   ├── login.post.js   # 用户登录
│   │   │   └── register.post.js # 用户注册
│   │   └── users/              # 用户相关API
│   │       └── profile.get.js  # 用户资料
│   └── utils/                  # 服务端工具
│       ├── database.js         # 数据库操作
│       ├── auth.js             # 认证工具
│       └── validation.js       # 数据验证
├── pages/                      # 页面组件
│   ├── index.vue               # 首页
│   ├── articles/               # 文章页面
│   │   ├── index.vue           # 文章列表
│   │   └── [id].vue            # 文章详情
│   ├── auth/                   # 认证页面
│   │   ├── login.vue           # 登录页
│   │   └── register.vue        # 注册页
│   └── profile.vue             # 用户资料
├── components/                 # 组件
│   ├── ArticleCard.vue         # 文章卡片
│   ├── Navigation.vue          # 导航组件
│   └── UserProfile.vue         # 用户资料组件
├── composables/                # 组合式函数
│   ├── useAuth.js              # 认证逻辑
│   ├── useArticles.js          # 文章逻辑
│   └── useApi.js               # API调用
├── stores/                     # 状态管理
│   ├── auth.js                 # 认证状态
│   └── articles.js             # 文章状态
└── assets/                     # 静态资源
    └── css/
        └── main.css            # 全局样式

核心配置文件

typescript 复制代码
/**
 * Nuxt3配置文件
 * @description 完整的SSR博客系统配置
 */
// nuxt.config.ts
export default defineNuxtConfig({
  // 开启SSR
  ssr: true,
  
  // 模块配置
  modules: [
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss',
    '@vueuse/nuxt'
  ],
  
  // 运行时配置
  runtimeConfig: {
    // 服务端私有配置
    jwtSecret: process.env.JWT_SECRET,
    databaseUrl: process.env.DATABASE_URL,
    
    // 客户端公开配置
    public: {
      apiBase: '/api',
      appName: 'SSR博客系统'
    }
  },
  
  // 性能优化
  nitro: {
    // 路由缓存规则
    routeRules: {
      '/': { prerender: true },
      '/articles': { isr: 60 }, // 60秒ISR
      '/articles/**': { isr: 300 }, // 5分钟ISR
      '/api/**': { 
        cors: true,
        headers: { 'cache-control': 'max-age=60' }
      }
    },
    
    // 压缩静态资源
    compressPublicAssets: true
  },
  
  // CSS配置
  css: ['~/assets/css/main.css'],
  
  // 开发工具
  devtools: { enabled: true }
});
json 复制代码
{
  "name": "my-ssr-blog",
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "devDependencies": {
    "@nuxt/devtools": "latest",
    "nuxt": "^3.8.0"
  },
  "dependencies": {
    "@nuxtjs/tailwindcss": "^6.8.4",
    "@pinia/nuxt": "^0.5.1",
    "@vueuse/nuxt": "^10.5.0",
    "bcryptjs": "^2.4.3",
    "jsonwebtoken": "^9.0.2",
    "prisma": "^5.6.0",
    "@prisma/client": "^5.6.0"
  }
}

服务端API实现

javascript 复制代码
/**
 * 文章列表API
 * @description 获取分页的文章列表
 */
// server/api/articles/index.get.js
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const { page = 1, limit = 10, category = '', search = '' } = query;
  
  try {
    const articles = await getArticles({
      page: parseInt(page),
      limit: parseInt(limit),
      category,
      search
    });
    
    return {
      success: true,
      data: articles,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total: articles.length,
        hasMore: articles.length === parseInt(limit)
      }
    };
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '获取文章列表失败'
    });
  }
});

/**
 * 文章详情API
 * @description 获取单篇文章详情
 */
// server/api/articles/[id].get.js
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  
  if (!id || isNaN(parseInt(id))) {
    throw createError({
      statusCode: 400,
      statusMessage: '无效的文章ID'
    });
  }
  
  try {
    const article = await getArticleById(parseInt(id));
    
    if (!article) {
      throw createError({
        statusCode: 404,
        statusMessage: '文章不存在'
      });
    }
    
    // 增加阅读量
    await incrementViewCount(parseInt(id));
    
    return {
      success: true,
      data: article
    };
  } catch (error) {
    if (error.statusCode) throw error;
    
    throw createError({
      statusCode: 500,
      statusMessage: '获取文章详情失败'
    });
  }
});

/**
 * 用户登录API
 * @description 用户认证登录
 */
// server/api/auth/login.post.js
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const { email, password } = body;
  
  // 数据验证
  if (!email || !password) {
    throw createError({
      statusCode: 400,
      statusMessage: '邮箱和密码不能为空'
    });
  }
  
  try {
    // 查找用户
    const user = await getUserByEmail(email);
    
    if (!user) {
      throw createError({
        statusCode: 401,
        statusMessage: '用户不存在'
      });
    }
    
    // 验证密码
    const isValidPassword = await bcrypt.compare(password, user.password);
    
    if (!isValidPassword) {
      throw createError({
        statusCode: 401,
        statusMessage: '密码错误'
      });
    }
    
    // 生成JWT令牌
    const config = useRuntimeConfig();
    const token = jwt.sign(
      { userId: user.id, email: user.email },
      config.jwtSecret,
      { expiresIn: '7d' }
    );
    
    // 设置Cookie
    setCookie(event, 'auth-token', token, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 60 * 60 * 24 * 7 // 7天
    });
    
    return {
      success: true,
      data: {
        user: {
          id: user.id,
          name: user.name,
          email: user.email,
          avatar: user.avatar
        },
        token
      },
      message: '登录成功'
    };
  } catch (error) {
    if (error.statusCode) throw error;
    
    throw createError({
      statusCode: 500,
      statusMessage: '登录失败'
    });
  }
});

数据库操作工具

javascript 复制代码
/**
 * 数据库操作工具
 * @description 封装常用的数据库操作
 */
// server/utils/database.js
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

/**
 * 获取文章列表
 * @param {Object} options - 查询选项
 * @returns {Promise<Array>} 文章列表
 */
export const getArticles = async ({ page, limit, category, search }) => {
  const skip = (page - 1) * limit;
  
  const where = {
    published: true,
    ...(category && { category }),
    ...(search && {
      OR: [
        { title: { contains: search, mode: 'insensitive' } },
        { content: { contains: search, mode: 'insensitive' } },
        { tags: { has: search } }
      ]
    })
  };
  
  return await prisma.article.findMany({
    where,
    skip,
    take: limit,
    orderBy: { createdAt: 'desc' },
    include: {
      author: {
        select: { id: true, name: true, avatar: true }
      },
      _count: {
        select: { comments: true, likes: true }
      }
    }
  });
};

/**
 * 获取文章详情
 * @param {number} id - 文章ID
 * @returns {Promise<Object>} 文章详情
 */
export const getArticleById = async (id) => {
  return await prisma.article.findUnique({
    where: { id },
    include: {
      author: {
        select: { id: true, name: true, avatar: true, bio: true }
      },
      comments: {
        include: {
          author: {
            select: { id: true, name: true, avatar: true }
          }
        },
        orderBy: { createdAt: 'desc' }
      },
      _count: {
        select: { likes: true, views: true }
      }
    }
  });
};

/**
 * 增加文章阅读量
 * @param {number} id - 文章ID
 */
export const incrementViewCount = async (id) => {
  await prisma.article.update({
    where: { id },
    data: {
      views: {
        increment: 1
      }
    }
  });
};

/**
 * 根据邮箱获取用户
 * @param {string} email - 用户邮箱
 * @returns {Promise<Object>} 用户信息
 */
export const getUserByEmail = async (email) => {
  return await prisma.user.findUnique({
    where: { email }
  });
};

前端页面组件

vue 复制代码
<!-- 首页组件 -->
<!-- pages/index.vue -->
<template>
  <div class="home-page">
    <!-- 头部横幅 -->
    <section class="hero-section bg-gradient-to-r from-blue-600 to-purple-600 text-white py-20">
      <div class="container mx-auto px-4 text-center">
        <h1 class="text-5xl font-bold mb-6">{{ config.public.appName }}</h1>
        <p class="text-xl mb-8">分享技术,记录成长,构建更好的开发者社区</p>
        <NuxtLink 
          to="/articles" 
          class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors"
        >
          开始阅读
        </NuxtLink>
      </div>
    </section>
    
    <!-- 最新文章 -->
    <section class="latest-articles py-16">
      <div class="container mx-auto px-4">
        <h2 class="text-3xl font-bold text-center mb-12">最新文章</h2>
        
        <div v-if="pending" class="text-center">
          <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
          <p class="mt-4 text-gray-600">加载中...</p>
        </div>
        
        <div v-else-if="error" class="text-center text-red-600">
          <p>加载失败,请稍后重试</p>
        </div>
        
        <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
          <ArticleCard 
            v-for="article in latestArticles" 
            :key="article.id" 
            :article="article" 
          />
        </div>
        
        <div class="text-center mt-12">
          <NuxtLink 
            to="/articles" 
            class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
          >
            查看更多文章
          </NuxtLink>
        </div>
      </div>
    </section>
  </div>
</template>

<script setup>
/**
 * 首页组件
 * @description SSR博客系统首页
 */
const config = useRuntimeConfig();

// 获取最新文章
const { data: latestArticles, pending, error } = await useFetch('/api/articles', {
  query: { limit: 6 },
  key: 'latest-articles',
  transform: (data) => data.data || []
});

// SEO优化
useSeoMeta({
  title: `${config.public.appName} - 技术博客`,
  description: '分享前端开发技术,记录编程学习心得,构建开发者社区',
  keywords: 'Vue3, Nuxt3, SSR, 前端开发, 技术博客',
  ogTitle: `${config.public.appName} - 技术博客`,
  ogDescription: '分享前端开发技术,记录编程学习心得',
  ogType: 'website'
});
</script>
vue 复制代码
<!-- 文章详情页 -->
<!-- pages/articles/[id].vue -->
<template>
  <div class="article-detail">
    <div v-if="pending" class="loading-container">
      <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
      <p class="mt-4 text-center text-gray-600">加载中...</p>
    </div>
    
    <div v-else-if="error" class="error-container text-center py-20">
      <h1 class="text-2xl font-bold text-red-600 mb-4">文章加载失败</h1>
      <p class="text-gray-600 mb-8">{{ error.message }}</p>
      <NuxtLink to="/articles" class="bg-blue-600 text-white px-6 py-3 rounded-lg">
        返回文章列表
      </NuxtLink>
    </div>
    
    <article v-else class="max-w-4xl mx-auto px-4 py-8">
      <!-- 文章头部 -->
      <header class="mb-8">
        <h1 class="text-4xl font-bold mb-4">{{ article.title }}</h1>
        
        <div class="flex items-center justify-between text-gray-600 mb-6">
          <div class="flex items-center space-x-4">
            <img 
              :src="article.author.avatar || '/default-avatar.png'" 
              :alt="article.author.name"
              class="w-10 h-10 rounded-full"
            >
            <div>
              <p class="font-semibold">{{ article.author.name }}</p>
              <p class="text-sm">{{ formatDate(article.createdAt) }}</p>
            </div>
          </div>
          
          <div class="flex items-center space-x-4 text-sm">
            <span>👀 {{ article._count.views }}</span>
            <span>❤️ {{ article._count.likes }}</span>
            <span>💬 {{ article.comments.length }}</span>
          </div>
        </div>
        
        <!-- 标签 -->
        <div class="flex flex-wrap gap-2 mb-6">
          <span 
            v-for="tag in article.tags" 
            :key="tag"
            class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
          >
            #{{ tag }}
          </span>
        </div>
      </header>
      
      <!-- 文章内容 -->
      <div class="prose prose-lg max-w-none mb-12" v-html="article.content"></div>
      
      <!-- 作者信息 -->
      <div class="author-info bg-gray-50 p-6 rounded-lg mb-8">
        <div class="flex items-start space-x-4">
          <img 
            :src="article.author.avatar || '/default-avatar.png'" 
            :alt="article.author.name"
            class="w-16 h-16 rounded-full"
          >
          <div>
            <h3 class="text-xl font-semibold mb-2">{{ article.author.name }}</h3>
            <p class="text-gray-600">{{ article.author.bio || '这个作者很懒,什么都没有留下...' }}</p>
          </div>
        </div>
      </div>
      
      <!-- 评论区 -->
      <section class="comments-section">
        <h3 class="text-2xl font-bold mb-6">评论 ({{ article.comments.length }})</h3>
        
        <!-- 评论列表 -->
        <div class="space-y-6">
          <div 
            v-for="comment in article.comments" 
            :key="comment.id"
            class="comment bg-white p-4 rounded-lg border"
          >
            <div class="flex items-start space-x-3">
              <img 
                :src="comment.author.avatar || '/default-avatar.png'" 
                :alt="comment.author.name"
                class="w-8 h-8 rounded-full"
              >
              <div class="flex-1">
                <div class="flex items-center space-x-2 mb-2">
                  <span class="font-semibold">{{ comment.author.name }}</span>
                  <span class="text-gray-500 text-sm">{{ formatDate(comment.createdAt) }}</span>
                </div>
                <p class="text-gray-700">{{ comment.content }}</p>
              </div>
            </div>
          </div>
        </div>
      </section>
    </article>
  </div>
</template>

<script setup>
/**
 * 文章详情页组件
 * @description 展示单篇文章的详细内容
 */
const route = useRoute();
const articleId = route.params.id;

// 获取文章详情
const { data: article, pending, error } = await useFetch(`/api/articles/${articleId}`, {
  key: `article-${articleId}`,
  transform: (data) => data.data
});

// 格式化日期
const formatDate = (dateString) => {
  return new Date(dateString).toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};

// 动态SEO优化
if (article.value) {
  useSeoMeta({
    title: article.value.title,
    description: article.value.excerpt || article.value.title,
    keywords: article.value.tags.join(', '),
    ogTitle: article.value.title,
    ogDescription: article.value.excerpt || article.value.title,
    ogType: 'article',
    articleAuthor: article.value.author.name,
    articlePublishedTime: article.value.createdAt,
    articleTag: article.value.tags
  });
}

// 错误处理
if (error.value) {
  throw createError({
    statusCode: error.value.statusCode || 500,
    statusMessage: error.value.message || '文章加载失败'
  });
}
</script>

组合式函数

javascript 复制代码
/**
 * 文章相关的组合式函数
 * @description 封装文章相关的业务逻辑
 */
// composables/useArticles.js
export const useArticles = () => {
  /**
   * 获取文章列表
   * @param {Object} params - 查询参数
   * @returns {Promise} 文章列表数据
   */
  const getArticles = async (params = {}) => {
    const { page = 1, limit = 10, category = '', search = '' } = params;
    
    return await useFetch('/api/articles', {
      query: { page, limit, category, search },
      key: `articles-${page}-${limit}-${category}-${search}`,
      transform: (data) => data.data || []
    });
  };
  
  /**
   * 获取文章详情
   * @param {number} id - 文章ID
   * @returns {Promise} 文章详情数据
   */
  const getArticleById = async (id) => {
    return await useFetch(`/api/articles/${id}`, {
      key: `article-${id}`,
      transform: (data) => data.data
    });
  };
  
  /**
   * 搜索文章
   * @param {string} keyword - 搜索关键词
   * @returns {Promise} 搜索结果
   */
  const searchArticles = async (keyword) => {
    if (!keyword.trim()) return { data: [] };
    
    return await useFetch('/api/articles', {
      query: { search: keyword, limit: 20 },
      key: `search-${keyword}`,
      transform: (data) => data.data || []
    });
  };
  
  return {
    getArticles,
    getArticleById,
    searchArticles
  };
};

/**
 * 认证相关的组合式函数
 * @description 封装用户认证相关的业务逻辑
 */
// composables/useAuth.js
export const useAuth = () => {
  const user = ref(null);
  const isLoggedIn = computed(() => !!user.value);
  
  /**
   * 用户登录
   * @param {Object} credentials - 登录凭据
   * @returns {Promise} 登录结果
   */
  const login = async (credentials) => {
    try {
      const { data } = await $fetch('/api/auth/login', {
        method: 'POST',
        body: credentials
      });
      
      user.value = data.user;
      
      // 跳转到首页
      await navigateTo('/');
      
      return { success: true, data };
    } catch (error) {
      throw error;
    }
  };
  
  /**
   * 用户登出
   */
  const logout = async () => {
    try {
      await $fetch('/api/auth/logout', { method: 'POST' });
      user.value = null;
      await navigateTo('/');
    } catch (error) {
      console.error('登出失败:', error);
    }
  };
  
  /**
   * 获取当前用户信息
   */
  const getCurrentUser = async () => {
    try {
      const { data } = await $fetch('/api/users/profile');
      user.value = data;
      return data;
    } catch (error) {
      user.value = null;
      return null;
    }
  };
  
  return {
    user: readonly(user),
    isLoggedIn,
    login,
    logout,
    getCurrentUser
  };
};

启动和部署

bash 复制代码
# 安装依赖
pnpm install

# 开发环境启动
pnpm dev

# 构建生产版本
pnpm build

# 预览生产版本
pnpm preview

# 生成静态站点
pnpm generate

这个完整的SSR博客系统示例展示了:

  1. 完整的项目结构:从配置到组件的完整架构
  2. 服务端API设计:RESTful API的最佳实践
  3. 数据库操作:使用Prisma进行类型安全的数据库操作
  4. 用户认证:JWT令牌和Cookie的安全处理
  5. SEO优化:动态元数据和结构化数据
  6. 性能优化:缓存策略和预渲染配置
  7. 错误处理:完善的错误边界和用户反馈
  8. 组合式函数:业务逻辑的模块化封装

你可以基于这个示例快速搭建自己的SSR应用,并根据具体需求进行扩展和定制。


🔗 相关资源


💡 今日收获:掌握了5个SSR服务端渲染核心技巧,这些知识点在构建高性能全栈应用中非常实用。

如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀

相关推荐
Komorebi_99997 小时前
Vue3 provide/inject 详细组件关系说明
前端·javascript·vue.js
用户1412501665277 小时前
一文彻底掌握 ECharts:从配置解读到实战应用
前端
LRH7 小时前
React 架构设计:从 stack reconciler 到 fiber reconciler 的演进
前端
VIjolie7 小时前
文档/会议类应用的协同同步机制(OT/CRDT简要理解)
前端
不一样的少年_7 小时前
【前端效率工具】:告别右键另存,不到 50 行代码一键批量下载网页图片
前端·javascript·浏览器
golang学习记7 小时前
从0死磕全栈之Next.js 企业级 next.config.js 配置详解:打造高性能、安全、可维护的中大型项目
前端
1024小神7 小时前
next项目使用状态管理zustand说明
前端
Asort7 小时前
JavaScript设计模式(八):组合模式(Composite)——构建灵活可扩展的树形对象结构
前端·javascript·设计模式
刘永胜是我7 小时前
【iTerm2 实用技巧】解决两大顽疾:历史记录看不全 & 鼠标滚轮失灵
前端·iterm