第255集:Vue 3.3+服务器端组件

概述

Vue 3.3版本引入了服务器端组件(Server Components),这是一种在服务器上渲染并发送到客户端的组件类型。服务器端组件与传统的服务端渲染(SSR)有所不同,它允许开发者将应用拆分为服务器组件和客户端组件,充分利用两者的优势。本集将深入探讨服务器端组件的概念、类型、通信机制以及缓存策略,帮助开发者理解和应用这一重要新特性。

服务器端组件 vs SSR

在了解服务器端组件之前,我们需要区分它与传统SSR的区别:

传统SSR

  1. 全页面渲染:将整个应用在服务器上渲染为HTML
  2. 水合(Hydration):客户端接管HTML并使其交互化
  3. 共享代码:服务器和客户端运行相同的组件代码
  4. 性能开销:客户端需要下载和执行完整的组件代码

服务器端组件

  1. 组件级渲染:只在服务器上渲染特定组件
  2. 零水合:服务器组件不需要在客户端水合
  3. 代码分离:服务器组件代码完全不发送到客户端
  4. 轻量级客户端:客户端只需要下载和执行客户端组件代码
  5. 按需渲染:可以根据需要动态决定组件在服务器还是客户端渲染

服务器端组件类型

Vue 3.3+支持三种类型的组件文件:

1. 服务器组件 (.server.vue)

服务器组件只在服务器上执行和渲染,其代码不会被发送到客户端。

<!-- ServerComponent.server.vue -->
<template>
  <div class="server-component">
    <h2>服务器组件</h2>
    <p>当前时间:{{ serverTime }}</p>
    <p>服务器环境:{{ serverEnv }}</p>
    <ul>
      <li v-for="item in serverData" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// 只能在服务器上访问的模块
import fs from 'fs'
import path from 'path'

// 服务器端数据获取
const serverData = ref([])
const serverTime = ref(new Date().toLocaleString())
const serverEnv = ref(process.env.NODE_ENV || 'development')

// 服务器端生命周期钩子
onMounted(() => {
  console.log('服务器组件挂载')
  // 可以访问服务器文件系统
  const dataPath = path.join(process.cwd(), 'data.json')
  if (fs.existsSync(dataPath)) {
    const data = fs.readFileSync(dataPath, 'utf-8')
    serverData.value = JSON.parse(data)
  }
})
</script>

2. 客户端组件 (.client.vue)

客户端组件在客户端执行和渲染,与传统Vue组件类似。

<!-- ClientComponent.client.vue -->
<template>
  <div class="client-component">
    <h2>客户端组件</h2>
    <p>客户端时间:{{ clientTime }}</p>
    <p>浏览器信息:{{ browserInfo }}</p>
    <button @click="updateCount">点击计数:{{ count }}</button>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const count = ref(0)
const clientTime = ref(new Date().toLocaleString())
const browserInfo = ref('')

// 更新计数
const updateCount = () => {
  count.value++
}

// 客户端生命周期钩子
onMounted(() => {
  console.log('客户端组件挂载')
  // 可以访问浏览器API
  browserInfo.value = navigator.userAgent
  // 定时更新时间
  setInterval(() => {
    clientTime.value = new Date().toLocaleString()
  }, 1000)
})
</script>

3. 通用组件 (.vue)

通用组件可以在服务器和客户端都执行和渲染,需要兼容两边的环境。

<!-- UniversalComponent.vue -->
<template>
  <div class="universal-component">
    <h2>通用组件</h2>
    <p>组件类型:通用</p>
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
// 只能使用服务器和客户端都支持的API
// 不能使用浏览器特定API或服务器特定API
</script>

服务器组件与客户端组件的嵌套

Vue 3.3+允许服务器组件和客户端组件相互嵌套,但有一些规则需要遵守:

  1. 服务器组件可以包含客户端组件
  2. 客户端组件可以包含服务器组件
  3. 服务器组件不能在客户端组件的模板中直接使用
  4. 需要使用特殊的导入语法

服务器组件包含客户端组件

<!-- ServerParent.server.vue -->
<template>
  <div class="server-parent">
    <h1>服务器父组件</h1>
    <!-- 包含客户端组件 -->
    <ClientChild />
    <p>服务器组件内容</p>
  </div>
</template>

<script setup lang="ts">
// 从客户端组件导入
import ClientChild from './ClientChild.client.vue'
</script>

客户端组件包含服务器组件

<!-- ClientParent.client.vue -->
<template>
  <div class="client-parent">
    <h1>客户端父组件</h1>
    <p>客户端组件内容</p>
    <!-- 包含服务器组件 -->
    <ServerChild />
  </div>
</template>

<script setup lang="ts">
// 从服务器组件导入
import ServerChild from './ServerChild.server.vue'
</script>

服务器组件与客户端组件的通信

由于服务器组件和客户端组件运行在不同的环境中,它们之间的通信需要特殊的机制:

1. Props传递

服务器组件可以向客户端组件传递props,客户端组件也可以向服务器组件传递props:

<!-- ServerComponent.server.vue -->
<template>
  <div>
    <h2>服务器组件</h2>
    <!-- 向客户端组件传递props -->
    <ClientComponent :server-data="serverData" />
  </div>
</template>

<script setup lang="ts">
import ClientComponent from './ClientComponent.client.vue'

const serverData = ref([
  { id: 1, name: '服务器数据1' },
  { id: 2, name: '服务器数据2' }
])
</script>
<!-- ClientComponent.client.vue -->
<template>
  <div>
    <h3>客户端组件</h3>
    <ul>
      <li v-for="item in serverData" :key="item.id">{{ item.name }}</li>
    </ul>
    <!-- 向父组件(服务器组件)发送事件 -->
    <button @click="$emit('client-event', '客户端事件数据')">
      触发客户端事件
    </button>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{
  serverData: Array<{ id: number; name: string }>
}>()

const emit = defineEmits<{
  'client-event': [data: string]
}>()
</script>

2. 上下文共享

可以使用useContext钩子在服务器组件和客户端组件之间共享上下文:

<!-- App.vue -->
<template>
  <ContextProvider :app-context="appContext">
    <AppContent />
  </ContextProvider>
</template>

<script setup lang="ts">
import { createContext } from 'vue'

// 创建上下文
const AppContext = createContext({})

// 提供上下文
const appContext = ref({
  appName: 'My App',
  version: '1.0.0'
})
</script>
<!-- ServerComponent.server.vue -->
<template>
  <div>
    <h2>{{ appContext.appName }} - 服务器组件</h2>
  </div>
</template>

<script setup lang="ts">
import { useContext } from 'vue'
import { AppContext } from './App.vue'

// 使用上下文
const appContext = useContext(AppContext)
</script>
<!-- ClientComponent.client.vue -->
<template>
  <div>
    <h2>{{ appContext.appName }} - 客户端组件</h2>
    <p>版本:{{ appContext.version }}</p>
  </div>
</template>

<script setup lang="ts">
import { useContext } from 'vue'
import { AppContext } from './App.vue'

// 使用上下文
const appContext = useContext(AppContext)
</script>

3. 异步数据传递

服务器组件可以通过异步方式向客户端组件传递数据:

<!-- ServerComponent.server.vue -->
<template>
  <div>
    <h2>服务器组件</h2>
    <ClientComponent :async-data="asyncData" />
  </div>
</template>

<script setup lang="ts">
import ClientComponent from './ClientComponent.client.vue'

// 异步获取数据
const asyncData = ref(await fetchDataFromDatabase())

async function fetchDataFromDatabase() {
  // 模拟数据库查询
  await new Promise(resolve => setTimeout(resolve, 1000))
  return {
    id: 1,
    name: '异步数据',
    value: Math.random() * 100
  }
}
</script>

服务器组件缓存策略

为了提高性能,Vue 3.3+提供了多种服务器组件缓存策略:

1. 组件级缓存

可以为服务器组件添加缓存,避免重复渲染:

<!-- CachedServerComponent.server.vue -->
<template>
  <div class="cached-component">
    <h2>缓存服务器组件</h2>
    <p>缓存键:{{ cacheKey }}</p>
    <p>缓存时间:{{ cacheTime }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 缓存键,用于标识缓存项
const cacheKey = ref('my-cache-key')
const cacheTime = ref(new Date().toISOString())

// 设置缓存
useServerCache({
  key: cacheKey.value,
  ttl: 60 * 60 * 1000, // 1小时
  get: async () => {
    // 缓存数据生成逻辑
    return {
      cacheTime: new Date().toISOString()
    }
  }
})
</script>

2. 片段缓存

可以缓存服务器组件的部分内容:

<!-- FragmentCache.server.vue -->
<template>
  <div class="fragment-cache">
    <h2>片段缓存示例</h2>
    <!-- 缓存片段 -->
    <ServerCache :key="'fragment-cache-' + userId">
      <div class="cached-fragment">
        <p>用户ID:{{ userId }}</p>
        <p>用户信息:{{ userInfo }}</p>
      </div>
    </ServerCache>
    <!-- 不缓存的内容 -->
    <div class="non-cached">
      <p>当前时间:{{ currentTime }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const userId = ref(1)
const userInfo = ref(await fetchUserInfo(userId.value))
const currentTime = ref(new Date().toLocaleString())

async function fetchUserInfo(id: number) {
  // 模拟数据库查询
  await new Promise(resolve => setTimeout(resolve, 500))
  return `用户${id}的详细信息`
}
</script>

3. 缓存失效策略

可以根据需要手动使缓存失效:

<!-- CacheInvalidation.server.vue -->
<template>
  <div class="cache-invalidation">
    <h2>缓存失效示例</h2>
    <button @click="invalidateCache">使缓存失效</button>
    <CachedComponent />
  </div>
</template>

<script setup lang="ts">
import { useServerCache } from 'vue'
import CachedComponent from './CachedComponent.server.vue'

const invalidateCache = () => {
  // 使特定缓存键失效
  useServerCache().invalidate('my-cache-key')
  // 或使所有缓存失效
  useServerCache().invalidateAll()
}
</script>

服务器组件的异步依赖

服务器组件可以处理异步依赖,包括异步API调用、数据库查询等:

<!-- AsyncServerComponent.server.vue -->
<template>
  <div class="async-server-component">
    <h2>异步服务器组件</h2>
    <h3>用户列表</h3>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }} - {{ user.email }}
      </li>
    </ul>
    <h3>最近订单</h3>
    <ul>
      <li v-for="order in orders" :key="order.id">
        订单#{{ order.id }} - {{ order.amount }}元
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 并行获取多个异步依赖
const [users, orders] = await Promise.all([
  fetchUsers(),
  fetchRecentOrders()
])

// 异步获取用户数据
async function fetchUsers() {
  // 模拟API调用
  await new Promise(resolve => setTimeout(resolve, 800))
  return [
    { id: 1, name: '张三', email: 'zhangsan@example.com' },
    { id: 2, name: '李四', email: 'lisi@example.com' },
    { id: 3, name: '王五', email: 'wangwu@example.com' }
  ]
}

// 异步获取订单数据
async function fetchRecentOrders() {
  // 模拟API调用
  await new Promise(resolve => setTimeout(resolve, 600))
  return [
    { id: 101, amount: 199.99 },
    { id: 102, amount: 299.99 },
    { id: 103, amount: 399.99 }
  ]
}
</script>

服务器组件的应用场景

1. 数据密集型组件

对于需要显示大量数据的组件,如列表、表格等,可以使用服务器组件:

<!-- DataList.server.vue -->
<template>
  <div class="data-list">
    <h2>数据列表</h2>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>名称</th>
          <th>描述</th>
          <th>创建时间</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in data" :key="item.id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.description }}</td>
          <td>{{ formatDate(item.createdAt) }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 从数据库获取大量数据
const data = ref(await fetchLargeData())

async function fetchLargeData() {
  // 模拟从数据库获取1000条数据
  await new Promise(resolve => setTimeout(resolve, 1000))
  return Array.from({ length: 1000 }, (_, i) => ({
    id: i + 1,
    name: `项目${i + 1}`,
description: `这是项目${i + 1}的描述`,
    createdAt: new Date(Date.now() - i * 24 * 60 * 60 * 1000)
  }))
}

// 格式化日期
function formatDate(date: Date) {
  return new Date(date).toLocaleDateString()
}
</script>

2. 权限敏感组件

对于包含权限敏感数据的组件,可以使用服务器组件确保数据安全:

<!-- AdminPanel.server.vue -->
<template>
  <div class="admin-panel">
    <h2>管理员面板</h2>
    <p>欢迎,{{ currentUser.name }}!</p>
    <h3>用户管理</h3>
    <button @click="createUser">创建用户</button>
    <button @click="deleteUser">删除用户</button>
    <h3>系统设置</h3>
    <button @click="updateSettings">更新设置</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { checkPermission } from '../utils/permission'

// 检查用户权限
const currentUser = ref(await getCurrentUser())
if (!checkPermission(currentUser.value, 'admin')) {
  throw new Error('无管理员权限')
}

// 只能在服务器上访问的函数
async function getCurrentUser() {
  // 从服务器会话获取当前用户
  return {
    id: 1,
    name: '管理员',
    role: 'admin'
  }
}

function createUser() {
  // 只能在服务器上执行的操作
  console.log('创建用户')
}

function deleteUser() {
  // 只能在服务器上执行的操作
  console.log('删除用户')
}

function updateSettings() {
  // 只能在服务器上执行的操作
  console.log('更新设置')
}
</script>

3. 第三方API集成

对于需要调用第三方API的组件,可以使用服务器组件避免暴露API密钥:

<!-- ThirdPartyApi.server.vue -->
<template>
  <div class="third-party-api">
    <h2>第三方API集成</h2>
    <h3>天气信息</h3>
    <p>城市:{{ weather.city }}</p>
    <p>温度:{{ weather.temperature }}°C</p>
    <p>天气:{{ weather.description }}</p>
    <h3>股票行情</h3>
    <ul>
      <li v-for="stock in stocks" :key="stock.symbol">
        {{ stock.symbol }}: {{ stock.price }} {{ stock.change > 0 ? '↑' : '↓' }}{{ stock.change }}%
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 从服务器调用第三方API,避免暴露API密钥
const weather = ref(await fetchWeather('北京'))
const stocks = ref(await fetchStocks(['AAPL', 'GOOGL', 'MSFT']))

async function fetchWeather(city: string) {
  // 使用服务器端API密钥调用天气API
  const apiKey = process.env.WEATHER_API_KEY
  const response = await fetch(`https://api.weather.com/v1/current/${city}?apiKey=${apiKey}`)
  return response.json()
}

async function fetchStocks(symbols: string[]) {
  // 使用服务器端API密钥调用股票API
  const apiKey = process.env.STOCK_API_KEY
  const response = await fetch(`https://api.stock.com/v1/quotes?symbols=${symbols.join(',')}&apiKey=${apiKey}`)
  return response.json()
}
</script>

服务器组件最佳实践

1. 合理拆分组件

将应用拆分为服务器组件和客户端组件,根据组件的特性和需求选择合适的类型:

  • 服务器组件:数据密集型、权限敏感、第三方API集成、静态内容
  • 客户端组件:交互密集型、需要访问浏览器API、实时更新
  • 通用组件:简单的UI组件、不依赖特定环境

2. 优化服务器性能

  • 合理使用缓存,减少服务器负载
  • 优化数据库查询,使用索引和分页
  • 并行处理异步依赖,减少渲染时间
  • 避免在服务器组件中执行复杂的计算

3. 优化客户端体验

  • 为服务器组件添加加载状态
  • 合理使用客户端组件,提供良好的交互体验
  • 优化客户端组件的大小,减少下载时间
  • 使用代码分割,按需加载客户端组件

4. 安全考虑

  • 不要在客户端组件中暴露敏感数据
  • 不要在客户端组件中使用API密钥
  • 对服务器组件的输入进行验证和清理
  • 使用HTTPS保护服务器和客户端之间的通信

服务器组件的局限性

虽然服务器组件有很多优势,但也有一些局限性需要考虑:

  1. 学习曲线:需要理解服务器组件和客户端组件的区别和使用规则
  2. 开发环境复杂:需要配置服务器和客户端的构建环境
  3. 调试困难:服务器组件的调试需要在服务器端进行
  4. 兼容性:需要使用支持服务器组件的框架和工具
  5. 实时更新受限:服务器组件不能直接响应客户端事件

总结

Vue 3.3+的服务器端组件是一个重要的新特性,它允许开发者将应用拆分为服务器组件和客户端组件,充分利用两者的优势:

  1. 服务器组件:在服务器上渲染,代码不发送到客户端,适合数据密集型和权限敏感的场景
  2. 客户端组件:在客户端渲染,适合交互密集型和需要访问浏览器API的场景
  3. 通用组件:可以在服务器和客户端都运行,适合简单的UI组件

服务器组件与传统SSR相比,具有更好的性能和灵活性,可以显著减少客户端的代码大小和水合时间。通过合理使用服务器组件和客户端组件,开发者可以构建出性能更好、用户体验更佳的应用。

在下一集中,我们将探讨Vue 3.3+中的响应式优化改进,包括Proxy性能、toValue API、MaybeRef和MaybeRefOrGetter类型等内容。

« 上一篇 Vue 3.3+ Suspense增强功能:优化异步体验 下一篇 » Vue 3.3+响应式优化改进:提升性能与开发者体验