Skip to main content

流式传输、快照以及 SvelteKit 1.0 以来的其他新功能

SvelteKit 最新版本中的令人兴奋的改进

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

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

SvelteKit 使用 load 函数 来为特定路由检索数据。在页面之间导航时,SvelteKit 首先获取数据,然后再使用结果渲染页面。如果某些页面数据加载时间比其他数据长,这可能会成为问题,尤其是当这些数据不是必要的时——用户在所有数据准备好之前将看不到新页面的任何部分。

过去可以通过某些方式解决此问题。尤其是,可以在组件中获取加载较慢的数据,这样组件会先使用 load 中的数据渲染,然后再开始获取较慢的数据。但这种方式并不理想:数据获取的延迟更大,因为客户端渲染完成后才会开始获取数据,同时也不得不打破 SvelteKit 的 load 约定。

现在,在 SvelteKit 1.8 中,我们有了一个新解决方案:您可以从服务器端的 load 函数返回嵌套的 promise,SvelteKit 会在该 promise 解析前开始渲染页面。一旦解析完成,结果会被 流式传输 到页面。

例如,考虑以下 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}
	加载中...
{: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 将您的数据转换为 JavaScript 表达式 const
const data: {
    post: {
        title: string;
        content: string;
    };
    streamed: {
        comments: any;
    };
}
data
= {
post: {
    title: string;
    content: string;
}
post
: {
title: stringtitle: '我的酷博客文章', 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: '第一个评论!' }]
	});
</script>

对于客户端导航,我们使用一种稍有不同的机制。从服务器返回的数据以 换行分隔 JSON 的形式序列化,SvelteKit 使用类似的延迟机制通过 devalue.parse 重构这些值:

// 这会立即生成——注意 ["Promise",1]...
[{"post":1,"streamed":4},{"title":2,"content":3},"我的酷博客文章","...",{"comments":5},["Promise",6],1]

// ...然后此块会在 promise 解析后发送到浏览器
[{"id":1,"data":2},1,[3],{"comment":4},"第一个评论!"]

因为这样通过原生支持 promise,您可以将它们放在 load 返回数据中的任何地方(除了顶层,因为顶层的会自动等待),并且这些 promise 可以解析为 devalue 支持的任何数据类型——包括更多的 promise!

需要注意一点:此功能需要 JavaScript。因此,我们建议您仅流式传输非必要数据,以确保所有用户都能访问核心体验。

关于此功能的更多信息,请参阅 文档。您可以在 sveltekit-on-the-edge.vercel.app 上看到演示(位置信息被人为延迟并通过流式传输加载)或 在 Vercel 上部署您的版本,其中边缘函数和无服务器函数都支持流式传输。

我们感谢来自 Qwik、Remix、Solid、Marko、React 以及许多其他项目对这一想法的启发。

快照

在 SvelteKit 应用中,如果您开始填写表单后导航离开,返回时表单状态不会恢复——表单会以默认值重新创建。根据上下文,这可能会让用户感到不便。从 SvelteKit 1.5 开始,我们内置了一种解决方法:快照。

现在,您可以从 +page.svelte+layout.svelte 导出一个 snapshot 对象。该对象有两个方法:capturerestore。当用户离开页面时,capture 函数定义您想要存储的状态。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">评论</label>
	<textarea id="comment" bind:value={comment} />
	<button>提交评论</button>
</form>

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

更多信息,请参阅 文档

路由级部署配置

SvelteKit 使用特定平台的 适配器 将您的应用代码转换为用于生产环境部署。在此之前,您只能在应用范围内配置部署。例如,您可以将应用部署为边缘函数或无服务器函数,但不能同时支持两者。这让应用的某些部分无法利用边缘——如果任何路由需要 Node API,则不能将其部署到边缘。同样,对于其他部署配置,例如区域和内存分配,您必须选择一个值应用于整个应用程序。

现在,您可以在 +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 # 或 @sveltejs/adapter-vercel@latest

目前,只有 Vercel 适配器 实现了路由级配置,但相关基础设施已为其他平台做好准备。如果您是适配器作者,请查看 PR 中的更改以了解所需内容。

Vercel 的增量静态生成

路由级配置还解锁了另一个备受期待的功能——现在可以在部署到 Vercel 的 SvelteKit 应用中使用 增量静态生成(ISR)。ISR 将预渲染内容的性能和成本优势与动态渲染内容的灵活性相结合。

要为某个路由添加 ISR,只需在 config 对象中包含 isr 属性:

export const 
const config: {
    isr: {};
}
config
= {
isr: {}isr: { // 请参阅 Vercel 适配器文档以获取所需选项 } };

还有更多改进...

感谢所有在项目中使用 SvelteKit 并做出贡献的人。我们已经说过很多次了,Svelte 是一个社区项目,没有你的反馈和贡献,这一切都不可能实现。