mcpField
S
sveltekit

sveltekit

github.com/bobmatnyc开发78447 次安装15 周前更新
代码仓库

关于 sveltekit

SvelteKit 是面向 Svelte 的全栈框架,供开发者构建 Web 应用。它结合基于文件的路由、灵活的渲染模式、表单处理和部署适配器。

主要功能

  1. 01基于文件的路由
  2. 02类型安全的数据加载
  3. 03渐进增强表单处理
  4. 04SSR、SSG 与 SPA 渲染
  5. 05API 路由和服务端钩子
  6. 06多种部署适配器

适用场景

  1. 01构建带动态路由的 Web 应用
  2. 02为页面加载服务端或客户端数据
  3. 03实现可渐进增强的表单
  4. 04部署 SSR 或静态生成站点

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill sveltekit

安装后在 Claude Code 中输入/sveltekit确认已加载。

附带文件metadata.json

详细介绍

SvelteKit - 全栈 Svelte 框架

概述

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 端点

安装:

# Create new SvelteKit project
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

项目结构

标准 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

基于文件的路由

路由约定

文件名决定路由:

文件路由用途
+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-错误边界
基本路由
src/routes/
├── +page.svelte              # / (home)
├── about/
│   └── +page.svelte          # /about
├── contact/
│   └── +page.svelte          # /contact
└── pricing/
    └── +page.svelte          # /pricing
动态路由
src/routes/
└── blog/
    ├── +page.svelte          # /blog (list)
    ├── [slug]/
    │   └── +page.svelte      # /blog/my-post
    └── [category]/
        └── [slug]/
            └── +page.svelte  # /blog/tech/my-post

访问路由参数:


<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/
└── archive/
    └── [[year]]/
        └── [[month]]/
            └── +page.svelte  # /archive, /archive/2024, /archive/2024/11
// 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 参数
src/routes/
└── docs/
    └── [...path]/
        └── +page.svelte      # /docs/guide/intro, /docs/api/reference
// 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 函数

通用 Load(+page.ts)

在服务器端和客户端都会运行。必须使用 fetch 获取数据。

// 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
仅服务器端的 Load(+page.server.ts)

仅在服务器端运行。允许直接访问数据库。

// 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
  };
};
使用 Promise 流式传输
// 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' } })
  };
};

<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}

布局

共享布局

<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>
布局 Load
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    user: locals.user || null
  };
};
嵌套布局
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

使用 (name) 的布局组不会影响 URL 结构:

  • /dashboard 而不是 /(app)/dashboard
脱离布局

<script>
  let { children } = $props();
</script>

<div class="admin">
  {@render children()}
</div>


<form method="POST">
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">Login</button>
</form>

表单操作

基本表单操作
// 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;

<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>
命名操作
// 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;

<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>
渐进增强

<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}

API 路由(+server.ts)

REST 端点
// 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 });
};
动态 API 路由
// 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 });
};

Hooks

服务器端 Hooks(hooks.server.ts)
// 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);
};
客户端 Hooks(hooks.client.ts)
// 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',
  };
};

环境变量

静态环境变量
// 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;
动态环境变量
// 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())
  };
};

环境文件(.env):

# Public (exposed to browser)
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

预渲染和 SSR

预渲染选项
// 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)

预渲染整个站点:

// svelte.config.js
export default {
  kit: {
    prerender: {
      entries: ['*'],
      crawl: true
    }
  }
};
动态预渲染
// 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;

适配器

Vercel 适配器
npm install -D @sveltejs/adapter-vercel
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

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

export default {
  kit: {
    adapter: adapter({
      edge: false, // true for edge functions
      split: false
    })
  }
};
Node 适配器
npm install -D @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter({
      out: 'build',
      precompress: true,
      envPrefix: 'MY_'
    })
  }
};

运行生产服务器:

npm run build
node build
静态适配器(SSG)
npm install -D @sveltejs/adapter-static
// 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
    })
  }
};
Cloudflare Pages
npm install -D @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

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

测试

使用 Vitest 进行单元测试
// 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');
  });
});
组件测试
// 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();
  });
});
使用 Playwright 进行端到端测试
// 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"]', '[email protected]');
  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');
});
Load 函数测试
// 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');
  });
});

高级模式

并行加载
// 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
  };
};
依赖加载
// 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
  };
};
失效和重新加载
<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>
页面选项
// 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

部署示例

Vercel 部署
# Install Vercel CLI
npm install -g vercel

# Login
vercel login

# Deploy
vercel

# Production deploy
vercel --prod

vercel.json:

{
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "framework": "sveltekit"
}
Docker 部署(Node 适配器)

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"]
docker build -t my-sveltekit-app .
docker run -p 3000:3000 my-sveltekit-app
静态托管(Netlify、GitHub Pages)
# Build static site
npm run build

# Output in build/ directory
# Deploy build/ to static host

netlify.toml:

[build]
  command = "npm run build"
  publish = "build"

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

最佳实践

  1. 使用 +page.server.ts 执行敏感操作 - 将密钥保留在服务器端
  2. 利用渐进式增强 - 表单无需 JavaScript 也能正常工作
  3. 使用 $types 确保类型安全 - SvelteKit 自动生成类型
  4. 实现错误边界 - 使用 +error.svelte 优雅地处理错误
  5. 优化图片 - 使用 @sveltejs/enhanced-img 自动优化图片
  6. 启用预渲染 - 静态页面速度更快、成本更低
  7. 并行加载 - 使用 Promise.all() 并发获取数据
  8. 验证表单数据 - 使用 Zod 或类似工具进行模式验证
  9. 设置安全标头 - 使用钩子配置 CSP、CORS 等
  10. 使用 Playwright 测试 - 端到端测试可防止回归

常见模式

身份验证流程
// 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');
  }
};
// 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);
};
受保护的路由
// 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
  };
};
表单验证
// 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;

资源

总结

  • SvelteKit 是 Svelte 官方的全栈框架
  • 使用 +page.svelte、+layout.svelte、+server.ts 实现基于文件的路由
  • 加载函数提供类型安全的数据获取(通用和仅服务器端)
  • 表单操作通过原生 HTML 表单实现渐进式增强
  • SSR/SSG/SPA 模式可通过 prerender、ssr、csr 按路由控制
  • 适配器可部署到任何平台(Vercel、Netlify、Node、Cloudflare、静态托管)
  • 钩子提供类似中间件的功能,用于身份验证、日志记录和错误处理
  • TypeScript 优先,自动生成 $types,实现完整的类型安全
  • 使用 $env/static 和 $env/dynamic 模块处理环境变量
  • 使用 Vitest(单元测试)和 Playwright(端到端测试)进行测试

评论

支持 Markdown