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

Svelte 状态管理与路由集成:在路由切换时保持状态

2023-03-104.7k 阅读

Svelte 状态管理基础

1. Svelte 中的响应式状态

Svelte 以其简洁而强大的响应式系统闻名。在 Svelte 中,声明变量并使用 $: 前缀来创建响应式声明。例如:

<script>
    let count = 0;
    $: doubled = count * 2;
</script>

<button on:click={() => count++}>Increment</button>
<p>{count} doubled is {doubled}</p>

这里,count 是一个普通变量,而 doubled 是一个响应式变量。每当 count 发生变化时,doubled 会自动更新。这是 Svelte 状态管理的基础,简单直接且高效。

2. 组件间状态共享

在复杂应用中,组件之间需要共享状态。Svelte 提供了多种方式来实现这一点。一种常见的方法是通过父组件向子组件传递数据。例如:

<!-- Parent.svelte -->
<script>
    import Child from './Child.svelte';
    let message = 'Hello from parent';
</script>

<Child {message} />

<!-- Child.svelte -->
<script>
    export let message;
</script>

<p>{message}</p>

这种方式适用于父子组件之间的简单数据传递。但对于更复杂的场景,比如跨层级组件之间的数据共享,这种方法就显得繁琐。此时,可以使用 Svelte 的 store 机制。

3. Svelte 中的 Store

Svelte 的 store 是一种用于管理状态的机制,它可以在不同组件之间共享数据。Svelte 提供了 writablereadablederived 三种类型的 store。

3.1 Writable Store

writable 是最常用的 store 类型,它允许我们读写数据。以下是一个简单的示例:

<script>
    import { writable } from'svelte/store';
    const count = writable(0);
</script>

<button on:click={() => count.update(n => n + 1)}>Increment</button>
{#if $count}
    <p>The count is { $count }</p>
{/if}

在这个例子中,count 是一个 writable store。我们使用 $count 来访问 store 的值,使用 count.update 方法来更新值。

3.2 Readable Store

readable store 是只读的,适用于那些不需要被修改的状态,比如当前时间。

<script>
    import { readable } from'svelte/store';
    const currentTime = readable(new Date(), set => {
        const interval = setInterval(() => {
            set(new Date());
        }, 1000);
        return () => clearInterval(interval);
    });
</script>

<p>The current time is { $currentTime }</p>

这里,currentTime 是一个 readable store。它接受一个初始值和一个启动函数。启动函数返回一个清理函数,用于在不再需要 store 时清理资源。

3.3 Derived Store

derived store 基于其他 store 派生而来。例如,我们有一个表示摄氏温度的 store,我们可以派生一个表示华氏温度的 store。

<script>
    import { writable, derived } from'svelte/store';
    const celsius = writable(20);
    const fahrenheit = derived(celsius, $celsius => $celsius * 1.8 + 32);
</script>

<input type="number" bind:value={$celsius}>
<p>{ $celsius }°C is { $fahrenheit }°F</p>

fahrenheit 是基于 celsius 派生而来的 store。每当 celsius 的值发生变化时,fahrenheit 会自动更新。

Svelte 路由基础

1. 引入 Svelte Router

在 Svelte 项目中,我们通常使用 svelte - router - library 来实现路由功能。首先,通过 npm 安装:

npm install svelte - router - library

然后,在项目中导入并使用。例如,在 main.js 中:

import { render } from'svelte';
import { Router, Route, Link } from'svelte - router - library';
import App from './App.svelte';

render(
    <Router>
        <Route path="/" component={Home} />
        <Route path="/about" component={About} />
    </Router>,
    document.getElementById('app')
);

这里,Router 是路由的容器,Route 定义了具体的路由路径和对应的组件,Link 用于创建导航链接。

2. 基本路由导航

在组件中,我们可以使用 Link 组件来创建导航链接。例如:

<script>
    import { Link } from'svelte - router - library';
</script>

<nav>
    <Link to="/">Home</Link>
    <Link to="/about">About</Link>
</nav>

当用户点击这些链接时,路由会自动切换到对应的路径,并渲染相应的组件。

3. 动态路由

有时候我们需要处理动态路由,比如显示用户详情页,每个用户有不同的 ID。在 Svelte Router 中,可以这样定义动态路由:

<Route path="/user/:id" component={UserDetail} />

UserDetail.svelte 组件中,可以通过 $page.params.id 来获取动态参数 id

<script>
    import { page } from'svelte - router - library';
    let userId = $page.params.id;
</script>

<p>User detail for user {userId}</p>

状态管理与路由集成面临的挑战

1. 路由切换导致状态丢失

在传统的路由实现中,当路由切换时,组件会被销毁并重新创建。这就导致组件内部的状态会丢失。例如,我们有一个购物车页面,用户在购物车中添加了商品,当用户切换到其他页面再切换回来时,购物车的状态可能会被重置。

<!-- Cart.svelte -->
<script>
    let items = [];
    function addItem(item) {
        items.push(item);
    }
</script>

<button on:click={() => addItem('Product 1')}>Add to Cart</button>
<ul>
    {#each items as item}
        <li>{item}</li>
    {/each}
</ul>

如果这个 Cart.svelte 组件在路由切换时被销毁重建,items 数组会被重置为初始状态,用户添加的商品就丢失了。

2. 跨路由状态同步

除了状态丢失问题,跨路由状态同步也是一个挑战。例如,我们有一个全局的用户登录状态,在登录页面登录后,希望在其他页面能够同步显示用户已登录状态。但由于不同路由组件可能在不同的生命周期阶段,实现这种状态同步并不简单。

在路由切换时保持状态的方法

1. 使用 Svelte Store 保持状态

1.1 将状态存储在 Store 中

通过将需要保持的状态存储在 Svelte 的 store 中,可以避免路由切换导致的状态丢失。例如,对于前面的购物车示例,我们可以将 items 存储在一个 writable store 中。

<!-- Cart.svelte -->
<script>
    import { writable } from'svelte/store';
    const cartItems = writable([]);
    function addItem(item) {
        cartItems.update(items => {
            items.push(item);
            return items;
        });
    }
</script>

<button on:click={() => addItem('Product 1')}>Add to Cart</button>
<ul>
    {#each $cartItems as item}
        <li>{item}</li>
    {/each}
</ul>

这样,无论路由如何切换,只要 cartItems store 没有被重置,购物车的状态就会保持。

1.2 在不同路由组件中共享 Store

为了在不同路由组件中共享状态,我们可以将 store 作为模块导出。例如,创建一个 cartStore.js 文件:

import { writable } from'svelte/store';

export const cartItems = writable([]);

然后在不同的路由组件中导入并使用:

<!-- Cart.svelte -->
<script>
    import { cartItems } from './cartStore.js';
    function addItem(item) {
        cartItems.update(items => {
            items.push(item);
            return items;
        });
    }
</script>

<button on:click={() => addItem('Product 1')}>Add to Cart</button>
<ul>
    {#each $cartItems as item}
        <li>{item}</li>
    {/each}
</ul>

<!-- AnotherComponent.svelte -->
<script>
    import { cartItems } from './cartStore.js';
</script>

<p>The number of items in cart is { $cartItems.length }</p>

通过这种方式,不同路由组件可以共享购物车的状态,实现了跨路由的状态保持。

2. 利用 Svelte 的生命周期方法

2.1 onDestroy 与 onMount

Svelte 提供了 onDestroyonMount 生命周期方法,我们可以利用它们来在路由切换时保存和恢复状态。例如,假设我们有一个组件,它有一个滚动位置的状态。

<script>
    import { onDestroy, onMount } from'svelte';
    let scrollPosition = 0;
    onMount(() => {
        window.addEventListener('scroll', handleScroll);
    });
    onDestroy(() => {
        window.removeEventListener('scroll', handleScroll);
        localStorage.setItem('scrollPosition', scrollPosition);
    });
    function handleScroll() {
        scrollPosition = window.pageYOffset;
    }
    const savedPosition = localStorage.getItem('scrollPosition');
    if (savedPosition) {
        scrollPosition = parseInt(savedPosition);
        window.scrollTo(0, scrollPosition);
    }
</script>

onMount 中,我们添加滚动事件监听器。在 onDestroy 中,我们移除监听器并将当前滚动位置保存到 localStorage。当组件重新挂载时,我们从 localStorage 中读取滚动位置并恢复滚动。

2.2 在路由组件中应用生命周期方法

在路由组件中,可以类似地应用这些生命周期方法来保持状态。例如,对于一个列表组件,我们可以在组件销毁时保存当前的筛选条件,在组件重新挂载时恢复筛选条件。

<!-- ListComponent.svelte -->
<script>
    import { onDestroy, onMount } from'svelte';
    let filter = '';
    onMount(() => {
        const savedFilter = localStorage.getItem('listFilter');
        if (savedFilter) {
            filter = savedFilter;
        }
    });
    onDestroy(() => {
        localStorage.setItem('listFilter', filter);
    });
</script>

<input type="text" bind:value={filter} placeholder="Filter list">
<ul>
    {#each listItems.filter(item => item.includes(filter)) as item}
        <li>{item}</li>
    {/each}
</ul>

这样,在路由切换时,列表的筛选条件会被保持。

3. 路由参数与状态结合

3.1 将状态编码到路由参数中

有时候,我们可以将部分状态编码到路由参数中。例如,在一个分页列表的应用中,我们可以将当前页码作为路由参数。

<!-- ListPage.svelte -->
<script>
    import { page } from'svelte - router - library';
    let currentPage = parseInt($page.params.page) || 1;
    function goToPage(page) {
        // 使用 svelte - router - library 的导航方法
        navigate(`/list/${page}`);
    }
</script>

<button on:click={() => goToPage(currentPage - 1)} disabled={currentPage === 1}>Previous</button>
<button on:click={() => goToPage(currentPage + 1)}>Next</button>
<p>Current page: {currentPage}</p>

通过这种方式,当路由切换时,页码状态会随着路由参数的改变而保持。

3.2 从路由参数恢复状态

在组件挂载时,我们可以从路由参数中读取状态并恢复。例如,对于一个图片查看器应用,我们可以将图片的索引作为路由参数。

<!-- ImageViewer.svelte -->
<script>
    import { page } from'svelte - router - library';
    let imageIndex = parseInt($page.params.index) || 0;
    const images = ['image1.jpg', 'image2.jpg', 'image3.jpg'];
</script>

<img src={images[imageIndex]} alt={`Image ${imageIndex}`}>
<button on:click={() => {
    if (imageIndex > 0) {
        navigate(`/image/${imageIndex - 1}`);
    }
}} disabled={imageIndex === 0}>Previous</button>
<button on:click={() => {
    if (imageIndex < images.length - 1) {
        navigate(`/image/${imageIndex + 1}`);
    }
}}>Next</button>

这样,当用户在图片之间切换路由时,图片的显示状态会根据路由参数保持。

高级技巧与优化

1. 状态持久化策略

1.1 使用 IndexedDB 进行持久化

虽然 localStorage 可以满足简单的状态持久化需求,但对于大量数据或更复杂的数据结构,IndexedDB 是更好的选择。例如,对于一个包含大量用户设置的应用,我们可以使用 IndexedDB 来存储这些设置。

// 初始化 IndexedDB
function initIndexedDB() {
    return new Promise((resolve, reject) => {
        const request = window.indexedDB.open('settingsDB', 1);
        request.onupgradeneeded = event => {
            const db = event.target.result;
            db.createObjectStore('settings', { keyPath: 'id' });
        };
        request.onsuccess = event => {
            resolve(event.target.result);
        };
        request.onerror = event => {
            reject(event.target.error);
        };
    });
}

// 保存状态到 IndexedDB
async function saveSettings(settings) {
    const db = await initIndexedDB();
    const transaction = db.transaction(['settings'], 'readwrite');
    const store = transaction.objectStore('settings');
    store.put(settings);
    return new Promise((resolve, reject) => {
        transaction.oncomplete = () => resolve();
        transaction.onerror = () => reject();
    });
}

// 从 IndexedDB 读取状态
async function loadSettings() {
    const db = await initIndexedDB();
    const transaction = db.transaction(['settings'],'readonly');
    const store = transaction.objectStore('settings');
    return new Promise((resolve, reject) => {
        const request = store.get('settings - key');
        request.onsuccess = event => {
            resolve(event.target.result);
        };
        request.onerror = event => {
            reject(event.target.error);
        };
    });
}

在 Svelte 组件中,可以在适当的生命周期方法中调用这些函数来实现状态的持久化和恢复。

1.2 服务器端状态同步

对于一些关键的状态,如用户登录状态、订单状态等,与服务器端进行状态同步是很重要的。我们可以使用 WebSocket 或 RESTful API 来实现这一点。例如,使用 WebSocket 来实时同步用户的在线状态:

<script>
    const socket = new WebSocket('ws://your - server - url');
    socket.onopen = () => {
        socket.send('Hello, server!');
    };
    socket.onmessage = event => {
        const data = JSON.parse(event.data);
        // 根据服务器发送的数据更新状态
    };
    socket.onclose = () => {
        console.log('Connection closed');
    };
</script>

通过与服务器端的实时通信,可以确保在不同设备或页面间状态的一致性。

2. 性能优化

2.1 避免不必要的状态更新

在 Svelte 中,响应式系统会在状态变化时自动更新视图。但有时候,一些状态变化可能是不必要的,会导致性能问题。例如,在一个列表组件中,如果我们频繁更新一个不会影响列表渲染的属性,就会造成不必要的重绘。

<script>
    let list = [1, 2, 3];
    let someUnrelatedValue = 0;
    function updateUnrelatedValue() {
        someUnrelatedValue++;
    }
</script>

<button on:click={updateUnrelatedValue}>Update Unrelated Value</button>
<ul>
    {#each list as item}
        <li>{item}</li>
    {/each}
</ul>

在这个例子中,someUnrelatedValue 的更新不会影响列表的渲染,但 Svelte 的响应式系统仍然会触发视图更新。为了避免这种情况,我们可以将不影响视图的状态放在一个独立的对象中,并使用 Object.freeze 来防止不必要的更新。

<script>
    let list = [1, 2, 3];
    const settings = Object.freeze({
        someUnrelatedValue: 0
    });
    function updateUnrelatedValue() {
        settings.someUnrelatedValue++;
    }
</script>

<button on:click={updateUnrelatedValue}>Update Unrelated Value</button>
<ul>
    {#each list as item}
        <li>{item}</li>
    {/each}
</ul>

这样,settings 对象的变化不会触发视图更新,除非我们手动触发。

2.2 路由懒加载与状态预取

在大型应用中,路由组件可能比较大,加载时间较长。我们可以使用路由懒加载来提高应用的初始加载速度。同时,在路由切换前,可以预取相关组件和状态。例如,在 svelte - router - library 中,可以这样实现路由懒加载:

<Route path="/big - component" component={() => import('./BigComponent.svelte')} />

对于状态预取,可以在路由导航前,通过 API 调用获取相关数据并更新状态。例如:

<script>
    import { page, navigate } from'svelte - router - library';
    async function goToBigComponent() {
        // 预取数据
        const response = await fetch('/api/big - component - data');
        const data = await response.json();
        // 更新相关状态
        someStore.set(data);
        navigate('/big - component');
    }
</script>

<button on:click={goToBigComponent}>Go to Big Component</button>

通过这种方式,可以在路由切换时更快地显示内容,并保持状态的一致性。

实践案例分析

1. 电商应用案例

1.1 购物车状态保持

在一个电商应用中,购物车状态的保持至关重要。我们使用 Svelte Store 来管理购物车。首先,创建一个 cartStore.js 文件:

import { writable } from'svelte/store';

export const cart = writable([]);

export function addToCart(product) {
    cart.update(items => {
        const existingItem = items.find(item => item.id === product.id);
        if (existingItem) {
            existingItem.quantity++;
        } else {
            items.push({...product, quantity: 1 });
        }
        return items;
    });
}

export function removeFromCart(productId) {
    cart.update(items => items.filter(item => item.id!== productId));
}

在各个路由组件中,比如商品详情页和购物车页面,都可以导入并使用这个 cart store。

<!-- ProductDetail.svelte -->
<script>
    import { addToCart } from './cartStore.js';
    const product = { id: 1, name: 'Sample Product', price: 10 };
</script>

<button on:click={() => addToCart(product)}>Add to Cart</button>

<!-- Cart.svelte -->
<script>
    import { cart, removeFromCart } from './cartStore.js';
</script>

<ul>
    {#each $cart as item}
        <li>{item.name} - ${item.price} x {item.quantity}
            <button on:click={() => removeFromCart(item.id)}>Remove</button>
        </li>
    {/each}
</ul>

这样,无论用户在商品详情页添加商品到购物车后切换到其他页面,再回到购物车页面,购物车的状态都能保持。

1.2 用户登录状态与全局导航

用户登录状态也是电商应用中的重要状态。我们可以使用一个 userStore.js 来管理用户登录状态:

import { writable } from'svelte/store';

export const user = writable(null);

export async function login(username, password) {
    // 模拟登录请求
    const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
            'Content - Type': 'application/json'
        },
        body: JSON.stringify({ username, password })
    });
    const data = await response.json();
    if (data.success) {
        user.set(data.user);
    }
    return data.success;
}

export function logout() {
    user.set(null);
}

在全局导航组件中,根据用户登录状态显示不同的内容:

<!-- Navbar.svelte -->
<script>
    import { user, login, logout } from './userStore.js';
</script>

{#if $user}
    <p>Welcome, { $user.username }!</p>
    <button on:click={logout}>Logout</button>
{:else}
    <button on:click={() => {
        // 弹出登录框或导航到登录页面
    }}>Login</button>
{/if}

通过这种方式,实现了用户登录状态在不同路由组件间的同步和保持。

2. 博客应用案例

2.1 文章阅读状态保持

在一个博客应用中,用户可能希望在不同页面间保持文章的阅读进度。我们可以利用路由参数和 Svelte Store 来实现。首先,在路由配置中定义动态路由:

<Route path="/article/:id" component={Article} />

Article.svelte 组件中:

<script>
    import { page } from'svelte - router - library';
    import { writable } from'svelte/store';
    const articleId = $page.params.id;
    const readingProgress = writable(0);
    function updateProgress() {
        // 根据滚动位置或其他方式更新阅读进度
        readingProgress.set(newProgressValue);
    }
    onMount(() => {
        const savedProgress = localStorage.getItem(`article - ${articleId}-progress`);
        if (savedProgress) {
            readingProgress.set(parseFloat(savedProgress));
        }
        window.addEventListener('scroll', updateProgress);
    });
    onDestroy(() => {
        localStorage.setItem(`article - ${articleId}-progress`, $readingProgress);
        window.removeEventListener('scroll', updateProgress);
    });
</script>

<article>
    <!-- 文章内容 -->
</article>

这样,当用户在不同文章间切换路由时,每篇文章的阅读进度都能得到保持。

2.2 搜索与筛选状态

博客应用通常也有搜索和筛选功能。我们可以将搜索关键词和筛选条件存储在 Svelte Store 中,并在不同路由组件间共享。例如,创建一个 searchStore.js

import { writable } from'svelte/store';

export const searchQuery = writable('');
export const filterCategory = writable('all');

在搜索组件和文章列表组件中导入并使用这些 store:

<!-- Search.svelte -->
<script>
    import { searchQuery } from './searchStore.js';
</script>

<input type="text" bind:value={$searchQuery} placeholder="Search articles">

<!-- ArticleList.svelte -->
<script>
    import { searchQuery, filterCategory } from './searchStore.js';
    const articles = [/* 文章列表数据 */];
    const filteredArticles = articles.filter(article => {
        if ($filterCategory!== 'all' && article.category!== $filterCategory) {
            return false;
        }
        return article.title.includes($searchQuery) || article.content.includes($searchQuery);
    });
</script>

<ul>
    {#each filteredArticles as article}
        <li>{article.title}</li>
    {/each}
</ul>

通过这种方式,无论用户在搜索页面还是文章列表页面切换路由,搜索和筛选状态都能保持,提供了更好的用户体验。