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

SvelteKit 路由与状态管理:如何结合使用提升开发效率

2023-08-245.1k 阅读

SvelteKit 路由基础

在 SvelteKit 中,路由是基于文件系统的。这意味着,你只需要在 src/routes 目录下创建文件和目录,SvelteKit 就会自动为你生成相应的路由。

例如,在 src/routes 目录下创建一个 about.svelte 文件,SvelteKit 会自动生成一个 /about 的路由。这个 about.svelte 文件就如同一个普通的 Svelte 组件,但它同时也是一个路由组件,会在用户访问 /about 路径时被渲染。

<!-- src/routes/about.svelte -->
<script>
    // 这里可以编写组件相关的逻辑
</script>

<h1>About Page</h1>
<p>This is the about page of our application.</p>

如果想要创建嵌套路由,只需要在 src/routes 目录下创建子目录即可。比如,创建一个 src/routes/products 目录,然后在该目录下创建 index.svelte 文件,这个 index.svelte 文件对应的路由就是 /products。而如果在 products 目录下再创建一个 [productId].svelte 文件(方括号表示动态参数),那么这个文件对应的路由就是 /products/:productId,其中 productId 是动态参数,可以根据实际情况进行替换。

<!-- src/routes/products/[productId].svelte -->
<script>
    import { page } from '$app/stores';
    const { productId } = $page.params;
</script>

<h1>Product Details</h1>
<p>The details of product with ID: {productId}</p>

路由导航

在 SvelteKit 中进行路由导航非常简单。你可以使用 <a> 标签,只要其 href 属性的值是一个有效的 SvelteKit 路由路径,SvelteKit 就会自动处理导航,实现无刷新页面切换。

<!-- 在某个组件中 -->
<a href="/about">Go to About Page</a>

另外,SvelteKit 还提供了 goto 函数来进行编程式导航。你可以在组件的 script 部分导入并使用它。

<script>
    import { goto } from '$app/navigation';

    function goToProducts() {
        goto('/products');
    }
</script>

<button on:click={goToProducts}>Go to Products Page</button>

SvelteKit 状态管理基础

SvelteKit 本身并没有强制使用特定的状态管理库,但是它提供了一些内置的工具来帮助进行状态管理,特别是在应用程序级别。

$app/stores 模块提供了几个有用的存储。其中,page 存储包含了当前页面的信息,如路径、参数等,我们在前面的路由示例中已经使用过。

另一个重要的存储是 session 存储,它可以用于在不同页面之间共享状态。例如,假设你有一个用户登录状态,你可以在用户登录成功后,将登录状态存储到 session 中,然后在其他页面通过订阅 session 存储来获取这个状态。

<script>
    import { session } from '$app/stores';
    let user;
    $session.subscribe((data) => {
        user = data.user;
    });
</script>

{#if user}
    <p>Welcome, {user.name}</p>
{:else}
    <p>Please log in.</p>
{/if}

结合路由与状态管理提升开发效率

  1. 基于路由的状态初始化 在某些情况下,你可能希望根据不同的路由来初始化不同的状态。例如,在一个博客应用中,当用户访问文章列表页面时,你可能希望初始化文章列表数据;而当用户访问单个文章页面时,你希望初始化该文章的详细数据。
<!-- src/routes/articles/index.svelte -->
<script>
    import { onMount } from'svelte';
    import { articles } from '$lib/stores';

    onMount(() => {
        // 模拟从 API 获取文章列表
        fetch('/api/articles')
           .then((res) => res.json())
           .then((data) => {
                articles.set(data);
            });
    });
</script>

<h1>Article List</h1>
{#each $articles as article}
    <a href={`/articles/${article.id}`}>{article.title}</a>
{/each}
<!-- src/routes/articles/[articleId].svelte -->
<script>
    import { onMount } from'svelte';
    import { article } from '$lib/stores';
    import { page } from '$app/stores';
    const { articleId } = $page.params;

    onMount(() => {
        // 模拟从 API 获取单个文章
        fetch(`/api/articles/${articleId}`)
           .then((res) => res.json())
           .then((data) => {
                article.set(data);
            });
    });
</script>

<h1>{$article.title}</h1>
<p>{$article.content}</p>
  1. 状态变化驱动路由更新 有时候,状态的变化可能会触发路由的更新。例如,在一个多步骤表单应用中,当用户完成一个步骤并点击“下一步”时,表单状态发生变化,同时路由也应该更新到下一个步骤的页面。
<!-- src/routes/form/step1.svelte -->
<script>
    import { goto } from '$app/navigation';
    import { formData } from '$lib/stores';
    let name;

    function nextStep() {
        formData.update((data) => {
            data.name = name;
            return data;
        });
        goto('/form/step2');
    }
</script>

<h1>Step 1: Enter Your Name</h1>
<input type="text" bind:value={name}>
<button on:click={nextStep}>Next</button>
<!-- src/routes/form/step2.svelte -->
<script>
    import { formData } from '$lib/stores';
    const { name } = $formData;
</script>

<h1>Step 2: Confirm Your Name</h1>
<p>Your name is: {name}</p>
  1. 共享状态与路由参数结合 假设你有一个电商应用,在产品列表页面可以根据不同的筛选条件(如类别、价格范围等)来展示产品。这些筛选条件可以作为路由参数,同时也可以存储在共享状态中,以便在不同页面之间保持一致性。
<!-- src/routes/products/index.svelte -->
<script>
    import { onMount } from'svelte';
    import { products, filter } from '$lib/stores';
    import { page } from '$app/stores';

    const { category, minPrice, maxPrice } = $page.query;

    onMount(() => {
        let url = '/api/products';
        if (category) {
            url += `?category=${category}`;
        }
        if (minPrice) {
            url += `&minPrice=${minPrice}`;
        }
        if (maxPrice) {
            url += `&maxPrice=${maxPrice}`;
        }

        fetch(url)
           .then((res) => res.json())
           .then((data) => {
                products.set(data);
            });

        filter.update((f) => {
            f.category = category;
            f.minPrice = minPrice;
            f.maxPrice = maxPrice;
            return f;
        });
    });
</script>

{#each $products as product}
    <p>{product.name}</p>
{/each}
<!-- 在其他页面,如筛选条件设置页面 -->
<script>
    import { filter } from '$lib/stores';
    import { goto } from '$app/navigation';
    let newCategory;
    let newMinPrice;
    let newMaxPrice;

    function applyFilters() {
        let query = '';
        if (newCategory) {
            query += `category=${newCategory}`;
        }
        if (newMinPrice) {
            query += query? `&minPrice=${newMinPrice}` : `minPrice=${newMinPrice}`;
        }
        if (newMaxPrice) {
            query += query? `&maxPrice=${newMaxPrice}` : `maxPrice=${newMaxPrice}`;
        }

        filter.update((f) => {
            f.category = newCategory;
            f.minPrice = newMinPrice;
            f.maxPrice = newMaxPrice;
            return f;
        });

        goto(`/products?${query}`);
    }
</script>

<input type="text" placeholder="Category" bind:value={newCategory}>
<input type="number" placeholder="Min Price" bind:value={newMinPrice}>
<input type="number" placeholder="Max Price" bind:value={newMaxPrice}>
<button on:click={applyFilters}>Apply Filters</button>

路由加载状态与状态管理

在 SvelteKit 中,当页面进行路由切换时,会有一个加载状态。你可以利用这个加载状态来进行一些状态管理相关的操作,比如显示加载指示器。

<script>
    import { onMount } from'svelte';
    import { isLoading } from '$app/stores';
    let loadingMessage = 'Loading...';

    $isLoading.subscribe((loading) => {
        if (loading) {
            // 可以在这里进行一些状态初始化操作
            loadingMessage = 'Fetching data...';
        } else {
            // 加载完成后可以更新一些共享状态
        }
    });
</script>

{#if $isLoading}
    <p>{loadingMessage}</p>
{:else}
    <!-- 页面内容 -->
{/if}

处理路由错误与状态恢复

在路由过程中,可能会出现各种错误,比如 API 请求失败、参数不正确等。在 SvelteKit 中,你可以通过 error 函数来处理这些错误,并结合状态管理来进行错误状态的恢复。

<!-- src/routes/api/data.svelte -->
<script context="module">
    import { error } from '@sveltejs/kit';

    export async function load() {
        try {
            const res = await fetch('/api/someData');
            if (!res.ok) {
                throw new Error('Failed to fetch data');
            }
            const data = await res.json();
            return { data };
        } catch (e) {
            throw error(500, 'Internal Server Error');
        }
    }
</script>

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

{#if data}
    <p>{data}</p>
{:else}
    <p>Error loading data. Please try again later.</p>
</script>

在处理错误时,你还可以利用共享状态来记录错误信息,以便在整个应用程序中进行统一的错误处理和展示。

<script>
    import { errorState } from '$lib/stores';
    import { page } from '$app/stores';

    $page.subscribe((p) => {
        if (p.error) {
            errorState.set({
                message: p.error.message,
                status: p.error.status
            });
        }
    });
</script>

{#if $errorState}
    <div class="error">
        <p>{$errorState.message}</p>
        <p>Status: {$errorState.status}</p>
    </div>
{/if}

跨路由状态持久化

有时候,你可能希望某些状态在不同路由之间持久化,而不仅仅是在当前会话中有效。例如,用户的主题设置(黑暗模式/明亮模式),即使刷新页面或者切换路由,也应该保持不变。

SvelteKit 可以结合浏览器的本地存储来实现这种跨路由状态持久化。

<script>
    import { theme } from '$lib/stores';

    const storedTheme = localStorage.getItem('theme');
    if (storedTheme) {
        theme.set(storedTheme);
    }

    theme.subscribe((t) => {
        localStorage.setItem('theme', t);
    });
</script>

{#if $theme === 'dark'}
    <button on:click={() => theme.set('light')}>Switch to Light Mode</button>
{:else}
    <button on:click={() => theme.set('dark')}>Switch to Dark Mode</button>
{/if}

高级路由与状态管理技巧

  1. 动态路由与状态懒加载 在一些大型应用中,可能存在大量的动态路由,每个路由对应的状态数据量也较大。为了提高性能,可以采用状态懒加载的方式。

例如,在一个文档管理系统中,每个文档都有一个对应的路由 /documents/[documentId],而文档内容可能非常大。我们可以在用户真正访问该文档路由时,才加载文档的详细内容。

<!-- src/routes/documents/[documentId].svelte -->
<script>
    import { onMount } from'svelte';
    import { documentContent } from '$lib/stores';
    import { page } from '$app/stores';
    const { documentId } = $page.params;

    onMount(() => {
        // 懒加载文档内容
        fetch(`/api/documents/${documentId}`)
           .then((res) => res.text())
           .then((content) => {
                documentContent.set(content);
            });
    });
</script>

<h1>Document: {documentId}</h1>
{#if $documentContent}
    <div>{$documentContent}</div>
{:else}
    <p>Loading document...</p>
{/if}
  1. 路由过渡与状态动画 SvelteKit 支持路由过渡效果,结合状态管理,你可以实现更加丰富的动画效果。

比如,当用户从一个列表页面导航到详情页面时,你可以让列表项以动画的形式过渡到详情页面的相应位置,同时可以利用状态来控制动画的进度。

<!-- 列表页面 -->
<script>
    import { goto } from '$app/navigation';
    import { selectedItem } from '$lib/stores';
    function goToDetail(item) {
        selectedItem.set(item);
        goto(`/details/${item.id}`);
    }
</script>

{#each items as item}
    <div on:click={() => goToDetail(item)}>{item.name}</div>
{/each}
<!-- 详情页面 -->
<script>
    import { selectedItem } from '$lib/stores';
    import { onMount } from'svelte';

    let animationProgress = 0;

    onMount(() => {
        // 模拟动画过程,更新状态
        const interval = setInterval(() => {
            animationProgress += 10;
            if (animationProgress >= 100) {
                clearInterval(interval);
            }
        }, 100);
    });
</script>

{#if $selectedItem}
    <div style={`opacity: ${animationProgress / 100}`}>
        <h1>{$selectedItem.name}</h1>
        <p>{$selectedItem.description}</p>
    </div>
{/if}
  1. 嵌套路由与状态分层管理 对于复杂的应用,可能存在多层嵌套路由。在这种情况下,进行状态分层管理可以提高代码的可维护性。

例如,在一个电商应用中,有一个 products 路由,其下有 product/:productId 路由,而 product/:productId 路由又有 reviewsspecifications 等子路由。

<!-- src/routes/products/[productId]/index.svelte -->
<script>
    import { onMount } from'svelte';
    import { product } from '$lib/stores';
    import { page } from '$app/stores';
    const { productId } = $page.params;

    onMount(() => {
        // 加载产品基本信息
        fetch(`/api/products/${productId}`)
           .then((res) => res.json())
           .then((data) => {
                product.set(data);
            });
    });
</script>

<h1>{$product.name}</h1>
<p>{$product.description}</p>

<nav>
    <a href={`/products/${productId}/reviews`}>Reviews</a>
    <a href={`/products/${productId}/specifications`}>Specifications</a>
</nav>

{#if $page.url.pathname.includes('reviews')}
    <!-- 加载评论相关状态 -->
    <script>
        import { onMount } from'svelte';
        import { reviews } from '$lib/stores';

        onMount(() => {
            fetch(`/api/products/${productId}/reviews`)
               .then((res) => res.json())
               .then((data) => {
                    reviews.set(data);
                });
        });
    </script>

    {#each $reviews as review}
        <p>{review.text}</p>
    {/each}
{:else if $page.url.pathname.includes('specifications')}
    <!-- 加载规格相关状态 -->
    <script>
        import { onMount } from'svelte';
        import { specifications } from '$lib/stores';

        onMount(() => {
            fetch(`/api/products/${productId}/specifications`)
               .then((res) => res.json())
               .then((data) => {
                    specifications.set(data);
                });
        });
    </script>

    {#each $specifications as spec}
        <p>{spec.name}: {spec.value}</p>
    {/each}
{/if}

通过这种方式,不同层次的路由可以管理自己相关的状态,使得代码结构更加清晰,便于开发和维护。

性能优化与注意事项

  1. 避免不必要的状态更新 在结合路由与状态管理时,要注意避免不必要的状态更新。例如,如果某个状态的更新并不会影响当前页面的显示或者业务逻辑,就不应该进行更新。

在 Svelte 中,状态更新会触发组件的重新渲染,过多不必要的渲染会影响性能。可以通过 derived 存储来创建基于其他存储的只读状态,这样可以在一定程度上控制状态更新的频率。

<script>
    import { derived } from'svelte/store';
    import { cartItems } from '$lib/stores';

    const totalPrice = derived(cartItems, ($cartItems) => {
        return $cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
    });
</script>

<p>Total Price: {totalPrice}</p>

在这个例子中,只有当 cartItems 存储发生变化时,totalPrice 才会重新计算,避免了因其他无关状态变化导致的不必要更新。

  1. 路由预加载与状态缓存 对于一些常用的路由和相关状态,可以考虑进行预加载和缓存。例如,在一个新闻应用中,用户经常访问的新闻分类页面,可以在应用启动时就预加载相关的新闻列表数据,并缓存起来。
<script context="module">
    export async function load() {
        const popularCategoryRes = await fetch('/api/news/popular');
        const popularCategoryData = await popularCategoryRes.json();

        return {
            popularCategoryData
        };
    }
</script>

<script>
    import { onMount } from'svelte';
    import { popularNews } from '$lib/stores';
    import { page } from '$app/stores';

    const { popularCategoryData } = $page.data;

    onMount(() => {
        popularNews.set(popularCategoryData);
    });
</script>

这样,当用户访问热门新闻分类页面时,数据可以直接从缓存中获取,提高了页面加载速度。

  1. 内存管理与状态清理 当路由切换时,要注意及时清理不再使用的状态,以避免内存泄漏。例如,如果你在某个路由组件中创建了定时器或者订阅了一些外部数据源,在组件销毁时要及时清除这些操作。
<script>
    import { onDestroy } from'svelte';
    let interval;

    onMount(() => {
        interval = setInterval(() => {
            // 执行一些操作
        }, 1000);
    });

    onDestroy(() => {
        clearInterval(interval);
    });
</script>

同样,对于一些不再使用的状态存储,也应该考虑进行清理或者重置,以释放内存资源。

通过合理地结合 SvelteKit 的路由和状态管理,并且注意上述性能优化和注意事项,可以显著提升前端开发的效率,构建出高性能、可维护的应用程序。无论是小型项目还是大型复杂的应用,这些技巧都能帮助开发者更好地组织代码和管理应用的状态与路由逻辑。