跳转到内容

03 · 数据模型与 Astro DB

书签数据是 三层嵌套:分区 → 卡片组 → 链接。Astro DB 用三张表扁平存储,应用层再组装成树。

// src/lib/bookmarks/types.ts(简化)
interface BookmarkData {
title: string
url: string
description?: string
badgeText?: string
badgeVariant?: string
extraLinks?: { title: string; url: string }[]
sortOrder: number
}
interface BookmarkCardData {
title: string
sortOrder: number
bookmarks: BookmarkData[]
}
interface BookmarkSectionData {
title: string
sortOrder: number
stagger: boolean // 公开页卡片是否交错布局
cards: BookmarkCardData[]
}

stagger 是展示层字段,不影响 DB 关系,但会随 seed 写入 BookmarkSection 表。

db/config.ts
const BookmarkSection = defineTable({
columns: {
id: column.number({ primaryKey: true }),
title: column.text(),
sortOrder: column.number(),
stagger: column.boolean({ default: true }),
},
});
const BookmarkCard = defineTable({
columns: {
id: column.number({ primaryKey: true }),
sectionId: column.number(),
title: column.text(),
sortOrder: column.number(),
},
});
const Bookmark = defineTable({
columns: {
id: column.number({ primaryKey: true }),
cardId: column.number(),
title: column.text(),
url: column.text(),
description: column.text({ optional: true }),
badgeText: column.text({ optional: true }),
badgeVariant: column.text({ optional: true }),
extraLinks: column.text({ optional: true }), // JSON 字符串
sortOrder: column.number(),
},
});

extraLinks 存 JSON 字符串——Astro DB 无原生 JSON 列,查询时再 JSON.parse

真正维护的数据在 db/data/bookmarks.ts

export const bookmarkSections: BookmarkSectionData[] = [
{
title: "常用",
sortOrder: 0,
stagger: true,
cards: [
{
title: "文档",
sortOrder: 0,
bookmarks: [
{ title: "Astro", url: "https://astro.build", sortOrder: 0 },
],
},
],
},
];

为什么用 TS 而不是 JSON?

  • 可带类型与注释
  • 管理端保存时整文件重写,格式稳定
  • 直接进 Git diff,review 友好
db/seed.ts
import { bookmarkSections } from "./data/bookmarks";
export default async function seed() {
let sectionId = 1, cardId = 1, bookmarkId = 1;
for (const section of bookmarkSections) {
await db.insert(BookmarkSection).values({ id: sectionId, /* … */ });
for (const card of section.cards) {
await db.insert(BookmarkCard).values({ id: cardId, sectionId, /* … */ });
if (card.bookmarks.length > 0) {
await db.insert(Bookmark).values(
card.bookmarks.map((b) => ({ id: bookmarkId++, cardId, /* … */ })),
);
}
cardId++;
}
sectionId++;
}
}

dev / build 时 Astro DB 自动执行 seed。改 bookmarks.ts 后重启 dev 或触发 HMR 即可刷新内存库。

getBookmarkSections() 分三次查询,按 sortOrder 排序,再用 Map 归并:

src/lib/bookmarks/queries.ts
export async function getBookmarkSections(): Promise<BookmarkSectionData[]> {
const sections = await db.select().from(BookmarkSection).orderBy(asc(BookmarkSection.sortOrder));
const cards = await db.select().from(BookmarkCard).orderBy(asc(BookmarkCard.sortOrder));
const bookmarks = await db.select().from(Bookmark).orderBy(asc(Bookmark.sortOrder));
// cardsBySection、bookmarksByCard → 组装树
return sections.map(/* … */);
}

书签量级在数百条时,三次查询 + 内存 join 足够;无需 SQL JOIN API。

保存时 serializeBookmarkSections() 会:

  1. 按数组下标 重算 sortOrder(拖拽排序后保证连续)
  2. 去掉空 optional 字段
  3. 输出完整 TS 文件(含 interface 与 export const bookmarkSections

写文件前会备份为 bookmarks.ts.bak

  1. 打开 db/data/bookmarks.ts,在任意 card 的 bookmarks 数组追加一项

  2. 重启 vpr dev(或等待 DB 重新 seed)

  3. 访问 /bookmarks/,确认新链接出现

  4. 可选:用 getBookmarkSections 在临时 Astro 页面打印调试

仓库提供 scripts/migrate-bookmarks.mjs,可从历史 MDX 书签页批量导入到 bookmarks.ts。适合从纯文档方案迁移到结构化数据。

  • 三层模型 对应三张 DB 表,TS 文件是唯一可提交数据源
  • seed 在构建管线中灌数据;queries 负责树形组装
  • sortOrder 由应用层维护,保存时归一化