ARTICLE
sveltekit 数据加载与表单提交
sveltekit 数据加载与表单提交
July 24, 2025
11 MIN READ
数据加载h2
页面数据加载h3
SvelteKit 使用 +page.js
或 +page.server.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
<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
在服务端加载数据:
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
为动态路由创建数据加载:
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
创建联系表单:
<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
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
文件:
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
文件:
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
创建认证工具函数:
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
import { requireAdmin } from '$lib/auth.js';
export function load({ locals }) { requireAdmin(locals.user);
return { user: locals.user };}
import { redirectIfAuthenticated } from '$lib/auth.js';
export function load({ locals }) { redirectIfAuthenticated(locals.user);}
实践项目:受保护的管理后台(8-10 分钟)h2
现在让我们创建一个完整的管理后台系统:
创建管理后台布局h3
<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
<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
<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>