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

Svelte自定义Store高级技巧:支持异步状态更新的设计方案

2023-05-103.8k 阅读

Svelte自定义Store高级技巧:支持异步状态更新的设计方案

理解Svelte Store基础

在Svelte中,Store是一种简单而强大的状态管理机制。最基本的Store可以通过writable函数创建。例如:

<script>
    import { writable } from'svelte/store';

    const count = writable(0);
</script>

<button on:click={() => count.update(n => n + 1)}>Increment</button>
<span>{$count}</span>

这里writable创建了一个可写的Store,$count用于在组件中读取Store的值,count.update方法用于更新Store的值。这种简单的机制在许多场景下都能很好地工作,但当涉及到异步操作时,就需要一些额外的设计。

异步状态更新的挑战

想象一个场景,我们需要从API获取数据并更新Store。例如,我们有一个获取用户信息的API:

<script>
    import { writable } from'svelte/store';

    const user = writable(null);

    async function fetchUser() {
        const response = await fetch('/api/user');
        const data = await response.json();
        user.set(data);
    }
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $user}
    <p>{$user.name}</p>
{/if}

这种方式虽然可以工作,但在复杂的应用中,特别是当多个异步操作相互依赖或者需要处理错误时,代码会变得难以维护。而且,在异步操作进行时,我们无法准确地表示状态(比如加载中状态)。

设计支持异步状态更新的Store

定义异步Store结构

我们可以设计一个自定义的异步Store,它不仅能存储最终的数据,还能管理加载状态和错误状态。

function asyncStore() {
    let value;
    let loading = false;
    let error = null;

    const subscribers = [];

    const set = (newValue) => {
        value = newValue;
        loading = false;
        error = null;
        subscribers.forEach(subscriber => subscriber({ value, loading, error }));
    };

    const startLoading = () => {
        loading = true;
        subscribers.forEach(subscriber => subscriber({ value, loading, error }));
    };

    const setError = (newError) => {
        error = newError;
        loading = false;
        subscribers.forEach(subscriber => subscriber({ value, loading, error }));
    };

    const subscribe = (run) => {
        run({ value, loading, error });
        subscribers.push(run);
        return () => {
            const index = subscribers.indexOf(run);
            if (index!== -1) {
                subscribers.splice(index, 1);
            }
        };
    };

    return { subscribe, set, startLoading, setError };
}

这里我们定义了一个asyncStore函数,它返回一个对象,包含subscribesetstartLoadingsetError方法。subscribe方法用于订阅Store的变化,set方法用于设置最终的值,startLoading用于表示开始加载,setError用于设置错误。

使用异步Store

<script>
    const userStore = asyncStore();

    async function fetchUser() {
        userStore.startLoading();
        try {
            const response = await fetch('/api/user');
            const data = await response.json();
            userStore.set(data);
        } catch (error) {
            userStore.setError(error);
        }
    }
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>{$userStore.value.name}</p>
{/if}

在组件中,我们使用userStore来管理用户数据的异步获取。当点击按钮时,先调用startLoading表示开始加载,成功获取数据后调用set设置值,出错时调用setError设置错误。

处理异步依赖

多个异步操作顺序执行

有时候,我们需要多个异步操作顺序执行。例如,先获取用户信息,然后根据用户信息获取用户的订单。

<script>
    const userStore = asyncStore();
    const orderStore = asyncStore();

    async function fetchUserAndOrders() {
        userStore.startLoading();
        try {
            const userResponse = await fetch('/api/user');
            const userData = await userResponse.json();
            userStore.set(userData);

            orderStore.startLoading();
            const orderResponse = await fetch(`/api/orders/${userData.id}`);
            const orderData = await orderResponse.json();
            orderStore.set(orderData);
        } catch (error) {
            userStore.setError(error);
            orderStore.setError(error);
        }
    }
</script>

<button on:click={fetchUserAndOrders}>Fetch User and Orders</button>
{#if $userStore.loading}
    <p>Loading user...</p>
{:else if $userStore.error}
    <p>Error fetching user: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>User: {$userStore.value.name}</p>
    {#if $orderStore.loading}
        <p>Loading orders...</p>
    {:else if $orderStore.error}
        <p>Error fetching orders: {$orderStore.error.message}</p>
    {:else if $orderStore.value}
        <p>Orders: {JSON.stringify($orderStore.value)}</p>
    {/if}
{/if}

这里我们按顺序执行了两个异步操作,并且根据不同的Store状态来显示相应的信息。

多个异步操作并行执行

在某些情况下,我们可以并行执行多个异步操作以提高效率。例如,同时获取用户信息和用户的设置。

<script>
    const userStore = asyncStore();
    const settingsStore = asyncStore();

    async function fetchUserAndSettings() {
        userStore.startLoading();
        settingsStore.startLoading();

        try {
            const [userResponse, settingsResponse] = await Promise.all([
                fetch('/api/user'),
                fetch('/api/settings')
            ]);

            const userData = await userResponse.json();
            const settingsData = await settingsResponse.json();

            userStore.set(userData);
            settingsStore.set(settingsData);
        } catch (error) {
            userStore.setError(error);
            settingsStore.setError(error);
        }
    }
</script>

<button on:click={fetchUserAndSettings}>Fetch User and Settings</button>
{#if $userStore.loading || $settingsStore.loading}
    <p>Loading...</p>
{:else if $userStore.error || $settingsStore.error}
    <p>Error: {($userStore.error || $settingsStore.error).message}</p>
{:else if $userStore.value && $settingsStore.value}
    <p>User: {$userStore.value.name}</p>
    <p>Settings: {JSON.stringify($settingsStore.value)}</p>
{/if}

通过Promise.all我们并行执行了两个异步操作,并统一处理错误和设置Store的值。

错误处理与重试机制

错误处理策略

在异步操作中,错误处理至关重要。我们在前面的示例中已经简单地通过catch块来捕获错误并设置到Store中。但在实际应用中,我们可能需要更复杂的错误处理策略。例如,根据不同的错误类型进行不同的提示。

<script>
    const userStore = asyncStore();

    async function fetchUser() {
        userStore.startLoading();
        try {
            const response = await fetch('/api/user');
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            const data = await response.json();
            userStore.set(data);
        } catch (error) {
            if (error.message.includes('HTTP error')) {
                userStore.setError(new Error('Network issue, please try again later.'));
            } else {
                userStore.setError(error);
            }
        }
    }
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>{$userStore.value.name}</p>
{/if}

这里我们根据错误信息判断是否为HTTP错误,如果是则给出更友好的提示。

重试机制

有时候,错误可能是暂时的,我们希望提供重试功能。我们可以在asyncStore的基础上添加重试逻辑。

function asyncStoreWithRetry() {
    let value;
    let loading = false;
    let error = null;
    let retryCount = 0;
    const maxRetries = 3;

    const subscribers = [];

    const set = (newValue) => {
        value = newValue;
        loading = false;
        error = null;
        retryCount = 0;
        subscribers.forEach(subscriber => subscriber({ value, loading, error }));
    };

    const startLoading = () => {
        loading = true;
        subscribers.forEach(subscriber => subscriber({ value, loading, error }));
    };

    const setError = (newError) => {
        error = newError;
        loading = false;
        subscribers.forEach(subscriber => subscriber({ value, loading, error }));
    };

    const retry = async () => {
        if (retryCount < maxRetries) {
            retryCount++;
            startLoading();
            try {
                // 这里假设存在一个异步操作函数fetchData
                const data = await fetchData();
                set(data);
            } catch (error) {
                setError(error);
            }
        }
    };

    const subscribe = (run) => {
        run({ value, loading, error });
        subscribers.push(run);
        return () => {
            const index = subscribers.indexOf(run);
            if (index!== -1) {
                subscribers.splice(index, 1);
            }
        };
    };

    return { subscribe, set, startLoading, setError, retry };
}
<script>
    const userStore = asyncStoreWithRetry();

    async function fetchData() {
        // 模拟异步操作
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (Math.random() < 0.5) {
                    resolve({ name: 'John' });
                } else {
                    reject(new Error('Simulated error'));
                }
            }, 1000);
        });
    }

    async function fetchUser() {
        userStore.startLoading();
        try {
            const data = await fetchData();
            userStore.set(data);
        } catch (error) {
            userStore.setError(error);
        }
    }
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
    <button on:click={$userStore.retry}>Retry</button>
{:else if $userStore.value}
    <p>{$userStore.value.name}</p>
{/if}

这里我们在asyncStoreWithRetry中添加了重试逻辑,用户可以点击重试按钮尝试重新获取数据,最多重试3次。

与Svelte Reactive Statements结合

Svelte的响应式语句可以与我们的异步Store很好地结合。例如,我们可以根据Store的状态动态地更新其他UI元素。

<script>
    const userStore = asyncStore();

    async function fetchUser() {
        userStore.startLoading();
        try {
            const response = await fetch('/api/user');
            const data = await response.json();
            userStore.set(data);
        } catch (error) {
            userStore.setError(error);
        }
    }

    let showExtraInfo = false;

    $: {
        if ($userStore.value && $userStore.value.isAdmin) {
            showExtraInfo = true;
        } else {
            showExtraInfo = false;
        }
    }
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>{$userStore.value.name}</p>
    {#if showExtraInfo}
        <p>You have admin privileges.</p>
    {/if}
{/if}

这里我们使用Svelte的$:响应式语句,根据userStore中的用户数据判断是否显示额外信息。

性能优化与内存管理

性能优化

在异步操作频繁的应用中,性能优化很重要。例如,避免不必要的重新渲染。我们可以通过derived Store来实现。

<script>
    import { derived } from'svelte/store';
    const userStore = asyncStore();

    const userFullName = derived(userStore, ($userStore) => {
        if ($userStore.value) {
            return `${$userStore.value.firstName} ${$userStore.value.lastName}`;
        }
        return '';
    });
</script>

<button on:click={() => { /* 模拟获取用户操作 */ }}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>{$userFullName}</p>
{/if}

通过derived Store,我们只在userStore的相关部分发生变化时更新userFullName,避免了不必要的重新渲染。

内存管理

当组件销毁时,我们需要确保取消所有未完成的异步操作以避免内存泄漏。我们可以使用Svelte的onDestroy生命周期函数。

<script>
    import { onDestroy } from'svelte';
    const userStore = asyncStore();
    let controller;

    async function fetchUser() {
        controller = new AbortController();
        const signal = controller.signal;
        userStore.startLoading();
        try {
            const response = await fetch('/api/user', { signal });
            const data = await response.json();
            userStore.set(data);
        } catch (error) {
            if (!(error instanceof DOMException && error.name === 'AbortError')) {
                userStore.setError(error);
            }
        }
    }

    onDestroy(() => {
        if (controller) {
            controller.abort();
        }
    });
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>{$userStore.value.name}</p>
{/if}

这里我们使用AbortController来取消未完成的fetch操作,当组件销毁时调用controller.abort()

与第三方库集成

与Axios集成

Axios是一个常用的HTTP客户端库。我们可以将它与我们的异步Store集成。

<script>
    import axios from 'axios';
    const userStore = asyncStore();

    async function fetchUser() {
        userStore.startLoading();
        try {
            const response = await axios.get('/api/user');
            userStore.set(response.data);
        } catch (error) {
            userStore.setError(error);
        }
    }
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>{$userStore.value.name}</p>
{/if}

通过Axios,我们可以更方便地进行HTTP请求,并且结合我们的异步Store来管理状态。

与GraphQL集成

如果我们的后端使用GraphQL,我们可以使用graphql-request库来集成。

<script>
    import { request } from 'graphql-request';
    const userStore = asyncStore();

    const query = `
        query {
            user {
                name
            }
        }
    `;

    async function fetchUser() {
        userStore.startLoading();
        try {
            const data = await request('/graphql', query);
            userStore.set(data.user);
        } catch (error) {
            userStore.setError(error);
        }
    }
</script>

<button on:click={fetchUser}>Fetch User</button>
{#if $userStore.loading}
    <p>Loading...</p>
{:else if $userStore.error}
    <p>Error: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>{$userStore.value.name}</p>
{/if}

这里我们使用graphql-request库发送GraphQL查询,并通过异步Store管理状态。

跨组件共享异步Store

在大型应用中,我们可能需要在多个组件之间共享异步Store。我们可以通过将Store定义在一个单独的文件中,然后在不同组件中导入。

// userStore.js
import { asyncStore } from './asyncStore.js';

export const userStore = asyncStore();
// Component1.svelte
<script>
    import { userStore } from './userStore.js';

    async function fetchUser() {
        userStore.startLoading();
        try {
            const response = await fetch('/api/user');
            const data = await response.json();
            userStore.set(data);
        } catch (error) {
            userStore.setError(error);
        }
    }
</script>

<button on:click={fetchUser}>Fetch User in Component1</button>
// Component2.svelte
<script>
    import { userStore } from './userStore.js';
</script>

{#if $userStore.loading}
    <p>Loading in Component2...</p>
{:else if $userStore.error}
    <p>Error in Component2: {$userStore.error.message}</p>
{:else if $userStore.value}
    <p>User in Component2: {$userStore.value.name}</p>
{/if}

通过这种方式,不同组件可以共享同一个异步Store,从而实现状态的统一管理。

测试异步Store

单元测试

我们可以使用Jest来测试异步Store。例如,测试asyncStore的基本功能。

import { asyncStore } from './asyncStore.js';

describe('asyncStore', () => {
    let store;

    beforeEach(() => {
        store = asyncStore();
    });

    it('should have initial state', () => {
        let value;
        store.subscribe(v => value = v);
        expect(value.value).toBe(undefined);
        expect(value.loading).toBe(false);
        expect(value.error).toBe(null);
    });

    it('should set value correctly', () => {
        store.set({ name: 'John' });
        let value;
        store.subscribe(v => value = v);
        expect(value.value.name).toBe('John');
        expect(value.loading).toBe(false);
        expect(value.error).toBe(null);
    });

    it('should set loading state', () => {
        store.startLoading();
        let value;
        store.subscribe(v => value = v);
        expect(value.loading).toBe(true);
    });

    it('should set error state', () => {
        const error = new Error('Test error');
        store.setError(error);
        let value;
        store.subscribe(v => value = v);
        expect(value.error).toBe(error);
        expect(value.loading).toBe(false);
    });
});

这里我们测试了asyncStore的初始状态、设置值、设置加载状态和设置错误状态的功能。

集成测试

对于涉及异步操作的集成测试,我们可以使用Cypress。例如,测试一个组件中异步Store的行为。

describe('Component with asyncStore', () => {
    it('should fetch user correctly', () => {
        cy.visit('/');
        cy.get('button').contains('Fetch User').click();
        cy.get('p').contains('User: John').should('be.visible');
    });

    it('should show error when fetch fails', () => {
        cy.intercept('/api/user', {
            statusCode: 500,
            body: { error: 'Server error' }
        });
        cy.visit('/');
        cy.get('button').contains('Fetch User').click();
        cy.get('p').contains('Error: Server error').should('be.visible');
    });
});

这里我们使用Cypress模拟用户操作,并验证组件在不同情况下的行为,包括成功获取数据和获取数据失败的情况。

通过以上设计方案,我们可以在Svelte中实现功能强大且易于维护的异步状态更新的自定义Store,无论是处理简单的异步操作还是复杂的异步依赖、错误处理和性能优化,都能有很好的应对策略。同时,通过与第三方库集成、跨组件共享以及完善的测试,我们可以构建健壮的前端应用。