跳至主要內容

自 SvelteKit 1.0 以來的串流、快照和其他新功能

最新版 SvelteKit 中令人興奮的改進

自 SvelteKit 1.0 發布以來,Svelte 團隊一直努力不懈。讓我們來談談自發布以來推出的一些主要新功能:串流非必要資料快照路由層級設定

在 load 函式中串流非必要資料

SvelteKit 使用 load 函式來擷取給定路由的資料。當在頁面之間導覽時,它會先擷取資料,然後使用結果呈現頁面。如果頁面的某些資料比其他資料需要更長的時間載入,這可能會是一個問題,尤其是當資料不是必要時——在所有資料準備好之前,使用者看不到新頁面的任何部分。

有一些方法可以解決這個問題。特別是,您可以在元件本身中擷取緩慢的資料,這樣它會先使用來自 load 的資料呈現,然後開始擷取緩慢的資料。但這不是理想的:由於您直到客戶端呈現後才開始擷取,資料會被延遲得更久,而且您還必須打破 SvelteKit 的 load 慣例。

現在,在 SvelteKit 1.8 中,我們有了一個新的解決方案:您可以從伺服器 load 函式返回一個巢狀 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 返回的資料中的任何位置(除了最上層,因為我們會自動為您等待這些 Promise),它們可以使用 devalue 支援的任何資料類型解析 — 包括更多的 Promise!

一個注意事項:此功能需要 JavaScript。因此,我們建議您僅串流非必要的資料,以便所有使用者都可以使用核心體驗。

如需此功能的更多資訊,請參閱文件。您可以在 sveltekit-on-the-edge.vercel.app (位置資料是人為延遲並串流的) 上看到一個演示,或在 Vercel 上部署您自己的演示,其中 Edge Functions 和無伺服器函式都支援串流。

我們很感謝此想法先前實作的啟發,包括 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 使用平台特定的 轉接器 來轉換您的應用程式程式碼以部署到生產環境。到目前為止,您必須在應用程式層級設定您的部署。例如,您可以將應用程式部署為邊緣函式或無伺服器函式,但不能同時部署。這使得無法為應用程式的某些部分利用邊緣 - 如果任何路由需要 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 # or @sveltejs/adapter-vercel@latest

目前,只有 Vercel 轉接器 實作了路由特定設定,但有實作其他平台設定的基礎構建區塊。如果您是轉接器作者,請參閱 PR 中的變更,以了解需要什麼。

在 Vercel 上增量靜態再生

路由層級設定也解鎖了另一個備受期待的功能 - 您現在可以使用增量靜態再生 (ISR) 和部署到 Vercel 的 SvelteKit 應用程式。ISR 提供了預先呈現內容的效能和成本優勢,以及動態呈現內容的彈性。

若要將 ISR 新增至路由,請在您的 config 物件中包含 isr 屬性

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

還有更多...

感謝所有在專案中貢獻和使用 SvelteKit 的人。我們之前說過,但 Svelte 是一個社群專案,如果沒有您的回饋和貢獻,這是不可能實現的。