【Svelte】加载数据实现响应式的正确方式
在 +page.ts
中使用load
加载数据时,必须等待load
处理完毕,页面才能获取数据并显示。
下面是一个关于 SvelteKit 中 +page.ts
(或 +layout.ts
) 的 load
函数工作方式的常见误解。
为什么在 +page.ts
中更新页面内容不起作用:
load
函数优先于组件渲染:load
函数在+page.svelte
组件(或任何会显示你的加载指示器的 UI 部分)被挂载或开始根据load
返回的数据渲染其初始状态之前执行。- (针对 UI 的)同步执行上下文: 即使
is_loading.value = true
理论上可以更新一个 store,UI 也不会对它做出反应,直到load
函数完成其整个执行并返回数据。SvelteKit 需要这些数据来准备组件的 props。 - 没有即时 UI 重新渲染机制: 在
load
函数内部没有一种机制可以“暂停”其执行,触发 UI 更新以显示加载指示器,然后再恢复。load
函数必须完成它的工作(包括await fetch
),然后组件才能开始考虑渲染。
SvelteKit 的正确方式:在 +page.svelte
组件中使用 {#await}
块
SvelteKit 旨在优雅地处理这种精确的场景。与其尝试在 load
函数内部管理 loading
状态,不如从 load
函数返回一个 Promise
,然后在你的 +page.svelte
组件中使用 Svelte 内置的 {#await}
块。
以下是实现方法:
1. 修改 +page.ts
以返回一个 Promise:
你直接返回 Promise
本身,而不是 await
fetch
调用并返回数据。
// src/routes/your-page/+page.ts
import type { PageLoad } from './$types';
import { API } from '$lib/api'; // 假设你有一个 API 配置export const load: PageLoad = async ({ fetch }) => {// 我们创建一个函数,它返回 fetch 的 Promiseconst fetchRoomData = async () => {const response = await fetch(API.room); // 你的实际 fetch 调用if (!response.ok) {throw new Error('Failed to load room data');}return await response.json();};// 关键改变:直接返回 Promise,不要在这里 await 它!return {roomData: fetchRoomData()};
};
2. 修改 +page.svelte
以使用 {#await}
:
现在,在你的组件中,你将收到 data.roomData
作为一个 Promise。然后,你可以使用 {#await}
根据 Promise 的状态显示不同的内容。
<!-- src/routes/your-page/+page.svelte -->
<script lang="ts">import type { PageData } from './$types';// 这个 'data' prop 将包含来自 +page.ts 的 'roomData' Promiseexport let data: PageData;
</script><h1>房间详情</h1>{#await data.roomData}<!-- 在 Promise 待处理时显示的内容(这里放加载指示器!) --><div class="spinner-container"><div class="spinner"></div><p>正在加载房间数据...</p></div>
{:then room}<!-- 在 Promise 成功解决时显示的内容 --><p>房间名称: {room.name}</p><p>容量: {room.capacity}</p><p>状态: {room.status}</p><!-- 渲染其他房间属性 -->
{:catch error}<!-- 如果 Promise 拒绝(fetch 失败)时显示的内容 --><p style="color: red;">错误: {error.message}</p><p>请稍后再试。</p>
{/await}<style>.spinner-container {display: flex;flex-direction: column;align-items: center;gap: 10px;padding: 20px;border: 1px solid #eee;border-radius: 8px;background-color: #f9f9f9;}.spinner {border: 4px solid rgba(0, 0, 0, 0.1);width: 36px;height: 36px;border-radius: 50%;border-left-color: #007bff; /* SvelteKit 蓝色或你的品牌色 */animation: spin 1s ease infinite;}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }}
</style>
解释:
- 当用户导航到
/your-page
时,SvelteKit 会运行+page.ts
。 +page.ts
立即返回一个包含roomData
的对象,roomData
是一个最终会以房间数据解决的Promise
。- SvelteKit 将此对象传递给
+page.svelte
。 +page.svelte
开始渲染。{#await data.roomData}
块会立即进入其{:pending}
状态,因为data.roomData
仍然是一个待处理的 Promise。你的加载指示器和“正在加载…”消息会显示出来。- 在组件渲染并显示加载指示器时,
fetchRoomData()
的 Promise 会在后台继续解决。 - 一旦
fetchRoomData()
解决(或拒绝),{#await}
块会自动更新,显示{:then room}
内容或{:catch error}
内容。
这种方法确保加载指示器在组件渲染后立即可见,在数据到达之前,从而提供了更好的用户体验。
考虑使用 +page.server.ts
进行数据获取:
对于大多数不依赖客户端浏览器 API(如 localStorage
)的数据获取场景,通常建议使用 +page.server.ts
来编写你的 load
函数。这提供了:
- 服务器端渲染 (SSR): 数据在服务器上获取,并直接包含在初始 HTML 中,使得页面在首次加载时完全渲染。
- 安全性: API 密钥和敏感逻辑可以保留在服务器上。
- 性能: 更快的初始加载,因为浏览器不必等待 JavaScript 执行然后再获取数据。
如果你将 fetch
移动到 +page.server.ts
,那里的 load
函数会 await
fetch
,组件将直接接收已解决的数据。对于后续的客户端导航,SvelteKit 会在后台执行 fetch
,如果数据没有立即可用,会隐式使用 {#await}
。
然而,当你想在客户端导航期间显示加载指示器,或从 load
函数返回 Promise 时,上述的 {#await}
模式仍然是处理待处理数据的正确方式。