ARTICLE

sveltekit 数据加载与表单提交

sveltekit 数据加载与表单提交

July 24, 2025
11 MIN READ

数据加载h2

页面数据加载h3

SvelteKit 使用 +page.js+page.server.js 来加载数据。

让我们创建一个博客页面:

src/routes/blog/+page.js
export async function load() {
// 模拟从API获取数据
const posts = [
{
id: 1,
title: "SvelteKit入门指南",
excerpt: "从零开始学习SvelteKit...",
author: "希嘉嘉",
date: "2024-01-15",
slug: "sveltekit-guide",
},
{
id: 2,
title: "现代CSS技巧",
excerpt: "掌握现代CSS布局技术...",
author: "希嘉嘉",
date: "2024-01-10",
slug: "modern-css-tips",
},
{
id: 3,
title: "JavaScript最佳实践",
excerpt: "编写高质量的JavaScript代码...",
author: "希嘉嘉",
date: "2024-01-05",
slug: "javascript-best-practices",
},
];
return {
posts,
};
}

使用加载的数据h3

src/routes/blog/+page.svelte
<script lang="ts">
let { data }: {
data: {
posts: Array<{
title: string;
excerpt: string;
author: string;
date: string;
slug: string;
}>;
};
} = $props();
let { posts } = data;
</script>
<div class="max-w-4xl mx-auto p-5">
<h1 class="text-3xl font-bold text-gray-800 mb-8">博客文章</h1>
<div class="space-y-8">
{#each posts as post}
<article class="bg-white p-6 rounded-lg shadow-md">
<h2 class="text-xl font-semibold text-gray-800 mb-4">{post.title}</h2>
<p class="text-gray-600 leading-relaxed mb-5">{post.excerpt}</p>
<div class="flex gap-5 text-sm text-gray-500 mb-4">
<span>作者:{post.author}</span>
<span>{new Date(post.date).toLocaleDateString()}</span>
</div>
<a href="/blog/{post.slug}" class="text-orange-500 font-medium hover:underline">阅读全文</a>
</article>
{/each}
</div>
</div>

服务端数据加载h3

使用 +page.server.js 在服务端加载数据:

src/routes/products/+page.server.js
export async function load({ fetch, url }) {
// 获取查询参数
const search = url.searchParams.get("search") || "";
const category = url.searchParams.get("category") || "";
// 模拟API调用
const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
const posts = await response.json();
// 模拟产品数据
const products = [
{
id: 1,
name: "无线耳机",
price: 299,
category: "electronics",
image: "https://via.placeholder.com/300x200",
description: "高品质音效,长续航时间",
},
{
id: 2,
name: "智能手表",
price: 899,
category: "electronics",
image: "https://via.placeholder.com/300x200",
description: "健康监测,运动追踪",
},
{
id: 3,
name: "编程书",
price: 99,
category: "books",
image: "https://via.placeholder.com/300x200",
description: "深入浅出,实用性强",
},
];
// 过滤产品
let filteredProducts = $state(products);
if (search) {
filteredProducts = filteredProducts.filter(
(product) => product.name.toLowerCase().includes(search.toLowerCase()) || product.description.toLowerCase().includes(search.toLowerCase())
);
}
if (category) {
filteredProducts = filteredProducts.filter((product) => product.category === category);
}
return {
products: filteredProducts,
search,
category,
totalCount: filteredProducts.length,
};
}

动态路由数据加载h3

为动态路由创建数据加载:

src/routes/blog/[slug]/+page.server.js
export async function load({ params, fetch }) {
const { slug } = params;
// 模拟从API获取文章数据
const posts = {
"sveltekit-guide": {
title: "SvelteKit入门指南",
content: "SvelteKit是一个强大的全栈框架...",
author: "希嘉嘉",
date: "2024-01-15",
tags: ["SvelteKit", "前端", "教程"],
},
"modern-css-tips": {
title: "现代CSS技巧",
content: "CSS Grid和Flexbox让布局变得简单...",
author: "希嘉嘉",
date: "2024-01-10",
tags: ["CSS", "布局", "响应式"],
},
"javascript-best-practices": {
title: "JavaScript最佳实践",
content: "编写高质量的JavaScript代码...",
author: "希嘉嘉",
date: "2024-01-05",
tags: ["JavaScript", "最佳实践", "代码质量"],
},
};
const post = posts[slug];
if (!post) {
throw error(404, "文章未找到");
}
return {
post,
};
}

表单提交h2

基本表单处理h3

创建联系表单:

src/routes/contact/+page.svelte
<script>
import { enhance } from '$app/forms';
let formData = $state({
name: "",
email: "",
subject: "",
message: ""
});
let submitting = $state(false);
let success = $state(false);
let error = $state(null);
</script>
<div class="max-w-2xl mx-auto p-5">
<h1 class="text-3xl font-bold text-gray-800 mb-8">联系我们</h1>
{#if success}
<div class="text-center py-10 px-5 bg-green-50 border border-green-200 rounded-lg text-green-800">
<h3 class="text-xl font-semibold mb-3">消息发送成功!</h3>
<p class="mb-4">我们会尽快回复您。</p>
<button class="px-6 py-2 bg-green-600 text-white border-none rounded cursor-pointer hover:bg-green-700 transition-colors" onclick={() => success = false}>发送新消息</button>
</div>
{:else}
<form
method="POST"
use:enhance={() => {
submitting = true;
error = null;
return async ({ result }) => {
submitting = false;
if (result.type === 'success') {
success = true;
formData = { name: "", email: "", subject: "", message: "" };
} else {
error = result.error?.message || '发送失败,请重试';
}
};
}}
>
{#if error}
<div class="p-4 mb-6 bg-red-50 border border-red-200 rounded text-red-800">
{error}
</div>
{/if}
<div class="mb-6">
<label for="name" class="block mb-2 font-semibold text-gray-700">姓名 *</label>
<input
id="name"
name="name"
type="text"
bind:value={formData.name}
required
class="w-full px-4 py-3 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
<div class="mb-6">
<label for="email" class="block mb-2 font-semibold text-gray-700">邮箱 *</label>
<input
id="email"
name="email"
type="email"
bind:value={formData.email}
required
class="w-full px-4 py-3 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
<div class="mb-6">
<label for="subject" class="block mb-2 font-semibold text-gray-700">主题 *</label>
<input
id="subject"
name="subject"
type="text"
bind:value={formData.subject}
required
class="w-full px-4 py-3 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
<div class="mb-6">
<label for="message" class="block mb-2 font-semibold text-gray-700">消息 *</label>
<textarea
id="message"
name="message"
bind:value={formData.message}
rows="5"
required
class="w-full px-4 py-3 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-orange-500 resize-vertical"
></textarea>
</div>
<button type="submit" disabled={submitting} class="w-full py-3 px-6 bg-orange-500 text-white border-none rounded text-lg cursor-pointer hover:bg-orange-600 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed">
{submitting ? '发送中...' : '发送消息'}
</button>
</form>
{/if}
</div>

服务端表单处理h3

src/routes/contact/+page.server.js
import { fail } from "@sveltejs/kit";
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const name = data.get("name");
const email = data.get("email");
const subject = data.get("subject");
const message = data.get("message");
// 验证数据
if (!name || !email || !subject || !message) {
return fail(400, {
error: "请填写所有必填字段",
});
}
if (!email.includes("@")) {
return fail(400, {
error: "请输入有效的邮箱地址",
});
}
// 模拟发送邮件
console.log("发送邮件:", { name, email, subject, message });
// 这里可以添加实际的邮件发送逻辑
// await sendEmail({ name, email, subject, message });
return {
success: true,
};
},
};

中间件h2

服务端钩子h3

创建 hooks.server.js 文件:

src/hooks.server.js
import { redirect } from "@sveltejs/kit";
export async function handle({ event, resolve }) {
// 获取用户信息(模拟)
const user = await getUserFromSession(event);
event.locals.user = user;
// 记录请求日志
console.log(`${new Date().toISOString()} - ${event.request.method} ${event.url.pathname}`);
// 检查认证
if (event.url.pathname.startsWith("/admin") && !user?.isAdmin) {
throw redirect(302, "/login");
}
const response = await resolve(event);
// 添加安全头
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
return response;
}
async function getUserFromSession(event) {
// 模拟从session获取用户信息
const session = event.cookies.get("session");
if (session) {
// 这里应该验证session并返回用户信息
return {
id: 1,
name: "张三",
email: "zhangsan@example.com",
isAdmin: false,
};
}
return null;
}
export async function handleError({ error, event }) {
console.error("服务器错误:", error);
return {
message: "服务器内部错误",
code: error?.code ?? "UNKNOWN",
};
}

客户端钩子h3

创建 hooks.client.js 文件:

src/hooks.client.js
import { goto } from "$app/navigation";
import { page } from "$app/stores";
export async function handle({ event, resolve }) {
// 客户端路由拦截
if (event.url.pathname === "/old-page") {
throw redirect(301, "/new-page");
}
const response = await resolve(event);
return response;
}
// 监听页面变化
page.subscribe(($page) => {
// 发送页面浏览统计
if (typeof gtag !== "undefined") {
gtag("config", "GA_MEASUREMENT_ID", {
page_path: $page.url.pathname,
});
}
// 滚动到顶部
window.scrollTo(0, 0);
});

认证中间件h3

创建认证工具函数:

src/lib/auth.js
import { redirect } from "@sveltejs/kit";
export function requireAuth(user) {
if (!user) {
throw redirect(302, "/login");
}
}
export function requireAdmin(user) {
if (!user?.isAdmin) {
throw redirect(302, "/");
}
}
export function redirectIfAuthenticated(user) {
if (user) {
throw redirect(302, "/dashboard");
}
}

使用认证中间件h3

src/routes/admin/+layout.server.js
import { requireAdmin } from '$lib/auth.js';
export function load({ locals }) {
requireAdmin(locals.user);
return {
user: locals.user
};
}
src/routes/login/+page.server.js
import { redirectIfAuthenticated } from '$lib/auth.js';
export function load({ locals }) {
redirectIfAuthenticated(locals.user);
}

实践项目:受保护的管理后台(8-10 分钟)h2

现在让我们创建一个完整的管理后台系统:

创建管理后台布局h3

src/routes/admin/+layout.svelte
<script lang="ts">
let { data }: {
data: {
user: {
name: string;
};
};
} = $props();
let { user } = data;
let sidebarOpen = $state(false);
let menuItems = $state([
{ href: '/admin', label: '仪表板', icon: '📊' },
{ href: '/admin/users', label: '用户管理', icon: '👥' },
{ href: '/admin/posts', label: '文章管理', icon: '📝' },
{ href: '/admin/settings', label: '系统设置', icon: '⚙️' }
];
</script>
<div class="admin-layout">
<!-- 侧边栏 -->
<aside class="sidebar" class:open={sidebarOpen}>
<div class="sidebar-header">
<h2>管理后台</h2>
<button class="close-btn" onclick={() => sidebarOpen = false}>×</button>
</div>
<nav class="sidebar-nav">
{#each menuItems as item}
<a
href={item.href}
class:active={$page.url.pathname === item.href}
>
<span class="icon">{item.icon}</span>
<span class="label">{item.label}</span>
</a>
{/each}
</nav>
<div class="sidebar-footer">
<div class="user-info">
<p>欢迎,{user.name}</p>
<a href="/logout" class="logout-btn">退出登录</a>
</div>
</div>
</aside>
<!-- 主内容区 -->
<main class="main">
<header class="main-header">
<button class="menu-btn" onclick={() => sidebarOpen = true}>
</button>
<h1>管理后台</h1>
</header>
<div class="main-content">
<slot />
</div>
</main>
<!-- 遮罩层 -->
{#if sidebarOpen}
<div class="overlay" onclick={() => sidebarOpen = false}></div>
{/if}
</div>

创建仪表板页面h3

src/routes/admin/+page.svelte
<script>
let stats = $state({
users: 1250,
posts: 89,
comments: 456,
views: 12500
};
let recentUsers = $state([
{ id: 1, name: "张三", email: "zhangsan@example.com", joined: "2024-01-15" },
{ id: 2, name: "李四", email: "lisi@example.com", joined: "2024-01-14" },
{ id: 3, name: "王五", email: "wangwu@example.com", joined: "2024-01-13" }
];
let recentPosts = $state([
{ id: 1, title: "SvelteKit入门指南", author: "希嘉嘉", published: "2024-01-15" },
{ id: 2, title: "现代CSS技巧", author: "希嘉嘉", published: "2024-01-10" },
{ id: 3, title: "JavaScript最佳实践", author: "希嘉嘉", published: "2024-01-05" }
];
</script>
<div class="dashboard">
<h2>仪表板</h2>
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-content">
<h3>{stats.users}</h3>
<p>总用户数</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">📝</div>
<div class="stat-content">
<h3>{stats.posts}</h3>
<p>文章数量</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">💬</div>
<div class="stat-content">
<h3>{stats.comments}</h3>
<p>评论数量</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">👁️</div>
<div class="stat-content">
<h3>{stats.views}</h3>
<p>页面浏览</p>
</div>
</div>
</div>
<!-- 最近活动 -->
<div class="recent-activity">
<div class="recent-section">
<h3>最近注册用户</h3>
<div class="user-list">
{#each recentUsers as user}
<div class="user-item">
<div class="user-avatar">
{user.name.charAt(0)}
</div>
<div class="user-info">
<h4>{user.name}</h4>
<p>{user.email}</p>
<small>注册于 {user.joined}</small>
</div>
</div>
{/each}
</div>
</div>
<div class="recent-section">
<h3>最近发布文章</h3>
<div class="post-list">
{#each recentPosts as post}
<div class="post-item">
<h4>{post.title}</h4>
<p>作者: {post.author}</p>
<small>发布于 {post.published}</small>
</div>
{/each}
</div>
</div>
</div>
</div>

创建用户管理页面h3

src/routes/admin/users/+page.svelte
<script>
import { enhance } from '$app/forms';
let users = $state([
{ id: 1, name: "张三", email: "zhangsan@example.com", role: "user", status: "active" },
{ id: 2, name: "李四", email: "lisi@example.com", role: "admin", status: "active" },
{ id: 3, name: "王五", email: "wangwu@example.com", role: "user", status: "inactive" }
];
let searchQuery = $state("");
let selectedRole = $state("");
$: filteredUsers = users.filter(user => {
const matchesSearch = !searchQuery ||
user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.toLowerCase());
const matchesRole = !selectedRole || user.role === selectedRole;
return matchesSearch && matchesRole;
});
function deleteUser(id) {
if (confirm('确定要删除这个用户吗?')) {
users = users.filter(user => user.id !== id);
}
}
</script>
<div class="users-page">
<div class="page-header">
<h2>用户管理</h2>
<a href="/admin/users/new" class="add-btn">添加用户</a>
</div>
<!-- 筛选器 -->
<div class="filters">
<input
bind:value={searchQuery}
placeholder="搜索用户..."
type="text"
/>
<select bind:value={selectedRole}>
<option value="">所有角色</option>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<!-- 用户表格 -->
<div class="users-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{#each filteredUsers as user}
<tr>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<span class="role-badge {user.role}">
{user.role === 'admin' ? '管理员' : '用户'}
</span>
</td>
<td>
<span class="status-badge {user.status}">
{user.status === 'active' ? '活跃' : '非活跃'}
</span>
</td>
<td>
<div class="actions">
<a href="/admin/users/{user.id}/edit" class="edit-btn">编辑</a>
<button
class="delete-btn"
onclick={() => deleteUser(user.id)}
>
删除
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if filteredUsers.length === 0}
<div class="empty-state">
<p>没有找到匹配的用户</p>
</div>
{/if}
</div>