从 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 依赖上, 一次重新部署时 出现了依赖兼容问题:

Bash
@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 日将不再支持构建部署:

Bash
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 也升级到较新版本。
JSON
"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 单独配置。
JavaScript
    adapter: vercel({
-     edge: false,
+     runtime: 'edge',

    prerender: {
-     default: true,
    },
-    vite:{}

单独移出的 vite.config.ts 很简单:

TypeScript
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

JSON
"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

JSON
"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

HTML, XML
<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

HTML, XML
<script>
  export let data;
</script>

<script>
  const { posts } = data;
</script>

load 由 context="module" 变为 PageServerLoad,放在 +page.server.js 中处理。 当前版本直接读取 Markdown 数据,不再让服务器页面请求自己站内的 API:

JavaScript
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';:

JavaScript
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:

JavaScript
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 参数,真正的边界是预渲染发生在 构建阶段,构建器无法为任意查询参数自动生成静态页面。这里的排序又不是用户可见的页面状态, 所以我删掉了这层查询参数,让构建结果保持确定性:

更新前的代码:

JavaScript
export async function GET({ url}) {
  const sorted = url.searchParams.get('sorted');
  const sortedDraws = await getAllDraws({ sorted });
  return new Response(JSON.stringify(sortedDraws));
}

更新后的代码:

JavaScript
export async function GET() {
  const sortedDraws = await getAllDraws({ sorted: true });
  return new Response(JSON.stringify(sortedDraws));
}

此外,我还增加了性能监控: src/routes/+layout.svelte

JavaScript
+ 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.