宋英南 發表於 2025-3-19 21:44:00

SvelteKit 最新中文文档教程(6)—— 状态管理

<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>如果您习惯于构建仅客户端的应用程序,在跨服务端和客户端的应用中进行状态管理可能会让人感到望而生畏。本节提供了一些避免常见陷阱的建议。</p>
<h2 id="避免在服务端共享状态">避免在服务端共享状态</h2>
<p>浏览器是<em>有状态的</em> — 状态在用户与应用程序交互时存储在内存中。相反,服务端是<em>无状态的</em> — 响应的内容完全取决于请求的内容。</p>
<p>从概念上来说是这样的。实际上,服务端通常是长期运行的,并由多个用户共享。因此,避免在共享变量中存储数据非常重要。例如,考虑以下代码:</p>
<pre><code class="language-js">// @errors: 7034 7005
/// file: +page.server.js
let user;

/** @type {import('./$types').PageServerLoad} */
export function load() {
        return { user };
}

/** @satisfies {import('./$types').Actions} */
export const actions = {
        default: async ({ request }) =&gt; {
                const data = await request.formData();

                // 永远不要这样做!
                user = {
                        name: data.get('name'),
                        embarrassingSecret: data.get('secret')
                };
        }
};
</code></pre>
<p><code>user</code> 变量被所有连接到这个服务器的人共享。如果 Alice 提交了一个尴尬的秘密,而 Bob 在她之后访问页面,Bob 就会知道 Alice 的秘密。此外,当 Alice 当天晚些时候返回网站时,服务器可能已经重启,丢失了她的数据。</p>
<p>相反,您应该使用 <code>cookies</code> 对用户进行<em>认证</em>,并将数据持久化到数据库中。</p>
<h2 id="load-函数中不要有副作用">load 函数中不要有副作用</h2>
<p>出于同样的原因,您的 <code>load</code> 函数应该是<em>纯函数</em> — 没有副作用(除了偶尔的 <code>console.log(...)</code>)。例如,您可能会想在 <code>load</code> 函数中写入 store 或全局状态,以便在组件中使用这个值:</p>
<pre><code class="language-js">/// file: +page.js
// @filename: ambient.d.ts
declare module '$lib/user' {
        export const user: { set: (value: any) =&gt; void };
}

// @filename: index.js
// ---cut---
import { user } from '$lib/user';

/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
        const response = await fetch('/api/user');

        // 永远不要这样做!
        user.set(await response.json());
}
</code></pre>
<p>与前面的例子一样,这将一个用户的信息放在了<em>所有</em>用户共享的地方。相反,应该直接返回数据...</p>
<pre><code class="language-js">/// file: +page.js
/** @type {import('./$types').PageServerLoad} */
export async function load({ fetch }) {
        const response = await fetch('/api/user');

+++        return {
                user: await response.json()
        };+++
}
</code></pre>
<p>...然后将它传递给需要它的组件,或使用 <code>page.data</code>。</p>
<p>如果您不使用 SSR,那么就不会有意外将一个用户数据暴露给另一个用户的风险。但您仍然应该避免在 <code>load</code> 函数中产生副作用 — 这样您的应用程序会更容易理解。</p>
<h2 id="使用带上下文的状态和-stores">使用带上下文的状态和 stores</h2>
<p>您可能会疑惑,如果我们不能使用全局状态,我们如何使用 <code>page.data</code> 和其他 app 状态(或 app stores)。答案是 app 状态和 app stores 在服务端使用 Svelte 的 context API — 状态(或 store)通过 <code>setContext</code> 附加到组件树上,当您订阅时,通过 <code>getContext</code> 检索它。我们可以用同样的方式处理我们自己的状态:</p>
<pre><code class="language-svelte">&lt;!--- file: src/routes/+layout.svelte ---&gt;
&lt;script&gt;
        import { setContext } from 'svelte';

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

        // 将引用我们状态的函数
        // 传递给上下文,供子组件访问
        setContext('user', () =&gt; data.user);
&lt;/script&gt;
</code></pre>
<pre><code class="language-svelte">&lt;!--- file: src/routes/user/+page.svelte ---&gt;
&lt;script&gt;
        import { getContext } from 'svelte';

        // 从上下文中获取 user store
        const user = getContext('user');
&lt;/script&gt;

&lt;p&gt;Welcome {user().name}&lt;/p&gt;
</code></pre>
<blockquote>
<p>[!NOTE] 我们传递一个函数到 <code>setContext</code> 以保持跨边界的响应性。在这里阅读更多相关信息</p>
</blockquote>
<blockquote>
<p>[!LEGACY]<br>
您也可以使用 <code>svelte/store</code> 中的 stores 来实现这一点,但在使用 Svelte 5 时,建议使用通用响应性。</p>
</blockquote>
<p>在通过 SSR 渲染页面时,在更深层次的页面或组件中更新基于上下文的状态值不会影响父组件中的值,因为在状态值更新时父组件已经被渲染完成。</p>
<p>相比之下,在客户端(当启用 CSR 时,这是默认设置)这个值会被传播,层级更高的组件、页面和布局会对新值作出反应。因此,为了避免在水合过程中状态更新时值"闪烁",通常建议将状态向下传递给组件,而不是向上传递。</p>
<p>如果您不使用 SSR(并且可以保证将来也不需要使用 SSR),那么您可以安全地将状态保存在共享模块中,而无需使用 context API。</p>
<h2 id="组件和页面状态会被保留">组件和页面状态会被保留</h2>
<p>当您在应用程序中导航时,SvelteKit 会复用现有的布局和页面组件。例如,如果您有这样的路由...</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();

        // 这段代码有 BUG!
        const wordCount = data.content.split(' ').length;
        const estimatedReadingTime = wordCount / 250;
&lt;/script&gt;

&lt;header&gt;
        &lt;h1&gt;{data.title}&lt;/h1&gt;
        &lt;p&gt;Reading time: {Math.round(estimatedReadingTime)} minutes&lt;/p&gt;
&lt;/header&gt;

&lt;div&gt;{@html data.content}&lt;/div&gt;
</code></pre>
<p>...那么从 <code>/blog/my-short-post</code> 导航到 <code>/blog/my-long-post</code> 不会导致布局、页面和其他组件被销毁和重新创建。相反,<code>data</code> 属性(以及 <code>data.title</code> 和 <code>data.content</code>)将会更新(就像任何其他 Svelte 组件一样),而且因为代码不会重新运行,像 <code>onMount</code> 和 <code>onDestroy</code> 这样的生命周期方法不会重新运行,<code>estimatedReadingTime</code> 也不会重新计算。</p>
<p>相反,我们需要使这个值变成<em>响应式</em>:</p>
<pre><code class="language-svelte">/// file: src/routes/blog//+page.svelte
&lt;script&gt;
        /** @type {{ data: import('./$types').PageData }} */
        let { data } = $props();

+++        let wordCount = $derived(data.content.split(' ').length);
        let estimatedReadingTime = $derived(wordCount / 250);+++
&lt;/script&gt;
</code></pre>
<blockquote>
<p>[!NOTE] 如果您需要在导航后重新运行 <code>onMount</code> 和 <code>onDestroy</code> 中的代码,您可以分别使用 afterNavigate 和 beforeNavigate。</p>
</blockquote>
<p>像这样复用组件意味着侧边栏滚动状态等会被保留,您可以轻松地在变化的值之间实现动画效果。如果您确实需要在导航时完全销毁并重新挂载一个组件,您可以使用这种模式:</p>
<pre><code class="language-svelte">&lt;script&gt;
        import { page } from '$app/state';
&lt;/script&gt;

{#key page.url.pathname}
        &lt;BlogPost title={data.title} content={data.title} /&gt;
{/key}
</code></pre>
<h2 id="在-url-中存储状态">在 URL 中存储状态</h2>
<p>如果您有需要让状态能够在页面重新加载后依然保持,比如表格上的过滤器或排序规则,URL 搜索参数(如 <code>?sort=price&amp;order=ascending</code>)是存储它们的好地方。您可以把它们放在 <code>&lt;a href="..."&gt;</code> 或 <code>&lt;form action="..."&gt;</code> 属性中,或通过 <code>goto('?key=value')</code> 以编程的方式设置它们。它们可以在 <code>load</code> 函数中通过 <code>url</code> 参数访问,在组件中通过 <code>page.url.searchParams</code> 访问。</p>
<h2 id="在快照中存储临时状态">在快照中存储临时状态</h2>
<p>某些 UI 状态,比如"列表是否展开?",是可以丢弃的 — 如果用户导航离开或刷新页面,状态丢失并不要紧。在某些情况下,您<em>确实</em>希望在用户导航到另一个页面并返回时数据能够保持,但将状态存储在 URL 或数据库中会显得过度。对于这种情况,SvelteKit 提供了 快照,让您可以将组件状态与历史记录条目关联起来。</p>
<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/18781916
頁: [1]
查看完整版本: SvelteKit 最新中文文档教程(6)—— 状态管理