sveltekit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SvelteKit - Full-Stack Svelte Framework

SvelteKit - 基于Svelte的全栈框架

Overview

概述

SvelteKit is the official full-stack framework for Svelte, providing file-based routing, server-side rendering (SSR), static site generation (SSG), form handling with progressive enhancement, and deployment adapters for any platform.
Key Features:
  • File-based routing: Automatic routes from
    src/routes/
    directory structure
  • Load functions: Type-safe data fetching (
    +page.ts
    ,
    +page.server.ts
    )
  • Form actions: Native form handling with progressive enhancement
  • SSR/SSG/SPA: Flexible rendering modes with per-route control
  • Adapters: Deploy to Vercel, Netlify, Node.js, Cloudflare, and more
  • TypeScript-first: Generated types from
    $types
    for type safety
  • Hooks: Middleware-like
    handle
    ,
    handleError
    ,
    handleFetch
  • API routes:
    +server.ts
    files for REST endpoints
Installation:
bash
undefined
SvelteKit是Svelte官方的全栈框架,提供基于文件的路由、服务端渲染(SSR)、静态站点生成(SSG)、支持渐进式增强的表单处理,以及适用于任意平台的部署适配器。
核心特性:
  • 基于文件的路由: 从
    src/routes/
    目录结构自动生成路由
  • Load函数: 类型安全的数据获取(
    +page.ts
    +page.server.ts
  • 表单操作: 原生表单处理,支持渐进式增强
  • SSR/SSG/SPA: 灵活的渲染模式,支持按路由控制
  • 适配器: 可部署至Vercel、Netlify、Node.js、Cloudflare等平台
  • TypeScript优先: 从
    $types
    生成类型,保障类型安全
  • Hooks: 类中间件的
    handle
    handleError
    handleFetch
  • API路由: 通过
    +server.ts
    文件创建REST端点
安装:
bash
undefined

Create new SvelteKit project

创建新的SvelteKit项目

npm create svelte@latest my-app cd my-app npm install npm run dev -- --open
npm create svelte@latest my-app cd my-app npm install npm run dev -- --open

Templates: skeleton, demo app, library

模板选项:骨架项目、演示应用、库

Choices: TypeScript, ESLint, Prettier, Playwright, Vitest

可选项:TypeScript、ESLint、Prettier、Playwright、Vitest

undefined
undefined

Project Structure

项目结构

Standard SvelteKit Layout

标准SvelteKit目录结构

my-sveltekit-app/
├── src/
│   ├── routes/                   # File-based routing
│   │   ├── +page.svelte         # / (home page)
│   │   ├── +page.ts             # Universal load function
│   │   ├── +page.server.ts      # Server-only load function
│   │   ├── +layout.svelte       # Shared layout
│   │   ├── +layout.ts           # Layout load function
│   │   ├── +error.svelte        # Error page
│   │   ├── about/
│   │   │   └── +page.svelte     # /about
│   │   ├── blog/
│   │   │   ├── +page.svelte     # /blog (list)
│   │   │   ├── +page.server.ts  # Load posts
│   │   │   └── [slug]/
│   │   │       ├── +page.svelte # /blog/my-post
│   │   │       └── +page.server.ts
│   │   └── api/
│   │       └── posts/
│   │           └── +server.ts   # GET /api/posts
│   ├── lib/
│   │   ├── components/
│   │   ├── server/              # Server-only utilities
│   │   │   └── database.ts
│   │   ├── stores/
│   │   └── utils/
│   ├── hooks.server.ts          # Server hooks
│   ├── hooks.client.ts          # Client hooks
│   ├── app.html                 # HTML template
│   └── app.d.ts                 # TypeScript declarations
├── static/                       # Static assets (robots.txt, favicon)
├── tests/                        # Playwright tests
├── svelte.config.js             # SvelteKit configuration
├── vite.config.ts               # Vite configuration
└── package.json
my-sveltekit-app/
├── src/
│   ├── routes/                   # 基于文件的路由
│   │   ├── +page.svelte         # /(首页)
│   │   ├── +page.ts             # 通用Load函数(客户端+服务端)
│   │   ├── +page.server.ts      # 仅服务端的Load函数
│   │   ├── +layout.svelte       # 共享布局
│   │   ├── +layout.ts           # 布局Load函数
│   │   ├── +error.svelte        # 错误页面
│   │   ├── about/
│   │   │   └── +page.svelte     # /about
│   │   ├── blog/
│   │   │   ├── +page.svelte     # /blog(列表页)
│   │   │   ├── +page.server.ts  # 加载文章
│   │   │   └── [slug]/
│   │   │       ├── +page.svelte # /blog/my-post
│   │   │       └── +page.server.ts
│   │   └── api/
│   │       └── posts/
│   │           └── +server.ts   # GET /api/posts
│   ├── lib/
│   │   ├── components/
│   │   ├── server/              # 仅服务端工具
│   │   │   └── database.ts
│   │   ├── stores/
│   │   └── utils/
│   ├── hooks.server.ts          # 服务端Hooks
│   ├── hooks.client.ts          # 客户端Hooks
│   ├── app.html                 # HTML模板
│   └── app.d.ts                 # TypeScript声明
├── static/                       # 静态资源(robots.txt、favicon)
├── tests/                        # Playwright测试
├── svelte.config.js             # SvelteKit配置
├── vite.config.ts               # Vite配置
└── package.json

File-Based Routing

基于文件的路由

Route Conventions

路由约定

File naming determines routing:
FileRoutePurpose
+page.svelte
/
Page component
+page.ts
-Universal load (client + server)
+page.server.ts
-Server-only load
+layout.svelte
-Shared layout
+layout.ts
-Layout load
+layout.server.ts
-Server layout load
+server.ts
/api/...
API endpoint (GET/POST/etc)
+error.svelte
-Error boundary
文件名决定路由:
文件路由用途
+page.svelte
/
页面组件
+page.ts
-通用Load函数(客户端+服务端)
+page.server.ts
-仅服务端的Load函数
+layout.svelte
-共享布局
+layout.ts
-布局Load函数
+layout.server.ts
-仅服务端的布局Load函数
+server.ts
/api/...
API端点(GET/POST等)
+error.svelte
-错误边界

Basic Routes

基础路由

src/routes/
├── +page.svelte              # / (home)
├── about/
│   └── +page.svelte          # /about
├── contact/
│   └── +page.svelte          # /contact
└── pricing/
    └── +page.svelte          # /pricing
src/routes/
├── +page.svelte              # /(首页)
├── about/
│   └── +page.svelte          # /about
├── contact/
│   └── +page.svelte          # /contact
└── pricing/
    └── +page.svelte          # /pricing

Dynamic Routes

动态路由

src/routes/
└── blog/
    ├── +page.svelte          # /blog (list)
    ├── [slug]/
    │   └── +page.svelte      # /blog/my-post
    └── [category]/
        └── [slug]/
            └── +page.svelte  # /blog/tech/my-post
Access route params:
svelte
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data } = $props<{ data: PageData }>();
</script>

<article>
  <h1>{data.post.title}</h1>
  <p>{data.post.content}</p>
</article>
src/routes/
└── blog/
    ├── +page.svelte          # /blog(列表页)
    ├── [slug]/
    │   └── +page.svelte      # /blog/my-post
    └── [category]/
        └── [slug]/
            └── +page.svelte  # /blog/tech/my-post
访问路由参数:
svelte
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data } = $props<{ data: PageData }>();
</script>

<article>
  <h1>{data.post.title}</h1>
  <p>{data.post.content}</p>
</article>

Optional Parameters

可选参数

src/routes/
└── archive/
    └── [[year]]/
        └── [[month]]/
            └── +page.svelte  # /archive, /archive/2024, /archive/2024/11
typescript
// src/routes/archive/[[year]]/[[month]]/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params }) => {
  const year = params.year || new Date().getFullYear();
  const month = params.month || null;

  return {
    year,
    month,
    posts: await fetchPosts({ year, month })
  };
};
src/routes/
└── archive/
    └── [[year]]/
        └── [[month]]/
            └── +page.svelte  # /archive, /archive/2024, /archive/2024/11
typescript
// src/routes/archive/[[year]]/[[month]]/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ params }) => {
  const year = params.year || new Date().getFullYear();
  const month = params.month || null;

  return {
    year,
    month,
    posts: await fetchPosts({ year, month })
  };
};

Rest Parameters

剩余参数

src/routes/
└── docs/
    └── [...path]/
        └── +page.svelte      # /docs/guide/intro, /docs/api/reference
typescript
// src/routes/docs/[...path]/+page.ts
export const load: PageLoad = async ({ params }) => {
  const path = params.path; // "guide/intro"
  const segments = path.split('/'); // ["guide", "intro"]

  return {
    doc: await fetchDoc(path)
  };
};
src/routes/
└── docs/
    └── [...path]/
        └── +page.svelte      # /docs/guide/intro, /docs/api/reference
typescript
// src/routes/docs/[...path]/+page.ts
export const load: PageLoad = async ({ params }) => {
  const path = params.path; // "guide/intro"
  const segments = path.split('/'); // ["guide", "intro"]

  return {
    doc: await fetchDoc(path)
  };
};

Load Functions

Load函数

Universal Load (+page.ts)

通用Load函数(+page.ts)

Runs on both server and client. Must use
fetch
for data fetching.
typescript
// src/routes/products/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch, params, url }) => {
  const response = await fetch('/api/products');
  const products = await response.json();

  return {
    products,
    searchQuery: url.searchParams.get('q') || ''
  };
};

// Prerendering options
export const prerender = true; // Static generation
export const ssr = false;      // Disable SSR (SPA mode)
export const csr = true;       // Enable client-side rendering
同时在服务端和客户端运行,必须使用
fetch
进行数据获取。
typescript
// src/routes/products/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch, params, url }) => {
  const response = await fetch('/api/products');
  const products = await response.json();

  return {
    products,
    searchQuery: url.searchParams.get('q') || ''
  };
};

// 预渲染选项
export const prerender = true; // 静态生成
export const ssr = false;      // 禁用SSR(SPA模式)
export const csr = true;       // 启用客户端渲染

Server-Only Load (+page.server.ts)

仅服务端的Load函数(+page.server.ts)

Runs only on server. Direct database access allowed.
typescript
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';

export const load: PageServerLoad = async ({ locals, cookies }) => {
  // Check authentication
  if (!locals.user) {
    throw redirect(303, '/login');
  }

  // Direct database query (server-only)
  const stats = await db.query.stats.findFirst({
    where: eq(stats.userId, locals.user.id)
  });

  // Sensitive data stays on server
  const apiKey = process.env.SECRET_API_KEY;
  const data = await fetchPrivateData(apiKey);

  return {
    stats,
    userData: data
  };
};
仅在服务端运行,允许直接访问数据库。
typescript
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';

export const load: PageServerLoad = async ({ locals, cookies }) => {
  // 检查认证状态
  if (!locals.user) {
    throw redirect(303, '/login');
  }

  // 直接查询数据库(仅服务端)
  const stats = await db.query.stats.findFirst({
    where: eq(stats.userId, locals.user.id)
  });

  // 敏感数据保留在服务端
  const apiKey = process.env.SECRET_API_KEY;
  const data = await fetchPrivateData(apiKey);

  return {
    stats,
    userData: data
  };
};

Streaming with Promises

使用Promise流式加载

typescript
// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  return {
    // Immediate data
    featured: await db.posts.findMany({ where: { featured: true } }),

    // Streamed data (loads async)
    recent: db.posts.findMany({ orderBy: { createdAt: 'desc' } }),
    popular: db.posts.findMany({ orderBy: { views: 'desc' } })
  };
};
svelte
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data } = $props<{ data: PageData }>();
</script>

<h2>Featured</h2>
{#each data.featured as post}
  <article>{post.title}</article>
{/each}

<h2>Recent</h2>
{#await data.recent}
  <p>Loading recent posts...</p>
{:then posts}
  {#each posts as post}
    <article>{post.title}</article>
  {/each}
{/await}

<h2>Popular</h2>
{#await data.popular}
  <p>Loading popular posts...</p>
{:then posts}
  {#each posts as post}
    <article>{post.title}</article>
  {/each}
{/await}
typescript
// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  return {
    // 即时加载的数据
    featured: await db.posts.findMany({ where: { featured: true } }),

    // 流式加载的数据(异步加载)
    recent: db.posts.findMany({ orderBy: { createdAt: 'desc' } }),
    popular: db.posts.findMany({ orderBy: { views: 'desc' } })
  };
};
svelte
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data } = $props<{ data: PageData }>();
</script>

<h2>精选文章</h2>
{#each data.featured as post}
  <article>{post.title}</article>
{/each}

<h2>最新文章</h2>
{#await data.recent}
  <p>加载最新文章中...</p>
{:then posts}
  {#each posts as post}
    <article>{post.title}</article>
  {/each}
{/await}

<h2>热门文章</h2>
{#await data.popular}
  <p>加载热门文章中...</p>
{:then posts}
  {#each posts as post}
    <article>{post.title}</article>
  {/each}
{/await}

Layouts

布局

Shared Layout

共享布局

svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import Header from '$lib/components/Header.svelte';
  import Footer from '$lib/components/Footer.svelte';
  import type { LayoutData } from './$types';

  let { data, children } = $props<{ data: LayoutData, children: any }>();
</script>

<div class="app">
  <Header user={data.user} />

  <main>
    {@render children()}
  </main>

  <Footer />
</div>

<style>
  .app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }

  main {
    flex: 1;
  }
</style>
svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import Header from '$lib/components/Header.svelte';
  import Footer from '$lib/components/Footer.svelte';
  import type { LayoutData } from './$types';

  let { data, children } = $props<{ data: LayoutData, children: any }>();
</script>

<div class="app">
  <Header user={data.user} />

  <main>
    {@render children()}
  </main>

  <Footer />
</div>

<style>
  .app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }

  main {
    flex: 1;
  }
</style>

Layout Load

布局Load函数

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

export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    user: locals.user || null
  };
};
typescript
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    user: locals.user || null
  };
};

Nested Layouts

嵌套布局

src/routes/
├── +layout.svelte              # Root layout (all pages)
├── (app)/
│   ├── +layout.svelte          # App layout (dashboard, settings)
│   ├── dashboard/
│   │   └── +page.svelte        # Uses: root + app layouts
│   └── settings/
│       └── +page.svelte
└── (marketing)/
    ├── +layout.svelte          # Marketing layout (about, pricing)
    ├── about/
    │   └── +page.svelte        # Uses: root + marketing layouts
    └── pricing/
        └── +page.svelte
Layout groups with
(name)
don't affect URL structure
:
  • /dashboard
    not
    /(app)/dashboard
src/routes/
├── +layout.svelte              # 根布局(所有页面)
├── (app)/
│   ├── +layout.svelte          # 应用布局(仪表盘、设置)
│   ├── dashboard/
│   │   └── +page.svelte        # 使用:根布局 + 应用布局
│   └── settings/
│       └── +page.svelte
└── (marketing)/
    ├── +layout.svelte          # 营销布局(关于我们、定价)
    ├── about/
    │   └── +page.svelte        # 使用:根布局 + 营销布局
    └── pricing/
        └── +page.svelte
使用
(name)
的布局组不影响URL结构
:
  • 实际路由为
    /dashboard
    而非
    /(app)/dashboard

Breaking Out of Layouts

突破布局嵌套

svelte
<!-- src/routes/admin/+layout.svelte -->
<script>
  let { children } = $props();
</script>

<div class="admin">
  {@render children()}
</div>
svelte
<!-- src/routes/admin/login/+page@.svelte -->
<!-- @ breaks out to root layout, skipping admin layout -->
<form method="POST">
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">Login</button>
</form>
svelte
<!-- src/routes/admin/+layout.svelte -->
<script>
  let { children } = $props();
</script>

<div class="admin">
  {@render children()}
</div>
svelte
<!-- src/routes/admin/login/+page@.svelte -->
<!-- @ 表示突破到根布局,跳过admin布局 -->
<form method="POST">
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">登录</button>
</form>

Form Actions

表单操作

Basic Form Actions

基础表单操作

typescript
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions = {
  // Default action (form without action attribute)
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');

    if (!email || !password) {
      return fail(400, { email, missing: true });
    }

    const user = await authenticateUser(email, password);
    if (!user) {
      return fail(401, { email, incorrect: true });
    }

    cookies.set('session', user.sessionToken, {
      path: '/',
      httpOnly: true,
      sameSite: 'strict',
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 24 * 7 // 1 week
    });

    throw redirect(303, '/dashboard');
  }
} satisfies Actions;
svelte
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
  import type { ActionData } from './$types';

  let { form } = $props<{ form?: ActionData }>();
</script>

<form method="POST">
  <input
    name="email"
    type="email"
    value={form?.email ?? ''}
    required
  />

  <input name="password" type="password" required />

  {#if form?.missing}
    <p class="error">Please fill in all fields</p>
  {/if}

  {#if form?.incorrect}
    <p class="error">Invalid email or password</p>
  {/if}

  <button type="submit">Log in</button>
</form>
typescript
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions = {
  // 默认操作(表单未指定action属性时)
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');

    if (!email || !password) {
      return fail(400, { email, missing: true });
    }

    const user = await authenticateUser(email, password);
    if (!user) {
      return fail(401, { email, incorrect: true });
    }

    cookies.set('session', user.sessionToken, {
      path: '/',
      httpOnly: true,
      sameSite: 'strict',
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 24 * 7 // 1周
    });

    throw redirect(303, '/dashboard');
  }
} satisfies Actions;
svelte
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
  import type { ActionData } from './$types';

  let { form } = $props<{ form?: ActionData }>();
</script>

<form method="POST">
  <input
    name="email"
    type="email"
    value={form?.email ?? ''}
    required
  />

  <input name="password" type="password" required />

  {#if form?.missing}
    <p class="error">请填写所有字段</p>
  {/if}

  {#if form?.incorrect}
    <p class="error">邮箱或密码无效</p>
  {/if}

  <button type="submit">登录</button>
</form>

Named Actions

命名表单操作

typescript
// src/routes/todos/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  return {
    todos: await db.todos.findMany()
  };
};

export const actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const text = data.get('text');

    if (!text) {
      return fail(400, { text, missing: true });
    }

    await db.todos.create({ data: { text, done: false } });
    return { success: true };
  },

  toggle: async ({ request }) => {
    const data = await request.formData();
    const id = data.get('id');

    const todo = await db.todos.findUnique({ where: { id } });
    await db.todos.update({
      where: { id },
      data: { done: !todo.done }
    });

    return { toggled: true };
  },

  delete: async ({ request }) => {
    const data = await request.formData();
    const id = data.get('id');

    await db.todos.delete({ where: { id } });
    return { deleted: true };
  }
} satisfies Actions;
svelte
<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
  import type { PageData, ActionData } from './$types';

  let { data, form } = $props<{ data: PageData, form?: ActionData }>();
</script>

<h1>Todos</h1>

{#if form?.success}
  <p class="success">Todo created!</p>
{/if}

<form method="POST" action="?/create">
  <input name="text" placeholder="What needs to be done?" required />
  <button type="submit">Add</button>
</form>

{#each data.todos as todo}
  <div>
    <form method="POST" action="?/toggle">
      <input type="hidden" name="id" value={todo.id} />
      <input
        type="checkbox"
        checked={todo.done}
        onchange={(e) => e.currentTarget.form?.requestSubmit()}
      />
      <span class:done={todo.done}>{todo.text}</span>
    </form>

    <form method="POST" action="?/delete">
      <input type="hidden" name="id" value={todo.id} />
      <button type="submit">Delete</button>
    </form>
  </div>
{/each}

<style>
  .done {
    text-decoration: line-through;
    opacity: 0.6;
  }
</style>
typescript
// src/routes/todos/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  return {
    todos: await db.todos.findMany()
  };
};

export const actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const text = data.get('text');

    if (!text) {
      return fail(400, { text, missing: true });
    }

    await db.todos.create({ data: { text, done: false } });
    return { success: true };
  },

  toggle: async ({ request }) => {
    const data = await request.formData();
    const id = data.get('id');

    const todo = await db.todos.findUnique({ where: { id } });
    await db.todos.update({
      where: { id },
      data: { done: !todo.done }
    });

    return { toggled: true };
  },

  delete: async ({ request }) => {
    const data = await request.formData();
    const id = data.get('id');

    await db.todos.delete({ where: { id } });
    return { deleted: true };
  }
} satisfies Actions;
svelte
<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
  import type { PageData, ActionData } from './$types';

  let { data, form } = $props<{ data: PageData, form?: ActionData }>();
</script>

<h1>待办事项</h1>

{#if form?.success}
  <p class="success">待办事项已创建!</p>
{/if}

<form method="POST" action="?/create">
  <input name="text" placeholder="需要做什么?" required />
  <button type="submit">添加</button>
</form>

{#each data.todos as todo}
  <div>
    <form method="POST" action="?/toggle">
      <input type="hidden" name="id" value={todo.id} />
      <input
        type="checkbox"
        checked={todo.done}
        onchange={(e) => e.currentTarget.form?.requestSubmit()}
      />
      <span class:done={todo.done}>{todo.text}</span>
    </form>

    <form method="POST" action="?/delete">
      <input type="hidden" name="id" value={todo.id} />
      <button type="submit">删除</button>
    </form>
  </div>
{/each}

<style>
  .done {
    text-decoration: line-through;
    opacity: 0.6;
  }
</style>

Progressive Enhancement

渐进式增强

svelte
<!-- src/routes/search/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageData, ActionData } from './$types';

  let { data, form } = $props<{ data: PageData, form?: ActionData }>();
  let isLoading = $state(false);
</script>

<form
  method="POST"
  use:enhance={() => {
    isLoading = true;

    return async ({ update }) => {
      await update();
      isLoading = false;
    };
  }}
>
  <input name="query" placeholder="Search..." />
  <button type="submit" disabled={isLoading}>
    {isLoading ? 'Searching...' : 'Search'}
  </button>
</form>

{#if form?.results}
  <ul>
    {#each form.results as result}
      <li>{result.title}</li>
    {/each}
  </ul>
{/if}
svelte
<!-- src/routes/search/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageData, ActionData } from './$types';

  let { data, form } = $props<{ data: PageData, form?: ActionData }>();
  let isLoading = $state(false);
</script>

<form
  method="POST"
  use:enhance={() => {
    isLoading = true;

    return async ({ update }) => {
      await update();
      isLoading = false;
    };
  }}
>
  <input name="query" placeholder="搜索..." />
  <button type="submit" disabled={isLoading}>
    {isLoading ? '搜索中...' : '搜索'}
  </button>
</form>

{#if form?.results}
  <ul>
    {#each form.results as result}
      <li>{result.title}</li>
    {/each}
  </ul>
{/if}

API Routes (+server.ts)

API路由(+server.ts)

REST Endpoints

REST端点

typescript
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ url }) => {
  const limit = Number(url.searchParams.get('limit')) || 10;
  const offset = Number(url.searchParams.get('offset')) || 0;

  const posts = await db.posts.findMany({
    take: limit,
    skip: offset,
    orderBy: { createdAt: 'desc' }
  });

  return json(posts);
};

export const POST: RequestHandler = async ({ request, locals }) => {
  if (!locals.user) {
    return json({ error: 'Unauthorized' }, { status: 401 });
  }

  const data = await request.json();

  const post = await db.posts.create({
    data: {
      title: data.title,
      content: data.content,
      authorId: locals.user.id
    }
  });

  return json(post, { status: 201 });
};
typescript
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ url }) => {
  const limit = Number(url.searchParams.get('limit')) || 10;
  const offset = Number(url.searchParams.get('offset')) || 0;

  const posts = await db.posts.findMany({
    take: limit,
    skip: offset,
    orderBy: { createdAt: 'desc' }
  });

  return json(posts);
};

export const POST: RequestHandler = async ({ request, locals }) => {
  if (!locals.user) {
    return json({ error: '未授权' }, { status: 401 });
  }

  const data = await request.json();

  const post = await db.posts.create({
    data: {
      title: data.title,
      content: data.content,
      authorId: locals.user.id
    }
  });

  return json(post, { status: 201 });
};

Dynamic API Routes

动态API路由

typescript
// src/routes/api/posts/[id]/+server.ts
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ params }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id }
  });

  if (!post) {
    throw error(404, 'Post not found');
  }

  return json(post);
};

export const PATCH: RequestHandler = async ({ params, request, locals }) => {
  if (!locals.user) {
    throw error(401, 'Unauthorized');
  }

  const post = await db.posts.findUnique({ where: { id: params.id } });

  if (post.authorId !== locals.user.id) {
    throw error(403, 'Forbidden');
  }

  const data = await request.json();
  const updated = await db.posts.update({
    where: { id: params.id },
    data: { title: data.title, content: data.content }
  });

  return json(updated);
};

export const DELETE: RequestHandler = async ({ params, locals }) => {
  if (!locals.user) {
    throw error(401, 'Unauthorized');
  }

  const post = await db.posts.findUnique({ where: { id: params.id } });

  if (post.authorId !== locals.user.id) {
    throw error(403, 'Forbidden');
  }

  await db.posts.delete({ where: { id: params.id } });

  return new Response(null, { status: 204 });
};
typescript
// src/routes/api/posts/[id]/+server.ts
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ params }) => {
  const post = await db.posts.findUnique({
    where: { id: params.id }
  });

  if (!post) {
    throw error(404, '文章未找到');
  }

  return json(post);
};

export const PATCH: RequestHandler = async ({ params, request, locals }) => {
  if (!locals.user) {
    throw error(401, '未授权');
  }

  const post = await db.posts.findUnique({ where: { id: params.id } });

  if (post.authorId !== locals.user.id) {
    throw error(403, '禁止访问');
  }

  const data = await request.json();
  const updated = await db.posts.update({
    where: { id: params.id },
    data: { title: data.title, content: data.content }
  });

  return json(updated);
};

export const DELETE: RequestHandler = async ({ params, locals }) => {
  if (!locals.user) {
    throw error(401, '未授权');
  }

  const post = await db.posts.findUnique({ where: { id: params.id } });

  if (post.authorId !== locals.user.id) {
    throw error(403, '禁止访问');
  }

  await db.posts.delete({ where: { id: params.id } });

  return new Response(null, { status: 204 });
};

Hooks

Hooks

Server Hooks (hooks.server.ts)

服务端Hooks(hooks.server.ts)

typescript
// src/hooks.server.ts
import { redirect, type Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';

// Authentication middleware
const auth: Handle = async ({ event, resolve }) => {
  const sessionToken = event.cookies.get('session');

  if (sessionToken) {
    event.locals.user = await getUserFromSession(sessionToken);
  }

  return resolve(event);
};

// Logging middleware
const logging: Handle = async ({ event, resolve }) => {
  const start = Date.now();
  const response = await resolve(event);
  const duration = Date.now() - start;

  console.log(`${event.request.method} ${event.url.pathname} ${response.status} ${duration}ms`);

  return response;
};

// Protected routes middleware
const protect: Handle = async ({ event, resolve }) => {
  if (event.url.pathname.startsWith('/admin')) {
    if (!event.locals.user?.isAdmin) {
      throw redirect(303, '/login');
    }
  }

  if (event.url.pathname.startsWith('/dashboard')) {
    if (!event.locals.user) {
      throw redirect(303, '/login');
    }
  }

  return resolve(event);
};

// Combine hooks in sequence
export const handle = sequence(auth, logging, protect);

// Error handling
export const handleError = async ({ error, event }) => {
  console.error('Error:', error);

  return {
    message: 'An unexpected error occurred',
    code: error?.code ?? 'UNKNOWN'
  };
};

// Fetch handling (modify requests)
export const handleFetch = async ({ request, fetch }) => {
  // Add auth headers to internal API calls
  if (request.url.startsWith('https://api.example.com')) {
    request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`);
  }

  return fetch(request);
};
typescript
// src/hooks.server.ts
import { redirect, type Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';

// 认证中间件
const auth: Handle = async ({ event, resolve }) => {
  const sessionToken = event.cookies.get('session');

  if (sessionToken) {
    event.locals.user = await getUserFromSession(sessionToken);
  }

  return resolve(event);
};

// 日志中间件
const logging: Handle = async ({ event, resolve }) => {
  const start = Date.now();
  const response = await resolve(event);
  const duration = Date.now() - start;

  console.log(`${event.request.method} ${event.url.pathname} ${response.status} ${duration}ms`);

  return response;
};

// 受保护路由中间件
const protect: Handle = async ({ event, resolve }) => {
  if (event.url.pathname.startsWith('/admin')) {
    if (!event.locals.user?.isAdmin) {
      throw redirect(303, '/login');
    }
  }

  if (event.url.pathname.startsWith('/dashboard')) {
    if (!event.locals.user) {
      throw redirect(303, '/login');
    }
  }

  return resolve(event);
};

// 按顺序组合hooks
export const handle = sequence(auth, logging, protect);

// 错误处理
export const handleError = async ({ error, event }) => {
  console.error('错误:', error);

  return {
    message: '发生了意外错误',
    code: error?.code ?? 'UNKNOWN'
  };
};

// Fetch处理(修改请求)
export const handleFetch = async ({ request, fetch }) => {
  // 为内部API调用添加认证头
  if (request.url.startsWith('https://api.example.com')) {
    request.headers.set('Authorization', `Bearer ${process.env.API_TOKEN}`);
  }

  return fetch(request);
};

Client Hooks (hooks.client.ts)

客户端Hooks(hooks.client.ts)

typescript
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';

export const handleError: HandleClientError = async ({ error, event }) => {
  console.error('Client error:', error);

  // Send to error tracking service
  if (typeof window !== 'undefined') {
    // Sentry, LogRocket, etc.
  }

  return {
    message: 'Something went wrong',
  };
};
typescript
// src/hooks.client.ts
import type { HandleClientError } from '@sveltejs/kit';

export const handleError: HandleClientError = async ({ error, event }) => {
  console.error('客户端错误:', error);

  // 发送至错误追踪服务
  if (typeof window !== 'undefined') {
    // Sentry、LogRocket等
  }

  return {
    message: '出了点问题',
  };
};

Environment Variables

环境变量

Static Environment Variables

静态环境变量

typescript
// src/lib/config.ts
import { env } from '$env/static/public';
import { env as privateEnv } from '$env/static/private';

// Public variables (available in browser)
export const PUBLIC_API_URL = env.PUBLIC_API_URL;
export const PUBLIC_SITE_NAME = env.PUBLIC_SITE_NAME;

// Private variables (server-only)
export const DATABASE_URL = privateEnv.DATABASE_URL;
export const SECRET_KEY = privateEnv.SECRET_KEY;
typescript
// src/lib/config.ts
import { env } from '$env/static/public';
import { env as privateEnv } from '$env/static/private';

// 公开变量(浏览器可访问)
export const PUBLIC_API_URL = env.PUBLIC_API_URL;
export const PUBLIC_SITE_NAME = env.PUBLIC_SITE_NAME;

// 私有变量(仅服务端)
export const DATABASE_URL = privateEnv.DATABASE_URL;
export const SECRET_KEY = privateEnv.SECRET_KEY;

Dynamic Environment Variables

动态环境变量

typescript
// src/routes/+page.server.ts
import { env } from '$env/dynamic/private';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  // Can change at runtime
  const apiUrl = env.API_URL;

  return {
    data: await fetch(apiUrl).then(r => r.json())
  };
};
Environment file (.env):
bash
undefined
typescript
// src/routes/+page.server.ts
import { env } from '$env/dynamic/private';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
  // 运行时可变更
  const apiUrl = env.API_URL;

  return {
    data: await fetch(apiUrl).then(r => r.json())
  };
};
环境文件(.env):
bash
undefined

Public (exposed to browser)

公开(暴露给浏览器)

PUBLIC_API_URL=https://api.example.com PUBLIC_ANALYTICS_ID=UA-123456789
PUBLIC_API_URL=https://api.example.com PUBLIC_ANALYTICS_ID=UA-123456789

Private (server-only)

私有(仅服务端)

DATABASE_URL=postgres://localhost:5432/mydb SECRET_KEY=super-secret-key STRIPE_SECRET_KEY=sk_live_abc123
undefined
DATABASE_URL=postgres://localhost:5432/mydb SECRET_KEY=super-secret-key STRIPE_SECRET_KEY=sk_live_abc123
undefined

Prerendering and SSR

预渲染与SSR

Prerendering Options

预渲染选项

typescript
// src/routes/blog/+page.ts
export const prerender = true; // Prerender at build time
export const ssr = true;        // Server-side render (default)
export const csr = true;        // Client-side render (default)
Prerender entire site:
javascript
// svelte.config.js
export default {
  kit: {
    prerender: {
      entries: ['*'],
      crawl: true
    }
  }
};
typescript
// src/routes/blog/+page.ts
export const prerender = true; // 构建时预渲染
export const ssr = true;        // 服务端渲染(默认)
export const csr = true;        // 客户端渲染(默认)
预渲染整个站点:
javascript
// svelte.config.js
export default {
  kit: {
    prerender: {
      entries: ['*'],
      crawl: true
    }
  }
};

Dynamic Prerendering

动态预渲染

typescript
// src/routes/blog/[slug]/+page.server.ts
import type { EntryGenerator, PageServerLoad } from './$types';

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

  return { post };
};

// Generate static pages for all posts at build time
export const entries: EntryGenerator = async () => {
  const posts = await db.posts.findMany();

  return posts.map(post => ({
    slug: post.slug
  }));
};

export const prerender = true;
typescript
// src/routes/blog/[slug]/+page.server.ts
import type { EntryGenerator, PageServerLoad } from './$types';

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

  return { post };
};

// 构建时为所有文章生成静态页面
export const entries: EntryGenerator = async () => {
  const posts = await db.posts.findMany();

  return posts.map(post => ({
    slug: post.slug
  }));
};

export const prerender = true;

Adapters

适配器

Vercel Adapter

Vercel适配器

bash
npm install -D @sveltejs/adapter-vercel
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
  kit: {
    adapter: adapter({
      runtime: 'edge', // or 'nodejs'
      regions: ['iad1', 'sfo1'],
      split: false
    })
  }
};
bash
npm install -D @sveltejs/adapter-vercel
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
  kit: {
    adapter: adapter({
      runtime: 'edge', // 或 'nodejs'
      regions: ['iad1', 'sfo1'],
      split: false
    })
  }
};

Netlify Adapter

Netlify适配器

bash
npm install -D @sveltejs/adapter-netlify
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-netlify';

export default {
  kit: {
    adapter: adapter({
      edge: false, // true for edge functions
      split: false
    })
  }
};
bash
npm install -D @sveltejs/adapter-netlify
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-netlify';

export default {
  kit: {
    adapter: adapter({
      edge: false, // true 表示使用边缘函数
      split: false
    })
  }
};

Node Adapter

Node适配器

bash
npm install -D @sveltejs/adapter-node
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter({
      out: 'build',
      precompress: true,
      envPrefix: 'MY_'
    })
  }
};
Run production server:
bash
npm run build
node build
bash
npm install -D @sveltejs/adapter-node
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter({
      out: 'build',
      precompress: true,
      envPrefix: 'MY_'
    })
  }
};
运行生产服务器:
bash
npm run build
node build

Static Adapter (SSG)

静态适配器(SSG)

bash
npm install -D @sveltejs/adapter-static
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: '200.html', // SPA fallback
      precompress: false
    })
  }
};
bash
npm install -D @sveltejs/adapter-static
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: '200.html', // SPA回退页
      precompress: false
    })
  }
};

Cloudflare Pages

Cloudflare Pages

bash
npm install -D @sveltejs/adapter-cloudflare
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter({
      routes: {
        include: ['/*'],
        exclude: ['<build>']
      }
    })
  }
};
bash
npm install -D @sveltejs/adapter-cloudflare
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter({
      routes: {
        include: ['/*'],
        exclude: ['<build>']
      }
    })
  }
};

Testing

测试

Unit Tests with Vitest

使用Vitest进行单元测试

typescript
// src/lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate } from './utils';

describe('formatDate', () => {
  it('formats date correctly', () => {
    const date = new Date('2024-01-15');
    expect(formatDate(date)).toBe('January 15, 2024');
  });
});
typescript
// src/lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate } from './utils';

describe('formatDate', () => {
  it('正确格式化日期', () => {
    const date = new Date('2024-01-15');
    expect(formatDate(date)).toBe('January 15, 2024');
  });
});

Component Tests

组件测试

typescript
// src/lib/components/Button.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import Button from './Button.svelte';

describe('Button', () => {
  it('renders with text', () => {
    const { getByText } = render(Button, {
      props: { text: 'Click me' }
    });

    expect(getByText('Click me')).toBeInTheDocument();
  });

  it('calls onclick handler', async () => {
    const handleClick = vi.fn();
    const { getByText } = render(Button, {
      props: { text: 'Click me', onclick: handleClick }
    });

    const button = getByText('Click me');
    await fireEvent.click(button);

    expect(handleClick).toHaveBeenCalledOnce();
  });
});
typescript
// src/lib/components/Button.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import Button from './Button.svelte';

describe('Button', () => {
  it('渲染带文本的按钮', () => {
    const { getByText } = render(Button, {
      props: { text: '点击我' }
    });

    expect(getByText('点击我')).toBeInDocument();
  });

  it('调用点击处理器', async () => {
    const handleClick = vi.fn();
    const { getByText } = render(Button, {
      props: { text: '点击我', onclick: handleClick }
    });

    const button = getByText('点击我');
    await fireEvent.click(button);

    expect(handleClick).toHaveBeen调用一次();
  });
});

E2E Tests with Playwright

使用Playwright进行E2E测试

typescript
// tests/login.test.ts
import { expect, test } from '@playwright/test';

test('user can log in', async ({ page }) => {
  await page.goto('/login');

  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('Dashboard');
});

test('login validation works', async ({ page }) => {
  await page.goto('/login');

  await page.click('button[type="submit"]');

  await expect(page.locator('.error')).toContainText('Please fill in all fields');
});
typescript
// tests/login.test.ts
import { expect, test } from '@playwright/test';

test('用户可以登录', async ({ page }) => {
  await page.goto('/login');

  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('仪表盘');
});

test('登录验证生效', async ({ page }) => {
  await page.goto('/login');

  await page.click('button[type="submit"]');

  await expect(page.locator('.error')).toContainText('请填写所有字段');
});

Load Function Tests

Load函数测试

typescript
// src/routes/blog/+page.server.test.ts
import { describe, it, expect, vi } from 'vitest';
import { load } from './+page.server';

vi.mock('$lib/server/database', () => ({
  db: {
    posts: {
      findMany: vi.fn(() => Promise.resolve([
        { id: '1', title: 'Post 1' },
        { id: '2', title: 'Post 2' }
      ]))
    }
  }
}));

describe('blog page load', () => {
  it('loads posts', async () => {
    const result = await load({ params: {}, url: new URL('http://localhost') } as any);

    expect(result.posts).toHaveLength(2);
    expect(result.posts[0].title).toBe('Post 1');
  });
});
typescript
// src/routes/blog/+page.server.test.ts
import { describe, it, expect, vi } from 'vitest';
import { load } from './+page.server';

vi.mock('$lib/server/database', () => ({
  db: {
    posts: {
      findMany: vi.fn(() => Promise.resolve([
        { id: '1', title: '文章1' },
        { id: '2', title: '文章2' }
      ]))
    }
  }
}));

describe('博客页面加载', () => {
  it('加载文章列表', async () => {
    const result = await load({ params: {}, url: new URL('http://localhost') } as any);

    expect(result.posts).toHaveLength(2);
    expect(result.posts[0].title).toBe('文章1');
  });
});

Advanced Patterns

高级模式

Parallel Loading

并行加载

typescript
// src/routes/dashboard/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
  // Load data in parallel
  const [user, notifications, settings] = await Promise.all([
    db.users.findUnique({ where: { id: locals.user.id } }),
    db.notifications.findMany({ where: { userId: locals.user.id } }),
    db.settings.findUnique({ where: { userId: locals.user.id } })
  ]);

  return {
    user,
    notifications,
    settings
  };
};
typescript
// src/routes/dashboard/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
  // 并行加载数据
  const [user, notifications, settings] = await Promise.all([
    db.users.findUnique({ where: { id: locals.user.id } }),
    db.notifications.findMany({ where: { userId: locals.user.id } }),
    db.settings.findUnique({ where: { userId: locals.user.id } })
  ]);

  return {
    user,
    notifications,
    settings
  };
};

Dependent Loading

依赖加载

typescript
// src/routes/profile/[username]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, parent }) => {
  // Wait for parent layout data
  const { user } = await parent();

  const profile = await db.profiles.findUnique({
    where: { username: params.username }
  });

  if (!profile) {
    throw error(404, 'Profile not found');
  }

  // Load posts only if profile exists
  const posts = await db.posts.findMany({
    where: { authorId: profile.id },
    orderBy: { createdAt: 'desc' }
  });

  return {
    profile,
    posts,
    isOwnProfile: user?.id === profile.id
  };
};
typescript
// src/routes/profile/[username]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, parent }) => {
  // 等待父布局数据加载完成
  const { user } = await parent();

  const profile = await db.profiles.findUnique({
    where: { username: params.username }
  });

  if (!profile) {
    throw error(404, '个人资料未找到');
  }

  // 仅当个人资料存在时加载文章
  const posts = await db.posts.findMany({
    where: { authorId: profile.id },
    orderBy: { createdAt: 'desc' }
  });

  return {
    profile,
    posts,
    isOwnProfile: user?.id === profile.id
  };
};

Invalidation and Reloading

失效与重载

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

  async function refresh() {
    // Reload current page data
    await invalidateAll();
  }

  async function refreshPosts() {
    // Reload specific data
    await invalidate('/api/posts');
  }

  async function refreshUser() {
    // Reload data depending on specific URL
    await invalidate(url => url.pathname.startsWith('/api/user'));
  }
</script>

<button onclick={refresh}>Refresh All</button>
<button onclick={refreshPosts}>Refresh Posts</button>
svelte
<script lang="ts">
  import { invalidate, invalidateAll } from '$app/navigation';
  import { page } from '$app/stores';

  async function refresh() {
    // 重载当前页面数据
    await invalidateAll();
  }

  async function refreshPosts() {
    // 重载特定数据
    await invalidate('/api/posts');
  }

  async function refreshUser() {
    // 重载与特定URL相关的数据
    await invalidate(url => url.pathname.startsWith('/api/user'));
  }
</script>

<button onclick={refresh}>全部刷新</button>
<button onclick={refreshPosts}>刷新文章</button>

Page Options

页面选项

typescript
// src/routes/admin/+page.ts
export const ssr = false;      // Disable server-side rendering
export const csr = true;       // Enable client-side rendering
export const prerender = false; // Disable prerendering
export const trailingSlash = 'always'; // /page/ instead of /page
typescript
// src/routes/admin/+page.ts
export const ssr = false;      // 禁用服务端渲染
export const csr = true;       // 启用客户端渲染
export const prerender = false; // 禁用预渲染
export const trailingSlash = 'always'; // 使用 /page/ 而非 /page

Deployment Examples

部署示例

Vercel Deployment

Vercel部署

bash
undefined
bash
undefined

Install Vercel CLI

安装Vercel CLI

npm install -g vercel
npm install -g vercel

Login

登录

vercel login
vercel login

Deploy

部署

vercel
vercel

Production deploy

生产环境部署

vercel --prod

**vercel.json**:
```json
{
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "framework": "sveltekit"
}
vercel --prod

**vercel.json**:
```json
{
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "framework": "sveltekit"
}

Docker Deployment (Node Adapter)

Docker部署(Node适配器)

Dockerfile:
dockerfile
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production

FROM node:20-alpine

WORKDIR /app
COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .

EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "build"]
bash
docker build -t my-sveltekit-app .
docker run -p 3000:3000 my-sveltekit-app
Dockerfile:
dockerfile
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production

FROM node:20-alpine

WORKDIR /app
COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .

EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "build"]
bash
docker build -t my-sveltekit-app .
docker run -p 3000:3000 my-sveltekit-app

Static Hosting (Netlify, GitHub Pages)

静态托管(Netlify、GitHub Pages)

bash
undefined
bash
undefined

Build static site

构建静态站点

npm run build
npm run build

Output in build/ directory

输出在 build/ 目录

Deploy build/ to static host

将 build/ 部署至静态托管平台


**netlify.toml**:
```toml
[build]
  command = "npm run build"
  publish = "build"

[[redirects]]
  from = "/*"
  to = "/200.html"
  status = 200

**netlify.toml**:
```toml
[build]
  command = "npm run build"
  publish = "build"

[[redirects]]
  from = "/*"
  to = "/200.html"
  status = 200

Best Practices

最佳实践

  1. Use
    +page.server.ts
    for sensitive operations
    - Keep secrets server-side
  2. Leverage progressive enhancement - Forms work without JavaScript
  3. Use
    $types
    for type safety
    - Auto-generated types from SvelteKit
  4. Implement error boundaries - Use
    +error.svelte
    for graceful errors
  5. Optimize images - Use
    @sveltejs/enhanced-img
    for automatic optimization
  6. Enable prerendering - Static pages are faster and cheaper
  7. Use parallel loading -
    Promise.all()
    for concurrent data fetching
  8. Validate form data - Use Zod or similar for schema validation
  9. Set security headers - Use hooks for CSP, CORS, etc.
  10. Test with Playwright - E2E tests prevent regressions
  1. 使用
    +page.server.ts
    处理敏感操作
    - 敏感数据保留在服务端
  2. 利用渐进式增强 - 表单在无JavaScript时也能工作
  3. 使用
    $types
    保障类型安全
    - SvelteKit自动生成类型
  4. 实现错误边界 - 使用
    +error.svelte
    优雅处理错误
  5. 优化图片 - 使用
    @sveltejs/enhanced-img
    自动优化
  6. 启用预渲染 - 静态页面更快、成本更低
  7. 使用并行加载 - 用
    Promise.all()
    并发获取数据
  8. 验证表单数据 - 使用Zod等工具进行 schema 验证
  9. 设置安全头 - 通过hooks配置CSP、CORS等
  10. 使用Playwright测试 - E2E测试防止回归问题

Common Patterns

常见模式

Authentication Flow

认证流程

typescript
// src/routes/login/+page.server.ts
export const actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const user = await authenticate(data.get('email'), data.get('password'));

    cookies.set('session', user.sessionToken, {
      path: '/',
      httpOnly: true,
      sameSite: 'strict',
      secure: true,
      maxAge: 60 * 60 * 24 * 7
    });

    throw redirect(303, '/dashboard');
  }
};
typescript
// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
  const sessionToken = event.cookies.get('session');
  if (sessionToken) {
    event.locals.user = await getUserFromSession(sessionToken);
  }
  return resolve(event);
};
typescript
// src/routes/login/+page.server.ts
export const actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const user = await authenticate(data.get('email'), data.get('password'));

    cookies.set('session', user.sessionToken, {
      path: '/',
      httpOnly: true,
      sameSite: 'strict',
      secure: true,
      maxAge: 60 * 60 * 24 * 7
    });

    throw redirect(303, '/dashboard');
  }
};
typescript
// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
  const sessionToken = event.cookies.get('session');
  if (sessionToken) {
    event.locals.user = await getUserFromSession(sessionToken);
  }
  return resolve(event);
};

Protected Routes

受保护路由

typescript
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
  if (!locals.user) {
    throw redirect(303, '/login');
  }

  return {
    user: locals.user
  };
};
typescript
// src/routes/dashboard/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals }) => {
  if (!locals.user) {
    throw redirect(303, '/login');
  }

  return {
    user: locals.user
  };
};

Form Validation

表单验证

typescript
// src/routes/register/+page.server.ts
import { fail } from '@sveltejs/kit';
import { z } from 'zod';
import type { Actions } from './$types';

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword']
});

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const formData = Object.fromEntries(data);

    const result = schema.safeParse(formData);

    if (!result.success) {
      return fail(400, {
        errors: result.error.flatten().fieldErrors,
        data: formData
      });
    }

    await createUser(result.data);
    throw redirect(303, '/login');
  }
} satisfies Actions;
typescript
// src/routes/register/+page.server.ts
import { fail } from '@sveltejs/kit';
import { z } from 'zod';
import type { Actions } from './$types';

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: "密码不匹配",
  path: ['confirmPassword']
});

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const formData = Object.fromEntries(data);

    const result = schema.safeParse(formData);

    if (!result.success) {
      return fail(400, {
        errors: result.error.flatten().fieldErrors,
        data: formData
      });
    }

    await createUser(result.data);
    throw redirect(303, '/login');
  }
} satisfies Actions;

Resources

资源

Summary

总结

  • SvelteKit is the official full-stack framework for Svelte
  • File-based routing with
    +page.svelte
    ,
    +layout.svelte
    ,
    +server.ts
  • Load functions provide type-safe data fetching (universal and server-only)
  • Form actions enable progressive enhancement with native HTML forms
  • SSR/SSG/SPA modes with per-route control via
    prerender
    ,
    ssr
    ,
    csr
  • Adapters deploy to any platform (Vercel, Netlify, Node, Cloudflare, static)
  • Hooks provide middleware-like functionality for auth, logging, error handling
  • TypeScript-first with auto-generated
    $types
    for complete type safety
  • Environment variables with
    $env/static
    and
    $env/dynamic
    modules
  • Testing with Vitest (unit) and Playwright (E2E)
  • SvelteKit是Svelte官方的全栈框架
  • 基于文件的路由,使用
    +page.svelte
    +layout.svelte
    +server.ts
  • Load函数提供类型安全的数据获取(通用和仅服务端)
  • 表单操作通过原生HTML表单实现渐进式增强
  • SSR/SSG/SPA模式,支持按路由通过
    prerender
    ssr
    csr
    控制
  • 适配器可部署至任意平台(Vercel、Netlify、Node、Cloudflare、静态托管)
  • Hooks提供类中间件功能,用于认证、日志、错误处理
  • TypeScript优先,自动生成
    $types
    保障完全类型安全
  • 环境变量通过
    $env/static
    $env/dynamic
    模块管理
  • 测试支持Vitest(单元测试)和Playwright(E2E测试)