从 SvelteKit pre-1.0 迁移到 SvelteKit 2:路由、预渲染与 Vercel 实录
为什么
补充说明: 本文记录的是 SvelteKit pre-1.0 → 2、Svelte 3 → 4 的迁移,不是 Svelte 5 迁移指南。当前网站仍运行在 SvelteKit 2、Svelte 4 和 Vite 5 上,下面保留 当时遇到的错误、修改和部署边界,便于排查同类旧项目。
我在《How I Rebuilt My Personal Blog with SvelteKit and MDsveX》
中记录过 2022 年的重写。后来网站仍停留在 1.0.0-next.350 等 pre-1.0 依赖上,
一次重新部署时
出现了依赖兼容问题:
@sveltejs/vite-plugin-svelte directly depended upon svelte, throw error
SyntaxError: Named export 'createFilter' not found. The requested module 'vite' is a CommonJS module%2C which may not support all module.exports as named exports.此外,目前在 Vercel 上用的 Node.js 版本仍然是 16.x,Vercel 在 2025 年 1 月 31 日将不再支持构建部署:
Error: Node.js version 16.x has reached End-of-Life. Deployments created on or after 2025-01-31 will fail to build. Please set Node.js Version to 20.x in your Project Settings to use Node.js 20所以是时候做一波升级了。
怎么做
这次迁移同时涉及 SvelteKit 和 Svelte。我分别参考了官方的
Migrating to SvelteKit v2 和
Svelte 4 migration guide,再结合项目里的
实际错误逐项修复。SvelteKit 2 的最低依赖组合是 Node.js 18.13、Svelte 4、Vite 5、
TypeScript 5 和 @sveltejs/vite-plugin-svelte 3。
1.依赖升级
svelte升级到 4.x。@sveltejs/kit升级到 2.x,并添加@sveltejs/vite-plugin-svelte取代了内置 Vite 处理逻辑。vite升级到 5.x,当然后续我们需要适配 Vite 5 的变更。- 其他依赖如
typescript、svelte-preprocess、svelte 部署相关的 adapter 也升级到较新版本。
"devDependencies": {
- "@sveltejs/adapter-auto": "1.0.0-next.50",
- "@sveltejs/adapter-static": "1.0.0-next.29",
- "@sveltejs/adapter-vercel": "1.0.0-next.58",
- "@sveltejs/kit": "1.0.0-next.350",
+ "@sveltejs/adapter-auto": "^3.2.4",
+ "@sveltejs/adapter-static": "^3.0.4",
+ "@sveltejs/adapter-vercel": "^4.0.5",
+ "@sveltejs/kit": "^2.5.27",
+ "@sveltejs/vite-plugin-svelte": "^3.1.2",
- "svelte": "^3.44.0",
- "svelte-preprocess": "^4.10.5",
- "vite": "^2.9.1",
+ "svelte": "^4.2.19",
+ "svelte-preprocess": "^5.0.3",
+ "vite": "^5.4.5",
+ "typescript": "^5.x"
}相应的,需要对 svelte.config.js 更新一些配置,主要包括:
- 简化
prerender的配置 具体的 prerender 会在页面中配置。 vercel-adaptor开启runtime: 'edge'。- 移除
vite配置,通过vite.config.ts单独配置。
adapter: vercel({
- edge: false,
+ runtime: 'edge',
prerender: {
- default: true,
},
- vite:{}单独移出的 vite.config.ts 很简单:
import path from 'path';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
resolve: {
alias: {
$draw: path.resolve('./draw'),
$blog: path.resolve('./blog'),
$note: path.resolve('./note'),
$src: path.resolve('./src'),
}
},
// allows vite access to ./posts
server: {
fs: {
allow: ['./']
}
},
});此外,为了适配新的 Svelte 版本,我调整了 package.json 里的 dev 脚本,使其兼容新的 vite 配置。
svelte-kit命令替换为vite,适配新版本。svelte-kit sync已经不再默认需要。
旧版 SvelteKit pre-1.0
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"check": "svelte-check --tsconfig tsconfig.json",
"prepare": "svelte-kit sync"
}新版 SvelteKit 2
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check"
}2.路由调整
由于 SvelteKit 基于文件系统的路由规则上发生了 breaking change(具体可看SvelteKit Routing)我的路由结构也需要调整:
- 采用 嵌套路由,优化页面组件结构。
- 使用新的 PageServerLoad 取代旧版 load 方法。
- 配置 PageConfig 以支持 SSG(静态页面生成)。
组件文件命名变更:
| 旧版 (SvelteKit pre-1.0) | 新版 (SvelteKit 2) |
|---|---|
_layout.svelte | +layout.svelte |
_error.svelte | +error.svelte |
index@page.svelte | +page.svelte |
load 函数变更:
旧版 SvelteKit pre-1.0
<script context="module">
export async function load({ fetch }) {
const res = await fetch('/api/posts');
return { props: { posts: await res.json() } };
}
</script>
<script>
export let posts;
</script>新版 SvelteKit 2
<script>
export let data;
</script>
<script>
const { posts } = data;
</script>load 由 context="module" 变为 PageServerLoad,放在 +page.server.js 中处理。
当前版本直接读取 Markdown 数据,不再让服务器页面请求自己站内的 API:
import { getAllPosts } from '$src/lib/utils/posts';
export async function load() {
return {
posts: await getAllPosts({ sorted: true })
};
}详情页只返回 meta、slug、HTML 和阅读时间等可序列化数据;如果 slug 不存在,则通过
error(404, 'Post not found') 返回真正的 404,而不是把空模块继续传给页面变成 500。
前面提到 sveltekit 支持对每个页面单独配置 prerender,理论上说对于博客这种静态没太多交互的站点,每个页面都应该配置成 prerender,构建和部署时就生成页面,也就是 SSG,而不是需要 server-render 的 SSR。但由于我不排除未来还会增加一些动态内容,在 SSR 的 manifest 中保留页面记录,所以我的配置是 export const prerender = 'auto';:
import { getAllDraws } from '$src/lib/utils/draws';
import { getAllPosts } from '$src/lib/utils/posts';
/** @type {import('./$types').PageServerLoad} */
export async function load() {
const [liteDraws, litePosts] = await Promise.all([
getAllDraws({ sorted: true, limit: 6 }),
getAllPosts({ sorted: true, limit: 6 })
]);
return { liteDraws, litePosts };
}
export const prerender = 'auto';RSS 之类的纯静态路由也需要改造,方式是增加 src/routes/rss.xml/+server.js:
export async function GET() {
const sortedPosts = await getAllPosts({ sorted: true });
const body = xml(sortedPosts);
return new Response(body, {
headers: {
'Cache-Control': 'max-age=0, s-max-age=3600',
'Content-Type': 'application/rss+xml; charset=utf-8'
}
});
}
export const prerender = true;重点在于配置 prerender 让它在构建时生成,同时让 server handler 返回 Response 对象。
3.Vercel 部署问题
在尝试 SSG 时,我最初把问题归因于 Vercel 不支持在 PageServerLoad 中读取
searchParams。这个判断并不准确:服务器端可以读取 URL 参数,真正的边界是预渲染发生在
构建阶段,构建器无法为任意查询参数自动生成静态页面。这里的排序又不是用户可见的页面状态,
所以我删掉了这层查询参数,让构建结果保持确定性:
更新前的代码:
export async function GET({ url}) {
const sorted = url.searchParams.get('sorted');
const sortedDraws = await getAllDraws({ sorted });
return new Response(JSON.stringify(sortedDraws));
}更新后的代码:
export async function GET() {
const sortedDraws = await getAllDraws({ sorted: true });
return new Response(JSON.stringify(sortedDraws));
}此外,我还增加了性能监控: src/routes/+layout.svelte
+ import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit';
+ injectSpeedInsights();总结
本次博客技术栈升级的核心变化:
- 从 SvelteKit pre-1.0 升级到 SvelteKit 2,并将 Svelte、Vite 和 TypeScript 调整到兼容版本。
- 采用新的
PageServerLoad和页面配置方式保留预渲染能力,同时兼容未来 SSR。 - 删除不必要的查询参数排序,让预渲染输出保持确定性。
- 路由结构调整,提高代码可维护性。
- 优化 Vercel 部署流程。
CURRENTLY BUILDING
EveryCityMap
Style, share, and export maps for 535 curated cities. Save a free 2160px PNG, or make 4096px artwork and wallpapers.