MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SvelteKit 路由最佳实践:构建高效可维护的前端应用

2024-02-157.9k 阅读

SvelteKit 路由基础

路由文件结构

在 SvelteKit 中,路由是基于文件系统的。项目的 src/routes 目录是路由定义的核心区域。每个文件和目录在这个结构中都代表了一个路由。例如,src/routes/index.svelte 代表应用的根路由,访问应用的根路径时,该组件会被渲染。

假设我们有一个简单的博客应用,在 src/routes/blog 目录下,src/routes/blog/index.svelte 会是博客列表页面的路由组件,负责展示所有博客文章的列表。而 src/routes/blog/[slug].svelte 则用于展示单个博客文章,这里的 [slug] 是一个动态参数,它可以匹配任何路径段。例如,/blog/my-first-post 这样的路径会匹配到 [slug].svelte 组件,并将 my-first-post 作为 slug 参数传递给组件。

基本路由导航

在 SvelteKit 应用中,导航到不同路由可以使用 <a> 标签或者 $app/navigation 模块中的函数。使用 <a> 标签进行导航是最直观的方式,就像在普通 HTML 中一样。例如:

<a href="/about">About Us</a>

这样,当用户点击 “About Us” 链接时,应用会导航到 /about 路由对应的页面。

如果想通过 JavaScript 代码控制导航,可以使用 $app/navigation 模块中的 goto 函数。首先,需要从该模块导入 goto

<script>
  import { goto } from '$app/navigation';
  function navigateToAbout() {
    goto('/about');
  }
</script>

<button on:click={navigateToAbout}>Go to About</button>

这里,当按钮被点击时,goto('/about') 函数会触发导航到 /about 路由。

动态路由

动态路由参数的使用

动态路由参数在构建灵活的应用时非常有用。以之前提到的博客应用为例,src/routes/blog/[slug].svelte 组件可以接收 slug 参数来展示特定的博客文章。在组件中,可以通过 $page.params 获取这些参数。

<script>
  import { $page } from '$app/stores';
  const { slug } = $page.params;
  // 根据 slug 从 API 获取对应的博客文章数据
  let blogPost;
  // 模拟从 API 获取数据
  if (slug === 'my-first-post') {
    blogPost = {
      title: 'My First Post',
      content: 'This is the content of my first post.'
    };
  }
</script>

<h1>{blogPost.title}</h1>
<p>{blogPost.content}</p>

在这个例子中,通过解构 $page.params 得到 slug 参数,然后根据 slug 的值从模拟的数据源中获取对应的博客文章数据并展示。

动态路由与数据预取

SvelteKit 支持在路由组件加载前预取数据。对于动态路由,可以在 load 函数中进行数据预取。例如,在 src/routes/blog/[slug].svelte 组件中:

<script context="module">
  export async function load({ params }) {
    const response = await fetch(`https://api.example.com/blog/${params.slug}`);
    const data = await response.json();
    return {
      blogPost: data
    };
  }
</script>

<script>
  export let data;
  const { blogPost } = data;
</script>

<h1>{blogPost.title}</h1>
<p>{blogPost.content}</p>

load 函数中,通过 params 获取动态参数 slug,然后使用 fetch 从 API 获取博客文章数据。返回的数据通过 data 变量传递给组件,组件通过解构 data 来使用这些数据。

嵌套路由

嵌套路由的结构

嵌套路由允许在一个路由组件中嵌套其他路由组件,这在构建复杂布局时非常有用。例如,在一个电商应用中,产品详情页面可能有多个子视图,如产品描述、评论、相关产品等。可以通过嵌套路由来实现这种结构。

src/routes/products 目录下,products/index.svelte 可以是产品列表页面。而 products/[productId] 目录可以包含嵌套路由。例如,products/[productId]/index.svelte 是产品详情的主视图,products/[productId]/description.svelte 是产品描述子视图,products/[productId]/reviews.svelte 是产品评论子视图。

嵌套路由的实现

在父路由组件中,使用 <svelte:component>$page 来渲染嵌套路由。例如,在 products/[productId]/index.svelte 中:

<script>
  import { $page } from '$app/stores';
  const { data } = $page;
  const { product } = data;
</script>

<h1>{product.title}</h1>

{#if $page.route.id.endsWith('/products/[productId]/description')}
  <svelte:component this={import('./description.svelte')} product={product} />
{:else if $page.route.id.endsWith('/products/[productId]/reviews')}
  <svelte:component this={import('./reviews.svelte')} product={product} />
{/if}

这里,根据当前路由的 id,决定渲染哪个嵌套路由组件。并且可以将从父组件获取的数据(如 product)传递给嵌套路由组件。

在嵌套路由组件中,如 products/[productId]/description.svelte,可以接收传递过来的数据:

<script>
  export let product;
</script>

<p>{product.description}</p>

这样就实现了嵌套路由及其数据传递。

路由加载与错误处理

路由加载函数

SvelteKit 的路由组件可以定义 load 函数,用于在组件渲染前加载数据。load 函数接收一个参数对象,包含 params(动态路由参数)、fetch(用于发起网络请求的函数)等。

例如,在一个用户详情页面 src/routes/users/[userId].svelte 中:

<script context="module">
  export async function load({ params, fetch }) {
    const response = await fetch(`https://api.example.com/users/${params.userId}`);
    if (!response.ok) {
      throw new Error('User not found');
    }
    const user = await response.json();
    return {
      user
    };
  }
</script>

<script>
  export let data;
  const { user } = data;
</script>

<h1>{user.name}</h1>
<p>{user.email}</p>

load 函数中,通过 params 获取 userId,使用 fetch 从 API 获取用户数据。如果请求失败,抛出一个错误。

错误处理

在 SvelteKit 中,可以在 src/routes/__error.svelte 中定义全局错误处理组件。当 load 函数抛出错误时,会渲染这个组件。

<script>
  export let error;
</script>

<h1>Error</h1>
<p>{error.message}</p>

这里,error 是在 load 函数中抛出的错误对象,通过 error.message 展示错误信息。

也可以在单个路由组件中处理错误,例如:

<script context="module">
  export async function load({ params, fetch }) {
    try {
      const response = await fetch(`https://api.example.com/users/${params.userId}`);
      if (!response.ok) {
        throw new Error('User not found');
      }
      const user = await response.json();
      return {
        user
      };
    } catch (error) {
      return {
        status: 404,
        error: 'User not found'
      };
    }
  }
</script>

<script>
  export let data;
  if (data.status === 404) {
    // 自定义 404 页面处理
    <h1>404 - User Not Found</h1>
  } else {
    const { user } = data;
    <h1>{user.name}</h1>
    <p>{user.email}</p>
  }
</script>

在这个例子中,在 load 函数的 catch 块中返回一个包含 statuserror 的对象,在组件中根据 status 判断并进行自定义错误处理。

路由布局

创建布局组件

布局组件可以用于定义应用的通用结构,如导航栏、侧边栏、页脚等。在 SvelteKit 中,可以在 src/routes 目录下创建布局文件。例如,创建一个 _layout.svelte 文件:

<script>
  import Navbar from '$lib/Navbar.svelte';
  import Footer from '$lib/Footer.svelte';
</script>

<Navbar />
<slot />
<Footer />

这里,NavbarFooter 是自定义的组件,<slot> 用于插入具体路由组件的内容。

使用布局组件

在其他路由组件中,默认会使用 _layout.svelte 作为布局。例如,在 src/routes/about.svelte 中:

<script>
  // 组件逻辑
</script>

<h1>About Us</h1>
<p>Here is some information about our company.</p>

当访问 /about 路由时,_layout.svelte 的结构会被应用,NavbarFooter 会显示,而 about.svelte 的内容会插入到 <slot> 的位置。

如果某个路由不想使用默认布局,可以在该路由组件中设置 layout: false

<script context="module">
  export const layout = false;
</script>

<script>
  // 组件逻辑
</script>

<h1>Special Page Without Layout</h1>
<p>This page has its own unique layout.</p>

这样,这个路由组件就不会使用 _layout.svelte 的布局。

路由与 SEO

优化页面标题和元数据

对于 SEO 来说,页面标题和元数据非常重要。在 SvelteKit 中,可以在路由组件中设置这些信息。例如,在 src/routes/blog/[slug].svelte 组件中:

<script context="module">
  import { setHead } from '@sveltejs/kit';
  export async function load({ params }) {
    const response = await fetch(`https://api.example.com/blog/${params.slug}`);
    const blogPost = await response.json();
    setHead({
      title: blogPost.title,
      meta: [
        { name: 'description', content: blogPost.excerpt }
      ]
    });
    return {
      blogPost
    };
  }
</script>

<script>
  export let data;
  const { blogPost } = data;
</script>

<h1>{blogPost.title}</h1>
<p>{blogPost.content}</p>

这里使用 setHead 函数设置页面标题和元数据。title 被设置为博客文章的标题,meta 数组中定义了描述元数据。

处理页面重定向

合理的重定向对于 SEO 也很关键。在 SvelteKit 中,可以在 load 函数中进行重定向。例如,对于旧的博客文章链接可能需要重定向到新的链接:

<script context="module">
  export async function load({ params }) {
    const oldSlugMapping = {
      'old-blog-post': 'new-blog-post'
    };
    if (oldSlugMapping[params.slug]) {
      throw {
        status: 301,
        redirect: `/blog/${oldSlugMapping[params.slug]}`
      };
    }
    const response = await fetch(`https://api.example.com/blog/${params.slug}`);
    const blogPost = await response.json();
    return {
      blogPost
    };
  }
</script>

<script>
  export let data;
  const { blogPost } = data;
</script>

<h1>{blogPost.title}</h1>
<p>{blogPost.content}</p>

在这个例子中,如果 params.slug 匹配到旧的 slug 映射,就抛出一个包含 statusredirect 的对象,实现 301 重定向到新的博客文章链接。

路由性能优化

代码拆分与懒加载

SvelteKit 会自动进行代码拆分,只有在需要时才加载路由组件的代码。这对于提高应用的初始加载性能非常有帮助。例如,对于一些不常用的路由,如用户设置页面 src/routes/settings.svelte,在用户点击进入该页面时才加载其代码。

在嵌套路由中,嵌套的路由组件也会按需加载。例如,在电商应用的产品详情页面,产品评论子视图 products/[productId]/reviews.svelte 的代码只有在用户切换到评论标签时才会加载。

缓存策略

在路由数据加载时,可以设置合适的缓存策略来提高性能。例如,如果博客文章数据不经常变化,可以在 load 函数中使用 fetchcache 选项:

<script context="module">
  export async function load({ params }) {
    const response = await fetch(`https://api.example.com/blog/${params.slug}`, {
      cache: 'force-cache'
    });
    const blogPost = await response.json();
    return {
      blogPost
    };
  }
</script>

<script>
  export let data;
  const { blogPost } = data;
</script>

<h1>{blogPost.title}</h1>
<p>{blogPost.content}</p>

这里使用 cache: 'force-cache' 策略,优先从缓存中获取数据,如果缓存中有数据,即使数据可能不是最新的,也会使用缓存数据,从而提高加载速度。

路由与国际化

多语言路由

在国际化应用中,可能需要根据语言切换路由。SvelteKit 可以通过自定义路由规则来实现多语言路由。例如,对于英语和法语,可以这样定义路由结构:

src/routes
├── en
│   ├── index.svelte
│   ├── about.svelte
├── fr
│   ├── index.svelte
│   ├── about.svelte

这样,/en/fr 前缀分别代表英语和法语的路由。在应用中,可以通过语言选择器来切换路由。例如:

<script>
  import { goto } from '$app/navigation';
  function switchToFrench() {
    goto('/fr/about');
  }
  function switchToEnglish() {
    goto('/en/about');
  }
</script>

<button on:click={switchToFrench}>Switch to French</button>
<button on:click={switchToEnglish}>Switch to English</button>

语言相关数据加载

在不同语言的路由组件中,可能需要加载不同语言的数据。可以在 load 函数中根据当前语言加载相应的数据。例如,在 src/routes/en/about.sveltesrc/routes/fr/about.svelte 中:

// src/routes/en/about.svelte
<script context="module">
  export async function load() {
    const response = await fetch('https://api.example.com/en/about');
    const data = await response.json();
    return {
      aboutInfo: data
    };
  }
</script>

<script>
  export let data;
  const { aboutInfo } = data;
</script>

<h1>{aboutInfo.title}</h1>
<p>{aboutInfo.content}</p>
// src/routes/fr/about.svelte
<script context="module">
  export async function load() {
    const response = await fetch('https://api.example.com/fr/about');
    const data = await response.json();
    return {
      aboutInfo: data
    };
  }
</script>

<script>
  export let data;
  const { aboutInfo } = data;
</script>

<h1>{aboutInfo.title}</h1>
<p>{aboutInfo.content}</p>

这里根据不同的语言前缀,从不同的 API 端点加载相应语言的数据。

通过以上各种 SvelteKit 路由的最佳实践,可以构建出高效、可维护且功能丰富的前端应用,无论是小型项目还是大型复杂的应用,都能满足不同的需求。从基础的路由定义到动态路由、嵌套路由,再到路由的性能优化、SEO 以及国际化等方面,每一个环节都紧密相连,共同构成了一个完善的前端路由体系。