EmDash 与 Astro 深度集成,提供完整的 CMS 体验。本文说明关键架构决策以及各模块如何协作。
高层级概览
┌──────────────────────────────────────────────────────────────────┐
│ 你的 Astro 站点 │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ EmDash 集成 │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ 内容 │ │ 后台 │ │ 插件 │ │ │
│ │ │ API │ │ 面板 │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └───────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 数据层 │ │ │
│ │ │ 数据库 (D1/SQLite) + 存储 (R2/S3) │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Astro 框架 │ │
│ │ Live Collections · 中间件 · 会话 │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
EmDash 以 Astro 集成的形式运行。它为后台面板和 REST API 注入路由,为 Live Collections 提供内容加载器,并管理数据库迁移和存储连接。
数据库优先的 Schema
与传统 CMS 在代码中定义 schema 不同,EmDash 将 schema 定义存储在数据库本身中。两个系统表追踪你的内容结构:
_emdash_collections— 集合元数据(slug、标签、功能)_emdash_fields— 每个集合的字段定义
当你通过后台 UI 创建一个包含 title 和 price 字段的 “products” 集合时,EmDash 会:
- 在
_emdash_collections和_emdash_fields中插入记录 - 运行
ALTER TABLE创建带有相应列的ec_products表
这种设计实现了:
- 运行时 schema 修改 — 无需代码更改或重新构建即可创建和编辑内容类型
- 非开发人员友好的设置 — 内容编辑者可以通过 UI 设计他们的数据模型
- 真实的 SQL 列 — 适当的索引、外键和查询优化
每个集合一张表
每个集合都有自己的 SQLite 表,带有 ec_ 前缀:
-- 添加 "posts" 集合时创建
CREATE TABLE ec_posts (
-- 系统列(始终存在)
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
status TEXT DEFAULT 'draft', -- draft, published, scheduled
author_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
published_at TEXT,
deleted_at TEXT, -- 软删除
version INTEGER DEFAULT 1, -- 乐观锁
-- 内容列(来自你的字段定义)
title TEXT NOT NULL,
content JSON, -- Portable Text
excerpt TEXT
);
为什么使用每个集合一张表而不是单个包含 JSON 的内容表?
- 真实的 SQL 列支持适当的索引和查询
- 外键正常工作
- Schema 在数据库中自我记录
- 字段访问无 JSON 解析开销
- 数据库工具可以直接检查 schema
Live Collections 集成
EmDash 使用 Astro 6 的 Live Collections 在运行时提供内容。内容更改立即可用,无需静态重建。
emdashLoader() 实现了 Astro 的 LiveLoader 接口:
// src/live.config.ts
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
使用提供的包装函数查询内容:
import { getEmDashCollection, getEmDashEntry } from "emdash";
// 获取所有已发布的文章
const { entries: posts } = await getEmDashCollection("posts");
// 获取草稿
const { entries: drafts } = await getEmDashCollection("posts", {
status: "draft",
});
// 通过 slug 获取单个条目
const { entry: post } = await getEmDashEntry("posts", "my-post-slug");
路由注入
EmDash 集成使用 Astro 的 injectRoute API 添加后台和 API 路由:
| 路径模式 | 用途 |
|---|---|
/_emdash/admin/[...path] | 后台面板 SPA |
/_emdash/api/manifest | 后台清单(集合、插件) |
/_emdash/api/content/[collection] | 内容条目的 CRUD |
/_emdash/api/media/* | 媒体库操作 |
/_emdash/api/schema/* | Schema 管理 |
/_emdash/api/settings | 站点设置 |
/_emdash/api/menus/* | 导航菜单 |
/_emdash/api/taxonomies/* | 分类、标签、自定义分类法 |
路由从 emdash 包注入 — 不会复制到你的项目中。
数据层
EmDash 使用 Kysely 跨所有支持的数据库进行类型安全的 SQL 查询:
SQLite
使用 sqlite({ url: "file:./data.db" }) 进行本地开发
D1
Cloudflare 的无服务器 SQL,使用 d1({ binding: "DB" })
libSQL
远程 SQLite,使用 libsql({ url: "...", authToken: "..." })
数据库配置在 astro.config.mjs 中传递给集成:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
import { local } from "emdash/storage";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
存储抽象
媒体文件与数据库分开存储。EmDash 支持:
- 本地文件系统 — 开发和简单部署
- Cloudflare R2 — 边缘的 S3 兼容对象存储
- S3 兼容 — 任何 S3 兼容的对象存储
上传使用签名 URL 进行直接客户端到存储的上传,绕过 Workers 的请求体大小限制。
插件架构
插件通过类似 WordPress 的钩子系统扩展 EmDash:
- 内容钩子 —
content:beforeSave、content:afterSave、content:beforeDelete、content:afterDelete - 媒体钩子 —
media:beforeUpload、media:afterUpload - 隔离存储 — 每个插件都有命名空间的 KV 访问
- 后台 UI 扩展 — 仪表板小部件、设置页面、自定义字段编辑器
插件可以以两种模式运行:
- 原生 — 完全访问主机环境(用于第一方插件)
- 沙盒 — 在 V8 isolates 中运行,具有基于能力的权限(用于 Cloudflare 上的第三方插件)
// astro.config.mjs
import { seoPlugin } from "@emdash-cms/plugin-seo";
emdash({
plugins: [seoPlugin({ maxTitleLength: 60 })],
});
请求流程
典型的内容请求遵循此路径:
- Astro 接收请求 — 你的页面组件运行
- 查询内容 —
getEmDashCollection()调用 Astro 的getLiveCollection() - 加载器执行 —
emdashLoader通过 Kysely 查询相应的ec_*表 - 返回数据 — 条目映射到 Astro 的条目格式,包含
id、slug和data - 页面渲染 — 你的组件接收内容并渲染 HTML
对于后台请求:
- 中间件认证 — 验证会话令牌
- API 路由处理请求 — 通过仓储进行 CRUD 操作
- 触发钩子 —
beforeCreate、afterUpdate等 - 数据库更新 — Kysely 执行 SQL
- 返回响应 — 向后台 SPA 返回 JSON 响应
虚拟模块
EmDash 在构建时生成虚拟模块以配置运行时:
| 模块 | 用途 |
|---|---|
virtual:emdash/config | 数据库和存储配置 |
virtual:emdash/dialect | 数据库方言工厂 |
virtual:emdash/plugin-admins | 插件后台 UI 的静态导入 |
这种方法确保打包工具可以正确解析和 tree-shake 插件代码。