写代码的时候,数据缓存这事儿整不好就容易翻车。尤其用SvelteKit搞项目,请求后端数据一多,每次都重新拉取,页面卡顿不说,后端也得累够呛。但浏览器自带的缓存机制,配合点小技巧,就能让数据飞一样地展示出来。这篇就唠唠咋在SvelteKit里给数据加缓存,还能随时手动清掉,甚至编辑完不用重新请求页面直接刷新界面。
缓存咋配
给接口加个头
后端接口返回数据的时候,顺手给响应头里塞个cache-control。这玩意儿告诉浏览器:“这堆数据你存60秒,60秒内别再来烦我了”。
// routes/api/todos/+server.js
import { json } from "@sveltejs/kit";
import { getTodos } from "$lib/data/todoData";
export async function GET({ url, setHeaders }) {
const search = url.searchParams.get("search") || "";
// 加缓存头,60秒内直接读本地
setHeaders({
"cache-control": "max-age=60"
});
const todos = await getTodos(search);
return json(todos);
}这段代码干的事儿特简单:每次请求/api/todos,都告诉浏览器存60秒。同一个搜索词在这60秒内再访问,浏览器直接掏本地缓存,连网络请求都省了。调试的时候记得把开发者工具的“Disable cache”复选框取消掉,不然死活看不到效果。
缓存存哪了
头一回打开/list页面,SvelteKit会在服务端把数据请求好,然后序列化扔给客户端。服务端响应时带着cache-control头,SvelteKit自己也会认这个头,在60秒内重复调用同一个接口,直接复用之前的数据。等页面加载完,在搜索框里敲字,每次敲完回车,浏览器会发起新的fetch请求。这时候如果搜的是60秒内搜过的词,网络面板里能看到请求直接走(disk cache),速度嗖嗖的。
注意一点:服务端渲染那一下,每次刷新页面都会重新请求后端,不管缓存窗口过没过。这是SvelteKit的设计,不碍事,客户端后续的请求才享受缓存红利。
手动失效
缓存刷新的痛点
设了60秒自动过期,但万一改了数据想立刻让缓存失效咋办?比如编辑了一个待办事项,希望下次搜索立马拿到最新的。浏览器缓存又不会自己长眼睛,得手动给它一棍子。套路就是在请求URL后面挂一个会变的参数,参数一变,浏览器就觉得这是个新请求,乖乖去后端拿新数据。
用cookie存版本号
在根布局的+layout.server.js里塞一个cookie,存一个时间戳当版本号。
// routes/+layout.server.js
export function load({ cookies, isDataRequest }) {
// 只有真正的初次请求才设置新cookie
const initialRequest = !isDataRequest;
const cacheValue = initialRequest ? +new Date() : cookies.get("todos-cache");
if (initialRequest) {
cookies.set("todos-cache", cacheValue, {
path: "/",
httpOnly: false // 让客户端也能读到这个cookie
});
}
return {
todosCacheBust: cacheValue
};
}httpOnly: false平时不建议开,但这里存的只是一个数字,没啥敏感信息,开了方便客户端读取。isDataRequest用来区分是不是因为invalidate触发的重新加载,避免每次刷新页面都重置cookie。
请求时带上版本号
改一下页面加载器,从cookie里捞出版本号,拼到请求URL后面。
// routes/list/+page.js
import { getCurrentCookieValue } from "$lib/util/cookieUtils";
export async function load({ fetch, parent, url }) {
const parentData = await parent();
// 客户端读cookie,服务端用父布局传下来的值
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 };
}客户端读cookie那段工具代码,判断document存不存在,不存在就返回空,然后降级用服务端传下来的值。这样同构代码跑得顺溜。
编辑后刷掉缓存
在编辑待办的表单动作里,把cookie更新成新的时间戳。
// routes/list/+page.server.js
export const actions = {
async editTodo({ request, cookies }) {
const formData = await request.formData();
const id = formData.get("id");
const newTitle = formData.get("title");
// 假装更新数据库
updateTodo(id, newTitle);
// 刷新缓存版本号
cookies.set("todos-cache", +new Date(), {
path: "/",
httpOnly: false
});
}
};下次再请求/api/todos时,URL里的cache参数变了,浏览器就不会命中旧缓存,乖乖去后端拿最新数据。
即时更新
场景描述
编辑完一个待办,如果这个待办的关键词变了(比如原来叫“买苹果”,改成了“买香蕉”),当前页面的搜索结果里它可能就不该出现了。但默认流程是:提交表单 -> 后端改数据 -> 刷新cookie -> 页面加载器重新跑 -> 整个列表重新请求。网络好的时候还行,但总归有个延迟。想实现编辑完那一瞬间,界面直接变,连那个额外的网络请求都省掉,咋整?
把数据变成store
把加载器返回的数据包一层writable。
// routes/list/+page.js
import { writable } from "svelte/store";
export async function load({ fetch, url }) {
const resp = await fetch(`/api/todos?...`);
const todos = await resp.json();
return {
todos: writable(todos) // 变成可写store
};
}页面里取数据时,前面加个$符号。
<!-- routes/list/+page.svelte -->
<script>
export let data;
$: ({ todos } = data);
</script>
{#each $todos as t}
<tr>
<td>{t.title}</td>
<!-- 其他列 -->
</tr>
{/each}用enhance劫持表单
给编辑表单加上use:enhance,传一个处理函数。
<form
use:enhance={executeSave}
method="post"
action="?/editTodo"
>
<input name="id" value="{t.id}" type="hidden" />
<input name="title" value="{t.title}" />
<button>保存</button>
</form>executeSave函数长这样:
function executeSave({ data }) {
const id = data.get("id");
const newTitle = data.get("title");
// 返回一个异步函数,等后端保存完执行
return async () => {
todos.update(list =>
list.map(todo =>
todo.id == id ? { ...todo, title: newTitle } : todo
)
);
};
}这个返回的异步函数会在后端editTodo动作完成后调用。关键点在于:返回了这个函数,SvelteKit就不会自动重新运行页面的加载器了。但后端那边照样更新了数据库、刷新了cookie。所以界面上的store被手动更新,数据立刻变样,同时下次搜索或者切回来再加载,拿到的也是新数据(因为cookie版本号变了)。这一套组合拳打下来,既丝滑又准确。
重载耍法
加个手动刷新按钮
有时候用户想主动拉一下最新数据,不管缓存窗口过没过。加个按钮,触发一个表单动作。
<script>
import { enhance } from "$app/forms";
import { invalidate } from "$app/navigation";
let reloading = false;
const reloadTodos = () => {
reloading = true;
return async () => {
await invalidate("reload:todos");
reloading = false;
};
};
</script>
<form method="POST" action="?/reloadTodos" use:enhance={reloadTodos}>
<button disabled={reloading}>
{reloading ? "刷新中..." : "重载待办"}
</button>
</form>后端动作只需要刷新cookie:
// routes/list/+page.server.js
export const actions = {
async reloadTodos({ cookies }) {
cookies.set("todos-cache", +new Date(), {
path: "/",
httpOnly: false
});
}
};精准重载不连累别人
默认情况下,执行表单动作后SvelteKit会把整个页面的所有加载器都无效掉。但如果只想让待办列表重新请求,其他部分(比如用户头像、全局配置)不动,可以用invalidate配合depends。
先在加载器里声明依赖:
// routes/list/+page.js
export async function load({ fetch, url, depends }) {
depends("reload:todos"); // 声明这个加载器依赖一个自定义key
// ... 其余请求代码不变
}然后在按钮的处理函数里只无效这个key:
const reloadTodos = () => {
reloading = true;
return async () => {
await invalidate("reload:todos");
reloading = false;
};
};这样一点击按钮,只有依赖reload:todos的加载器会重新跑,别的纹丝不动。性能拿捏得死死的。
还有一个更野的路子:直接无效掉/api/todos路径。invalidate函数可以接受一个URL匹配函数:
invalidate(url => url.pathname === "/api/todos");但这需要拼出完整的URL(包括查询参数和缓存版本号),不如depends方式清爽。看个人喜好,哪个顺手用哪个。
代码仓库
这篇所有示例代码都扔网上了,包括即时更新分支和重载按钮分支。需要完整跑一遍的,直接去扒下来耍。记住一点:缓存这东西,不是每个项目都要搞。后端响应够快、并发不大,老老实实每次请求新数据最省心。等哪天页面卡得不行了,再掏出这些招儿来救场。
