沈智诚 發表於 2025-3-15 15:43:00

SvelteKit 最新中文文档教程(3)—— 数据加载

<h2 id="前言">前言</h2>
<p>Svelte,一个语法简洁、入门容易,面向未来的前端框架。</p>
<p>从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,<strong>从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1</strong>:</p>
<p><img src="https://yayujs-blog.oss-cn-beijing.aliyuncs.com/405488775-48df16b1-939c-489b-8d52-6071869893f0.png"></p>
<p>Svelte 以其独特的编译时优化机制著称,具有<strong>轻量级</strong>、<strong>高性能</strong>、<strong>易上手</strong>等特性,<strong>非常适合构建轻量级 Web 项目</strong>。</p>
<p>为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。</p>
<p>如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p>
<h2 id="数据加载">数据加载</h2>
<p>在渲染一个 <code>+page.svelte</code> 组件(及其包含的 <code>+layout.svelte</code> 组件)之前,我们通常需要获取一些数据。这是通过定义 <code>load</code> 函数来实现的。</p>
<h2 id="页面数据">页面数据</h2>
<p>一个 <code>+page.svelte</code> 文件可以有一个同级的 <code>+page.js</code> 文件,该文件导出一个 <code>load</code> 函数,该函数的返回值可以通过 <code>data</code> 属性在页面中使用:</p>
<pre><code class="language-js">/// file: src/routes/blog//+page.js
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
        return {
                post: {
                        title: `Title for ${params.slug} goes here`,
                        content: `Content for ${params.slug} goes here`
                }
        };
}
</code></pre>
<pre><code class="language-svelte">&lt;!--- file: src/routes/blog//+page.svelte ---&gt;
&lt;script&gt;
        /** @type {{ data: import('./$types').PageData }} */
        let { data } = $props();
&lt;/script&gt;

&lt;h1&gt;{data.post.title}&lt;/h1&gt;
&lt;div&gt;{@html data.post.content}&lt;/div&gt;
</code></pre>
<blockquote>
<p>[!LEGACY]<br>
在 Svelte 4 中,您需要使用 <code>export let data</code> 代替</p>
</blockquote>
<p>得益于生成的 <code>$types</code> 模块,我们获得了完整的类型安全性。</p>
<p><code>+page.js</code> 文件中的 <code>load</code> 函数在服务端和浏览器上都会运行(除非与 <code>export const ssr = false</code> 结合使用,在这种情况下它将仅在浏览器中运行)。如果您的 <code>load</code> 函数应该始终在服务端上运行(例如,因为它使用了私有环境变量或访问数据库),那么它应该放在 <code>+page.server.js</code> 中。</p>
<p>一个更贴合实际的博客文章 <code>load</code> 函数示例,它只在服务端上运行并从数据库中获取数据。可能如下所示:</p>
<pre><code class="language-js">/// file: src/routes/blog//+page.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
        export function getPost(slug: string): Promise&lt;{ title: string, content: string }&gt;
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
        return {
                post: await db.getPost(params.slug)
        };
}
</code></pre>
<p>注意类型从 <code>PageLoad</code> 变为 <code>PageServerLoad</code>,因为服务端 <code>load</code> 函数可以访问额外的参数。要了解何时使用 <code>+page.js</code> 和何时使用 <code>+page.server.js</code>文档:高级路由 请参阅 Universal 与 server。</p>
<h2 id="布局数据">布局数据</h2>
<p>您的 <code>+layout.svelte</code> 文件也可以通过 <code>+layout.js</code> 或 <code>+layout.server.js</code> 加载数据。</p>
<pre><code class="language-js">/// file: src/routes/blog//+layout.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
        export function getPostSummaries(): Promise&lt;Array&lt;{ title: string, slug: string }&gt;&gt;
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
        return {
                posts: await db.getPostSummaries()
        };
}
</code></pre>
<pre><code class="language-svelte">&lt;!--- file: src/routes/blog//+layout.svelte ---&gt;
&lt;script&gt;
        /** @type {{ data: import('./$types').LayoutData, children: Snippet }} */
        let { data, children } = $props();
&lt;/script&gt;

&lt;main&gt;
        &lt;!-- +page.svelte 在此处被 `@render` --&gt;
        {@render children()}
&lt;/main&gt;

&lt;aside&gt;
        &lt;h2&gt;More posts&lt;/h2&gt;
        &lt;ul&gt;
                {#each data.posts as post}
                        &lt;li&gt;
                                &lt;a href="/blog/{post.slug}"&gt;
                                        {post.title}
                                &lt;/a&gt;
                        &lt;/li&gt;
                {/each}
        &lt;/ul&gt;
&lt;/aside&gt;
</code></pre>
<p>布局 <code>load</code> 函数返回的数据对子 <code>+layout.svelte</code> 组件和 <code>+page.svelte</code> 组件以及它"所属"的布局都可用。</p>
<pre><code class="language-svelte">/// file: src/routes/blog//+page.svelte
&lt;script&gt;
        +++import { page } from '$app/state';+++

        /** @type {{ data: import('./$types').PageData }} */
        let { data } = $props();

+++        // 我们可以访问 `data.posts` 因为它是从
        // 父布局的 `load` 函数返回的
        let index = $derived(data.posts.findIndex(post =&gt; post.slug === page.params.slug));
        let next = $derived(data.posts);+++
&lt;/script&gt;

&lt;h1&gt;{data.post.title}&lt;/h1&gt;
&lt;div&gt;{@html data.post.content}&lt;/div&gt;

+++{#if next}
        &lt;p&gt;Next post: &lt;a href="/blog/{next.slug}"&gt;{next.title}&lt;/a&gt;&lt;/p&gt;
{/if}+++
</code></pre>
<blockquote>
<p>[!NOTE] 如果多个 <code>load</code> 函数返回具有相同键的数据,最后一个会"胜出" —— 布局 <code>load</code> 返回 <code>{ a: 1, b: 2 }</code> 而页面 <code>load</code> 返回 <code>{ b: 3, c: 4 }</code> 的结果将是 <code>{ a: 1, b: 3, c: 4 }</code>。</p>
</blockquote>
<h2 id="pagedata">page.data</h2>
<p><code>+page.svelte</code> 组件及其上面的每个 <code>+layout.svelte</code> 组件都可以访问自己的数据以及其所有父组件的数据。</p>
<p>在某些情况下,我们可能需要相反的效果 - 父布局可能需要访问页面数据或来自子布局的数据。例如,根布局可能想要访问从 <code>+page.js</code> 或 <code>+page.server.js</code> 中的 <code>load</code> 函数返回的 <code>title</code> 属性。这可以通过 <code>page.data</code> 实现:</p>
<pre><code class="language-svelte">&lt;!--- file: src/routes/+layout.svelte ---&gt;
&lt;script&gt;
        import { page } from '$app/state';
&lt;/script&gt;

&lt;svelte:head&gt;
        &lt;title&gt;{page.data.title}&lt;/title&gt;
&lt;/svelte:head&gt;
</code></pre>
<p><code>page.data</code> 的类型信息由 <code>App.PageData</code> 提供。</p>
<blockquote>
<p>[!LEGACY] &gt; <code>$app/state</code> 是在 SvelteKit 2.12 中添加的。如果您使用的是早期版本或使用 Svelte 4,请使用 <code>$app/stores</code> 代替。它提供了一个具有相同接口的 <code>page</code> store,您可以订阅它,例如 <code>$page.data.title</code>。</p>
</blockquote>
<h2 id="universal-vs-server">Universal vs server</h2>
<p>正如我们所见,有两种类型的 <code>load</code> 函数:</p>
<ul>
<li><code>+page.js</code> 和 <code>+layout.js</code> 文件导出的在服务端和浏览器上都运行的<strong>通用</strong> <code>load</code> 函数</li>
<li><code>+page.server.js</code> 和 <code>+layout.server.js</code> 文件导出的只在服务端运行的<strong>服务端</strong> <code>load</code> 函数</li>
</ul>
<p>从概念上讲,它们是相同的东西,但有一些重要的区别需要注意。</p>
<h3 id="何时运行哪个-load-函数">何时运行哪个 load 函数?</h3>
<p>服务端 <code>load</code> 函数<strong>总是</strong>在服务端上运行。</p>
<p>默认情况下,通用 <code>load</code> 函数在用户首次访问页面时在 SSR 期间在服务端上运行。然后它们会在水合过程中再次运行,复用来自 fetch 请求的任何响应。所有后续调用通用 <code>load</code> 函数都发生在浏览器中。您可以通过页面选项自定义该行为。如果您禁用了服务端渲染,您将获得一个 SPA,通用 <code>load</code> 函数<strong>始终</strong>在客户端运行。</p>
<p>如果一个路由同时包含通用和服务端 <code>load</code> 函数,服务端 <code>load</code> 函数会先运行。</p>
<p>除非您预渲染页面 - 在这种情况下,它会在构建时被调用,否则 <code>load</code> 函数会在运行时被调用。</p>
<h3 id="输入">输入</h3>
<p>通用和服务端 <code>load</code> 函数都可以访问描述请求的属性(<code>params</code>、<code>route</code> 和 <code>url</code>)以及各种函数(<code>fetch</code>、<code>setHeaders</code>、<code>parent</code>、<code>depends</code> 和 <code>untrack</code>)。这些在后面的章节中会描述。</p>
<p>服务端 <code>load</code> 函数使用 <code>ServerLoadEvent</code> 调用,它从 <code>RequestEvent</code> 继承 <code>clientAddress</code>、<code>cookies</code>、<code>locals</code>、<code>platform</code> 和 <code>request</code>。</p>
<p>通用 <code>load</code> 函数使用具有 <code>data</code> 属性的 <code>LoadEvent</code> 调用。如果您在 <code>+page.js</code> 和 <code>+page.server.js</code>(或 <code>+layout.js</code> 和 <code>+layout.server.js</code>)中都有 <code>load</code> 函数,则服务端 <code>load</code> 函数的返回值是通用 <code>load</code> 函数参数的 <code>data</code> 属性。</p>
<h3 id="输出">输出</h3>
<p>通用 <code>load</code> 函数可以返回包含任何值的对象,包括自定义类和组件构造函数等内容。</p>
<p>服务端 <code>load</code> 函数必须返回可以用 devalue 序列化的数据 - 任何可以用 JSON 表示的内容,以及像 <code>BigInt</code>、<code>Date</code>、<code>Map</code>、<code>Set</code> 和 <code>RegExp</code> 这样的内容,或重复/循环引用 - 这样它才能通过网络传输。您的数据可以包含promises,在这种情况下它将被流式传输到浏览器。</p>
<h3 id="何时使用哪个">何时使用哪个</h3>
<p>当您需要直接访问数据库或文件系统,或需要使用私有环境变量时,服务端 <code>load</code> 函数很方便。</p>
<p>当您需要从外部 API <code>fetch</code> 数据且不需要私有凭据时,通用 <code>load</code> 函数很有用,因为 SvelteKit 可以直接从 API 获取数据而无需通过服务端。当您需要返回无法序列化的内容(如 Svelte 组件构造函数)时,它们也很有用。</p>
<p>在极少数情况下,您可能需要同时使用两者 - 例如,您可能需要返回一个使用服务端数据初始化的自定义类的实例。当同时使用两者时,服务端 <code>load</code> 的返回值<strong>不会</strong>直接传递给页面,而是传递给通用 <code>load</code> 函数(作为 <code>data</code> 属性):</p>
<pre><code class="language-js">/// file: src/routes/+page.server.js
/** @type {import('./$types').PageServerLoad} */
export async function load() {
        return {
                serverMessage: 'hello from server load function'
        };
}
</code></pre>
<pre><code class="language-js">/// file: src/routes/+page.js
// @errors: 18047
/** @type {import('./$types').PageLoad} */
export async function load({ data }) {
        return {
                serverMessage: data.serverMessage,
                universalMessage: 'hello from universal load function'
        };
}
</code></pre>
<h2 id="使用-url-数据">使用 URL 数据</h2>
<p>通常 <code>load</code> 函数以某种方式依赖于 URL。为此,<code>load</code> 函数提供了 <code>url</code>、<code>route</code> 和 <code>params</code>。</p>
<h3 id="url">url</h3>
<p><code>URL</code> 的一个实例,包含诸如 <code>origin</code>、<code>hostname</code>、<code>pathname</code> 和 <code>searchParams</code>(包含解析后的查询字符串,作为 <code>URLSearchParams</code> 对象)等属性。在 <code>load</code> 期间无法访问 <code>url.hash</code>,因为它在服务端上不可用。</p>
<blockquote>
<p>[!NOTE] 在某些环境中,这是在服务端渲染期间从请求头派生的。例如,如果您使用 adapter-node,您可能需要配置适配器以使 URL 正确。</p>
</blockquote>
<h3 id="route">route</h3>
<p>包含当前路由目录相对于 <code>src/routes</code> 的名称:</p>
<pre><code class="language-js">/// file: src/routes/a//[...c]/+page.js
/** @type {import('./$types').PageLoad} */
export function load({ route }) {
        console.log(route.id); // '/a//[...c]'
}
</code></pre>
<h3 id="params">params</h3>
<p><code>params</code> 是从 <code>url.pathname</code> 和 <code>route.id</code> 派生的。</p>
<p>给定一个 <code>route.id</code> 为 <code>/a//[...c]</code> 且 <code>url.pathname</code> 为 <code>/a/x/y/z</code> 时,<code>params</code> 对象将如下所示:</p>
<pre><code class="language-json">{
        "b": "x",
        "c": "y/z"
}
</code></pre>
<h2 id="发起-fetch-请求">发起 fetch 请求</h2>
<p>要从外部 API 或 <code>+server.js</code> 处理程序获取数据,您可以使用提供的 <code>fetch</code> 函数,它的行为与原生 <code>fetch</code> web API完全相同,但有一些额外的功能:</p>
<ul>
<li>它可以在服务端上发起带凭据的请求,因为它继承了页面请求的 <code>cookie</code> 和 <code>authorization</code> 标头。</li>
<li>它可以在服务端上发起相对请求(通常,当在服务端上下文中使用时,<code>fetch</code> 需要带有源的 URL)。</li>
<li>内部请求(例如对 <code>+server.js</code> 路由的请求)在服务端上运行时直接转到处理函数,无需 HTTP 调用的开销。</li>
<li>在服务端渲染期间,通过钩入 <code>text</code>、<code>json</code> 和 <code>arrayBuffer</code> 方法来捕获响应并将其内联到渲染的 HTML 中 Response 对象。请注意,除非通过 <code>filterSerializedResponseHeaders</code> 显式包含,否则标头将不会被序列化。</li>
<li>在水合过程中,响应将从 HTML 中读取,确保一致性并防止额外的网络请求 - 如果在使用浏览器 <code>fetch</code> 而不是 <code>loadfetch</code> 时,在浏览器控制台中收到警告,这就是原因。</li>
</ul>
<pre><code class="language-js">/// file: src/routes/items//+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
        const res = await fetch(`/api/items/${params.id}`);
        const item = await res.json();

        return { item };
}
</code></pre>
<h2 id="cookies">Cookies</h2>
<p>服务端 <code>load</code> 函数可以获取和设置<code>cookies</code>。</p>
<pre><code class="language-js">/// file: src/routes/+layout.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function getUser(sessionid: string | undefined): Promise&lt;{ name: string, avatar: string }&gt;
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies }) {
const sessionid = cookies.get('sessionid');

return {
    user: await db.getUser(sessionid)
};
}
</code></pre>
<p>只有当目标主机与 SvelteKit 应用程序相同或是其更具体的子域名时,Cookie 才会通过提供的 <code>fetch</code> 函数传递。</p>
<p>例如,如果 SvelteKit 正在为 my.domain.com 提供服务:</p>
<ul>
<li>domain.com 将不会接收 cookies</li>
<li>my.domain.com 将会接收 cookies</li>
<li>api.domain.com 将不会接收 cookies</li>
<li>sub.my.domain.com 将会接收 cookies</li>
</ul>
<p>当设置 <code>credentials: 'include'</code> 时,其他 cookies 将不会被传递,因为 SvelteKit 无法知道哪个 cookie 属于哪个域(浏览器不会传递这些信息),所以转发任何 cookie 都是不安全的。使用 handleFetch hook 钩子来解决这个问题。</p>
<h2 id="headers">Headers</h2>
<p>服务端和通用 <code>load</code> 函数都可以访问 <code>setHeaders</code> 函数,当在服务端上运行时,可以为响应设置头部信息。(在浏览器中运行时,setHeaders 不会产生效果。)这在你想要缓存页面时很有用,例如:</p>
<pre><code class="language-js">// @errors: 2322 1360
/// file: src/routes/products/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, setHeaders }) {
        const url = `https://cms.example.com/products.json`;
        const response = await fetch(url);

        // Headers are only set during SSR, caching the page's HTML
        // for the same length of time as the underlying data.
        setHeaders({
                age: response.headers.get('age'),
                'cache-control': response.headers.get('cache-control')
        });

        return response.json();
}
</code></pre>
<p>多次设置相同的标头(即使在不同的 <code>load</code> 函数中)是一个错误。使用 <code>setHeaders</code> 函数时,每个标头只能设置一次。你不能使用 <code>setHeaders</code> 添加 <code>set-cookie</code> 标头 — 应该使用<code>cookies.set(name, value, options)</code> 代替。</p>
<h2 id="使用父级数据">使用父级数据</h2>
<p>有时候让 <code>load</code> 函数访问父级 <code>load</code> 函数中的数据是很有用的,这可以通过 <code>await parent()</code> 实现:</p>
<pre><code class="language-js">/// file: src/routes/+layout.js
/** @type {import('./$types').LayoutLoad} */
export function load() {
        return { a: 1 };
}
</code></pre>
<pre><code class="language-js">/// file: src/routes/abc/+layout.js
/** @type {import('./$types').LayoutLoad} */
export async function load({ parent }) {
        const { a } = await parent();
        return { b: a + 1 };
}
</code></pre>
<pre><code class="language-js">/// file: src/routes/abc/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ parent }) {
        const { a, b } = await parent();
        return { c: a + b };
}
</code></pre>
<pre><code class="language-svelte">&lt;!--- file: src/routes/abc/+page.svelte ---&gt;
&lt;script&gt;
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();
&lt;/script&gt;

&lt;!-- renders `1 + 2 = 3` --&gt;
&lt;p&gt;{data.a} + {data.b} = {data.c}&lt;/p&gt;
</code></pre>
<blockquote>
<p>[!NOTE] 注意,<code>+page.js</code> 中的 <code>load</code> 函数接收来自两个布局 <code>load</code> 函数的合并数据,而不仅仅是直接父级的数据。</p>
</blockquote>
<p>在 <code>+page.server.js</code> 和 <code>+layout.server.js</code> 内部,<code>parent</code> 从父级 <code>+layout.server.js</code> 文件返回数据。</p>
<p>在 <code>+page.js</code> 或 <code>+layout.js</code> 中,它将返回父级<code>+layout.js</code> 文件中的数据。然而,缺失的 <code>+layout.js</code> 会被视为 <code>({ data }) =&gt; data</code> 函数,这意味着它也会返回未被 <code>+layout.js</code> 文件"遮蔽"的父级 <code>+layout.server.js</code> 文件中的数据。</p>
<p>使用 <code>await parent()</code> 时要注意避免瀑布流。例如,<code>getData(params)</code> 并不依赖于调用 <code>parent()</code> 的结果,所以我们应该先调用它以避免延迟渲染。</p>
<pre><code class="language-js">/// file: +page.js
// @filename: ambient.d.ts
declare function getData(params: Record&lt;string, string&gt;): Promise&lt;{ meta: any }&gt;

// @filename: index.js
// ---cut---
/** @type {import('./$types').PageLoad} */
export async function load({ params, parent }) {
---const parentData = await parent();---
const data = await getData(params);
+++const parentData = await parent();+++

return {
    ...data,
    meta: { ...parentData.meta, ...data.meta }
};
}
</code></pre>
<h2 id="errors">Errors</h2>
<p>如果在 <code>load</code> 期间抛出错误,将渲染最近的 <code>+error.svelte</code>。对于预期的错误,使用来自 <code>@sveltejs/kit</code> 的 <code>error</code> 辅助函数来指定 HTTP 状态码和可选消息:</p>
<pre><code class="language-js">/// file: src/routes/admin/+layout.server.js
// @filename: ambient.d.ts
declare namespace App {
interface Locals {
    user?: {
      name: string;
      isAdmin: boolean;
    }
}
}

// @filename: index.js
// ---cut---
import { error } from '@sveltejs/kit';

/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
    error(401, 'not logged in');
}

if (!locals.user.isAdmin) {
    error(403, 'not an admin');
}
}
</code></pre>
<p>调用 <code>error(...)</code> 将抛出一个异常,这使得在辅助函数内部停止执行变得容易。</p>
<p>如果抛出了一个<em>意外</em>错误,SvelteKit 将调用 <code>handleError</code> 并将其视为 500 内部错误。</p>
<blockquote>
<p>[!NOTE] 在 SvelteKit 1.x 中,你必须自己 <code>throw</code> 错误</p>
</blockquote>
<h2 id="redirects">Redirects</h2>
<p>要重定向用户,请使用来自 <code>@sveltejs/kit</code> 的 <code>redirect</code> 辅助函数,以指定用户应被重定向到的位置以及一个 <code>3xx</code> 状态码。与 <code>error(...)</code> 类似,调用 <code>redirect(...)</code> 将抛出一个异常,这使得在辅助函数内部停止执行变得容易。</p>
<pre><code class="language-js">/// file: src/routes/user/+layout.server.js
// @filename: ambient.d.ts
declare namespace App {
interface Locals {
    user?: {
      name: string;
    }
}
}

// @filename: index.js
// ---cut---
import { redirect } from '@sveltejs/kit';

/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
    redirect(307, '/login');
}
}
</code></pre>
<blockquote>
<p>[!NOTE] 不要在 <code>try {...}</code> 块内使用 <code>redirect()</code>,因为重定向会立即触发 catch 语句。</p>
</blockquote>
<p>在浏览器中,你也可以在 <code>load</code> 函数之外使用来自 <code>$app.navigation</code> 的 <code>goto</code> 通过编程的方式进行导航。</p>
<blockquote>
<p>[!NOTE] 在 SvelteKit 1.x 中,你必须自己 <code>throw</code> 这个 <code>redirect</code></p>
</blockquote>
<h2 id="streaming-with-promises">Streaming with promises</h2>
<p>当使用服务端 <code>load</code> 时,Promise 将在 resolve 时流式传输到浏览器。如果你有较慢的、非必要的数据,这很有用,因为你可以在所有数据可用之前开始渲染页面:</p>
<pre><code class="language-js">/// file: src/routes/blog//+page.server.js
// @filename: ambient.d.ts
declare global {
const loadPost: (slug: string) =&gt; Promise&lt;{ title: string, content: string }&gt;;
const loadComments: (slug: string) =&gt; Promise&lt;{ content: string }&gt;;
}

export {};

// @filename: index.js
// ---cut---
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
    // make sure the `await` happens at the end, otherwise we
    // can't start loading comments until we've loaded the post
    comments: loadComments(params.slug),
    post: await loadPost(params.slug)
};
}
</code></pre>
<p>这对创建骨架加载状态很有用,例如:</p>
<pre><code class="language-svelte">&lt;!--- file: src/routes/blog//+page.svelte ---&gt;
&lt;script&gt;
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();
&lt;/script&gt;

&lt;h1&gt;{data.post.title}&lt;/h1&gt;
&lt;div&gt;{@html data.post.content}&lt;/div&gt;

{#await data.comments}
Loading comments...
{:then comments}
{#each comments as comment}
    &lt;p&gt;{comment.content}&lt;/p&gt;
{/each}
{:catch error}
&lt;p&gt;error loading comments: {error.message}&lt;/p&gt;
{/await}
</code></pre>
<p>在流式传输数据时,请注意正确处理 Promise rejections。具体来说,如果懒加载的 Promise 在渲染开始前失败(此时会被捕获)且没有以某种方式处理错误,服务器可能会因 "unhandled promise rejection" 错误而崩溃。</p>
<p>当在 <code>load</code> 函数中直接使用 SvelteKit 的 <code>fetch</code> 时,SvelteKit 会为您处理这种情况。对于其他 Promise,只需为 Promise 添加一个空的 <code>catch</code> 即可将其标记为已处理。</p>
<pre><code class="language-js">/// file: src/routes/+page.server.js
/** @type {import('./$types').PageServerLoad} */
export function load({ fetch }) {
        const ok_manual = Promise.reject();
        ok_manual.catch(() =&gt; {});

        return {
                ok_manual,
                ok_fetch: fetch('/fetch/that/could/fail'),
                dangerous_unhandled: Promise.reject()
        };
}
</code></pre>
<blockquote>
<p>[!NOTE] 在不支持流式传输的平台上(如 AWS Lambda 或 Firebase),响应将被缓冲。这意味着页面只会在所有 promise resolve 后才会渲染。如果您使用代理(例如 NGINX),请确保它不会缓冲来自代理服务器的响应。</p>
</blockquote>
<blockquote>
<p>[!NOTE] 流式数据传输只有在启用 JavaScript 时才能工作。如果页面是服务端渲染的,您应该避免从通用 <code>load</code> 函数返回 promise,因为这些 promise 不会被流式传输 —— 相反,当函数在浏览器中重新运行时,promise 会被重新创建。</p>
</blockquote>
<blockquote>
<p>[!NOTE] 一旦响应开始流式传输,就无法更改响应的标头和状态码,因此您无法 <code>setHeaders</code> 或抛出重定向到流式 promise 内。</p>
</blockquote>
<blockquote>
<p>[!NOTE] 在 SvelteKit 1.x 中,顶层 promise 会自动 awaited,只有嵌套的 promise 才会流式传输。</p>
</blockquote>
<h2 id="并行加载">并行加载</h2>
<p>在渲染(或导航到)页面时,SvelteKit 会同时运行所有 <code>load</code> 函数,避免请求瀑布。在客户端导航期间,多个服务器 <code>load</code> 函数的调用结果会被组合到单个响应中。一旦所有 <code>load</code> 函数都返回结果,页面就会被渲染。</p>
<h2 id="重新运行-load-函数">重新运行 load 函数</h2>
<p>SvelteKit 会追踪每个 <code>load</code> 函数的依赖关系,以避免在导航过程中不必要的重新运行。</p>
<p>例如,给定一对这样的 <code>load</code> 函数...</p>
<pre><code class="language-js">/// file: src/routes/blog//+page.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function getPost(slug: string): Promise&lt;{ title: string, content: string }&gt;
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
    post: await db.getPost(params.slug)
};
}
</code></pre>
<pre><code class="language-js">/// file: src/routes/blog//+layout.server.js
// @filename: ambient.d.ts
declare module '$lib/server/database' {
export function getPostSummaries(): Promise&lt;Array&lt;{ title: string, slug: string }&gt;&gt;
}

// @filename: index.js
// ---cut---
import * as db from '$lib/server/database';

/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
return {
    posts: await db.getPostSummaries()
};
}
</code></pre>
<p>...其中 <code>+page.server.js</code> 中的函数在从 <code>/blog/trying-the-raw-meat-diet</code> 导航到 <code>/blog/i-regret-my-choices</code> 时会重新运行,因为 <code>params.slug</code> 发生了变化。而 <code>+layout.server.js</code> 中的函数则不会重新运行,因为数据仍然有效。换句话说,我们不会第二次调用 <code>db.getPostSummaries()</code>。</p>
<p>如果父级 <code>load</code> 函数重新运行,调用了 <code>await parent()</code> 的 <code>load</code> 函数也会重新运行。</p>
<p>依赖追踪在 <code>load</code> 函数返回后不再适用 — 例如,在嵌套的 promise 中访问 <code>params.x</code> 不会在 <code>params.x</code> 改变时导致函数重新运行。(别担心,如果你不小心这样做了,在开发环境中会收到警告。)相反,应该在 <code>load</code> 函数的主体中访问参数。</p>
<p>搜索参数的追踪独立于 URL 的其余部分。例如,在 <code>load</code> 函数中访问 <code>event.url.searchParams.get("x")</code> 将使该 <code>load</code> 函数在从 <code>?x=1</code> 导航到 <code>?x=2</code> 时重新运行,但从 <code>?x=1&amp;y=1</code> 导航到 <code>?x=1&amp;y=2</code> 时则不会重新运行。</p>
<h3 id="取消依赖追踪">取消依赖追踪</h3>
<p>在极少数情况下,你可能希望将某些内容排除在依赖追踪机制之外。你可以使用提供的 <code>untrack</code> 函数实现这一点:</p>
<pre><code class="language-js">/// file: src/routes/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ untrack, url }) {
        // Untrack url.pathname so that path changes don't trigger a rerun
        if (untrack(() =&gt; url.pathname === '/')) {
                return { message: 'Welcome!' };
        }
}
</code></pre>
<h3 id="手动失效">手动失效</h3>
<p>你还可以使用 <code>invalidate(url)</code> 重新运行适用于当前页面的 <code>load</code> 函数,它会重新运行所有依赖于 <code>url</code> 的 <code>load</code> 函数,以及使用 <code>invalidateAll()</code> 重新运行每个 <code>load</code> 函数。服务端加载函数永远不会自动依赖于获取数据的 <code>url</code>,以避免将秘密泄露给客户端。</p>
<p>如果一个 <code>load</code> 函数调用了 <code>fetch(url)</code> 或 <code>depends(url)</code>,那么它就依赖于 <code>url</code>。注意,<code>url</code> 可以是以 <code></code>开头的自定义标识符:</p>
<pre><code class="language-js">/// file: src/routes/random-number/+page.js
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, depends }) {
        // load reruns when `invalidate('https://api.example.com/random-number')` is called...
        const response = await fetch('https://api.example.com/random-number');

        // ...or when `invalidate('app:random')` is called
        depends('app:random');

        return {
                number: await response.json()
        };
}
</code></pre>
<pre><code class="language-svelte">&lt;!--- file: src/routes/random-number/+page.svelte ---&gt;
&lt;script&gt;
import { invalidate, invalidateAll } from '$app/navigation';

/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();

function rerunLoadFunction() {
    // any of these will cause the `load` function to rerun
    invalidate('app:random');
    invalidate('https://api.example.com/random-number');
    invalidate(url =&gt; url.href.includes('random-number'));
    invalidateAll();
}
&lt;/script&gt;

&lt;p&gt;random number: {data.number}&lt;/p&gt;
&lt;button onclick={rerunLoadFunction}&gt;Update random number&lt;/button&gt;
</code></pre>
<h3 id="load-函数何时重新运行">load 函数何时重新运行?</h3>
<p>总的来说,<code>load</code> 函数在以下情况下会重新运行:</p>
<ul>
<li>它引用了 <code>params</code> 中已更改值的属性</li>
<li>它引用了 <code>url</code> 的某个属性(如 <code>url.pathname</code> 或<code>url.search</code>)且该属性的值已更改。<code>request.url</code> 中的属性不会被追踪</li>
<li>它调用 <code>url.searchParams.get(...)</code>、<code>url.searchParams.getAll(...)</code> 或 <code>url.searchParams.has(...)</code>,且相关参数发生变化。访问 <code>url.searchParams</code> 的其他属性与访问 <code>url.search</code>具有相同的效果。</li>
<li>它调用 <code>await parent()</code> 且父 <code>load</code> 函数重新运行</li>
<li>当子 <code>load</code> 函数调用 <code>await parent()</code> 并重新运行,且父函数是服务端 <code>load</code> 函数</li>
<li>它通过 <code>fetch</code>(仅限通用 load)或 <code>depends</code> 声明了对特定 URL 的依赖,且该 URL 被 <code>invalidate(url)</code> 标记为无效</li>
<li>所有活动的 <code>load</code> 函数都被 <code>invalidateAll()</code> 强制重新运行</li>
</ul>
<p><code>params</code> 和 <code>url</code> 可以在响应 <code>&lt;a href=".."&gt;</code> 链接点击、<code>&lt;form&gt;</code> 交互<code>goto</code> 调用或 <code>重定向</code> 时发生变化。</p>
<p>注意,重新运行 <code>load</code> 函数将更新相应 <code>+layout.svelte</code> 或 <code>+page.svelte</code> 中的 <code>data</code> 属性;这不会导致组件重新创建。因此,内部状态会被保留。如果这不是你想要的,你可以在<code>afterNavigate</code> 回调中重置所需内容,或者用 <code>{#key ...}</code> 块包装你的组件。</p>
<h2 id="对身份验证的影响">对身份验证的影响</h2>
<p>数据加载的几个特性对身份验证有重要影响:</p>
<ul>
<li>布局 <code>load</code> 函数不会在每个请求时运行,例如在子路由之间的客户端导航期间。(load函数何时重新运行?)</li>
<li>布局和页面 <code>load</code> 函数会同时运行,除非调用了 <code>await parent()</code>。如果布局 <code>load</code> 抛出错误,页面 <code>load</code> 函数会运行,但客户端将不会收到返回的数据。</li>
</ul>
<p>有几种可能的策略来确保在受保护代码之前进行身份验证检查。</p>
<p>为防止数据瀑布并保留布局 <code>load</code> 缓存:</p>
<ul>
<li>使用 hooks 在任何 <code>load</code> 函数运行之前保护多个路由</li>
<li>在 <code>+page.server.js</code> <code>load</code> 函数中直接使用身份验证守卫进行特定路由保护</li>
</ul>
<p>在 <code>+layout.server.js</code> 中放置身份验证守卫要求所有子页面在受保护代码之前调用 <code>await parent()</code>。除非每个子页面都依赖于<code>await parent()</code> 返回的数据,否则其他选项会更有性能优势。</p>
<h2 id="拓展阅读">拓展阅读</h2>
<ul>
<li>教程:数据加载</li>
<li>教程:错误和重定向</li>
<li>教程:高级加载</li>
</ul>
<h2 id="svelte-中文文档">Svelte 中文文档</h2>
<p>点击查看中文文档 - SvelteKit 数据加载。</p>
<p>系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p><br><br>
来源:https://www.cnblogs.com/yayujs/p/18773769
頁: [1]
查看完整版本: SvelteKit 最新中文文档教程(3)—— 数据加载