Skip to main content

迁移到 SvelteKit v2

从 SvelteKit 版本 1 升级到版本 2 应该基本上是无缝的。这里列出了一些需要注意的重大变更。您可以使用 npx sv migrate sveltekit-2 来自动迁移其中的一些变更。

我们强烈建议在升级到 2.0 之前先升级到最新的 1.x 版本,这样您就可以利用有针对性的弃用警告。我们还建议先升级到 Svelte 4:SvelteKit 1.x 的后期版本支持它,而 SvelteKit 2.0 则需要它。

redirect 和 error 不再需要您来抛出

之前,您必须自己 throwerror(...)redirect(...) 返回的值。在 SvelteKit 2 中不再需要这样做 — 调用这些函数就足够了。

import { function error(status: number, body: App.Error): never (+1 overload)

Throws an error with a HTTP status code and an optional message. When called during request handling, this will cause SvelteKit to return an error response without invoking handleError. Make sure you’re not catching the thrown error, which would prevent SvelteKit from handling it.

@paramstatus The HTTP status code. Must be in the range 400-599.
@parambody An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
@throwsHttpError This error instructs SvelteKit to initiate HTTP error handling.
@throwsError If the provided status is invalid (not between 400 and 599).
error
} from '@sveltejs/kit'
// ... throw error(500, '出错了');
function error(status: number, body?: {
    message: string;
} extends App.Error ? App.Error | string | undefined : never): never (+1 overload)

Throws an error with a HTTP status code and an optional message. When called during request handling, this will cause SvelteKit to return an error response without invoking handleError. Make sure you’re not catching the thrown error, which would prevent SvelteKit from handling it.

@paramstatus The HTTP status code. Must be in the range 400-599.
@parambody An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
@throwsHttpError This error instructs SvelteKit to initiate HTTP error handling.
@throwsError If the provided status is invalid (not between 400 and 599).
error
(500, '出错了');

svelte-migrate 将为您自动完成这些更改。

如果错误或重定向是在 try {...} 块内抛出的(提示:不要这样做!),您可以使用从 @sveltejs/kit 导入的 isHttpErrorisRedirect 来区分它们和意外错误。

当接收到没有指定 pathSet-Cookie 头时,浏览器会设置 cookie 路径为相关资源的父路径。这种行为既不实用也不直观,而且经常导致bug,因为开发者期望 cookie 适用于整个域名。

从 SvelteKit 2.0 开始,在调用 cookies.set(...)cookies.delete(...)cookies.serialize(...) 时需要设置 path,以避免歧义。大多数情况下,您可能想使用 path: '/',但您也可以设置其他路径,包括相对路径 — '' 表示”当前路径”,'.' 表示”当前目录”。

/** @type {import('./$types').PageServerLoad} */
export function 
function load({ cookies }: {
    cookies: any;
}): {
    response: any;
}
@type{import('./$types').PageServerLoad}
load
({ cookies: anycookies }) {
cookies: anycookies.set(const name: void
@deprecated
name
, value, { path: stringpath: '/' });
return { response: anyresponse } }

svelte-migrate 将添加注释来突出显示需要调整的位置。

顶级 promise 不再自动等待

在 SvelteKit 版本 1 中,如果从 load 函数返回的对象的顶级属性是 promise,它们会被自动等待。随着流式传输的引入,这种行为变得有点尴尬,因为它强制您将流式数据嵌套在一层深度。

从版本 2 开始,SvelteKit 不再区分顶级和非顶级 promise。要恢复阻塞行为,请使用 await(在适当的情况下使用 Promise.all 来防止瀑布效应):

// If you have a single promise
/** @type {import('./$types').PageServerLoad} */
export async function function load(event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>
@type{import('./$types').PageServerLoad}
load
({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here

fetch
}) {
const const response: anyresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch(const url: stringurl).Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled The callback to execute when the Promise is resolved.
@paramonrejected The callback to execute when the Promise is rejected.
@returnsA Promise for the completion of which ever callback is executed.
then
(r: Responser => r: Responser.Body.json(): Promise<any>json());
return { response: anyresponse } }
// If you have multiple promises
/** @type {import('./$types').PageServerLoad} */
export async function function load(event: ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>
@type{import('./$types').PageServerLoad}
load
({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here

fetch
}) {
const a = fetch(url1).then(r => r.json()); const b = fetch(url2).then(r => r.json()); const [const a: anya, const b: anyb] = await var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.PromiseConstructor.all<[Promise<any>, Promise<any>]>(values: [Promise<any>, Promise<any>]): Promise<[any, any]> (+1 overload)

Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.

@paramvalues An array of Promises.
@returnsA new Promise.
all
([
fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch(const url1: stringurl1).Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled The callback to execute when the Promise is resolved.
@paramonrejected The callback to execute when the Promise is rejected.
@returnsA Promise for the completion of which ever callback is executed.
then
(r: Responser => r: Responser.Body.json(): Promise<any>json()),
fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch(const url2: stringurl2).Promise<Response>.then<any, never>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<any>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled The callback to execute when the Promise is resolved.
@paramonrejected The callback to execute when the Promise is rejected.
@returnsA Promise for the completion of which ever callback is executed.
then
(r: Responser => r: Responser.Body.json(): Promise<any>json()),
]); return { a: anya, b: anyb }; }

goto(...) 修改

goto(...) 不再接受外部 URL。若需跳转到外部 URL,请使用 window.location.href = url。现在 state 对象决定了 $page.state,并且必须遵循已声明的 App.PageState 接口。详情参考 浅层路由

路径默认改为相对路径

在 SvelteKit 1 中,app.html 中的 %sveltekit.assets% 在服务端渲染时默认会被替换为相对路径(例如 ...../.. 等,取决于渲染路径),除非显式将 paths.relative 配置选项设置为 false。对于从 $app/paths 导入的 baseassets,也是类似的情况,但前提是 paths.relative 配置选项显式设置为 true

在版本 2 中,这种不一致性已经修复。路径要么始终是相对路径,要么始终是绝对路径,具体取决于 paths.relative 的值。默认值为 true,因为这让应用程序更加便携:如果 base 超出了应用程序的预期(例如当您在 Internet Archive 查看时)或者在构建时未知(例如部署到 IPFS 等情况),破坏的可能性会更低。

服务端 fetch 请求不再可追踪

之前可以通过在服务端上的 fetch 请求追踪请求的 URL 以重新运行加载函数。但这可能会造成安全风险(私有 URL 泄露),因此该功能在之前由 dangerZone.trackServerFetches 设置控制,现在该设置已被移除。

preloadCode 参数必须以 base 为前缀

SvelteKit 提供了两个函数,preloadCodepreloadData,用于以编程方式加载与特定路径相关的代码和数据。在版本 1 中存在一个细微的不一致性——传递给 preloadCode 的路径不需要加上 base 路径(如果设置了),而传递给 preloadData 的路径则要求这样做。

这一问题在 SvelteKit 2 中已修复——在两种情况下,路径都应该以 base 为前缀(如果设置了该值)。

此外,preloadCode 现在只接受一个参数,而不是多个参数。

resolvePath 已被移除

SvelteKit 1 包含一个名为 resolvePath 的函数,用于将路由 ID(如 /blog/[slug])和一组参数(如 { slug: 'hello' })解析为路径名。然而,其返回值并不包括 base 路径,这在 base 被设置的情况下限制了其实用性。

因此,SvelteKit 2 用一个更合适命名的函数 resolveRoute 替代了 resolvePath,该函数从 $app/paths 导入,并且会考虑到 base 路径。

import { resolvePath } from '@sveltejs/kit';
import { base } from '$app/paths';
import { function resolveRoute(id: string, params: Record<string, string | undefined>): string

Populate a route ID with params to resolve a pathname.

@examplejs import { resolveRoute } from '$app/paths'; resolveRoute( `/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' } ); // `/blog/hello-world/something/else`
resolveRoute
} from '$app/paths';
const path = base + resolvePath('/blog/[slug]', { slug }); const const path: stringpath = function resolveRoute(id: string, params: Record<string, string | undefined>): string

Populate a route ID with params to resolve a pathname.

@examplejs import { resolveRoute } from '$app/paths'; resolveRoute( `/blog/[slug]/[...somethingElse]`, { slug: 'hello-world', somethingElse: 'something/else' } ); // `/blog/hello-world/something/else`
resolveRoute
('/blog/[slug]', { slug: anyslug });

svelte-migrate 会替您完成方法替换,但如果您随后在结果前加了 base,需要自行移除。

改进的错误处理

在 SvelteKit 1 中,错误处理不一致。一些错误会触发 handleError 钩子,但无法很好地区分状态(例如,判别 404 和 500 的唯一方法是查看 event.route.id 是否为 null);而另一些错误(比如没有 actions 的页面对 POST 请求的 405 错误)却完全不会触发 handleError,尽管它们应该触发。在后者的情况下,生成的 $page.error 会偏离 App.Error 类型(如果已指定)。

SvelteKit 2 通过为 handleError 钩子添加两个新属性 statusmessage 来清理了这一问题。对于从您的代码(或调用您的代码的库代码)抛出的错误,状态将为 500,消息将为 Internal Error。虽然 error.message 可能包含不应向用户暴露的敏感信息,但 message 是安全的。

动态环境变量不能在预渲染期间使用

$env/dynamic/public$env/dynamic/private 模块提供了访问运行时环境变量的功能,而与 $env/static/public$env/static/private 暴露的构建时环境变量不同。

在 SvelteKit 1 中,它们是相同的。因此,使用了“动态”环境变量的预渲染页面实际上“内嵌”了构建时的值,这是错误的。更糟的是,如果用户在导航到动态渲染页面之前恰好访问了预渲染页面,$env/dynamic/public 在浏览器中会填充这些过时的值。

因此,SvelteKit 2 中动态环境变量在预渲染期间不再可读——您应该使用 static 模块。如果用户访问了预渲染页面,SvelteKit 将向服务端请求 $env/dynamic/public 的最新值(默认从名为 _env.js 的模块中获取——可以通过 config.kit.env.publicModule 配置),而不是从服务端渲染的 HTML 中读取它们。

use:enhance 回调中的 form 和 data 已被移除

如果您为 use:enhance 提供一个回调,它会被调用时带有包含各种有用属性的对象。

在 SvelteKit 1 中,这些属性包括 formdata。这些属性很早之前就被弃用,改用了 formElementformData,现在它们在 SvelteKit 2 中已完全移除。

包含文件输入的表单必须使用 multipart/form-data

如果一个表单包含 <input type="file">,但未设置 enctype="multipart/form-data" 属性,非 JS 提交将会忽略文件。在进行 use:enhance 提交时,如果遇到这样的表单,SvelteKit 2 会抛出错误,从而确保在无 JavaScript 的情况下您的表单能正常工作。

生成的 tsconfig.json 更加严格

之前,生成的 tsconfig.json 会尽力在您的 tsconfig.json 中包含 pathsbaseUrl 时生成一个看上去有效的配置。在 SvelteKit 2 中,验证更加严格,如果您在 tsconfig.json 中使用了 pathsbaseUrl,会发出警告。这些设置被用来生成路径别名,您应该在 svelte.config.js 中使用 别名配置 来创建对应的别名,以同时为打包器创建别名。

getRequest 不再抛出错误

@sveltejs/kit/node 模块在 Node 环境中提供了一些辅助函数,其中包括 getRequest,它可以将 Node 的 ClientRequest 转换为标准 Request 对象。

在 SvelteKit 1 中,如果 Content-Length 头超过指定的大小限制,getRequest 可能会抛出错误。在 SvelteKit 2 中,这种错误在请求体(如果存在)被读取时才会抛出。这样可以实现更好的诊断和更简单的代码。

vitePreprocess 不再从 @sveltejs/kit/vite 导出

由于 @sveltejs/vite-plugin-svelte 现在是一个 peer 依赖,SvelteKit 2 不再重新导出 vitePreprocess。您应该直接从 @sveltejs/vite-plugin-svelte 导入它。

更新依赖要求

SvelteKit 2 要求 Node 18.13 或更高版本,以及以下最低依赖版本:

  • svelte@4
  • vite@5
  • typescript@5
  • @sveltejs/vite-plugin-svelte@3(现在作为 SvelteKit 的 peerDependency,之前是直接依赖)
  • @sveltejs/adapter-cloudflare@3(如果您在使用这些适配器)
  • @sveltejs/adapter-cloudflare-workers@2
  • @sveltejs/adapter-netlify@3
  • @sveltejs/adapter-node@2
  • @sveltejs/adapter-static@3
  • @sveltejs/adapter-vercel@4

svelte-migrate 会帮您更新 package.json

作为 TypeScript 升级的一部分,生成的 tsconfig.json(您的 tsconfig.json 会扩展它)现在使用 "moduleResolution": "bundler"(这是 TypeScript 团队推荐的方案,因为它能正确解析包中带有 exports 映射的类型)和 verbatimModuleSyntax(用以替代现有的 importsNotUsedAsValuespreserveValueImports 标志——如果您的 tsconfig.json 中有这些字段,请移除它们,svelte-migrate 会为您完成这些操作)。

SvelteKit 2.12: $app/stores 被弃用

SvelteKit 2.12 引入了基于 Svelte 5 Runes API$app/state$app/state 提供了 $app/stores 的所有功能,但在使用位置和方式上更加灵活。最重要的是,page 对象现在是细粒度的,例如 page.state 的更新不会使 page.data 无效,反之亦然。

因此,$app/stores 已被弃用,并将在 SvelteKit 3 中移除。我们建议您 升级到 Svelte 5,如果尚未完成,并迁移所有 $app/stores 的用法。大多数替换应该相当简单:将 $app/stores 导入替换为 $app/state,并移除各个使用点的 $ 前缀。

<script>
  import { page } from '$app/stores';
import { page } from '$app/state';
</script>

{$page.data}  
{page.data}

使用 npx sv migrate app-state 自动迁移 .svelte 组件中绝大多数 $app/stores 的用法。

在 GitHub 编辑此页面

上一页 下一页