架構(內部實作)

本頁內容

本頁面面向參與 EmDash 開發的人員,而非使用 EmDash 建置網站的使用者。它記錄了內部機制——表格佈局、Astro 整合、請求路徑、程式碼產生。使用 EmDash 不需要了解這些內容。如果你正在建置網站,請改為閱讀架構內容模型

Astro 整合

EmDash 作為 emdash 套件的 Astro 整合執行。在建置時:

  • 使用 Astro 的 injectRoute API 注入管理 SPA 和 REST 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/*分類、標籤、自訂分類法
  • 產生虛擬模組,使打包器可以解析和搖樹設定程式碼和外掛程式碼:

    模組用途
    virtual:emdash/config資料庫和儲存設定
    virtual:emdash/dialect資料庫方言工廠
    virtual:emdash/plugin-admins外掛管理 UI 的靜態匯入
  • 提供 Live Collections 載入器,管理遷移,並開啟儲存連接。

資料庫優先 Schema

Schema 定義存在於資料庫中,而非程式碼中。兩個系統表追蹤結構。

_emdash_collections 每個集合保存一列:

CREATE TABLE _emdash_collections (
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE NOT NULL,        -- "posts", "products"
  label TEXT NOT NULL,              -- "Blog Posts"
  label_singular TEXT,              -- "Post"
  description TEXT,
  icon TEXT,
  supports JSON,                    -- ["drafts", "revisions", "preview"]
  source TEXT,                      -- 建立方式
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT
);

source 欄位記錄來源:manual(管理 UI)、template:<name>(種子檔案)、import:wordpress(匯入器)或 discovered(從現有表格自動偵測)。

_emdash_fields 每個欄位保存一列,連結到其集合:

CREATE TABLE _emdash_fields (
  id TEXT PRIMARY KEY,
  collection_id TEXT REFERENCES _emdash_collections(id),
  slug TEXT NOT NULL,               -- 欄位名稱
  label TEXT NOT NULL,
  type TEXT NOT NULL,               -- 欄位類型
  column_type TEXT NOT NULL,        -- TEXT, REAL, INTEGER, JSON
  required INTEGER DEFAULT 0,
  unique_field INTEGER DEFAULT 0,
  default_value TEXT,
  validation JSON,
  widget TEXT,
  options JSON,
  sort_order INTEGER,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(collection_id, slug)
);

每個集合的內容表格

每個集合獲得自己的表格,前綴為 ec_。具有 titleprice 欄位的 products 集合產生:

CREATE TABLE ec_products (
  -- 系統欄位,始終存在
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE,
  status TEXT DEFAULT 'draft',
  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,
  price REAL
);

真實欄位(而非一張帶有 JSON blob 的表格)提供了正確的索引、可用的外來鍵、資料庫工具可以檢查的 schema,以及無需逐欄位 JSON 解析。

關注點保持分離:

關注點位置表格
Schema系統表格_emdash_collections, _emdash_fields
內容每個集合的表格ec_posts, ec_products, …
媒體獨立表格 + 儲存media 表格 + R2/S3
設定選項表格site: 前綴的 options

執行時期 Schema 變更

透過管理 UI 新增欄位會執行三個步驟:

  1. _emdash_fields 中插入記錄。
  2. 執行 ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>
  3. 重新產生用於驗證的 Zod schema。

SQLite 在執行時期支援新增、重新命名和刪除欄位(刪除需要 SQLite 3.35+)。不支援直接更改欄位的類型,因此 EmDash 透明地重建表格:建立新表格、複製列、刪除舊表格、重新命名新表格。

執行時期驗證

EmDash 在啟動時從欄位定義建構 Zod schema,並對每次建立和更新進行驗證:

function buildSchema(fields: Field[]): ZodSchema {
	const shape: Record<string, ZodType> = {};
	for (const field of fields) {
		let zodType = fieldTypeToZod(field.type);
		if (field.required) zodType = zodType.required();
		if (field.validation?.min !== undefined) zodType = zodType.min(field.validation.min);
		shape[field.slug] = zodType;
	}
	return z.object(shape);
}

資料層

EmDash 使用 Kysely 在所有支援的資料庫(SQLite、libSQL、Cloudflare D1 和 PostgreSQL)上提供型別安全的 SQL。方言由 virtual:emdash/dialect 從網站傳遞給整合的設定中選擇。

Live Collections 載入器

內容在執行時期透過 Astro 的 Live Collections 提供。emdashLoader() 實作 Astro 的 LiveLoader 介面,並註冊為單一 _emdash 集合:

import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";

export const collections = {
	_emdash: defineLiveCollection({ loader: emdashLoader() }),
};

單一 _emdash 集合包裝每種內容類型;當呼叫 getEmDashCollection("posts") 時,載入器按類型篩選。

請求路徑

來自頁面的內容請求:

  1. Astro 接收請求並執行頁面元件。
  2. getEmDashCollection() 呼叫 Astro 的 getLiveCollection()
  3. emdashLoader 透過 Kysely 查詢相關的 ec_* 表格。
  4. 列被對應為 Astro 的條目格式(idslugdata)。
  5. 元件渲染。

管理請求:

  1. 中介軟體驗證工作階段權杖。
  2. API 路由透過倉儲執行 CRUD。
  3. 生命週期鉤子觸發(例如 content:beforeSave)。
  4. Kysely 執行 SQL。
  5. 路由向管理 SPA 傳回 JSON。

管理面板內部

管理面板是一個 React 島嶼。Astro 提供外殼並在中介軟體中強制執行身分驗證;內部一切都是用戶端的,基於 TanStack Router、TanStack Query、TanStack Table、React Hook Form + Zod、TipTap 和 Kumo(Cloudflare 的 Base UI + Tailwind 設計系統)建構。

外殼路由在中介軟體中控制存取:

export async function onRequest({ request, locals }, next) {
	const session = await getSession(request);
	if (request.url.includes("/_emdash/admin")) {
		if (!session?.user) return redirect("/_emdash/admin/login");
		locals.user = session.user;
	}
	return next();
}

清單驅動的 UI

管理面板不會硬式編碼任何關於集合或外掛的內容。它取得 GET /_emdash/api/manifest,傳回請求使用者可以存取的集合、外掛和分類法,按角色篩選:

{
	"collections": [
		{
			"slug": "posts",
			"label": "Blog Posts",
			"icon": "file-text",
			"supports": ["drafts", "revisions", "preview"],
			"fields": [{ "slug": "title", "type": "string", "required": true }]
		}
	],
	"plugins": [{ "id": "audit-log", "label": "Audit Log" }],
	"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
	"version": "abc123"
}

導覽、表單和欄位編輯器從這個清單產生,因此 schema 和外掛變更無需管理面板重建即可顯示,Zod schema 保留在伺服器端。

外掛管理 UI

外掛管理進入點被收集到一個產生的靜態匯入虛擬模組中,使打包器可以解析和搖樹它們:

import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";

export const pluginAdmins = { seo: pluginAdmin0 };

富文字轉換

Portable Text 欄位在 TipTap(ProseMirror)中編輯。內容在載入和儲存邊界由 portableTextToProsemirror()prosemirrorToPortableText() 轉換。來自外掛或匯入的未知區塊作為唯讀佔位符保留。

簽署上傳

媒體上傳在配接器支援時使用直接到儲存的簽署 URL,否則使用同源串流端點:

  1. 用戶端請求上傳 URL(POST /api/media/upload-url)。
  2. 用戶端上傳到傳回的目標。相容 S3 的配接器可以傳回繞過 Worker 本體大小限制的簽署 URL;原生 R2 繫結和本地儲存傳回 EmDash 串流端點。
  3. 用戶端確認(POST /api/media/:id/confirm)。
  4. 伺服器擷取中繼資料(尺寸、MIME 類型)。

擴充內容匯入器

WordPress 匯入器建立在可插拔的 ImportSource 介面上。自訂來源實作 probe、analyze 和 fetch:

interface ImportSource {
	probe(input: ImportInput): Promise<ProbeResult>;
	analyze(input: ImportInput): Promise<AnalysisResult>;
	fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}

probe 驗證輸入並報告發現的內容,analyze 將來源文章類型對應到 EmDash 集合並標記 schema 差距,fetchContent 串流傳輸匯入管線透過管理面板使用的相同倉儲寫入的正規化條目。內建來源涵蓋 WordPress WXR、WordPress.com 和 WordPress REST API;註冊自訂來源以從其他系統匯入。