第255集:Vue 3.3+服务器端组件
概述
Vue 3.3版本引入了服务器端组件(Server Components),这是一种在服务器上渲染并发送到客户端的组件类型。服务器端组件与传统的服务端渲染(SSR)有所不同,它允许开发者将应用拆分为服务器组件和客户端组件,充分利用两者的优势。本集将深入探讨服务器端组件的概念、类型、通信机制以及缓存策略,帮助开发者理解和应用这一重要新特性。
服务器端组件 vs SSR
在了解服务器端组件之前,我们需要区分它与传统SSR的区别:
传统SSR
- 全页面渲染:将整个应用在服务器上渲染为HTML
- 水合(Hydration):客户端接管HTML并使其交互化
- 共享代码:服务器和客户端运行相同的组件代码
- 性能开销:客户端需要下载和执行完整的组件代码
服务器端组件
- 组件级渲染:只在服务器上渲染特定组件
- 零水合:服务器组件不需要在客户端水合
- 代码分离:服务器组件代码完全不发送到客户端
- 轻量级客户端:客户端只需要下载和执行客户端组件代码
- 按需渲染:可以根据需要动态决定组件在服务器还是客户端渲染
服务器端组件类型
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+允许服务器组件和客户端组件相互嵌套,但有一些规则需要遵守:
- 服务器组件可以包含客户端组件
- 客户端组件可以包含服务器组件
- 服务器组件不能在客户端组件的模板中直接使用
- 需要使用特殊的导入语法
服务器组件包含客户端组件
<!-- 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保护服务器和客户端之间的通信
服务器组件的局限性
虽然服务器组件有很多优势,但也有一些局限性需要考虑:
- 学习曲线:需要理解服务器组件和客户端组件的区别和使用规则
- 开发环境复杂:需要配置服务器和客户端的构建环境
- 调试困难:服务器组件的调试需要在服务器端进行
- 兼容性:需要使用支持服务器组件的框架和工具
- 实时更新受限:服务器组件不能直接响应客户端事件
总结
Vue 3.3+的服务器端组件是一个重要的新特性,它允许开发者将应用拆分为服务器组件和客户端组件,充分利用两者的优势:
- 服务器组件:在服务器上渲染,代码不发送到客户端,适合数据密集型和权限敏感的场景
- 客户端组件:在客户端渲染,适合交互密集型和需要访问浏览器API的场景
- 通用组件:可以在服务器和客户端都运行,适合简单的UI组件
服务器组件与传统SSR相比,具有更好的性能和灵活性,可以显著减少客户端的代码大小和水合时间。通过合理使用服务器组件和客户端组件,开发者可以构建出性能更好、用户体验更佳的应用。
在下一集中,我们将探讨Vue 3.3+中的响应式优化改进,包括Proxy性能、toValue API、MaybeRef和MaybeRefOrGetter类型等内容。