Svelte自定义Store高级技巧:支持异步状态更新的设计方案
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
函数,它返回一个对象,包含subscribe
、set
、startLoading
和setError
方法。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,无论是处理简单的异步操作还是复杂的异步依赖、错误处理和性能优化,都能有很好的应对策略。同时,通过与第三方库集成、跨组件共享以及完善的测试,我们可以构建健壮的前端应用。