SvelteKit 踩坑记:+page.js 里忘了 ...data,服务端数据全没了

前两天写一个页面,同一个路由下我同时放了 +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() 带上完整类型。

数据流长这样

flowchart TD A[浏览器发起请求] --> B[+page.server.js load] B -->|仅服务端执行<br/>返回可序列化数据| C[SSR 阶段:<br/>数据序列化内联到 HTML] C --> D[+page.js load<br/>参数 data = server 返回值] D -->|必须手动展开 ...data<br/>否则数据丢失| E[+page.js return 新对象] E --> F[+page.svelte<br/>let { data } = $props()] F --> G[HTML 返回浏览器] G --> H[浏览器 hydration] H --> I[+page.js load 再次执行] I -->|读取内联数据<br/>避免重复请求| J[+page.svelte 水合] style B fill:#ffe0b2 style D fill:#c8e6c9 style I fill:#c8e6c9 style F fill:#bbdefb style J fill:#bbdefb

如果图挂了,按这个顺序理解也行:

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(),省得又对着空白字段排查半天。

相关推荐
Csvn12 分钟前
Nginx 配置语法与上下文:一次搞懂继承、变量与用 map 替掉 if
后端·nginx
律宏阔16 分钟前
微信小程序 + Taro + 多语言实现与国际化检查实录
前端·微信小程序·taro
Arone聊架构16 分钟前
把 WeKnora v0.9 + 多站点客服工单塞进 1.9G 小鸡:248MB 内存压测、纯 Go 解析桥与 SQLite 写锁踩坑实录
后端
泡海椒23 分钟前
jquick-pdf 饼图(JPie)实战:Java 生成贷款五级分类资产质量报告
后端
学长毕设1 小时前
基于SpringBoot的景点管理系统-附源码
java·spring boot·后端
deli0072 小时前
Redis 缓存穿透、击穿、雪崩:9 组流量实测数据库怎么被打穿
前端
江华森2 小时前
5Mbps 小水管也能扛:K3s 上 k6 压测 + HPA 弹性伸缩实测
前端
shuangyueliao2 小时前
103 个小游戏的商店素材,我一张图都没手动做:截图 / 封面 / 打包 / 提审全自动
前端
ZhengEnCi2 小时前
前端项目为什么需要一份统一的 theme.css:用 CSS 变量管住全局样式与主题切换
前端·css·前端工程化