视图不存逻辑:把数据装配全部收进 src/server

页面里只剩路由导出与组件引入,取数、拼路径、算日期全部移到 src/server。说说这么分的收益,以及它逼出来的一个类型约束。

这个站有四十多个页面,但任何一页的 frontmatter 都只有三五行。不是页面简单,是页面被规定不许聪明。

一条规矩

页面与组件的 frontmatter 里不取数据、不拼路径、不做数据加工。所有装配在 src/server/*,每个页面导出一个返回完整视图模型的函数,getStaticPaths 由 src/server/paths.ts 统一生成,模型放进 props。

标准页面长这样:

---
import '@/styles/pages/_blog.scss';
import { blogListPaths } from '@/server/paths';

export const getStaticPaths = blogListPaths;

const { page } = Astro.props;

const { default: BaseLayout } = await import('@/layouts/BaseLayout.astro');
const { default: PostList } = await import('@/components/PostList.astro');
---
<BaseLayout locale={page.locale} meta={page.meta}>
  <PostList cards={page.cards} empty={page.empty} />
</BaseLayout>

没有取数、没有 map、没有 new Date()、没有模板字符串拼路径。它退化成一份声明:这页用哪个布局、哪个组件。

为什么不许在组件里取数

理由不是洁癖,是三条具体的损失。

第一,同一个派生值会被算两次、三次。日期文案、阅读时长、卡片链接、canonical 路径,任何一处留在视图里,就会出现「列表页这么写、文章页那么写」的分叉。

第二,取数与模板混在一起之后,没有地方能单独回答「这一页为什么是这些数据」。构建期的数据问题只能靠渲染结果反推。

第三,多语言把这个问题放大。文案函数一旦散进模板,翻译键会随页面数量线性增长地重复。

视图模型长什么样

以博客列表为例,BlogListPage 一次性给出 title subtitle empty cards pagination meta。分页对象里连「上一页 / 下一页是不是 null」都算好了,视图只判断真假,不做算术。

判断「这一页有没有分页」是逻辑,不是展示,所以它必须在 server 层回答。

硬约束:类型必须一路带过来

getStaticPaths 返回的 PagePath<T> 必须保留泛型参数:

export interface PagePath<T> {
  params: Record<string, string>;
  props: { page: T };
}

一旦这里退化成 Record<string, unknown>,页面里的 Astro.props 就全是 unknown,page.cards 立刻报错。类型检查会把「图省事」这条路堵死,这是有意保留的。

代价

新增一页要动三个文件:server/x.ts 出模型、paths.ts 出路由、pages/x.astro 消费。页面不能「顺手」再改一下数据,要改就回 server 改。

如果一页只是静态文案,这套分法是纯开销。但只要有列表、分页或三语其中之一,视图层的自由度就会立刻变成不一致的来源。

← 返回文章列表

评论

…