用Sapper构建高性能SSR应用的实战经验与避坑指南

闪闪 框架 阅读 813
赞 31 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

我用 Sapper 做过三个上线项目,最长的维护了两年多。不是那种 demo 级别,是真实用户每天在用、SEO 要跑、SSR 渲染不能崩、API 会超时、图片加载会失败的项目。Sapper 已经停止维护了(SvelteKit 是官方接班人),但很多老项目还在跑,而且——说实话,它真挺稳。前提是,你得避开一些它默认不提醒、文档里也不说清楚的坑。

用Sapper构建高性能SSR应用的实战经验与避坑指南

先说最核心的一条:所有数据获取逻辑,必须收口到 preload 函数里,哪怕只是想读个 query 参数。我最早图省事,在 onMount 里发请求,结果首页 SEO 全挂,Google 抓到的全是 loading 状态。折腾半天发现:Sapper 的 SSR 是靠 preload 返回的 Promise resolve 后才生成 HTML 的,onMount 是纯客户端生命周期,服务端根本不会执行。

正确写法:

<script context="module">
  export async function preload({ params, query }) {
    const id = params.id;
    const res = await this.fetch(/api/posts/${id});
    const post = await res.json();

    // 这个对象会直接注入到组件 props 中
    return { post };
  }
</script>

<script>
  export let post; // 注意:这里必须声明,且名字要和 preload 返回的 key 一致
</script>

<h1>{post.title}</h1>
<p>{post.content}</p>

错误写法(别再这么干了):

<script>
  import { onMount } from 'svelte';

  let post = null;

  onMount(async () => {
    const res = await fetch(/api/posts/1);
    post = await res.json(); // ❌ 服务端不执行,SSR 失效,首屏白屏或 loading
  });
</script>

这几种错误写法,别再踩坑了

1. 在 preload 里直接调用 fetch,却不处理跨域或 base URL

本地开发没问题,一上生产就 404。因为 fetch('/api/xxx') 在服务端执行时,走的是 Node.js 的 HTTP 请求,不是浏览器环境,所以它不会自动拼 http://localhost:3000,也不会走 webpack dev server 的 proxy。我第一次部署到 Vercel 上,所有 preload 请求都 404,查了三小时才发现:服务端 fetch 默认发到 localhost:3000,而我的 API 部署在 https://jztheme.com/api

解决方案:统一配置 API 地址,服务端和客户端共用。

// src/node_modules/@app/config.js
export const API_BASE = process.browser
  ? 'https://jztheme.com/api'
  : 'https://jztheme.com/api';
// 在 preload 里
import { API_BASE } from '$lib/config.js';

export async function preload({ params }) {
  const res = await this.fetch(${API_BASE}/posts/${params.id}); // ✅
  return { post: await res.json() };
}

2. 把异步逻辑塞进 <script> 标签顶层,以为能等 preload

Sapper 不会帮你等。下面这段代码会报错:Cannot destructure property 'post' of 'undefined'

<script context="module">
  export async function preload({ params }) {
    return { post: { id: 1, title: 'Hello' } };
  }
</script>

<script>
  export let post;
  const { id, title } = post; // ❌ 此时 post 还没注入,解构就崩了
</script>

正确做法:要么加空值判断,要么用 $$props 检查,但我更推荐用 if 块在模板里兜底:

{#if post}
  <h1>{post.title}</h1>
{:else}
  <div class="loading">Loading...</div>
{/if}

3. 在 preload 里 throw 错误,却不定义 error.svelte

Sapper 会静默失败,页面空白,控制台连 error 都不打。必须在 src/routes/_error.svelte 里写个兜底页,否则用户点错一个链接就看到白屏。我有个项目上线三天,客服反馈“文章页打不开”,最后发现是某条数据里字段名从 author_name 改成了 authorNamepreload 里直接 post.author_name.toUpperCase() 崩了,没 error 页面,就卡死。

实际项目中的坑

静态资源路径别信 public/ 直接访问

比如你放了个 public/images/logo.svg,在组件里写 <img src="/images/logo.svg">,本地开发 OK,但部署到子路径(如 /blog/)下就 404。Sapper 的 base 配置只影响路由,不影响静态资源引用。我的解法是:全部走 $lib/assets/,配合 Rollup 别名:

// rollup.config.js
alias({
  entries: [
    { find: '$lib/assets', replacement: path.resolve('src/lib/assets') }
  ]
})
<!-- 这样写,打包时会被正确解析 -->
<img src="$lib/assets/logo.svg" alt="logo" />

动态导入(code-splitting)别乱用 import()await

我在某个详情页里试过:const Component = await import('./Dynamic.svelte'),结果 build 报错,提示 “dynamic imports not supported in SSR”。Sapper 的服务端渲染不支持顶层 await import。真要动态加载,得包一层 onMount,或者用 component={Dynamic} + let Dynamic = null + onMount(() => import(...)) 的方式。

最后一个小细节:CSS 作用域问题

Sapper 默认开 css: true,但如果你在 preload 里用了 this.fetch,又在组件里写了 :global(.foo),某些版本会把 global 样式漏掉。我的 fix 是:统一关掉组件级 CSS,改用 __layout.svelte 引全局 CSS,组件内只用内联 style 或 CSS-in-JS 库(比如 svelte-use-css)。

结语

以上是我用 Sapper 做真实项目两年多踩出来、修过来、压箱底的最佳实践。没有银弹,有些方案不是最优,但足够稳定;有些坑修了还是有边角 case,但不影响主流程。Sapper 已停更,但只要你的项目还跑着,这些经验就还有用。

如果你有更好的 SSR 数据流组织方式、更优雅的错误边界处理、或者怎么让 preload 和 TypeScript 类型推导配合得更好,欢迎评论区交流。这个系列我还会写 SvelteKit 迁移实战,下篇见。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论