如何用Svelte 4、SvelteKit结合本地存储实现认证前阻止路由渲染
SvelteKit客户端本地存储认证路由保护方案
针对你遇到的受保护路由在重定向前提前渲染的问题,核心解决思路是在路由渲染流程的最早期拦截未认证请求,阻止页面内容输出。以下是具体实现方案:
1. 全局认证状态管理
创建一个Svelte Store同步本地存储的用户信息,统一管理登录/登出状态:
// src/lib/stores/auth.js import { writable } from 'svelte/store'; function createAuthStore() { // 仅在客户端环境读取localStorage const storedUser = typeof window !== 'undefined' ? JSON.parse(localStorage.getItem('user') || 'null') : null; const { subscribe, set } = writable(storedUser); return { subscribe, // 登录:保存用户信息到localStorage和Store login: (userData) => { localStorage.setItem('user', JSON.stringify(userData)); set(userData); }, // 登出:清除本地存储和Store状态 logout: () => { localStorage.removeItem('user'); set(null); } }; } export const authStore = createAuthStore();
2. 根布局级路由拦截(核心)
利用SvelteKit的根布局+layout.js的客户端Load函数,在所有页面渲染前检查认证状态,直接拦截未认证请求:
// src/routes/+layout.js import { redirect } from '@sveltejs/kit'; export async function load({ url }) { // 仅在客户端执行(服务端无localStorage) if (typeof window === 'undefined') return; const user = localStorage.getItem('user'); // 定义无需认证即可访问的公开路由 const publicRoutes = ['/login', '/register']; // 未登录且访问受保护路由:重定向到登录页,携带原路由参数 if (!user && !publicRoutes.includes(url.pathname)) { throw redirect(307, `/login?redirectTo=${encodeURIComponent(url.pathname)}`); } // 已登录却访问登录页:重定向到主页或原目标路由 if (user && url.pathname === '/login') { const redirectTo = url.searchParams.get('redirectTo') || '/'; throw redirect(307, redirectTo); } }
3. 登录页面实现
完成登录逻辑,登录成功后跳转到用户原本要访问的页面:
<!-- src/routes/login/+page.svelte --> <script> import { authStore } from '$lib/stores/auth'; import { page } from '$app/stores'; import { goto } from '$app/navigation'; let email = ''; let password = ''; async function handleLogin() { // 替换为你的实际认证逻辑(比如调用后端API验证) if (!email || !password) { alert('请输入邮箱和密码'); return; } // 模拟用户信息 const user = { email, id: '123' }; authStore.login(user); // 跳转到原目标路由,默认主页 const redirectTo = $page.url.searchParams.get('redirectTo') || '/'; await goto(redirectTo); } </script> <div class="login-container"> <h2>登录</h2> <input type="email" bind:value={email} placeholder="邮箱" required /> <input type="password" bind:value={password} placeholder="密码" required /> <button on:click={handleLogin}>登录</button> </div> <style> .login-container { max-width: 300px; margin: 2rem auto; padding: 1rem; border: 1px solid #eee; border-radius: 8px; } input { display: block; width: 100%; margin: 0.5rem 0; padding: 0.5rem; box-sizing: border-box; } button { width: 100%; padding: 0.5rem; background: #0070f3; color: white; border: none; border-radius: 4px; cursor: pointer; } </style>
4. 优化:避免服务端渲染时的内容泄漏
由于服务端无法访问localStorage,第一次加载受保护路由时,服务端会先渲染页面内容,客户端验证后才重定向。可通过根布局添加加载状态解决:
<!-- src/routes/+layout.svelte --> <script> import { onMount } from 'svelte'; import { authStore } from '$lib/stores/auth'; import { page } from '$app/stores'; import { goto } from '$app/navigation'; let isAuthChecked = false; onMount(async () => { const user = localStorage.getItem('user'); const publicRoutes = ['/login', '/register']; if (!user && !publicRoutes.includes($page.url.pathname)) { await goto(`/login?redirectTo=${encodeURIComponent($page.url.pathname)}`); } else if (user && $page.url.pathname === '/login') { const redirectTo = $page.url.searchParams.get('redirectTo') || '/'; await goto(redirectTo); } isAuthChecked = true; }); </script> {#if !isAuthChecked} <div style="text-align: center; margin-top: 2rem;">加载中...</div> {:else} <slot /> {/if}
关键注意事项
- 根布局的Load函数会在所有子路由之前执行,是拦截未认证请求的最佳时机,能彻底避免受保护路由渲染。
- 始终在客户端环境下访问
localStorage,避免服务端渲染时出现错误。 - 携带
redirectTo参数提升用户体验,让用户登录后直接回到原本要访问的页面。
内容的提问来源于stack exchange,提问作者Maciel Eliezer
相关产品推荐
相关产品推荐

