在 SvelteKit 中缓存数据

在 SvelteKit 中缓存数据

My 以前的帖子 是对 SvelteKit 的广泛概述,我们在其中看到了它对于 Web 开发来说是多么出色的工具。 这篇文章将分叉我们在那里所做的,并深入探讨每个开发人员最喜欢的主题: 缓存. 因此,如果您还没有阅读我的上一篇文章,请务必阅读。 这篇文章的代码 可在 GitHub 上获得以及 现场演示.

这篇文章是关于数据处理的。 我们将添加一些基本的搜索功能,这些功能将修改页面的查询字符串(使用内置的 SvelteKit 功能),并重新触发页面的加载程序。 但是,我们将添加一些缓存,而不是仅仅重新查询我们的(假想的)数据库,因此重新搜索之前的搜索(或使用后退按钮)将快速显示之前从缓存中检索到的数据。 我们将了解如何控制缓存数据保持有效的时间长度,更重要的是,如何手动使所有缓存值无效。 作为锦上添花,我们将研究如何在更改后在客户端手动更新当前屏幕上的数据,同时仍然清除缓存。

这将是一篇比我通常写的大部分内容更长、更难的帖子,因为我们涵盖了更难的主题。 这篇文章将主要向您展示如何实现流行数据实用程序的常见功能,例如 反应查询; 但我们不会引入外部库,而只会使用网络平台和 SvelteKit 功能。

不幸的是,网络平台的功能有点低级,所以我们将做比您可能习惯的更多的工作。 好处是我们不需要任何外部库,这将有助于保持捆绑包的大小。 除非你有充分的理由,否则请不要使用我将向你展示的方法。 缓存很容易出错,正如您将看到的那样,您的应用程序代码会有些复杂。 希望您的数据存储速度快,并且您的 UI 很好,允许 SvelteKit 始终请求任何给定页面所需的数据。 如果是,请不要管它。 享受简单。 但这篇文章将向您展示一些技巧,以应对何时不再出现这种情况。

说到反应查询,它 刚被释放 为了苗条! 因此,如果您发现自己依赖于手动缓存技术 很多, 请务必检查该项目,看看它是否有帮助。

配置

在我们开始之前,让我们做一些小的改变 我们之前的代码. 这将为我们提供一个借口来查看 SvelteKit 的其他一些功能,更重要的是,为我们的成功做好准备。

首先,让我们将加载器中的数据加载到 +page.server.jsAPI路线. 我们将创建一个 +server.js 文件 routes/api/todos,然后添加一个 GET 功能。 这意味着我们现在可以获取(使用默认的 GET 动词)到 /api/todos 小路。 我们将添加与以前相同的数据加载代码。

import { json } from "@sveltejs/kit";
import { getTodos } from "$lib/data/todoData"; export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; const todos = await getTodos(search); return json(todos);
}

接下来,让我们使用我们拥有的页面加载器,并将文件重命名为 +page.server.js+page.js (或 .ts 如果你已经搭建了你的项目以使用 TypeScript)。 这将我们的加载器更改为“通用”加载器而不是服务器加载器。 SvelteKit 文档 解释差异,但是一个通用加载器在服务器和客户端上运行。 我们的优势之一是 fetch 调用我们的新端点将直接从我们的浏览器运行(在初始加载后),使用浏览器的本机 fetch 功能。 我们稍后会添加标准的 HTTP 缓存,但现在,我们要做的就是调用端点。

export async function load({ fetch, url, setHeaders }) { const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}`); const todos = await resp.json(); return { todos, };
}

现在让我们添加一个简单的表单到我们的 /list 页面:

<div class="search-form"> <form action="/zh-CN/list"> <label>Search</label> <input autofocus name="search" /> </form>
</div>

是的,表单可以直接定位到我们的普通页面加载器。 现在我们可以在搜索框中添加搜索词,点击 输入,并且“搜索”项将附加到 URL 的查询字符串,这将重新运行我们的加载程序并搜索我们的待办事项。

搜索表单
在 SvelteKit 中缓存数据

让我们也增加我们的延迟 todoData.js 文件 /lib/data. 当我们完成这篇文章时,这将使我们很容易看到何时缓存和不缓存数据。

export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 500));

请记住,这篇文章的完整代码是 全部在 GitHub 上,如果你需要引用它。

基本缓存

让我们开始添加一些缓存到我们的 /api/todos 端点。 我们会回到我们的 +server.js 文件并添加我们的第一个缓存控制标头。

setHeaders({ "cache-control": "max-age=60",
});

…这将使整个函数看起来像这样:

export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; setHeaders({ "cache-control": "max-age=60", }); const todos = await getTodos(search); return json(todos);
}

我们很快就会看到手动失效,但是这个函数只是说将这些 API 调用缓存 60 秒。 将此设置为任何你想要的,并根据您的用例, stale-while-revalidate 可能也值得研究。

就这样,我们的查询正在缓存。

在 DevTools 中缓存。
在 SvelteKit 中缓存数据

备注 确保你 取消勾选 在开发工具中禁用缓存的复选框。

请记住,如果您在应用程序上的初始导航是列表页面,则这些搜索结果将在内部缓存到 SvelteKit,因此当返回该搜索时不要期望在 DevTools 中看到任何内容。

什么被缓存,在哪里

我们应用程序的第一个服务器渲染负载(假设我们从 /list 页面)将在服务器上获取。 SvelteKit 将序列化并将此数据发送给我们的客户端。 更重要的是,它会观察 Cache-Control 在响应上,并且将知道在缓存窗口(我们在放置示例中将其设置为 60 秒)内将此缓存数据用于该端点调用。

初始加载后,当您开始在页面上搜索时,您应该会看到从浏览器到 /api/todos 列表。 当您搜索您已经搜索过的内容时(在过去 60 秒内),响应应该会立即加载,因为它们已被缓存。

这种方法特别酷的地方在于,由于这是通过浏览器的本机缓存进行缓存,因此即使您重新加载页面(与初始服务器端加载,它始终调用端点新鲜,即使它是在最近 60 秒内调用的)。

显然数据可以随时更改,因此我们需要一种手动清除此缓存的方法,我们将在接下来查看。

缓存失效

现在,数据将被缓存 60 秒。 无论如何,一分钟后,将从我们的数据存储中提取新数据。 您可能想要更短或更长的时间段,但是如果您更改某些数据并希望立即清除缓存以便您的下一个查询是最新的,会发生什么情况? 我们将通过向我们发送到新的 URL 的 URL 添加一个查询破坏值来解决这个问题 /todos 端点。

让我们将这个缓存清除值存储在 cookie 中。 该值可以在服务器上设置,但仍可在客户端读取。 让我们看一些示例代码。

我们可以创建一个 +layout.server.js 文件在我们的根 routes 文件夹。 这将在应用程序启动时运行,并且是设置初始 cookie 值的理想位置。

export function load({ cookies, isDataRequest }) { const initialRequest = !isDataRequest; const cacheValue = initialRequest ? +new Date() : cookies.get("todos-cache"); if (initialRequest) { cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false }); } return { todosCacheBust: cacheValue, };
}

您可能已经注意到 isDataRequest 价值。 请记住,布局将在客户端代码调用时重新运行 invalidate(),或者我们运行服务器操作的任何时候(假设我们不关闭默认行为)。 isDataRequest 表示那些重新运行,所以我们只设置 cookie 如果那是 false; 否则,我们会发送已经存在的内容。

httpOnly: false 标志也很重要。 这允许我们的客户端代码读取这些 cookie 值 document.cookie. 这通常是一个安全问题,但在我们的例子中,这些是允许我们缓存或缓存 bust 的无意义数字。

读取缓存值

我们的通用装载机就是我们的 /todos 端点。 这在服务器或客户端上运行,我们需要读取我们刚刚设置的缓存值,无论我们身在何处。 如果我们在服务器上就相对容易了:我们可以调用 await parent() 从父布局获取数据。 但是在客户端,我们需要使用一些粗略的代码来解析 document.cookie:

export function getCookieLookup() { if (typeof document !== "object") { return {}; } return document.cookie.split("; ").reduce((lookup, v) => { const parts = v.split("="); lookup[parts[0]] = parts[1]; return lookup; }, {});
} const getCurrentCookieValue = name => { const cookies = getCookieLookup(); return cookies[name] ?? "";
};

幸运的是,我们只需要一次。

发送缓存值

但现在我们需要 提交 这个价值对我们 /todos 端点。

import { getCurrentCookieValue } from "$lib/util/cookieUtils"; export async function load({ fetch, parent, url, setHeaders }) { const parentData = await parent(); const cacheBust = getCurrentCookieValue("todos-cache") || parentData.todosCacheBust; const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}&cache=${cacheBust}`); const todos = await resp.json(); return { todos, };
}

getCurrentCookieValue('todos-cache') 检查它以查看我们是否在客户端上(通过检查文档类型),如果是,则不返回任何内容,此时我们知道我们在服务器上。 然后它使用我们布局中的值。

破坏缓存

但是, 形成一种 我们真的会在需要时更新缓存清除值吗? 由于它存储在 cookie 中,我们可以从任何服务器操作中这样调用它:

cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false });

实施

从这里开始都是下坡路; 我们已经完成了艰苦的工作。 我们已经涵盖了我们需要的各种网络平台原语,以及它们的去向。 现在让我们找点乐子,编写应用程序代码将它们结合在一起。

出于稍后会变得清晰的原因,让我们从向我们的代码添加编辑功能开始 /list 页。 我们将为每个待办事项添加第二个表行:

import { enhance } from "$app/forms";
<tr> <td colspan="4"> <form use:enhance method="post" action="?/editTodo"> <input name="id" value="{t.id}" type="hidden" /> <input name="title" value="{t.title}" /> <button>Save</button> </form> </td>
</tr>

而且,当然,我们需要为我们的 /list 页。 动作只能进去 .server 页面,所以我们将添加一个 +page.server.js 请通过 WestEd 就业网页,在我们的 /list 文件夹。 (是的,一个 +page.server.js 文件可以共存旁边 +page.js 文件。)

import { getTodo, updateTodo, wait } from "$lib/data/todoData"; export const actions = { async editTodo({ request, cookies }) { const formData = await request.formData(); const id = formData.get("id"); const newTitle = formData.get("title"); await wait(250); updateTodo(id, newTitle); cookies.set("todos-cache", +new Date(), { path: "/", httpOnly: false }); },
};

我们正在抓取表单数据,强制延迟,更新我们的待办事项,然后,最重要的是,清除我们的缓存 bust cookie。

让我们试一试。 重新加载您的页面,然后编辑其中一个待办事项。 您应该会在片刻之后看到表值更新。 如果您查看 DevTool 中的“网络”选项卡,您将看到对 /todos 端点,返回您的新数据。 简单,默认情况下工作。

保存数据
在 SvelteKit 中缓存数据

即时更新

如果我们想避免在更新待办事项后发生的获取,而是直接在屏幕上更新修改后的项目怎么办?

这不仅仅是性能问题。 如果您搜索“post”,然后从列表中的任何待办事项中删除“post”一词,它们将在编辑后从列表中消失,因为它们不再出现在该页面的搜索结果中。 你可以通过一些有品位的动画来改善用户体验,但假设我们想要 不能 重新运行该页面的加载功能,但仍然清除缓存并更新修改后的待办事项,以便用户可以看到编辑。 SvelteKit 使这成为可能——让我们看看如何做!

首先,让我们对加载器做一点改动。 与其返回我们的待办事项,不如返回一个 可写存储 包含我们的待办事项。

return { todos: writable(todos),
};

之前,我们访问我们的待办事项 data 道具,我们不拥有并且无法更新。 但是 Svelte 允许我们在他们自己的存储中返回我们的数据(假设我们使用的是通用加载器,我们就是这样)。 我们只需要再对我们的 /list 页面上发布服务提醒。

而不是这个:

{#each todos as t}

......我们需要这样做,因为 todos 它本身现在是一家商店。:

{#each $todos as t}

现在我们的数据像以前一样加载。 但由于 todos 是可写存储,我们可以更新它。

首先,让我们为我们的 use:enhance 属性:

<form use:enhance={executeSave} on:submit={runInvalidate} method="post" action="?/editTodo"
>

这将在提交之前运行。 接下来我们写一下:

function executeSave({ data }) { const id = data.get("id"); const title = data.get("title"); return async () => { todos.update(list => list.map(todo => { if (todo.id == id) { return Object.assign({}, todo, { title }); } else { return todo; } }) ); };
}

这个函数提供了一个 data 使用我们的表单数据对象。 我们 回报 将运行的异步函数 after 我们的编辑完成了。 文档 解释所有这一切,但通过这样做,我们关闭了 SvelteKit 的默认表单处理,它会重新运行我们的加载程序。 这正是我们想要的! (我们可以很容易地恢复默认行为,正如文档所解释的那样。)

我们现在打电话 update 关于我们 todos 数组,因为它是一家商店。 就是这样。 编辑待办事项后,我们的更改会立即显示出来,我们的缓存也会被清除(和以前一样,因为我们在我们的文件中设置了一个新的 cookie 值 editTodo 形式动作)。 因此,如果我们搜索然后导航回此页面,我们将从我们的加载器获取新数据,这将正确排除任何已更新的待办事项。

立即更新的代码 在 GitHub 上可用.

深层发掘

我们可以在任何服务器加载函数(或服务器操作)中设置 cookie,而不仅仅是根布局。 因此,如果某些数据仅在单个布局或什至单个页面下使用,您可以在那里设置该 cookie 值。 而且,如果你是 不能 执行我刚刚展示的手动更新屏幕数据的技巧,而不是希望您的加载程序在突变后重新运行,那么您始终可以在该加载函数中设置一个新的 cookie 值,而无需任何检查 isDataRequest. 它最初会设置,然后任何时候你运行一个服务器操作,页面布局将自动失效并重新调用你的加载器,在你的通用加载器被调用之前重新设置缓存破坏字符串。

编写重新加载函数

让我们通过构建最后一个功能来结束:重新加载按钮。 让我们给用户一个按钮,该按钮将清除缓存,然后重新加载当前查询。

我们将添加一个简单的表单操作:

async reloadTodos({ cookies }) { cookies.set('todos-cache', +new Date(), { path: '/', httpOnly: false });
},

在实际项目中,您可能不会复制/粘贴相同的代码以在多个地方以相同的方式设置相同的 cookie,但对于这篇文章,我们将进行优化以实现简单性和可读性。

现在让我们创建一个表单来发布到它:

<form method="POST" action="?/reloadTodos" use:enhance> <button>Reload todos</button>
</form>

这样可行!

重新加载后的用户界面。
在 SvelteKit 中缓存数据

我们可以结束并继续,但让我们稍微改进一下这个解决方案。 具体来说,让我们在页面上提供反馈,告诉用户正在重新加载。 此外,默认情况下,SvelteKit 操作无效 一切. 当前页面层次结构中的每个布局、页面等都将重新加载。 可能有些数据在根布局中加载过一次,我们不需要使其失效或重新加载。

所以,让我们稍微关注一下,只在调用此函数时重新加载我们的待办事项。

首先,让我们传递一个函数来增强:

<form method="POST" action="?/reloadTodos" use:enhance={reloadTodos}>
import { enhance } from "$app/forms";
import { invalidate } from "$app/navigation"; let reloading = false;
const reloadTodos = () => { reloading = true; return async () => { invalidate("reload:todos").then(() => { reloading = false; }); };
};

我们正在设置一个新的 reloading 可变为 true开始 这个动作。 然后,为了覆盖使所有内容无效的默认行为,我们返回一个 async 功能。 这个函数将在我们的服务器操作完成时运行(它只是设置了一个新的 cookie)。

没有这个 async 函数返回,SvelteKit 将使一切无效。 因为我们提供了这个函数,所以它不会使任何东西失效,所以由我们来告诉它重新加载什么。 我们用 invalidate 功能。 我们称它为 reload:todos. 这个函数返回一个 promise,它在失效完成时解析,此时我们设置 reloadingfalse.

最后,我们需要将加载器与这个新的同步 reload:todos 失效值。 我们在加载程序中使用 depends 功能:

export async function load({ fetch, url, setHeaders, depends }) { depends('reload:todos'); // rest is the same

就是这样。 dependsinvalidate 是非常有用的功能。 很酷的是 invalidate 不像我们所做的那样只接受我们提供的任意值。 我们还可以提供一个 URL,SvelteKit 将跟踪该 URL,并使任何依赖于该 URL 的加载器失效。 为此,如果您想知道我们是否可以跳过对 depends 并使我们的无效 /api/todos 完全端点,你可以,但你必须提供 确切 网址,包括 search 术语(和我们的缓存值)。 因此,您可以将当前搜索的 URL 放在一起,或者匹配路径名,如下所示:

invalidate(url => url.pathname == "/api/todos");

就个人而言,我找到了使用的解决方案 depends 更明确和简单。 但是看 文档 当然,了解更多信息并自行决定。

如果你想看到重新加载按钮的作用,它的代码在 回购的这个分支.

分手的想法

这是一篇很长的文章,但希望不会让人不知所措。 在使用 SvelteKit 时,我们深入研究了缓存数据的各种方式。 其中大部分只是使用 web 平台原语添加正确的缓存和 cookie 值的问题,这些知识将在一般的 web 开发中为您服务,而不仅仅是 SvelteKit。

而且,这绝对是你的事 不需要一直. 可以说,只有当您 实际上需要他们. 如果您的数据存储快速有效地提供数据,并且您没有处理任何类型的扩展问题,那么执行我们在这里讨论的事情时,用不必要的复杂性来膨胀您的应用程序代码是没有意义的。

一如既往,编写清晰、干净、简单的代码,并在必要时进行优化。 这篇文章的目的是为您提供那些真正需要的优化工具。 我希望你喜欢它!

时间戳记:

更多来自 CSS技巧