视图不存逻辑:把数据装配全部收进 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 改。
如果一页只是静态文案,这套分法是纯开销。但只要有列表、分页或三语其中之一,视图层的自由度就会立刻变成不一致的来源。

评论
…