前两天写一个页面,同一个路由下我同时放了 +page.server.js 和 +page.js。服务端那边明明 return 了 user 和 serverTime,结果页面上死活拿不到,data 里只剩 +page.js 自己新加的那几个字段。
一开始我以为是服务端 load 没执行,打了半天日志,发现它跑得好好的。后来翻文档才反应过来:+page.js 的 load 一旦存在,它的返回值就会把 +page.server.js 的数据整个替换掉,不会自动合并。
这篇就把我踩过的这个坑和捋清楚的数据流记一下,免得下次再犯。顺便,代码都按 Svelte 5 的写法来,export let data 那套已经过时了。
我一开始的三个误解
第一个误解:以为 +page.server.js 和 +page.js 会像中间件一样层层叠加,数据自动往下传。实际上不是,后者的 return 就是最终结果。
第二个误解:以为 +page.js 只在浏览器跑。其实 SSR 首屏时它也在服务端跑一遍,水合时浏览器再跑一遍,只是浏览器那次会复用内联到 HTML 里的数据,不会重复请求。
第三个误解:以为 +page.server.js 返回什么,页面就能直接用什么。只有在没有 +page.js 的情况下才成立。
三个文件各自在干什么
+page.server.js 只在服务端跑,所以能碰数据库、私有环境变量、cookies、locals 这些东西。它的返回值必须能序列化成 JSON,因为要内联进 HTML 传给浏览器。反过来说,密钥这类东西千万别从这里返回,会直接暴露在客户端。
+page.js 是通用层,服务端和浏览器都会执行。它不限制返回值的类型,函数、类实例、动态 import 的组件都能从这里出去。但它的 load 拿到的是 { data },也就是 +page.server.js 的返回值,而且不会帮你合并。
+page.svelte 就是消费 data 的地方。Svelte 5 里不再用 export let data,而是:
svelte
<script>
let { data } = $props();
</script>
拿到的永远是 +page.js 的 return 值;如果没有 +page.js,才直接是服务端那份。SvelteKit 生成的 $types 模块还能让 $props() 带上完整类型。
数据流长这样
如果图挂了,按这个顺序理解也行:
kotlin
请求 → +page.server.js load(仅服务端)
→ +page.js load(拿到 data,记得 ...data)
→ +page.svelte(let { data } = $props())
→ HTML → 浏览器 hydration → +page.js 再跑一次 → 水合
代码大概是这样(Svelte 5 写法)
服务端那份没太大变化,load 还是普通的导出函数,类型上从 PageLoad 变成 PageServerLoad 就行:
javascript
// +page.server.js
export async function load({ cookies }) {
const user = await db.getUser(cookies.get('session'));
return {
user,
serverTime: new Date()
};
}
+page.js 里的 load 签名也没变,{ data } 照样拿服务端返回值,关键还是 ...data:
javascript
// +page.js
export async function load({ data, fetch }) {
const posts = await fetch('/api/posts').then(r => r.json());
// 少写这一行,上面的 user 和 serverTime 就没了
return {
...data,
posts,
greeting: `你好,${data.user.name}`
};
}
页面组件的变化最明显,export let data 换成 $props():
svelte
<!-- +page.svelte -->
<script>
let { data } = $props();
</script>
<h1>{data.greeting}</h1>
<p>服务器时间:{data.serverTime}</p>
SvelteKit 文档里也写得很清楚,+page.svelte 现在就是这个写法,旧的 export let 属于 Svelte 4 的遗留模式。
捋完之后的几点体会
最核心的一条没变:两个文件共存时,+page.js 的返回值就是最终结果,想要服务端数据就得自己 ...data 展开。 这个坑隐蔽在于不报错,只是数据悄悄没了。
另外几点顺手记一下。没有 +page.js 的时候不用管这些,服务端数据会直接透传。+page.server.js 总是先执行,结果作为 data 参数交给 +page.js。只要涉及敏感信息就老老实实放 +page.server.js,+page.js 是在浏览器里跑的,fetch 和返回值对用户都是可见的。函数、组件构造函数这类不能序列化的东西,只能从 +page.js 返回。
Svelte 5 里还有一个值得留意的点:+page.svelte 里的 data prop 在迁移后默认不再是深层响应式的。如果我在页面上直接改 data 里的某个字段做乐观更新,Svelte 4 的 export let 时代能触发视图更新,Svelte 5 的 $props() 就不一定了,社区里也有人踩过这个坑。我目前的习惯是:load 返回的数据只读消费,需要可变的本地状态就用 $state 单独声明,两边不混着用,省得跟响应式较劲。
现在再写这种双文件的页面,我会先把 ...data 敲上,再考虑加工逻辑,页面里也顺手把 export let 改成 $props(),省得又对着空白字段排查半天。