跳至主要内容

SvelteKit 1.0 之后的新功能,包括流式传输、快照等

最新版 SvelteKit 的激动人心改进

自 SvelteKit 1.0 发布以来,Svelte 团队一直在努力工作。让我们谈谈自发布以来的一些主要新功能:流式传输非必要数据快照路由级配置

在 load 函数中流式传输非必要数据

SvelteKit 使用load 函数来检索给定路由的数据。在页面之间导航时,它首先获取数据,然后使用结果呈现页面。如果页面的一些数据加载时间比其他数据长,这可能是一个问题,尤其是在数据不是必需的情况下 - 在所有数据准备就绪之前,用户将看不到新页面的任何部分。

有一些方法可以解决这个问题。特别是,您可以在组件本身中获取缓慢的数据,以便它首先使用来自 load 的数据进行渲染,然后开始获取缓慢的数据。但这并不理想:数据延迟得更多,因为您直到客户端渲染后才开始获取,而且您还必须破坏 SvelteKit 的 load 约定。

现在,在 SvelteKit 1.8 中,我们有了一个新的解决方案:您可以从服务器加载函数返回嵌套的 Promise,SvelteKit 将在它解析之前开始呈现页面。一旦完成,结果将被流式传输到页面。

例如,考虑以下 load 函数

export const const load: PageServerLoadload: PageServerLoad = () => {
	return {
		post: anypost: fetchPost(),
		
streamed: {
    comments: any;
}
streamed
: {
comments: anycomments: fetchComments() } }; };

SvelteKit 会在开始渲染页面之前自动等待 fetchPost 调用,因为它位于顶层。但是,它不会等待嵌套的 fetchComments 调用完成 - 页面将渲染,并且 data.streamed.comments 将是一个 Promise,它将在请求完成时解析。我们可以使用 Svelte 的await 块在相应的 +page.svelte 中显示加载状态

<script lang="ts">
	import type { PageData } from './$types';
	export let data: PageData;
</script>

<article>
	{data.post}
</article>

{#await data.streamed.comments}
	Loading...
{:then value}
	<ol>
		{#each value as comment}
			<li>{comment}</li>
		{/each}
	</ol>
{/await}

这里 streamed 属性没有什么特别之处 - 触发此行为所需的全部条件是在返回对象的顶层之外使用一个 Promise。

只有在您的应用程序托管平台支持的情况下,SvelteKit 才能流式传输响应。一般来说,任何基于 AWS Lambda 的平台(例如无服务器函数)都不支持流式传输,但任何传统的 Node.js 服务器或基于边缘的运行时都支持。请查看您的提供商的文档以确认。

如果您的平台不支持流式传输,数据仍然可用,但响应将被缓冲,并且页面将不会开始渲染,直到所有数据都已获取。

它是如何工作的?

为了使来自服务器 load 函数的数据到达浏览器,我们必须对其进行序列化。SvelteKit 使用一个名为devalue的库,它类似于 JSON.stringify,但更好 - 它可以处理 JSON 无法处理的值(如日期和正则表达式),它可以序列化包含自身的对象(或在数据中多次存在)而不会破坏身份,并且它可以保护您免受XSS 漏洞的影响。

当我们服务器端渲染页面时,我们告诉 devalue 将 Promise 序列化为创建延迟的函数调用。这是 SvelteKit 添加到页面的代码的简化版本

const const deferreds: Map<any, any>deferreds = new 
var Map: MapConstructor
new () => Map<any, any> (+3 overloads)
Map
();
var window: Window & typeof globalThiswindow.defer = (id) => { return new
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((fulfil: (value: unknown) => voidfulfil, reject: (reason?: any) => voidreject) => {
const deferreds: Map<any, any>deferreds.Map<any, any>.set(key: any, value: any): Map<any, any>

Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.

set
(id: anyid, { fulfil: (value: unknown) => voidfulfil, reject: (reason?: any) => voidreject });
}); }; var window: Window & typeof globalThiswindow.resolve = (id, data, error) => { const const deferred: anydeferred = const deferreds: Map<any, any>deferreds.Map<any, any>.get(key: any): any

Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.

@returnsReturns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
get
(id: anyid);
const deferreds: Map<any, any>deferreds.Map<any, any>.delete(key: any): boolean
@returnstrue if an element in the Map existed and has been removed, or false if the element does not exist.
delete
(id: anyid);
if (error: anyerror) { const deferred: anydeferred.reject(error: anyerror); } else { const deferred: anydeferred.fulfil(data: anydata); } }; // devalue converts your data into a JavaScript expression const
const data: {
    post: {
        title: string;
        content: string;
    };
    streamed: {
        comments: any;
    };
}
data
= {
post: {
    title: string;
    content: string;
}
post
: {
title: stringtitle: 'My cool blog post', content: stringcontent: '...' },
streamed: {
    comments: any;
}
streamed
: {
comments: anycomments: var window: Window & typeof globalThiswindow.defer(1) } };

此代码以及服务器端渲染的 HTML 的其余部分会立即发送到浏览器,但连接保持打开状态。稍后,当 Promise 解析时,SvelteKit 会将额外的 HTML 块推送到浏览器

<script>
	window.resolve(1, {
		data: [{ comment: 'First!' }]
	});
</script>

对于客户端导航,我们使用略有不同的机制。来自服务器的数据被序列化为换行分隔的 JSON,并且 SvelteKit 使用类似的延迟机制使用 devalue.parse 重建值

// this is generated immediately — note the ["Promise",1]...
[{"post":1,"streamed":4},{"title":2,"content":3},"My cool blog post","...",{"comments":5},["Promise",6],1]

// ...then this chunk is sent to the browser once the promise resolves
[{"id":1,"data":2},1,[3],{"comment":4},"First!"]

由于 Promise 以这种方式得到原生支持,因此您可以将它们放在从 load 返回的数据中的任何位置(除了顶层,因为我们会自动为您等待这些数据),并且它们可以使用 devalue 支持的任何类型的数据解析 - 包括更多 Promise!

一个警告:此功能需要 JavaScript。因此,我们建议您只在非必要数据中进行流式传输,以便所有用户都能获得核心体验。

有关此功能的更多信息,请参阅文档。您可以在sveltekit-on-the-edge.vercel.app(位置数据被人工延迟并流式传输)或在 Vercel 上部署您自己的应用程序查看演示,在 Edge 函数和无服务器函数中都支持流式传输。

我们感谢先前此想法的实现带来的灵感,包括 Qwik、Remix、Solid、Marko、React 以及许多其他项目。

快照

以前在 SvelteKit 应用程序中,如果您在开始填写表单后导航离开,然后返回,您的表单状态将不会恢复 - 表单将使用其默认值重新创建。根据上下文,这可能会让用户感到沮丧。从 SvelteKit 1.5 开始,我们有一种内置的方法来解决这个问题:快照。

现在,您可以从 +page.svelte+layout.svelte 导出一个 snapshot 对象。此对象具有两种方法:capturerestorecapture 函数定义了用户离开页面时要存储的状态。然后,SvelteKit 会将该状态与当前历史记录条目关联。如果用户导航回页面,则将使用您之前设置的状态调用 restore 函数。

例如,以下是如何捕获和恢复文本区域的值

<script lang="ts">
	import type { Snapshot } from './$types';

	let comment = '';

	export const snapshot: Snapshot = {
		capture: () => comment,
		restore: (value) => (comment = value)
	};
</script>

<form method="POST">
	<label for="comment">Comment</label>
	<textarea id="comment" bind:value={comment} />
	<button>Post comment</button>
</form>

虽然表单输入值和滚动位置等是常见的示例,但您可以在快照中存储任何您喜欢的 JSON 可序列化数据。快照数据存储在sessionStorage中,因此即使页面重新加载或用户导航到完全不同的网站,它也会持续存在。因为它位于 sessionStorage 中,所以您无法在服务器端渲染期间访问它。

更多信息,请参阅文档

路由级部署配置

SvelteKit 使用特定于平台的适配器来转换您的应用程序代码以部署到生产环境。到目前为止,您必须在应用程序范围内配置部署。例如,您可以将应用程序部署为 Edge 函数或无服务器函数,但不能同时部署两者。这使得无法利用应用程序部分的 Edge - 如果任何路由需要 Node API,那么您将无法将任何内容部署到 Edge。对于部署配置的其他方面(如区域和分配的内存)也是如此:您必须选择一个应用于整个应用程序中每个路由的值。

现在,您可以在 +server.js+page(.server).js+layout(.server).js 文件中导出 config 对象来控制这些路由的部署方式。在 +layout.js 中执行此操作将把配置应用于所有子页面。config 的类型对于每个适配器都是唯一的,因为它取决于您要部署到的环境。

import type { import ConfigConfig } from 'some-adapter';

export const const config: Configconfig: import ConfigConfig = {
	runtime: stringruntime: 'edge'
};

配置在顶层合并,因此您可以覆盖在布局中为树中更下层的页面设置的值。有关更多详细信息,请参阅文档

如果您部署到 Vercel,则可以通过安装最新版本的 SvelteKit 和您的适配器来利用此功能。这将需要对您的适配器版本进行重大升级,因为支持路由级配置的适配器需要 SvelteKit 1.5 或更高版本。

npm i @sveltejs/kit@latest
npm i @sveltejs/adapter-auto@latest # or @sveltejs/adapter-vercel@latest

目前,只有Vercel 适配器实现了特定于路由的配置,但构建块已存在,可以为其他平台实现此功能。如果您是适配器作者,请参阅PR 中的更改,以了解需要什么。

Vercel 上的增量静态再生

路由级配置还解锁了另一个备受期待的功能 - 您现在可以在部署到 Vercel 的 SvelteKit 应用程序中使用增量静态再生(ISR)。ISR 提供了预渲染内容的性能和成本优势,以及动态渲染内容的灵活性。

要向路由添加 ISR,请在您的 config 对象中包含 isr 属性

export const 
const config: {
    isr: {};
}
config
= {
isr: {}isr: { // see Vercel adapter docs for the required options } };

以及更多...

感谢所有为 SvelteKit 做出贡献并在其项目中使用 SvelteKit 的人。我们之前说过,Svelte 是一个社区项目,如果没有您的反馈和贡献,它是无法实现的。