sveltekit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSvelteKit - 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 directory structure
src/routes/ - 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 for type safety
$types - Hooks: Middleware-like ,
handle,handleErrorhandleFetch - API routes: files for REST endpoints
+server.ts
Installation:
bash
undefinedSvelteKit是Svelte官方的全栈框架,提供基于文件的路由、服务端渲染(SSR)、静态站点生成(SSG)、支持渐进式增强的表单处理,以及适用于任意平台的部署适配器。
核心特性:
- 基于文件的路由: 从目录结构自动生成路由
src/routes/ - Load函数: 类型安全的数据获取(、
+page.ts)+page.server.ts - 表单操作: 原生表单处理,支持渐进式增强
- SSR/SSG/SPA: 灵活的渲染模式,支持按路由控制
- 适配器: 可部署至Vercel、Netlify、Node.js、Cloudflare等平台
- TypeScript优先: 从生成类型,保障类型安全
$types - Hooks: 类中间件的、
handle、handleErrorhandleFetch - API路由: 通过文件创建REST端点
+server.ts
安装:
bash
undefinedCreate 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
undefinedundefinedProject 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.jsonmy-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.jsonFile-Based Routing
基于文件的路由
Route Conventions
路由约定
File naming determines routing:
| File | Route | Purpose |
|---|---|---|
| | Page component |
| - | Universal load (client + server) |
| - | Server-only load |
| - | Shared layout |
| - | Layout load |
| - | Server layout load |
| | API endpoint (GET/POST/etc) |
| - | Error boundary |
文件名决定路由:
| 文件 | 路由 | 用途 |
|---|---|---|
| | 页面组件 |
| - | 通用Load函数(客户端+服务端) |
| - | 仅服务端的Load函数 |
| - | 共享布局 |
| - | 布局Load函数 |
| - | 仅服务端的布局Load函数 |
| | API端点(GET/POST等) |
| - | 错误边界 |
Basic Routes
基础路由
src/routes/
├── +page.svelte # / (home)
├── about/
│ └── +page.svelte # /about
├── contact/
│ └── +page.svelte # /contact
└── pricing/
└── +page.svelte # /pricingsrc/routes/
├── +page.svelte # /(首页)
├── about/
│ └── +page.svelte # /about
├── contact/
│ └── +page.svelte # /contact
└── pricing/
└── +page.svelte # /pricingDynamic Routes
动态路由
src/routes/
└── blog/
├── +page.svelte # /blog (list)
├── [slug]/
│ └── +page.svelte # /blog/my-post
└── [category]/
└── [slug]/
└── +page.svelte # /blog/tech/my-postAccess 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/11typescript
// 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/11typescript
// 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/referencetypescript
// 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/referencetypescript
// 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 for data fetching.
fetchtypescript
// 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同时在服务端和客户端运行,必须使用进行数据获取。
fetchtypescript
// 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.svelteLayout groups with don't affect URL structure:
(name)- not
/dashboard/(app)/dashboard
src/routes/
├── +layout.svelte # 根布局(所有页面)
├── (app)/
│ ├── +layout.svelte # 应用布局(仪表盘、设置)
│ ├── dashboard/
│ │ └── +page.svelte # 使用:根布局 + 应用布局
│ └── settings/
│ └── +page.svelte
└── (marketing)/
├── +layout.svelte # 营销布局(关于我们、定价)
├── about/
│ └── +page.svelte # 使用:根布局 + 营销布局
└── pricing/
└── +page.svelte使用的布局组不影响URL结构:
(name)- 实际路由为而非
/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
undefinedtypescript
// 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
undefinedPublic (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
undefinedDATABASE_URL=postgres://localhost:5432/mydb
SECRET_KEY=super-secret-key
STRIPE_SECRET_KEY=sk_live_abc123
undefinedPrerendering 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-verceljavascript
// 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-verceljavascript
// 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-netlifyjavascript
// 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-netlifyjavascript
// 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-nodejavascript
// 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 buildbash
npm install -D @sveltejs/adapter-nodejavascript
// 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 buildStatic Adapter (SSG)
静态适配器(SSG)
bash
npm install -D @sveltejs/adapter-staticjavascript
// 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-staticjavascript
// 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-cloudflarejavascript
// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';
export default {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<build>']
}
})
}
};bash
npm install -D @sveltejs/adapter-cloudflarejavascript
// 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 /pagetypescript
// src/routes/admin/+page.ts
export const ssr = false; // 禁用服务端渲染
export const csr = true; // 启用客户端渲染
export const prerender = false; // 禁用预渲染
export const trailingSlash = 'always'; // 使用 /page/ 而非 /pageDeployment Examples
部署示例
Vercel Deployment
Vercel部署
bash
undefinedbash
undefinedInstall 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 /app/build build/
COPY /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-appDockerfile:
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 /app/build build/
COPY /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-appStatic Hosting (Netlify, GitHub Pages)
静态托管(Netlify、GitHub Pages)
bash
undefinedbash
undefinedBuild 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 = 200Best Practices
最佳实践
- Use for sensitive operations - Keep secrets server-side
+page.server.ts - Leverage progressive enhancement - Forms work without JavaScript
- Use for type safety - Auto-generated types from SvelteKit
$types - Implement error boundaries - Use for graceful errors
+error.svelte - Optimize images - Use for automatic optimization
@sveltejs/enhanced-img - Enable prerendering - Static pages are faster and cheaper
- Use parallel loading - for concurrent data fetching
Promise.all() - Validate form data - Use Zod or similar for schema validation
- Set security headers - Use hooks for CSP, CORS, etc.
- Test with Playwright - E2E tests prevent regressions
- 使用处理敏感操作 - 敏感数据保留在服务端
+page.server.ts - 利用渐进式增强 - 表单在无JavaScript时也能工作
- 使用保障类型安全 - SvelteKit自动生成类型
$types - 实现错误边界 - 使用优雅处理错误
+error.svelte - 优化图片 - 使用自动优化
@sveltejs/enhanced-img - 启用预渲染 - 静态页面更快、成本更低
- 使用并行加载 - 用并发获取数据
Promise.all() - 验证表单数据 - 使用Zod等工具进行 schema 验证
- 设置安全头 - 通过hooks配置CSP、CORS等
- 使用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
资源
- SvelteKit Docs: https://kit.svelte.dev/docs
- Svelte Tutorial: https://learn.svelte.dev
- Adapters: https://kit.svelte.dev/docs/adapters
- Deployment: https://kit.svelte.dev/docs/adapter-auto
- Discord: https://svelte.dev/chat
- SvelteKit文档: https://kit.svelte.dev/docs
- Svelte教程: https://learn.svelte.dev
- 适配器: https://kit.svelte.dev/docs/adapters
- 部署: https://kit.svelte.dev/docs/adapter-auto
- Discord社区: https://svelte.dev/chat
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,ssrcsr - 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 for complete type safety
$types - Environment variables with and
$env/staticmodules$env/dynamic - 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测试)