迁移到 SvelteKit v2
从 SvelteKit 版本 1 升级到版本 2 应该基本上是无缝的。这里列出了一些需要注意的重大变更。您可以使用 npx sv migrate sveltekit-2
来自动迁移其中的一些变更。
我们强烈建议在升级到 2.0 之前先升级到最新的 1.x 版本,这样您就可以利用有针对性的弃用警告。我们还建议先升级到 Svelte 4:SvelteKit 1.x 的后期版本支持它,而 SvelteKit 2.0 则需要它。
redirect 和 error 不再需要您来抛出
之前,您必须自己 throw
从 error(...)
和 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.
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.
svelte-migrate
将为您自动完成这些更改。
如果错误或重定向是在 try {...}
块内抛出的(提示:不要这样做!),您可以使用从 @sveltejs/kit
导入的 isHttpError
和 isRedirect
来区分它们和意外错误。
设置 cookie 时需要 path
当接收到没有指定 path
的 Set-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;
}
load({ cookies: any
cookies }) {
cookies: any
cookies.set(const name: void
name, value, { path: string
path: '/' });
return { response: any
response }
}
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>>
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: any
response = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch(const url: string
url).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.
then(r: Response
r => r: Response
r.Body.json(): Promise<any>
json());
return { response: any
response }
}
// 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>>
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: any
a, const b: any
b] = 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.
all([
fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch(const url1: string
url1).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.
then(r: Response
r => r: Response
r.Body.json(): Promise<any>
json()),
fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch(const url2: string
url2).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.
then(r: Response
r => r: Response
r.Body.json(): Promise<any>
json()),
]);
return { a: any
a, b: any
b };
}
goto(...) 修改
goto(...)
不再接受外部 URL。若需跳转到外部 URL,请使用 window.location.href = url
。现在 state
对象决定了 $page.state
,并且必须遵循已声明的 App.PageState
接口。详情参考 浅层路由。
路径默认改为相对路径
在 SvelteKit 1 中,app.html
中的 %sveltekit.assets%
在服务端渲染时默认会被替换为相对路径(例如 .
或 ..
或 ../..
等,取决于渲染路径),除非显式将 paths.relative
配置选项设置为 false
。对于从 $app/paths
导入的 base
和 assets
,也是类似的情况,但前提是 paths.relative
配置选项显式设置为 true
。
在版本 2 中,这种不一致性已经修复。路径要么始终是相对路径,要么始终是绝对路径,具体取决于 paths.relative
的值。默认值为 true
,因为这让应用程序更加便携:如果 base
超出了应用程序的预期(例如当您在 Internet Archive 查看时)或者在构建时未知(例如部署到 IPFS 等情况),破坏的可能性会更低。
服务端 fetch 请求不再可追踪
之前可以通过在服务端上的 fetch
请求追踪请求的 URL 以重新运行加载函数。但这可能会造成安全风险(私有 URL 泄露),因此该功能在之前由 dangerZone.trackServerFetches
设置控制,现在该设置已被移除。
preloadCode 参数必须以 base 为前缀
SvelteKit 提供了两个函数,preloadCode
和 preloadData
,用于以编程方式加载与特定路径相关的代码和数据。在版本 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.
resolveRoute } from '$app/paths';
const path = base + resolvePath('/blog/[slug]', { slug });
const const path: string
path = function resolveRoute(id: string, params: Record<string, string | undefined>): string
Populate a route ID with params to resolve a pathname.
resolveRoute('/blog/[slug]', { slug: any
slug });
svelte-migrate
会替您完成方法替换,但如果您随后在结果前加了 base
,需要自行移除。
改进的错误处理
在 SvelteKit 1 中,错误处理不一致。一些错误会触发 handleError
钩子,但无法很好地区分状态(例如,判别 404 和 500 的唯一方法是查看 event.route.id
是否为 null
);而另一些错误(比如没有 actions 的页面对 POST
请求的 405 错误)却完全不会触发 handleError
,尽管它们应该触发。在后者的情况下,生成的 $page.error
会偏离 App.Error
类型(如果已指定)。
SvelteKit 2 通过为 handleError
钩子添加两个新属性 status
和 message
来清理了这一问题。对于从您的代码(或调用您的代码的库代码)抛出的错误,状态将为 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 中,这些属性包括 form
和 data
。这些属性很早之前就被弃用,改用了 formElement
和 formData
,现在它们在 SvelteKit 2 中已完全移除。
包含文件输入的表单必须使用 multipart/form-data
如果一个表单包含 <input type="file">
,但未设置 enctype="multipart/form-data"
属性,非 JS 提交将会忽略文件。在进行 use:enhance
提交时,如果遇到这样的表单,SvelteKit 2 会抛出错误,从而确保在无 JavaScript 的情况下您的表单能正常工作。
生成的 tsconfig.json 更加严格
之前,生成的 tsconfig.json
会尽力在您的 tsconfig.json
中包含 paths
或 baseUrl
时生成一个看上去有效的配置。在 SvelteKit 2 中,验证更加严格,如果您在 tsconfig.json
中使用了 paths
或 baseUrl
,会发出警告。这些设置被用来生成路径别名,您应该在 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
(用以替代现有的 importsNotUsedAsValues
和 preserveValueImports
标志——如果您的 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
的用法。