该系列文章一共四篇,以下是系列文章链接:
SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念
Routing
SvelteKit 的核心是基于文件系统的路由器。您应用程序的路由(即用户可以访问的 URL 路径)由代码库中的目录定义:
src/routes
是根路径src/routes/about
创建了一个/about
路径src/routes/blog/[slug]
创建了一个带有参数slug
的路径,该参数可用于在用户请求像/blog/hello-world
这样的页面时动态加载数据。
您可以通过编辑项目配置将
src/routes
更改为不同的目录。
每个路由目录包含一个或多个路由文件,可以通过它们的+
前缀进行识别。
+page
+page.svelte
+page.svelte
组件定义了你的应用程序中的一个页面。默认情况下,页面在服务器端(SSR)上进行初始请求渲染,并在浏览器端(CSR)上进行后续导航渲染。
src/routes/+page.svelte
1 | <h1>Hello and welcome to my site!</h1> |
src/routes/about/+page.svelte
1 | <h1>About this site</h1> |
src/routes/blog/[slug]/+page.svelte
1 | <script> |
请注意,SvelteKit 使用
<a>
元素在路由之间导航,而不是特定于框架的<Link>
组件。
+page.js
通常情况下,在页面渲染之前需要加载一些数据。为此,我们添加一个 +page.js
模块,该模块导出一个 load
函数:
src/routes/blog/[slug]/+page.js
1 | import { error } from '@sveltejs/kit'; |
此功能与 +page.svelte
并行运行,这意味着它在服务器端渲染期间和客户端导航期间都在浏览器中运行。有关 API 的完整详细信息,请参见 load
。
除了 load
之外,+page.js
还可以导出配置页面行为的值:
export const prerender = true
或false
或'auto'
export const ssr = true
或false
export const csr = true
或false
您可以在页面选项中找到更多有关此类内容的信息。
+page.server.js
如果您的load
函数只能在服务器上运行 - 例如,如果它需要从数据库获取数据或者您需要访问私有环境变量(如API密钥)- 那么您可以将+page.js
重命名为+page.server.js
,并将PageLoad
类型更改为PageServerLoad
。
src/routes/blog/[slug]/+page.server.js
1 | import { error } from '@sveltejs/kit'; |
在客户端导航期间,SvelteKit 将从服务器加载此数据,这意味着返回的值必须可序列化使用 devalue。有关 API 的完整详细信息,请参见 load
。
与 +page.js
相似,+page.server.js
可以导出页面选项 - prerender
、ssr
和 csr
。
+page.server.js
文件还可以导出 actions
。如果 load
允许您从服务器读取数据,则 actions
允许您使用 <form>
元素将数据写入服务器。要了解如何使用它们,请参见表单操作部分。
+error
如果在load
过程中出现错误,SvelteKit 将呈现默认的错误页面。您可以通过添加 +error.svelte
文件来针对每个路由自定义此错误页面:
src/routes/blog/[slug]/+error.svelte
1 | <script> |
SvelteKit会“向上遍历树”,寻找最近的错误边界 - 如果上面的文件不存在,它将尝试src/routes/blog/+error.svelte
和src/routes/+error.svelte
,在呈现默认错误页面之前。如果失败(或者如果错误是从位于根+error
“上方”的根+layout
的load函数中抛出的),SvelteKit将退出并呈现静态回退错误页面,您可以通过创建一个src/error.html
文件来自定义该页面。
如果在+layout(.server).js
中发生错误,则树中最接近该布局(而不是旁边)的+error.svelte
文件是最接近的错误边界。
如果找不到路由(404),则使用src/routes/+error.svelte
(或默认错误页面,如果该文件不存在)。
当在
handle
函数或+server.js
请求处理程序内发生错误时,不会使用+error.svelte
。
+layout
到目前为止,我们已经将页面视为完全独立的组件 - 在导航时,现有的+page.svelte
组件将被销毁,并且一个新的组件将取代它。
但在许多应用程序中,有些元素应该在每个页面上可见,例如顶级导航或页脚。我们可以将它们放入布局中,而不是在每个+page.svelte
中重复它们。
+layout.svelte
要创建适用于每个页面的布局,请创建一个名为src/routes/+layout.svelte
的文件。默认布局(如果您没有自己带来)如下所示…
1 | <slot></slot> |
…但我们可以添加任何标记、样式和行为。唯一的要求是组件包括一个用于页面内容的
src/routes/+layout.svelte
1 | <nav> |
如果我们为 /
, /about
and /settings
…创建页面
src/routes/+page.svelte
1 | <h1>Home</h1> |
src/routes/about/+page.svelte
1 | <h1>About</h1> |
src/routes/settings/+page.svelte
1 | <h1>Settings</h1> |
…导航栏将始终可见,单击三个页面之间只会导致h1被替换。
布局可以嵌套。假设我们不仅有单个/settings
页面,而是有像/settings/profile
和/settings/notifications
这样的嵌套页面,并且具有共享子菜单(例如,请参见github.com/settings的实际示例)。
我们可以创建一个仅适用于/settings
以下页面的布局(同时继承具有顶级导航的根布局):
src/routes/settings/+layout.svelte
1 | <script> |
默认情况下,每个布局都会继承其上面的布局。有时这并不是您想要的 - 在这种情况下,高级布局可以帮助您。
+layout.js
就像 +page.svelte
从 +page.js
加载数据一样,你的+layout.svelte
组件可以从+layout.js
中的 load
函数获取数据。
src/routes/settings/+layout.js
1 | /** @type {import('./$types').LayoutLoad} */ |
如果一个名为 +layout.js
的文件导出页面选项 - prerender
、ssr
和 csr
,它们将作为子页面的默认值。
从布局的load
函数返回的数据也可用于所有其子页面:
src/routes/settings/profile/+page.svelte
1 | <script> |
通常情况下,在页面之间导航时,布局数据不会改变。SvelteKit 将在必要时智能重新运行
load
函数。
+layout.server.js
将您的布局load
函数移动到 +layout.server.js
并将 LayoutLoad
类型更改为 LayoutServerLoad
,即可在服务器上运行它。
与 +layout.js
一样,+layout.server.js
可以导出页面选项 - prerender
、ssr
和 csr
。
+server
除了页面外,您还可以使用+server.js
文件(有时称为“API路由”或“端点”)定义路由,从而完全控制响应。您的+server.js
文件导出与HTTP动词相对应的函数,如GET
、POST
、PATCH
、PUT
、DELETE
和OPTIONS
,这些函数接受RequestEvent
参数并返回一个Response
对象。
例如,我们可以创建一个带有GET
处理程序的/api/random-number
路由:
src/routes/api/random-number/+server.js
1 | import { error } from '@sveltejs/kit'; |
Response
的第一个参数可以是 ReadableStream
,这使得流式传输大量数据或创建服务器发送事件成为可能(除非部署到像 AWS Lambda 这样缓冲响应的平台)。
您可以使用 @sveltejs/kit
中的 error
、redirect
和 json
方法来方便地处理错误(但不一定要这样做)。
如果抛出错误(无论是 throw error(...)
还是意外错误),响应将是该错误的 JSON 表示形式或回退错误页面 —— 可以通过 src/error.html
自定义 —— 具体取决于 Accept
标头。在这种情况下,+error.svelte
组件将不会被渲染。您可以在此处阅读有关错误处理的更多信息。
创建
OPTIONS
处理程序时,请注意 Vite 将注入Access-Control-Allow-Origin
和Access-Control-Allow-Methods
标头 - 这些标头在生产环境中将不存在,除非您添加它们。
Receiving data
通过导出POST
/PUT
/PATCH
/DELETE
/OPTIONS
处理程序,可以使用+server.js
文件创建完整的API:
src/routes/add/+page.svelte
1 | <script> |
src/routes/api/add/+server.js
1 | import { json } from '@sveltejs/kit'; |
一般来说,表单操作是从浏览器向服务器提交数据的更好方式。
Content negotiation
+server.js
文件可以放置在与+page
文件相同的目录中,从而允许相同的路由成为页面或API端点。为了确定哪个是哪个,SvelteKit应用以下规则:
PUT
/PATCH
/DELETE
/OPTIONS
请求始终由+server.js
处理,因为它们不适用于页面- 如果接受标头优先考虑text/html(换句话说,这是浏览器页面请求),则
GET
/POST
请求被视为页面请求;否则它们将由+server.js
处理
$types
在上面的示例中,我们一直从 $types.d.ts
文件中导入类型。如果您使用 TypeScript(或带有 JSDoc 类型注释的 JavaScript)来处理根文件,则 SvelteKit 会在隐藏目录中为您创建此文件,以便在处理根文件时提供类型安全性。
例如,将 export let data
注释为 [PageData](对于 +layout.svelte
文件则是 LayoutData
),告诉 TypeScript 数据的类型是从 load
返回的任何内容:
src/routes/blog/[slug]/+page.svelte
1 | <script> |
反过来,使用PageLoad
、PageServerLoad
、LayoutLoad
或LayoutServerLoad
(分别用于+page.js
、+page.server.js
、+layout.js
和+layout.server.js
)为load
函数进行注释可以确保参数和返回值的正确类型。
如果您正在使用VS Code或任何支持语言服务器协议和TypeScript插件的IDE,则可以完全省略这些类型! Svelte的IDE工具将为您插入正确的类型,因此您无需自己编写即可获得类型检查。它还与我们的命令行工具svelte-check
一起使用。
您可以在我们关于省略$types
的博客文章中了解更多信息。
Other files
路由目录中的任何其他文件都将被 SvelteKit 忽略。这意味着您可以将组件和实用程序模块与需要它们的路由放在一起。
如果多个路由需要使用组件和模块,则最好将它们放在 $lib
中。
Loading data
在渲染 +page.svelte
组件(以及其包含的 +layout.svelte
组件)之前,我们通常需要获取一些数据。这是通过定义load
函数来完成的。
Page data
一个 +page.svelte
文件可以有一个兄弟文件 +page.js
,该文件导出一个load
函数,其返回值可通过 data
属性在页面中使用:
src/routes/blog/[slug]/+page.js
1 | /** @type {import('./$types').PageLoad} */ |
src/routes/blog/[slug]/+page.svelte
1 | <script> |
由于生成的 $types
模块,我们获得了完整的类型安全。
在 +page.js
文件中,一个load
函数会在服务器和浏览器上都运行。如果您的load
函数应该始终在服务器上运行(例如因为它使用私有环境变量或访问数据库),那么它将放置在 +page.server.js
中。
更实际版本的博客文章load
函数只在服务器上运行并从数据库中提取数据,可能看起来像这样:
src/routes/blog/[slug]/+page.server.js
1 | import * as db from '$lib/server/database'; |
请注意,类型已从PageLoad
更改为PageServerLoad
,因为服务器负载函数可以访问其他参数。要了解何时使用+page.js
和何时使用+page.server.js
,请参见Universal vs server。
Layout data
您的 +layout.svelte
文件也可以通过 +layout.js
或 +layout.server.js
加载数据。
src/routes/blog/[slug]/+layout.server.js
1 | import * as db from '$lib/server/database'; |
src/routes/blog/[slug]/+layout.svelte
1 | <script> |
从布局load
函数返回的数据可供子+layout.svelte
组件和+page.svelte
组件以及其所属的布局使用。
src/routes/blog/[slug]/+page.svelte
1 | <script> |
如果多个
load
函数返回具有相同键的数据,则最后一个“获胜”——布局load
返回{ a: 1,b: 2 }
和页面加载返回{ b: 3,c: 4 }
的结果将是{ a: 1,b:3,c:4}
。
$page.data
+page.svelte
组件以及它上面的每个 +layout.svelte
组件都可以访问自己的数据和所有父级数据。
在某些情况下,我们可能需要相反的操作 - 父布局可能需要访问页面数据或子布局中的数据。例如,根布局可能想要访问从 +page.js
或 +page.server.js
返回的 title
属性。这可以通过 $page.data
来实现:
src/routes/+layout.svelte
1 | <script> |
$page.data
的类型信息由App.PageData提供。
通用vs服务器
As we’ve seen, there are two types of load
function:
+page.js
and+layout.js
files export universalload
functions that run both on the server and in the browser+page.server.js
and+layout.server.js
files export serverload
functions that only run server-side
Conceptually, they’re the same thing, but there are some important differences to be aware of.
正如我们所看到的,有两种类型的load
函数:
+page.js 和 +layout.js 文件导出通用的加载函数,在服务器和浏览器上都可以运行
+page.server.js 和 +layout.server.js 文件导出仅在服务器端运行的服务器加载函数
从概念上讲,它们是相同的东西,但需要注意一些重要区别。
什么时候运行哪个加载函数?
服务器load
函数始终在服务器上运行。
默认情况下,通用的load
函数在 SSR 期间首次访问页面时在服务器上运行。然后,在水合作用期间再次运行,并重复使用来自获取请求的任何响应。所有后续调用通用load
函数都发生在浏览器中。您可以通过页面选项自定义此行为。如果禁用了服务器端渲染,则会获得 SPA,并且通用load
功能始终在客户端上运行。
除非您预渲染页面,否则将在运行时调用load
函数- 在这种情况下,它将在构建时间调用。
Input
通用和服务器load
函数都可以访问描述请求(params
、route
和 url
)以及各种函数(fetch
、setHeaders
、parent
和 depends
)的属性。这些在以下章节中进行了描述。
服务器load
函数使用 ServerLoadEvent
调用,该事件从 RequestEvent
继承 clientAddress
、cookies
、locals
、platform
和 request
属性。
通用load
函数使用 LoadEvent
调用,该事件具有 data
属性。如果您在 +page.js
和 +page.server.js
(或 +layout.js
和 +layout.server.js
)中都有load
函数,则服务器load
函数的返回值是通用load
函数参数的 data
属性。
Output
一个通用的load
函数可以返回一个包含任何值的对象,包括自定义类和组件构造函数等。
服务器load
函数必须返回可使用devalue序列化的数据 - 任何可以表示为JSON的内容以及BigInt
、Date
、Map
、Set
和RegExp
之类的内容,或者是重复/循环引用 - 以便它可以通过网络传输。您的数据可能包括promises,在这种情况下,它将被流式传输到浏览器。
在什么时间用哪个函数
服务器load
函数在需要直接从数据库或文件系统访问数据,或需要使用私有环境变量时非常方便。
通用的load
函数在需要从外部API去fetch
数据且不需要私人凭据时非常有用,因为SvelteKit可以直接从API获取数据而无需通过您的服务器。当您需要返回无法序列化的内容(例如Svelte组件构造函数)时,它们也很有用。
在极少数情况下,您可能需要同时使用两者 - 例如,您可能需要返回一个自定义类的实例,并将其初始化为来自服务器的数据。
Using URL data
通常,load
函数在某种程度上取决于URL。为此,load
函数提供了url
、route
和params
参数。
url
一个 URL
实例,包含诸如 origin
、hostname
、pathname
和 searchParams
(其中包含解析后的查询字符串作为 URLSearchParams
对象)等属性。由于服务器上不可用,因此在load
期间无法访问 url.hash
。
在某些环境中,这是在服务器端渲染期间从请求头中派生出来的。例如,如果您正在使用adapter-node,则可能需要配置适配器以使URL正确。
route
包含当前路由目录的名称,相对于 src/routes
:
src/routes/a/[b]/[…c]/+page.js
1 | /** @type {import('./$types').PageLoad} */ |
params
params
是从url.pathname
和route.id
派生出来的。
假设route.id
为 /a/[b]/[...c]
,url.pathname
为 /a/x/y/z
,则params
对象如下所示:
1 | { |
进行获取请求
从外部API或+server.js
处理程序获取数据,您可以使用提供的fetch
函数,它与native fetch web API具有相同的行为,并具有一些附加功能:
- 它可用于在服务器上进行凭证请求,因为它继承了页面请求的
cookie
和authorization
标头 - 它可以在服务器上进行相对请求(通常,在服务器上下文中使用时,
fetch
需要带有源URL) - 内部请求(例如
+server.js
路由)在运行时直接进入处理程序函数,而无需HTTP调用开销 - 在服务器端渲染期间,响应将被捕获并通过钩入
Response
对象的text
和json
方法内联到呈现的 HTML 中。请注意,除非通过filterSerializedResponseHeaders
显式包含,否则不会序列化标头。然后,在水合期间,响应将从 HTML 中读取,确保一致性并防止额外的网络请求 - 如果您在使用浏览器fetch
而不是load
fetch
时在浏览器控制台中收到警告,则原因就在于此。
src/routes/items/[id]/+page.js
1 | /** @type {import('./$types').PageLoad} */ |
只有当目标主机与 SvelteKit 应用程序相同或是其更具体的子域时,才会传递 Cookie。
Cookies and headers
服务器load
函数可以获取和设置cookies
。
src/routes/+layout.server.js
1 | import * as db from '$lib/server/database'; |
在设置 cookie 时,请注意
path
属性。默认情况下,cookie 的path
是当前的路径名。例如,在页面admin/user
上设置 cookie,则默认情况下该 cookie 只能在admin
页面中使用。在大多数情况下,您可能希望将路径设置为 ‘/‘,以使 cookie 在整个应用程序中可用。
服务器和通用load
函数都可以访问setHeaders
函数,该函数在服务器上运行时可以为响应设置标头。(在浏览器中运行时,setHeaders
没有效果。) 如果您想要页面被缓存,则这非常有用:
src/routes/products/+page.js
1 | /** @type {import('./$types').PageLoad} */ |
多次设置相同的标题(即使在不同的load
函数中)是错误的 - 您只能设置给定标题一次。您不能使用setHeaders
添加set-cookie
标头 - 请改用cookies.set(name,value,options)
。
Using parent data
有时候,一个load
函数需要访问其父级load
函数的数据是很有用的,这可以通过使用 await parent()
来实现:
src/routes/+layout.js
1 | /** @type {import('./$types').LayoutLoad} */ |
src/routes/abc/+layout.js
1 | /** @type {import('./$types').LayoutLoad} */ |
src/routes/abc/+page.js
1 | /** @type {import('./$types').PageLoad} */ |
src/routes/abc/+page.svelte
1 | <script> |
请注意,在
+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()
的结果,因此我们应该先调用它以避免延迟渲染。
+page.js
1 | /** @type {import('./$types').PageLoad} */ |
Errors
如果在load
过程中出现错误,则会呈现最近的 +error.svelte
。对于预期的错误,请使用 @sveltejs/kit
中的 error
帮助程序来指定 HTTP 状态代码和可选消息:
src/routes/admin/+layout.server.js
1 | import { error } from '@sveltejs/kit'; |
如果出现意外错误,SvelteKit将调用 handleError
并将其视为 500 内部错误。
Redirects
要重定向用户,请使用@sveltejs/kit
的 redirect
助手,指定应将其重定向到的位置以及3xx
状态代码。
src/routes/user/+layout.server.js
1 | import { redirect } from '@sveltejs/kit'; |
确保您没有捕获被抛出的重定向,否则将阻止 SvelteKit 处理它。
在浏览器中,您还可以使用$app.navigation
的goto
函数,在load
函数之外以编程方式导航。
使用 Promises 进行流式传输
返回对象的顶层承诺将被等待,这使得返回多个承诺而不创建瀑布变得容易。在使用服务器load
时,嵌套的承诺将随着它们的解决而流式传输到浏览器中。如果您有缓慢、非必要数据,则此功能很有用,因为您可以在所有数据可用之前开始呈现页面:
src/routes/+page.server.js
1 | /** @type {import('./$types').PageServerLoad} */ |
这对于创建骨架加载状态非常有用,例如:
src/routes/+page.svelte
1 | <script> |
在不支持流式传输的平台上,比如 AWS Lambda,响应将被缓冲。这意味着页面只有在所有承诺都解决后才会渲染出来。
只有在启用 JavaScript 时,流数据才能正常工作。如果页面是服务器渲染的,则应避免从通用
load
函数返回嵌套的 promises,因为这些不会被流式传输 - 相反,在浏览器中重新运行该函数时,promise 将被重新创建。
并行加载
在渲染(或导航到)页面时,SvelteKit 会同时运行所有load
函数,避免请求的瀑布流。在客户端导航期间,调用多个服务器load
函数的结果被分组为单个响应。一旦所有load
函数都返回了,页面就会被呈现出来。
重新运行加载函数
SvelteKit会跟踪每个load
函数的依赖关系,以避免在导航期间不必要地重新运行它。
例如,给定这样一对load
函数…
src/routes/blog/[slug]/+page.server.js
1 | import * as db from '$lib/server/database'; |
src/routes/blog/[slug]/+layout.server.js
1 | import * as db from '$lib/server/database'; |
…在 +page.server.js
中,如果我们从/blog/trying-the-raw-meat-diet
导航到 /blog/i-regret-my-choices
,params.slug
发生了变化,那么它将重新运行。而在 +layout.server.js
中不会重新运行,因为数据仍然有效。换句话说,我们不会第二次调用 db.getPostSummaries()
。
调用 await parent()
的 load
函数也会重新运行,如果父级 load
函数被重新运行的话。
依赖跟踪在 load
函数返回后不适用 - 例如,在嵌套的 promise
中访问 params.x
不会导致函数在 params.x
更改时重新运行。(别担心,在开发中如果你意外这样做了会收到警告)。相反,在您的 load
函数主体中访问参数。
Manual invalidation
您还可以使用invalidate(url)
重新运行适用于当前页面的加载函数,该函数将重新运行所有依赖于url的load
函数,并且可以使用invalidateAll()
重新运行每个加载函数。
如果一个load
函数调用fetch(url)
或depends(url)
,则它依赖于url
。请注意,url
可以是以[a-z]::
开头的自定义标识符。
src/routes/random-number/+page.js
1 | /** @type {import('./$types').PageLoad} */ |
src/routes/random-number/+page.svelte
1 | <script> |
总结一下,以下情况会导致负载函数重新运行:
- 它引用了
params
的某个属性,其值已更改 - 它引用了
url
的某个属性(例如url.pathname
或url.search
),其值已更改 - 它调用
await parent()
并且父级负载函数重新运行 - 通过
fetch
或depends
声明对特定 URL 的依赖关系,并使用invalidate(url)
标记该 URL 无效时 - 所有活动的加载函数都被强制重新运行以使用
invalidateAll()
params
和url
可能会因为 <a href="..">
链接点击、<form>
交互、goto
调用或redirect
而发生变化。
请注意,重新运行load
函数将更新相应的 +layout.svelte
或 +page.svelte
中的数据 prop;它不会导致组件被重新创建。因此,内部状态得到保留。如果这不是您想要的结果,则可以在 afterNavigate
回调中重置所需内容,并/或在 {#key ...}
块中包装组件。
Form actions
+page.server.js
文件可以导出操作,允许您使用<form>
元素将数据POST
到服务器。
在使用<form>
时,客户端JavaScript是可选的,但您可以轻松地通过JavaScript逐步增强表单交互以提供最佳用户体验。
Default actions
在最简单的情况下,一个页面声明了一个默认操作:
src/routes/login/+page.server.js
1 | /** @type {import('./$types').Actions} */ |
要从/login
页面调用此操作,只需添加一个<form>
即可,无需JavaScript:
src/routes/login/+page.svelte
1 | <form method="POST"> |
如果有人点击按钮,浏览器将通过POST
请求将表单数据发送到运行默认操作的服务器。
Actions总是使用
POST
请求,因为GET
请求不应该具有副作用。
我们还可以通过添加action
属性并指向页面来从其他页面调用该操作(例如,如果根布局中的导航中有登录小部件):
src/routes/+layout.svelte
1 | <form method="POST" action="/login"> |
Named actions
一个页面可以拥有多个命名动作,而不是只有一个default
动作:
src/routes/login/+page.server.js
1 | /** @type {import('./$types').Actions} */ |
要调用命名操作,请添加一个查询参数,名称前缀为 /
字符:
src/routes/login/+page.svelte
1 | <form method="POST" action="?/register"> |
src/routes/+layout.svelte
1 | <form method="POST" action="/login?/register"> |
除了 action
属性外,我们还可以在按钮上使用 formaction
属性将相同的表单数据 POST
到与父 <form>
不同的操作:
src/routes/login/+page.svelte
1 | <form method="POST"> |
我们不能在命名操作旁边设置默认操作,因为如果您向未重定向的命名操作POST,则查询参数将保留在URL中,这意味着下一个默认的POST将通过之前的命名操作进行。
解剖action
每个操作都会接收到一个 RequestEvent
对象,允许您使用request.formData()
读取数据。在处理请求后(例如通过设置 cookie 登录用户),该操作可以响应数据,这些数据将通过相应页面的form
属性和 $page.form
应用程序范围内直到下一次更新可用。
src/routes/login/+page.server.js
1 | /** @type {import('./$types').PageServerLoad} */ |
src/routes/login/+page.svelte
1 | <script> |
Validation errors
如果由于无效数据而无法处理请求,您可以将验证错误(以及先前提交的表单值)返回给用户,以便他们可以再次尝试。fail
函数允许您返回HTTP状态码(通常是400或422,在验证错误的情况下),以及数据。状态代码可通过$page.status
获得,表单数据可通过form
获得:
src/routes/login/+page.server.js
1 | import { fail } from '@sveltejs/kit'; |
请注意,作为一项预防措施,我们仅将电子邮件返回到页面 —— 而不是密码。
src/routes/login/+page.svelte
1 | <form method="POST" action="?/login"> |
返回的数据必须可序列化为JSON。除此之外,结构完全由您决定。例如,如果页面上有多个表单,则可以使用id
属性或类似方法来区分返回的表单数据所属于哪个<form>
。
Redirects
重定向(和错误)的工作方式与load
时完全相同:
src/routes/login/+page.server.js
1 | import { fail, redirect } from '@sveltejs/kit'; |
Loading data
当一个操作运行后,页面将被重新渲染(除非发生重定向或意外错误),并且该操作的返回值可用作form
属性提供给页面。这意味着您的页面load
函数将在操作完成后运行。
请注意,handle
在调用动作之前运行,并且不会在load
函数之前重新运行。这意味着如果例如您使用 handle
基于 cookie 来填充 event.locals
,则必须在设置或删除 cookie 时更新 event.locals
:
src/hooks.server.js
1 | /** @type {import('@sveltejs/kit').Handle} */ |
src/routes/account/+page.server.js
1 | /** @type {import('./$types').PageServerLoad} */ |
Progressive enhancement(渐进增强)
在前面的章节中,我们构建了一个可以在没有客户端JavaScript的情况下工作的/login
操作——看不到任何fetch
。这很棒,但是当JavaScript可用时,我们可以逐步增强我们的表单交互以提供更好的用户体验。
use:enhance
逐步增强表单的最简单方法是添加 use:enhance
操作:
src/routes/login/+page.svelte
1 | <script> |
是的,增强操作和
<form action>
都被称为“action”
,有点令人困惑。这些文档充满了行动。抱歉。
没有参数时,use:enhance
将模拟浏览器本机行为,只是没有完整的页面重新加载。它将:
- 在成功或无效响应时更新
form
属性$page.form
和$page.status
,但仅当操作在您提交的页面上时才更新。例如,如果您的表单看起来像<form action="/somewhere/else" ..>
,则不会更新form
和$page
。这是因为在本机表单提交情况下,您将被重定向到操作所在的页面。如果要无论如何更新它们,请使用applyAction
- 在成功响应中重置
<form>
元素并使所有数据失效,并使用invalidateAll
。 - 对于重定向响应调用
goto
。 - 如果发生错误,则呈现最近的
+error
边界。 - 将焦点重置为适当的元素。
要自定义行为,您可以提供一个 SubmitFunction
,在表单提交之前立即运行,并(可选)返回一个回调函数,该回调函数将与 ActionResult
一起运行。请注意,如果您返回了一个回调函数,则不会触发上述默认行为。要恢复它,请调用 update
。
1 | <form |
你可以使用这些函数来显示和隐藏加载界面等。
applyAction
如果您提供自己的回调函数,您可能需要重现默认的 use:enhance
行为的一部分,例如显示最近的 +error
边界。大多数情况下,调用传递给回调函数的 update
就足够了。如果您需要更多定制化,则可以使用 applyAction
:
src/routes/login/+page.svelte
1 | <script> |
applyAction(result)
的行为取决于 result.type
:
success
,failure
— 将$page.status
设置为result.status
,并将表单和$page.form
更新为result.data
(与从enhance
提交的位置无关,不同于update
)redirect
— 调用goto(result.location)
error
— 使用result.error
渲染最近的+error
边界
在所有情况下,焦点都会被重置。
自定义事件监听器
我们也可以自己实现渐进增强,而不使用 use:enhance
,在 <form>
上使用普通的事件监听器:
src/routes/login/+page.svelte
1 | <script> |
请注意,在使用 $app/forms
中相应的方法进一步处理响应之前,您需要对其进行反序列化。JSON.parse()
不足以支持表单操作(如load
函数)返回 Date
或 BigInt
对象。
如果您在 +page.server.js
旁边还有一个 +server.js
,则默认情况下 fetch
请求将被路由到该位置。要将 POST
请求发送到 +page.server.js
中的操作,请使用自定义 x-sveltekit-action
标头:
1 | const response = await fetch(this.action, { |
可选方案
表单操作是向服务器发送数据的首选方式,因为它们可以逐步增强,但您也可以使用 ·+server.js· 文件来公开(例如)JSON API。以下是这种交互可能看起来像的方式:
send-message/+page.svelte
1 | <script> |
api/ci/+server.js
1 | /** @type {import('./$types').RequestHandler} */ |
GET vs POST
正如我们所见,要调用表单操作,必须使用 method="POST"
。
有些表单不需要将数据 POST
到服务器 - 比如搜索输入框。对于这些表单,您可以使用 method="GET"
(或者等效地根本不指定方法),SvelteKit 将像 <a>
元素一样处理它们,使用客户端路由器而不是完整的页面导航:
1 | <form action="/search"> |
提交此表单将导航到/search?q=...
并调用您的加载函数,但不会触发任何操作。与<a>
元素一样,您可以在 <form>
上设置 data-sveltekit-reload
、data-sveltekit-replacestate
、data-sveltekit-keepfocus
和 data-sveltekit-noscroll
属性来控制路由器的行为。
Page options
默认情况下,SvelteKit 会在服务器上首先渲染(或预渲染)任何组件,并将其作为 HTML 发送到客户端。然后它会再次在浏览器中呈现该组件,以使其交互,在一个称为水合的过程中。因此,您需要确保组件可以在两个地方运行。然后 SvelteKit 将初始化一个路由器来接管随后的导航。
您可以通过从 +page.js
或 +page.server.js
导出选项来逐页控制这些内容,也可以使用共享的 +layout.js
或 +layout.server.js
来控制一组页面。要定义整个应用程序的选项,请从根布局导出它们。子布局和页面覆盖父布局设置的值,因此例如您可以启用整个应用程序的预渲染功能,然后禁用需要动态呈现的页面。
您可以混合和匹配这些选项在应用程序不同区域内使用。例如你可以对营销页面进行最大速度预渲染、对动态页面进行服务器端呈现以提高 SEO 和可访问性,并通过仅在客户端上呈现将管理部分转换成 SPA 。这使得 SvelteKit 非常灵活多变。
预渲染
你的应用程序中至少有一些路由可以表示为在构建时生成的简单HTML文件。这些路由可以进行预渲染。
+page.js/+page.server.js/+server.js
1 | export const prerender = true; |
或者,您可以在根目录下的 +layout.js
或 +layout.server.js
中设置 export const prerender = true
,并预渲染除明确标记为不可预渲染的页面之外的所有内容:
+page.js/+page.server.js/+server.js
1 | export const prerender = false; |
具有 prerender = true
的路由将从用于动态 SSR 的清单中排除,使您的服务器(或无服务器/边缘函数)更小。在某些情况下,您可能希望预渲染一个路由,但也要将其包含在清单中(例如,在像 /blog/[slug]
这样的路由上,您希望预渲染最近/流行的内容但是对长尾进行服务器呈现)- 对于这些情况,有第三个选项,“auto”:
+page.js/+page.server.js/+server.js
1 | export const prerender = 'auto'; |
如果您的整个应用程序适合预渲染,您可以使用
adapter-static
,它将输出适用于任何静态 Web 服务器的文件。
预渲染器将从您的应用程序根目录开始,并为任何可预渲染页面或+server.js
路由生成文件。每个页面都会被扫描,以查找指向其他候选预渲染页面的<a>
元素 - 因此,通常不需要指定应访问哪些页面。如果确实需要指定预渲染器应访问哪些页面,则可以在prerender
配置中使用entries
选项进行设置。
在预渲染期间,从$app/environment
导入的building
值将为true
。
预渲染服务器路由
与其他页面选项不同,预渲染也适用于 +server.js
文件。这些文件不受布局的影响,但如果有任何从它们获取数据的页面,则会继承默认值。例如,如果一个 +page.js
包含此load
函数…
+page.js
1 | export const prerender = true; |
….那么如果 src/routes/my-server-route.json/+server.js
不包含自己的 export const prerender = false
,则将视为可预渲染。
什么时候不做预渲染
基本规则是:对于一个可以预渲染的页面,任何两个直接访问它的用户必须从服务器获取相同的内容。
并非所有页面都适合预渲染。任何预渲染的内容都将被所有用户看到。当然,您可以在预渲染页面中的 ·onMount· 中获取个性化数据,但这可能会导致较差的用户体验,因为它将涉及空白初始内容或加载指示器。
请注意,您仍然可以预渲染基于页面参数加载数据的页面,例如 src/routes/blog/[slug]/+page.svelte
路由。
在预渲染期间访问 url.searchParams
是被禁止的。如果需要使用它,请确保只在浏览器中这样做(例如在 onMount
中)。
具有操作的页面无法进行预渲染,因为服务器必须能够处理操作 POST
请求。
预渲染和服务器端渲染
如果将 ssr 选项设置为 false,则每个请求都会导致相同的空 HTML shell。由于这会导致不必要的工作,SvelteKit 默认预渲染任何找到的页面,其中prerender
没有明确设置为 false
。
路由冲突
由于预渲染会写入文件系统,因此不可能有两个端点导致目录和文件具有相同的名称。例如,src/routes/foo/+server.js
和 src/routes/foo/bar/+server.js
将尝试创建 foo
和 foo/bar
,这是不可能的。
因此,建议您始终包括文件扩展名 - src/routes/foo.json/+server.js
和src/routes/foo/bar.json/+server.js
将导致foo.json
和foo/bar.json
文件和谐地并存。
对于页面,我们通过编写foo/index.html
而不是foo
来避免这个问题。
故障排除
如果你遇到了像“以下路由被标记为可预渲染,但未进行预渲染”这样的错误,那是因为相关的路由(或者如果它是一个页面,则是其父布局)具有 export const prerender = true
,但该页面实际上没有被预渲染,因为它没有被预渲染爬虫访问到。
由于这些路由无法动态服务器端呈现,在人们尝试访问相关路由时会导致错误。解决方法有两种:
- 确保 SvelteKit 可以通过从
config.kit.prerender.entries
中跟随链接找到该路线。如果不能通过爬行其他入口点找到带参数的动态路径(即包含[parameters]
的页面),请将其添加到此选项中;否则,它们不会被预渲染,因为 SvelteKit 不知道参数应该具有什么值。未标记为可预渲染的页面将被忽略,并且它们与其他页面之间的链接也不会被爬行。 - 将
export const prerender = true
更改为export const prerender ='auto'
。使用'auto'
的路线可以进行动态服务器端呈现。
ssr
通常情况下,SvelteKit 首先在服务器上呈现您的页面,并将该 HTML 发送到客户端进行水合作用。如果您将 ssr
设置为 false
,则会呈现一个空的“外壳”页面。如果您的页面无法在服务器上呈现(例如,因为使用了仅限于浏览器的全局变量,如 document
),则这很有用,但在大多数情况下不建议这样做(请参见附录)。
+page.js
1 | export const ssr = false; |
如果你在根 +layout.js
中添加 export const ssr = false
,那么整个应用程序将只在客户端渲染 —— 这基本上意味着你把应用程序变成了单页应用。
csr
通常情况下,SvelteKit会将服务器渲染的HTML转换为交互式客户端渲染(CSR)页面。有些页面根本不需要JavaScript - 许多博客文章和“关于”页面属于这一类别。在这些情况下,您可以禁用CSR:
+page.js
1 | export const csr = false; |
如果
ssr
andcsr
都设置为false
, 那么不会有任何内容被预渲染!!!
trailingSlash
默认情况下,SvelteKit 会从 URL 中删除尾随斜杠 —— 如果您访问 /about/
,它将响应重定向到 /about
。您可以使用 trailingSlash
选项更改此行为,该选项可以是 'never'
(默认值)、'always'
或 'ignore'
。
与其他页面选项一样,您可以从 +layout.js
或 +layout.server.js
导出此值,并将其应用于所有子页面。您还可以从 +server.js
文件中导出配置。
src/routes/+layout.js
1 | export const trailingSlash = 'always'; |
此选项也会影响预渲染。如果 trailingSlash
为 always
,则像/about
这样的路由将生成一个 about/index.html
文件,否则它将创建 about.html
,反映静态 Web 服务器约定。
忽略尾部斜杠不是推荐的做法——相对路径的语义在两种情况下有所不同(从
/x
到./y
是/y
,但从/x/
到./y
是/x/y
),而且/x
和/x/
被视为不同的URL,这对SEO有害。
配置
通过适配器的概念,SvelteKit 可以在各种平台上运行。每个平台可能都有特定的配置来进一步调整部署 - 例如,在 Vercel 上,您可以选择将应用程序的某些部分部署在边缘,而将其他部分部署在无服务器环境中。
config
是一个具有键值对的对象,在顶层。除此之外,具体形状取决于您使用的适配器。每个适配器都应该提供 Config
接口进行导入以实现类型安全性。请查阅您所使用适配器的文档获取更多信息。
src/routes/+page.js
1 | /** @type {import('some-adapter').Config} */ |
config
对象在顶层合并(但不是更深的级别)。这意味着,如果您只想覆盖上面+layout.js
中的某些值,则无需在+page.js
中重复所有值。例如,此布局配置…
src/routes/+layout.js
1 | export const config = { |
…被此页面配置覆盖…
src/routes/+page.js
1 | export const config = { |
这导致该页面的配置值为 { runtime: 'edge', regions: ['us1', 'us2'], foo: { baz: true } }
for that page.
状态管理
如果你习惯于构建仅客户端应用程序,那么在跨服务器和客户端的应用程序中进行状态管理可能会让人感到害怕。本节提供了一些避免常见陷阱的提示。
避免在服务器上使用共享状态
浏览器是有状态的——随着用户与应用程序的交互,状态存储在内存中。另一方面,服务器是无状态的——响应内容完全由请求内容确定。
从概念上讲,就是这样。但实际上,服务器通常具有长寿命并且被多个用户共享。因此,在共享变量中不要存储数据非常重要。例如,请考虑以下代码:
+page.server.js
1 | let user; |
user
变量是由连接到此服务器的所有人共享的。如果爱丽丝提交了一个令人尴尬的秘密,而鲍勃在她之后访问了该页面,那么鲍勃将知道爱丽丝的秘密。此外,当爱丽丝在同一天晚些时候返回网站时,服务器可能已经重新启动,导致她的数据丢失。
相反地,您应该使用 cookie
对用户进行身份验证,并将数据持久化到数据库中。
负载无副作用
出于同样的原因,你的load
函数应该是纯函数——没有副作用(除了偶尔使用 console.log(...)
)。例如,你可能会想在load
函数中写入一个 store,以便在组件中使用该 store 值:
+page.js
1 | import { user } from '$lib/user'; |
与前面的例子一样,这将一个用户的信息放在所有用户共享的位置。相反,只需返回数据即可…
+page.js
1 | export async function load({ fetch }) { |
…并将其传递给需要它的组件,或使用 $page.data
。
如果您没有使用 SSR,则不会意外地将一个用户的数据暴露给另一个用户。但是,您仍应避免在load
函数中产生副作用-这样您的应用程序就更容易理解了。
使用带有上下文的存储
你可能会想知道,如果我们不能使用自己的存储库,我们如何能够使用 $page.data
和其他应用商店。答案是服务器上的应用商店使用 Svelte 的上下文 API - 存储库附加到组件树中,并通过setContext
进行订阅时检索 getContext
。我们可以对自己的存储库执行相同的操作:
src/routes/+layout.svelte
1 | <script> |
src/routes/user/+page.svelte
1 | <script> |
如果您不使用 SSR(并且可以保证将来不需要使用SSR),那么您可以安全地在共享模块中保留状态,而无需使用上下文API。
组件状态被保留
当您在应用程序中导航时,SvelteKit 会重复使用现有的布局和页面组件。例如,如果您有这样一个路由…
src/routes/blog/[slug]/+page.svelte
1 | <script> |
…然后从 /blog/my-short-post
导航到 /blog/my-long-post
不会导致组件被销毁和重新创建。数据属性(以及data.title
和data.content
)将发生更改,但由于代码没有重新运行,estimatedReadingTime
将不会被重新计算。
相反,我们需要使该值具有响应性:
src/routes/blog/[slug]/+page.svelte
1 | <script> |
像这样重复使用组件意味着侧边栏滚动状态等内容得以保留,而且您可以轻松地在不同值之间进行动画处理。但是,如果您确实需要在导航时完全销毁和重新挂载组件,则可以使用此模式:
1 | {#key $page.url.pathname} |
在URL中存储状态
如果您有应该在重新加载后保留并/或影响SSR的状态,例如表格上的过滤器或排序规则,则URL搜索参数(如?sort=price&order=ascending
)是放置它们的好地方。您可以将它们放在<a href="...">
或<form action="...">
属性中,也可以通过goto('?key=value')
以编程方式设置它们。可以通过url
参数在load
函数内部访问它们,在组件内部通过$page.url.searchParams
访问它们。
在快照中存储短暂状态
一些 UI 状态,例如“手风琴是否打开?”,是可丢弃的——如果用户导航离开或刷新页面,则状态丢失并不重要。在某些情况下,如果用户导航到另一页然后返回,您确实希望数据保持不变,但将状态存储在 URL 或数据库中会过度复杂化。为此,SvelteKit 提供了快照功能,让您将组件状态与历史记录条目关联起来。
该系列文章一共四篇,以下是系列文章链接:
SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念