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

前言
Svelte,一个语法简洁、入门容易,面向未来的前端框架。
从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1:

Svelte 以其独特的编译时优化机制著称,具有轻量级、高性能、易上手等特性,非常适合构建轻量级 Web 项目。
为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。
如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!
欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。
数据加载
在渲染一个 +page.svelte
组件(及其包含的 +layout.svelte
组件)之前,我们通常需要获取一些数据。这是通过定义 load
函数来实现的。
页面数据
一个 +page.svelte
文件可以有一个同级的 +page.js
文件,该文件导出一个 load
函数,该函数的返回值可以通过 data
属性在页面中使用:
[!LEGACY]在 Svelte 4 中,您需要使用
export let data
代替
得益于生成的 $types
模块,我们获得了完整的类型安全性。
+page.js
文件中的 load
函数在服务端和浏览器上都会运行(除非与 export const ssr = false
结合使用,在这种情况下它将仅在浏览器中运行)。如果您的 load
函数应该始终在服务端上运行(例如,因为它使用了私有环境变量或访问数据库),那么它应该放在 +page.server.js
中。
一个更贴合实际的博客文章 load
函数示例,它只在服务端上运行并从数据库中获取数据。可能如下所示:
注意类型从 PageLoad
变为 PageServerLoad
,因为服务端 load
函数可以访问额外的参数。要了解何时使用 +page.js
和何时使用 +page.server.js
文档:高级路由 请参阅 Universal 与 server。
布局数据
您的 +layout.svelte
文件也可以通过 +layout.js
或 +layout.server.js
加载数据。
布局 load
函数返回的数据对子 +layout.svelte
组件和 +page.svelte
组件以及它"所属"的布局都可用。
[!NOTE] 如果多个
load
函数返回具有相同键的数据,最后一个会"胜出" —— 布局load
返回{ a: 1, b: 2 }
而页面load
返回{ b: 3, c: 4 }
的结果将是{ a: 1, b: 3, c: 4 }
。
page.data
+page.svelte
组件及其上面的每个 +layout.svelte
组件都可以访问自己的数据以及其所有父组件的数据。
在某些情况下,我们可能需要相反的效果 - 父布局可能需要访问页面数据或来自子布局的数据。例如,根布局可能想要访问从 +page.js
或 +page.server.js
中的 load
函数返回的 title
属性。这可以通过 page.data
实现:
page.data
的类型信息由 App.PageData
提供。
[!LEGACY] >
$app/state
是在 SvelteKit 2.12 中添加的。如果您使用的是早期版本或使用 Svelte 4,请使用$app/stores
代替。它提供了一个具有相同接口的page
store,您可以订阅它,例如$page.data.title
。
Universal vs server
正如我们所见,有两种类型的 load
函数:
+page.js
和+layout.js
文件导出的在服务端和浏览器上都运行的通用load
函数+page.server.js
和+layout.server.js
文件导出的只在服务端运行的服务端load
函数
从概念上讲,它们是相同的东西,但有一些重要的区别需要注意。
何时运行哪个 load 函数?
服务端 load
函数总是在服务端上运行。
默认情况下,通用 load
函数在用户首次访问页面时在 SSR 期间在服务端上运行。然后它们会在水合过程中再次运行,复用来自 fetch 请求的任何响应。所有后续调用通用 load
函数都发生在浏览器中。您可以通过页面选项自定义该行为。如果您禁用了服务端渲染,您将获得一个 SPA,通用 load
函数始终在客户端运行。
如果一个路由同时包含通用和服务端 load
函数,服务端 load
函数会先运行。
除非您预渲染页面 - 在这种情况下,它会在构建时被调用,否则 load
函数会在运行时被调用。
输入
通用和服务端 load
函数都可以访问描述请求的属性(params
、route
和 url
)以及各种函数(fetch
、setHeaders
、parent
、depends
和 untrack
)。这些在后面的章节中会描述。
服务端 load
函数使用 ServerLoadEvent
调用,它从 RequestEvent
继承 clientAddress
、cookies
、locals
、platform
和 request
。
通用 load
函数使用具有 data
属性的 LoadEvent
调用。如果您在 +page.js
和 +page.server.js
(或 +layout.js
和 +layout.server.js
)中都有 load
函数,则服务端 load
函数的返回值是通用 load
函数参数的 data
属性。
输出
通用 load
函数可以返回包含任何值的对象,包括自定义类和组件构造函数等内容。
服务端 load
函数必须返回可以用 devalue 序列化的数据 - 任何可以用 JSON 表示的内容,以及像 BigInt
、Date
、Map
、Set
和 RegExp
这样的内容,或重复/循环引用 - 这样它才能通过网络传输。您的数据可以包含promises,在这种情况下它将被流式传输到浏览器。
何时使用哪个
当您需要直接访问数据库或文件系统,或需要使用私有环境变量时,服务端 load
函数很方便。
当您需要从外部 API fetch
数据且不需要私有凭据时,通用 load
函数很有用,因为 SvelteKit 可以直接从 API 获取数据而无需通过服务端。当您需要返回无法序列化的内容(如 Svelte 组件构造函数)时,它们也很有用。
在极少数情况下,您可能需要同时使用两者 - 例如,您可能需要返回一个使用服务端数据初始化的自定义类的实例。当同时使用两者时,服务端 load
的返回值不会直接传递给页面,而是传递给通用 load
函数(作为 data
属性):
使用 URL 数据
通常 load
函数以某种方式依赖于 URL。为此,load
函数提供了 url
、route
和 params
。
url
URL
的一个实例,包含诸如 origin
、hostname
、pathname
和 searchParams
(包含解析后的查询字符串,作为 URLSearchParams
对象)等属性。在 load
期间无法访问 url.hash
,因为它在服务端上不可用。
[!NOTE] 在某些环境中,这是在服务端渲染期间从请求头派生的。例如,如果您使用 adapter-node,您可能需要配置适配器以使 URL 正确。
route
包含当前路由目录相对于 src/routes
的名称:
params
params
是从 url.pathname
和 route.id
派生的。
给定一个 route.id
为 /a/[b]/[...c]
且 url.pathname
为 /a/x/y/z
时,params
对象将如下所示:
发起 fetch 请求
要从外部 API 或 +server.js
处理程序获取数据,您可以使用提供的 fetch
函数,它的行为与原生 fetch
web API完全相同,但有一些额外的功能:
它可以在服务端上发起带凭据的请求,因为它继承了页面请求的
cookie
和authorization
标头。它可以在服务端上发起相对请求(通常,当在服务端上下文中使用时,
fetch
需要带有源的 URL)。内部请求(例如对
+server.js
路由的请求)在服务端上运行时直接转到处理函数,无需 HTTP 调用的开销。在服务端渲染期间,通过钩入
text
、json
和arrayBuffer
方法来捕获响应并将其内联到渲染的 HTML 中 Response 对象。请注意,除非通过filterSerializedResponseHeaders
显式包含,否则标头将不会被序列化。在水合过程中,响应将从 HTML 中读取,确保一致性并防止额外的网络请求 - 如果在使用浏览器
fetch
而不是loadfetch
时,在浏览器控制台中收到警告,这就是原因。
Cookies
服务端 load
函数可以获取和设置cookies
。
只有当目标主机与 SvelteKit 应用程序相同或是其更具体的子域名时,Cookie 才会通过提供的 fetch
函数传递。
例如,如果 SvelteKit 正在为 my.domain.com 提供服务:
domain.com 将不会接收 cookies
my.domain.com 将会接收 cookies
api.domain.com 将不会接收 cookies
sub.my.domain.com 将会接收 cookies
当设置 credentials: 'include'
时,其他 cookies 将不会被传递,因为 SvelteKit 无法知道哪个 cookie 属于哪个域(浏览器不会传递这些信息),所以转发任何 cookie 都是不安全的。使用 handleFetch hook 钩子来解决这个问题。
Headers
服务端和通用 load
函数都可以访问 setHeaders
函数,当在服务端上运行时,可以为响应设置头部信息。(在浏览器中运行时,setHeaders 不会产生效果。)这在你想要缓存页面时很有用,例如:
多次设置相同的标头(即使在不同的 load
函数中)是一个错误。使用 setHeaders
函数时,每个标头只能设置一次。你不能使用 setHeaders
添加 set-cookie
标头 — 应该使用cookies.set(name, value, options)
代替。
使用父级数据
有时候让 load
函数访问父级 load
函数中的数据是很有用的,这可以通过 await parent()
实现:
[!NOTE] 注意,
+page.js
中的load
函数接收来自两个布局load
函数的合并数据,而不仅仅是直接父级的数据。
在 +page.server.js
和 +layout.server.js
内部,parent
从父级 +layout.server.js
文件返回数据。
在 +page.js
或 +layout.js
中,它将返回父级+layout.js
文件中的数据。然而,缺失的 +layout.js
会被视为 ({ data }) => data
函数,这意味着它也会返回未被 +layout.js
文件"遮蔽"的父级 +layout.server.js
文件中的数据。
使用 await parent()
时要注意避免瀑布流。例如,getData(params)
并不依赖于调用 parent()
的结果,所以我们应该先调用它以避免延迟渲染。
Errors
如果在 load
期间抛出错误,将渲染最近的 +error.svelte
。对于预期的错误,使用来自 @sveltejs/kit
的 error
辅助函数来指定 HTTP 状态码和可选消息:
调用 error(...)
将抛出一个异常,这使得在辅助函数内部停止执行变得容易。
如果抛出了一个意外错误,SvelteKit 将调用 handleError
并将其视为 500 内部错误。
[!NOTE] 在 SvelteKit 1.x 中,你必须自己
throw
错误
Redirects
要重定向用户,请使用来自 @sveltejs/kit
的 redirect
辅助函数,以指定用户应被重定向到的位置以及一个 3xx
状态码。与 error(...)
类似,调用 redirect(...)
将抛出一个异常,这使得在辅助函数内部停止执行变得容易。
[!NOTE] 不要在
try {...}
块内使用redirect()
,因为重定向会立即触发 catch 语句。
在浏览器中,你也可以在 load
函数之外使用来自 $app.navigation
的 goto
通过编程的方式进行导航。
[!NOTE] 在 SvelteKit 1.x 中,你必须自己
throw
这个redirect
Streaming with promises
当使用服务端 load
时,Promise 将在 resolve 时流式传输到浏览器。如果你有较慢的、非必要的数据,这很有用,因为你可以在所有数据可用之前开始渲染页面:
这对创建骨架加载状态很有用,例如:
在流式传输数据时,请注意正确处理 Promise rejections。具体来说,如果懒加载的 Promise 在渲染开始前失败(此时会被捕获)且没有以某种方式处理错误,服务器可能会因 "unhandled promise rejection" 错误而崩溃。
当在 load
函数中直接使用 SvelteKit 的 fetch
时,SvelteKit 会为您处理这种情况。对于其他 Promise,只需为 Promise 添加一个空的 catch
即可将其标记为已处理。
[!NOTE] 在不支持流式传输的平台上(如 AWS Lambda 或 Firebase),响应将被缓冲。这意味着页面只会在所有 promise resolve 后才会渲染。如果您使用代理(例如 NGINX),请确保它不会缓冲来自代理服务器的响应。
[!NOTE] 流式数据传输只有在启用 JavaScript 时才能工作。如果页面是服务端渲染的,您应该避免从通用
load
函数返回 promise,因为这些 promise 不会被流式传输 —— 相反,当函数在浏览器中重新运行时,promise 会被重新创建。
[!NOTE] 一旦响应开始流式传输,就无法更改响应的标头和状态码,因此您无法
setHeaders
或抛出重定向到流式 promise 内。
[!NOTE] 在 SvelteKit 1.x 中,顶层 promise 会自动 awaited,只有嵌套的 promise 才会流式传输。
并行加载
在渲染(或导航到)页面时,SvelteKit 会同时运行所有 load
函数,避免请求瀑布。在客户端导航期间,多个服务器 load
函数的调用结果会被组合到单个响应中。一旦所有 load
函数都返回结果,页面就会被渲染。
重新运行 load 函数
SvelteKit 会追踪每个 load
函数的依赖关系,以避免在导航过程中不必要的重新运行。
例如,给定一对这样的 load
函数...
...其中 +page.server.js
中的函数在从 /blog/trying-the-raw-meat-diet
导航到 /blog/i-regret-my-choices
时会重新运行,因为 params.slug
发生了变化。而 +layout.server.js
中的函数则不会重新运行,因为数据仍然有效。换句话说,我们不会第二次调用 db.getPostSummaries()
。
如果父级 load
函数重新运行,调用了 await parent()
的 load
函数也会重新运行。
依赖追踪在 load
函数返回后不再适用 — 例如,在嵌套的 promise 中访问 params.x
不会在 params.x
改变时导致函数重新运行。(别担心,如果你不小心这样做了,在开发环境中会收到警告。)相反,应该在 load
函数的主体中访问参数。
搜索参数的追踪独立于 URL 的其余部分。例如,在 load
函数中访问 event.url.searchParams.get("x")
将使该 load
函数在从 ?x=1
导航到 ?x=2
时重新运行,但从 ?x=1&y=1
导航到 ?x=1&y=2
时则不会重新运行。
取消依赖追踪
在极少数情况下,你可能希望将某些内容排除在依赖追踪机制之外。你可以使用提供的 untrack
函数实现这一点:
手动失效
你还可以使用 invalidate(url)
重新运行适用于当前页面的 load
函数,它会重新运行所有依赖于 url
的 load
函数,以及使用 invalidateAll()
重新运行每个 load
函数。服务端加载函数永远不会自动依赖于获取数据的 url
,以避免将秘密泄露给客户端。
如果一个 load
函数调用了 fetch(url)
或 depends(url)
,那么它就依赖于 url
。注意,url
可以是以 [a-z]
开头的自定义标识符:
load 函数何时重新运行?
总的来说,load
函数在以下情况下会重新运行:
它引用了
params
中已更改值的属性它引用了
url
的某个属性(如url.pathname
或url.search
)且该属性的值已更改。request.url
中的属性不会被追踪它调用
url.searchParams.get(...)
、url.searchParams.getAll(...)
或url.searchParams.has(...)
,且相关参数发生变化。访问url.searchParams
的其他属性与访问url.search
具有相同的效果。它调用
await parent()
且父load
函数重新运行当子
load
函数调用await parent()
并重新运行,且父函数是服务端load
函数它通过
fetch
(仅限通用 load)或depends
声明了对特定 URL 的依赖,且该 URL 被invalidate(url)
标记为无效所有活动的
load
函数都被invalidateAll()
强制重新运行
params
和 url
可以在响应 <a href="..">
链接点击、<form>
交互goto
调用或 重定向
时发生变化。
注意,重新运行 load
函数将更新相应 +layout.svelte
或 +page.svelte
中的 data
属性;这不会导致组件重新创建。因此,内部状态会被保留。如果这不是你想要的,你可以在afterNavigate
回调中重置所需内容,或者用 {#key ...}
块包装你的组件。
对身份验证的影响
数据加载的几个特性对身份验证有重要影响:
布局
load
函数不会在每个请求时运行,例如在子路由之间的客户端导航期间。(load函数何时重新运行?)布局和页面
load
函数会同时运行,除非调用了await parent()
。如果布局load
抛出错误,页面load
函数会运行,但客户端将不会收到返回的数据。
有几种可能的策略来确保在受保护代码之前进行身份验证检查。
为防止数据瀑布并保留布局 load
缓存:
使用 hooks 在任何
load
函数运行之前保护多个路由在
+page.server.js
load
函数中直接使用身份验证守卫进行特定路由保护
在 +layout.server.js
中放置身份验证守卫要求所有子页面在受保护代码之前调用 await parent()
。除非每个子页面都依赖于await parent()
返回的数据,否则其他选项会更有性能优势。
拓展阅读
Svelte 中文文档
点击查看中文文档 - SvelteKit 数据加载。
系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!
此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog
欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。
版权声明: 本文为 InfoQ 作者【冴羽】的原创文章。
原文链接:【http://xie.infoq.cn/article/23c1c2bdd68454725fd5c5b14】。文章转载请联系作者。
评论