Svelte 鉴权守卫(auth-guard)组件实现与优化
1. 需求背景
在前端项目中,常常需要对部分页面进行访问控制(如:未登录用户不能访问某些页面,未完善资料的用户需跳转到资料完善页)。为此,我们实现了一个 Svelte 组件 auth-guard,用于统一处理页面的鉴权与重定向逻辑。
2. 主要思路
- 区分公开页面与受保护页面:如首页
/、登录页/login为公开页面,其余页面需登录后访问。 - 未登录时自动跳转到登录页。
- 已登录但未设置昵称时,强制跳转到设置昵称页
/set-uname。 - 所有鉴权逻辑在 slot 内容渲染前完成,避免未授权内容闪现。
3. 关键实现
3.1 公开页面路径配置
const publicPaths = ['/', '/login'];
const isPublicPath = (path: string) => publicPaths.includes(path);
3.2 鉴权检查函数
const checkAuth = () => {
if (!browser) return;
const currentPath = page.url.pathname;
if (isPublicPath(currentPath)) return;
if (!$auth.isAuthenticated) {
goto('/login', { replaceState: true });
return;
}
if ($auth.isAuthenticated && (!$user.nickname || $user.nickname === '') && currentPath !== '/set-uname') {
goto('/set-uname', { replaceState: true });
return;
}
};
3.3 响应式监听
利用 $effect 监听认证状态和路径变化,及时执行鉴权逻辑:
$effect(() => {
if ((browser && !$auth.isAuthenticated) || page.url.pathname) {
checkAuth();
}
});
3.4 渲染控制
通过 Svelte 的条件渲染,确保 slot 只在鉴权通过时才渲染:
{#if $auth.isLoading || $user.isLoading}
<!-- 加载状态 -->
{:else if isPublicPath(page.url.pathname)}
<slot />
{:else if $auth.isAuthenticated && ($user.nickname || page.url.pathname === '/set-uname')}
<slot />
{:else}
<div></div> <!-- 鉴权中或重定向中占位 -->
{/if}
4. 常见问题与优化
问题:slot 内容提前渲染导致未授权内容闪现
原因:如果在 onMount 里执行鉴权,Svelte 会先渲染 slot,再执行 onMount,导致未授权内容短暂可见。
优化:去掉 onMount 里的 checkAuth(),只用 $effect 响应式监听,并在 slot 渲染前加条件判断。
问题:首页(/)或公开页面因未设置昵称无法访问
原因:slot 渲染条件未考虑公开页面,导致首页等页面在未设置昵称时也不渲染。
优化:在渲染条件中加入 isPublicPath(page.url.pathname),公开页面直接渲染 slot。
5. 总结
- 鉴权逻辑应在 slot 渲染前完成,避免未授权内容闪现。
- 公开页面应单独处理,避免被鉴权逻辑误伤。
- 利用 Svelte 的响应式和条件渲染,可以优雅地实现前端页面的访问控制。
如果 Svelte 要是像vue一样,有路由守卫,就不用这么麻烦了!
贴上完整代码
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { browser } from '$app/environment';
import { auth, me } from '$lib/stores';
import { onMount } from 'svelte';
let { children } = $props();
// 检查当传入路径是否需要登录(默认检查当前路径)
const checkPublicPath = (path: string) => {
// 当前路径(不要写默认的,必须用地方传,否则会失去响应式)
// const currentPath = page.url.pathname;
// 不需要登录的页面路径
const publicPaths = ['/', '/login'];
return publicPaths.includes(path);
};
// 检查是否需要重定向
const checkAuth = () => {
console.log('执行了');
// 只在客户端执行重定向逻辑
if (!browser) return;
// 如果当前路径不需要登录,直接显示
if (checkPublicPath(page.url.pathname)) {
return;
}
// 如果未登录且当前路径需要登录,直接重定向到登录页
if (!$auth.isAuthenticated) {
goto('/login', { replaceState: true });
return;
}
// 如果已登录但未设置用户名,跳转到set-uname
if (!$me.username && page.url.pathname !== '/set-uname') {
goto('/set-uname', { replaceState: true });
return;
}
// 如果在 /set-uname 页面,且已经有 username,跳转到 /home
if (page.url.pathname === '/set-uname' && $me.username) {
goto('/home', { replaceState: true });
return;
}
};
let mounted = false;
onMount(async () => {
if ($auth.isAuthenticated && !checkPublicPath(page.url.pathname)) {
await me.sync();
}
// 后续这里不执行,交给effect监听路由变化去做
if(!mounted){
checkAuth();
console.log('首次');
}
mounted = true;
});
// 监听认证状态和路径变化
$effect(() => {
// 只在需要登录的页面才执行 checkAuth
if (!checkPublicPath(page.url.pathname) && mounted) {
checkAuth();
}
});
</script>
{#if $auth.isLoading || $me.isLoading}
<!-- 加载状态 -->
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="text-center">
<div class="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p class="text-gray-600">加载中...</p>
</div>
</div>
<!-- 不需要权限的正常显示页面内容 -->
{:else if checkPublicPath(page.url.pathname)}
<!-- <slot /> -->
{@render children()}
<!-- 需要权限的页面内容展示时机:需要等待鉴权完成 && 存在用户名字段或者正在设置用户名页面(如果不判断路由则会导致先到其它路由再回set-uname) -->
{:else if $auth.isAuthenticated && ($me.username || page.url.pathname === '/set-uname')}
<!-- <slot /> -->
{@render children()}
<!-- {:else}
<div>鉴权中...</div> -->
{/if}

浙公网安备 33010602011771号