Svelte 使用 SPA 路由 History 模式
约 1575 字大约 5 分钟
2025-11-19
TODO:待完善
在 Svelte 生态系统中,如果你不使用 SvelteKit(它自带基于文件系统的路由),而是使用标准的 Vite + Svelte 搭建 SPA(单页应用),最合适、社区接受度最高且功能完备的库是 svelte-navigator。
- 原生支持 History 模式:默认就是 History API,不像某些库(如 svelte-spa-router)默认是 Hash 模式。
- 声明式路由:写法非常像 React Router (
<Router>, <Route>, <Link>),直观易懂。 - 无障碍支持(Accessibility):它会自动处理路由切换后的焦点管理,这对屏幕阅读器很友好。
- Hook 支持:提供了 useNavigate, useLocation, useParams 等实用的 Hooks。
npm install svelte-navigator假设你的入口文件是 App.svelte。
<script>
import { Router, Link, Route } from "svelte-navigator";
import Home from "./routes/Home.svelte";
import About from "./routes/About.svelte";
// User 详情页组件
import UserProfile from "./routes/UserProfile.svelte";
</script>
<!-- Router 组件包裹整个应用内容 -->
<Router>
<nav>
<!-- Link 组件用于导航,会自动处理 href 和点击事件 -->
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/user/123">用户 123</Link>
</nav>
<main>
<!-- Route 定义路径和对应的组件 -->
<!-- 基础路由 -->
<Route path="/" component={Home} />
<!-- 二级路由/普通路由 -->
<Route path="about" component={About} />
<!-- 带参数的路由 -->
<Route path="user/:id" component={UserProfile} />
</main>
</Router>
<style>
nav {
display: flex;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #ccc;
}
</style>路由懒加载 (Code Splitting)
Svelte 的懒加载非常优雅,不需要复杂的路由配置,直接利用 Svelte 原生的 {#await} 块配合动态 import() 即可。
<script>
import { Router, Link, Route } from "svelte-navigator";
import Home from "./routes/Home.svelte";
// 注意:不需要在顶部静态 import 那些大组件
</script>
<Router>
<nav>
<Link to="/">首页</Link>
<Link to="/heavy-page">大型页面 (懒加载)</Link>
</nav>
<Route path="/" component={Home} />
<!-- 懒加载实现 -->
<Route path="heavy-page">
<!-- 使用动态 import -->
{#await import('./routes/HeavyPage.svelte')}
<!-- 加载中状态 -->
<div>Loading...</div>
{:then module}
<!-- 加载成功,渲染组件,module.default 是组件本身 -->
<svelte:component this={module.default} />
{:catch error}
<!-- 错误处理 -->
<div>加载失败: {error.message}</div>
{/await}
</Route>
</Router>优点:Vite 会自动将 HeavyPage.svelte 分割成单独的 chunk(js文件),只有当用户访问该路由时才会下载。
常用实用选项与功能
获取路由参数 (useParams)
在 UserProfile.svelte (路径: /user/:id) 中:
<script>
import { useParams } from "svelte-navigator";
// useParams 返回一个 store
const params = useParams();
// 响应式获取 id
$: userId = $params.id;
</script>
<h1>用户 ID: {userId}</h1>编程式导航 (useNavigate)
在代码逻辑中跳转(例如登录成功后):
<script>
import { useNavigate } from "svelte-navigator";
const navigate = useNavigate();
function handleLogin() {
// 执行登录逻辑...
// 跳转到首页
navigate("/", { replace: true }); // replace: true 替换当前历史记录
// 或者后退
// navigate(-1);
}
</script>
<button on:click={handleLogin}>登录</button>404 页面 (通配符路由)
通常放在所有 Route 的最后面:
<script>
import NotFound from "./routes/NotFound.svelte";
</script>
<!-- 其他路由... -->
<!-- path="*" 匹配所有未命中的路径 -->
<Route path="*">
<NotFound />
</Route>路由守卫 (Protected Route)
Svelte 中做路由守卫通常是创建一个包装组件。
PrivateRoute.svelte:
<script>
import { Route, useNavigate } from "svelte-navigator";
import { onMount } from "svelte";
export let path;
export let component;
// 假设你有一个 store 存储登录状态
// import { isLoggedIn } from '../store';
let isLoggedIn = false; // 模拟未登录
const navigate = useNavigate();
onMount(() => {
if (!isLoggedIn) {
navigate("/login", { replace: true });
}
});
</script>
{#if isLoggedIn}
<Route {path} {component} />
{/if}在 svelte-navigator 中添加路由过渡动画(尤其是“纯透明度渐变”且不破坏布局),最大的挑战在于:当旧页面淡出、新页面淡入时,它们会同时存在于 DOM 中。如果不处理,新页面会被挤到旧页面下方。
最完美的解决方案是创建一个过渡包装组件,并利用 CSS Grid 让新旧页面在动画期间重叠在一起。
完整的实现步骤:
- 创建过渡包装组件
我们需要创建一个组件(例如 RouteTransitions.svelte),它必须作为 <Router> 的子组件使用,因为我们需要调用 useLocation 钩子。
在 src 下新建文件 RouteTransitions.svelte:
<!-- src/RouteTransitions.svelte -->
<script>
import { useLocation } from "svelte-navigator";
import { fade } from "svelte/transition";
const location = useLocation();
// 设置动画持续时间(毫秒)
const duration = 300;
</script>
<!--
核心原理:
使用 Grid 布局让子元素重叠在同一个网格区域 (grid-area: 1/1)。
这样当一个页面 fade-out,另一个 page fade-in 时,它们是原位重叠的。
-->
<div class="transition-container">
{#key $location.pathname}
<div
class="transition-wrapper"
in:fade={{ duration: duration, delay: duration }}
out:fade={{ duration: duration }}
>
<!-- 这里通过 slot 插入具体的 Route 定义 -->
<slot />
</div>
{/key}
</div>
<style>
.transition-container {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr;
width: 100%;
height: 100%; /* 确保占满父容器 */
overflow: hidden; /* 防止动画溢出 */
}
.transition-wrapper {
grid-area: 1 / 1; /* 强制新旧页面处于同一位置 */
width: 100%;
height: 100%;
}
</style>注意动画参数:
in:fade={{ delay: duration }}: 这是一个顺序淡入淡出的技巧。让新页面等待旧页面淡出后再淡入。- 如果你想要交叉淡入淡出(Cross Fade,新旧同时变),请删除
delay: duration。
- 集成到 App.svelte
在主文件中,将 <Route> 组件包裹在我们刚才写的 <RouteTransitions> 中。
<!-- src/App.svelte -->
<script>
import { Router, Link, Route } from "svelte-navigator";
import RouteTransitions from "./RouteTransitions.svelte";
import Home from "./routes/Home.svelte";
import About from "./routes/About.svelte";
</script>
<Router>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
</nav>
<!-- 将路由定义包裹在过渡组件中 -->
<main>
<RouteTransitions>
<!--
注意:Route 必须放在这里。
每次路径变化,RouteTransitions 会触发 #key 更新,
导致内部的 Route 销毁并重新挂载,从而触发动画。
-->
<Route path="/" component={Home} />
<Route path="about" component={About} />
</RouteTransitions>
</main>
</Router>
<style>
/* 简单的布局样式 */
nav {
padding: 1rem;
border-bottom: 1px solid #eee;
margin-bottom: 1rem;
}
main {
position: relative;
min-height: 500px; /* 给容器一个高度,防止动画时高度坍塌 */
}
</style>两种常见的 Fade 效果对比与调整
方案 A:交叉淡入淡出 (Cross Fade) —— 推荐
旧页面变淡的同时,新页面变实。视觉上最平滑,感觉像原生应用。
修改代码:去掉 in 的 delay。
CSS要求:必须使用上面提供的 grid-area: 1/1 样式,否则页面会上下跳动。
<!-- 修改 RouteTransitions.svelte -->
<div
class="transition-wrapper"
in:fade={{ duration: 300 }}
out:fade={{ duration: 300 }}
>
<slot />
</div>方案 B:先出后进 (Sequence Fade)
旧页面完全消失变白,新页面再慢慢显示。
修改代码:保留 delay。
CSS要求:Grid 布局依然推荐,防止布局闪烁。
<!-- 修改 RouteTransitions.svelte -->
<div
class="transition-wrapper"
in:fade={{ duration: 300, delay: 300 }}
out:fade={{ duration: 300 }}
>
<slot />
</div>