Chapter 08

数据加载:load 函数与 SSR/CSR

掌握 SvelteKit 强大的数据加载机制,实现服务端渲染、客户端渲染与流式加载

load 函数体系

两种 load 函数

SvelteKit 提供两种数据加载方式,通过不同文件承载:

+page.server.ts
服务端专用 load 函数。只在服务器运行,可安全访问数据库、文件系统、私有 API,不暴露给客户端。适合需要认证或敏感数据的场景。
+page.ts
通用 load 函数。在服务端 SSR 时运行,在客户端导航时也在浏览器运行。适合公开 API 调用,代码在两端共享。
+layout.server.ts
布局级服务端 load,返回的数据对该布局下所有页面可用。常用于加载用户认证信息。
PageData 类型
SvelteKit 自动生成的类型,表示 load 函数返回值的类型。通过 import type { PageData } from './$types' 引入,保证页面组件的类型安全。

+page.server.ts 服务端 load

// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db';  // 服务端专用模块

export const load: PageServerLoad = async ({
  params,       // 路由参数
  locals,       // 中间件传递的数据(如当前用户)
  cookies,      // Cookie 访问
  request,      // 原始 Request 对象
  url,          // URL 对象
  fetch,        // 增强版 fetch,自动携带 cookies
  setHeaders    // 设置响应头
}) => {
  // 认证检查
  if (!locals.user) {
    error(401, '请先登录');
  }

  // 数据库查询(服务端专用)
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  });

  if (!post) error(404, '文章不存在');

  // 设置缓存头
  setHeaders({ 'cache-control': 'max-age=3600' });

  // 返回值自动成为 PageData 的一部分
  return {
    post,
    author: locals.user
  };
};

+page.ts 通用 load

// src/routes/products/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch, url }) => {
  const category = url.searchParams.get('category') || 'all';

  // 这个 fetch 在服务端和客户端都能正常工作
  const res = await fetch(`/api/products?category=${category}`);
  const products = await res.json();

  return { products, category };
};

// 禁用 SSR(纯客户端渲染)
export const ssr = false;

// 禁用预加载
export const prerender = false;

在页面中使用 data

<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data } = $props<{ data: PageData }>();
  // data.post 和 data.author 已有完整类型信息
</script>

<svelte:head>
  <title>{data.post.title}</title>
</svelte:head>

<article>
  <h1>{data.post.title}</h1>
  <p class="meta">作者:{data.author.name}</p>
  <div class="content">{@html data.post.htmlContent}</div>
</article>

缓存刷新

<script lang="ts">
  import { invalidate, invalidateAll } from '$app/navigation';

  async function refreshData() {
    // 使特定 URL 的数据失效,触发重新加载
    await invalidate('app:user');   // 自定义 key
    await invalidate('/api/posts'); // URL key

    // 使所有数据失效(重新运行所有 load 函数)
    await invalidateAll();
  }
</script>

流式加载

// 流式加载:先快速返回重要数据,慢查询用 Promise 流式传入
export const load: PageServerLoad = async ({ params }) => {
  // 快速数据:立即返回
  const post = await db.post.findUnique({ where: { id: params.id } });

  // 慢速数据:不 await,作为 Promise 返回
  const commentsPromise = db.comment.findMany({
    where: { postId: params.id }
  });

  return {
    post,                          // 立即可用
    comments: commentsPromise      // 流式加载
  };
};
<!-- 在模板中处理流式数据 -->
<h1>{data.post.title}</h1>

{#await data.comments}
  <p>加载评论...</p>
{:then comments}
  {#each comments as comment}
    <div class="comment">{comment.content}</div>
  {/each}
{/await}
流式加载的 SEO 影响

流式加载时,页面 HTML 会分批发送给浏览器。对于 SEO 关键内容(标题、摘要),确保在同步数据中返回;评论、推荐等次要内容适合流式加载,既不影响 SEO 也改善了首次内容渲染(FCP)时间。

SSR/CSR/SSG 的深层原理

三种渲染模式的本质区别

理解这三种模式的工作原理,才能在每种场景下做出正确选择:

服务端渲染(SSR — Server-Side Rendering)
每次用户请求时,服务器实时执行 load 函数获取数据,生成完整的 HTML 后返回给浏览器。优点:首屏内容立即可见(对 SEO 友好)、数据始终最新(不存在缓存过期问题)。缺点:每次请求都有服务器计算开销。SvelteKit 默认开启 SSR。
关键配置:export const ssr = true;(默认)
客户端渲染(CSR — Client-Side Rendering)
服务器只返回一个空的 HTML 壳,JavaScript 在浏览器中运行,获取数据并渲染内容。优点:服务器负担轻(静态文件 CDN 托管)、交互性强。缺点:首屏白屏时间长(等 JS 加载执行)、SEO 差(爬虫看不到内容)。
关键配置:export const ssr = false;
静态站点生成(SSG — Static Site Generation)
构建时一次性执行所有 load 函数,生成纯静态 HTML/CSS/JS 文件。优点:极快的加载速度(CDN 全球分发)、无服务器成本。缺点:数据更新需要重新构建,不适合需要实时数据的页面。
关键配置:export const prerender = true;
混合渲染(Hybrid Rendering)
SvelteKit 支持在同一应用中混合使用三种模式:首页和博客文章使用 SSG(稳定内容),用户仪表板使用 SSR(实时数据),评论区使用 CSR(不需要 SEO)。每个路由独立配置,这是 SvelteKit 最强大的特性之一。

数据加载的执行时序

SSR 请求处理时序:

浏览器              SvelteKit 服务器              数据库
  |                       |                          |
  |--- GET /blog/my-post → |                          |
  |                       |--- load() 执行 -----------→|
  |                       |←-- post 数据 -------------|
  |                       | [将 data 注入模板]         |
  |←-- 完整 HTML ----------|                          |
  |                       |                          |
  | [浏览器渲染 HTML]      |                          |
  | [加载 JS bundle]       |                          |
  | [Hydration:事件绑定]  |                          |

Hydration(水合):
  服务器生成的静态 HTML 被 Svelte 的 JS 接管,
  添加事件监听器,变为可交互的应用。
  如果服务器端和客户端渲染结果不一致,
  会出现"hydration mismatch"警告。

load 函数的缓存与依赖追踪

// SvelteKit 会根据依赖自动决定是否重新执行 load
export const load: PageServerLoad = async ({ params, depends }) => {
  // 声明这个 load 函数依赖 'app:user' 标识符
  // 当 invalidate('app:user') 被调用时,自动重新执行此 load
  depends('app:user');

  const user = await getCurrentUser();
  return { user };
};

// 在组件中触发重新加载
import { invalidate, invalidateAll } from '$app/navigation';

async function handleAvatarUpload() {
  await uploadAvatar();
  // 使所有依赖 'app:user' 的 load 函数失效,触发重新获取
  await invalidate('app:user');
  // 或者使整个页面的所有数据失效:
  // await invalidateAll();
}
常见误区:load 函数中使用响应式语句

load 函数是普通的 async 函数,不是 Svelte 组件,无法使用 $state$derived 等 Svelte 5 响应式原语。数据变更的响应式更新应通过 invalidate() 触发重新执行 load 函数来实现,而不是在 load 内部使用响应式变量。

错误处理与 +error.svelte

// +page.server.ts:使用 error() 返回 HTTP 错误
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, locals }) => {
  const post = await db.post.findUnique({ where: { slug: params.slug } });

  if (!post) {
    // 404 错误:SvelteKit 渲染最近的 +error.svelte
    error(404, { message: `文章 "${params.slug}" 不存在` });
  }

  if (!post.published && !locals.user?.isAdmin) {
    error(403, { message: '无权访问未发布的文章' });
  }

  return { post };
};
<!-- src/routes/+error.svelte(全局错误页面)-->
<script lang="ts">
  import { page } from '$app/stores';
</script>

<svelte:head>
  <title>{$page.status} — 出错了</title>
</svelte:head>

<div class="error-page">
  <h1>{$page.status}</h1>
  <p>{$page.error?.message || '发生了未知错误'}</p>
  {#if $page.status === 404}
    <a href="/">返回首页</a>
  {:else if $page.status === 403}
    <a href="/login">前往登录</a>
  {:else}
    <button onclick={() => location.reload()}>刷新页面</button>
  {/if}
</div>

Layout load 与数据继承

layout.server.ts 的 load 函数返回的数据会自动合并到所有子页面的 data prop 中。这是在全应用范围传递数据(如用户信息、主题)的标准方式:

// src/routes/+layout.server.ts(根 layout)
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals, cookies }) => {
  // locals 由 handle 钩子填充(通常是解析后的用户 session)
  return {
    user: locals.user,         // 所有子页面通过 data.user 访问
    theme: cookies.get('theme') ?? 'light'
  };
};
<!-- 子页面 +page.svelte 访问 layout 数据 -->
<script lang="ts">
  import type { PageData } from './$types';
  let { data } = $props<{ data: PageData }>();

  // data 自动合并:本页 load + 所有上级 layout load 的返回值
  const { user, theme, post } = data;  // user/theme 来自 layout, post 来自本页
</script>

<h1>欢迎,{user?.name ?? '访客'}</h1>
<article>{post.title}</article>
本章小结