架构

本页内容

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 会:

  1. _emdash_collections_emdash_fields 中插入记录
  2. 运行 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:beforeSavecontent:afterSavecontent:beforeDeletecontent:afterDelete
  • 媒体钩子media:beforeUploadmedia:afterUpload
  • 隔离存储 — 每个插件都有命名空间的 KV 访问
  • 后台 UI 扩展 — 仪表板小部件、设置页面、自定义字段编辑器

插件可以以两种模式运行:

  1. 原生 — 完全访问主机环境(用于第一方插件)
  2. 沙盒 — 在 V8 isolates 中运行,具有基于能力的权限(用于 Cloudflare 上的第三方插件)
// astro.config.mjs
import { seoPlugin } from "@emdash-cms/plugin-seo";

emdash({
	plugins: [seoPlugin({ maxTitleLength: 60 })],
});

请求流程

典型的内容请求遵循此路径:

  1. Astro 接收请求 — 你的页面组件运行
  2. 查询内容getEmDashCollection() 调用 Astro 的 getLiveCollection()
  3. 加载器执行emdashLoader 通过 Kysely 查询相应的 ec_*
  4. 返回数据 — 条目映射到 Astro 的条目格式,包含 idslugdata
  5. 页面渲染 — 你的组件接收内容并渲染 HTML

对于后台请求:

  1. 中间件认证 — 验证会话令牌
  2. API 路由处理请求 — 通过仓储进行 CRUD 操作
  3. 触发钩子beforeCreateafterUpdate
  4. 数据库更新 — Kysely 执行 SQL
  5. 返回响应 — 向后台 SPA 返回 JSON 响应

虚拟模块

EmDash 在构建时生成虚拟模块以配置运行时:

模块用途
virtual:emdash/config数据库和存储配置
virtual:emdash/dialect数据库方言工厂
virtual:emdash/plugin-admins插件后台 UI 的静态导入

这种方法确保打包工具可以正确解析和 tree-shake 插件代码。

下一步