本指南將引導你從零開始建構一個原生外掛。原生外掛在與你的 Astro 網站相同的程序中執行,擁有對執行時期的完整存取權限,包括 React 管理頁面、Portable Text 元件和頁面片段。
如果你還沒有決定是要原生外掛還是沙盒外掛,請先閱讀選擇外掛格式。原生是需要 React 管理頁面、Portable Text 呈現元件或頁面片段的外掛的格式。
兩個部分,一個或兩個檔案
與沙盒外掛一樣,原生外掛提供兩個部分:
- 描述符工廠 — 回傳一個帶有
format: "native"和管理相關進入點的PluginDescriptor。在建置時由astro.config.mjs匯入。 createPlugin(options)函式 — 執行時期端。回傳definePlugin({ id, version, capabilities, hooks, routes, admin })的結果。
與沙盒外掛不同,兩個部分可以放在同一個檔案中,因為它們不在不同的環境中執行——整個外掛在同一程序中執行。套件的 "." 匯出指向一個同時匯出描述符工廠和 createPlugin(或 default)函式的檔案:
my-native-plugin/
├── src/
│ ├── index.ts # 描述符工廠 + createPlugin
│ ├── admin.tsx # React 管理元件(選用)
│ └── astro/ # PT 區塊呈現用 Astro 元件(選用)
│ └── index.ts
├── package.json
└── tsconfig.json
設定套件
以下 package.json 宣告了原生外掛需要的進入點和對等相依性:
{
"name": "@my-org/plugin-analytics",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./admin": {
"types": "./dist/admin.d.ts",
"import": "./dist/admin.js"
}
},
"files": ["dist"],
"peerDependencies": {
"emdash": "*",
"react": "^18.0.0"
}
}
將 emdash 和 react 保持為對等相依性,這樣宿主網站提供實際版本,你不會發布重複的套件。
撰寫描述符和執行時期
以下 src/index.ts 在一個檔案中定義了描述符工廠和 createPlugin 執行時期:
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export interface AnalyticsOptions {
enabled?: boolean;
maxEvents?: number;
}
export function analyticsPlugin(options: AnalyticsOptions = {}): PluginDescriptor {
return {
id: "analytics",
version: "0.1.0",
format: "native",
entrypoint: "@my-org/plugin-analytics",
options,
adminEntry: "@my-org/plugin-analytics/admin",
adminPages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
adminWidgets: [{ id: "events-today", title: "Events Today", size: "third" }],
};
}
export function createPlugin(options: AnalyticsOptions = {}) {
const maxEvents = options.maxEvents ?? 100;
return definePlugin({
id: "analytics",
version: "0.1.0",
capabilities: ["network:request"],
allowedHosts: ["api.analytics.example.com"],
storage: {
events: { indexes: ["type", "createdAt"] },
},
admin: {
entry: "@my-org/plugin-analytics/admin",
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
enabled: { type: "boolean", label: "Enabled", default: options.enabled ?? true },
},
pages: [{ path: "/dashboard", label: "Dashboard", icon: "chart" }],
widgets: [{ id: "events-today", title: "Events Today", size: "third" }],
},
hooks: {
"plugin:install": async (_event, ctx) => {
ctx.log.info("Analytics plugin installed", { maxEvents });
},
"content:afterSave": async (event, ctx) => {
const enabled = await ctx.kv.get<boolean>("settings:enabled");
if (enabled === false) return;
await ctx.storage.events.put(`evt_${Date.now()}`, {
type: "content:save",
contentId: event.content.id,
createdAt: new Date().toISOString(),
});
},
},
routes: {
stats: {
handler: async (ctx) => {
const today = new Date().toISOString().split("T")[0];
const count = await ctx.storage.events.count({
createdAt: { gte: today },
});
return { today: count };
},
},
},
});
}
export default createPlugin;
此設定的關鍵細節:
format: "native"是必需的。"native"也是預設值,但在每個描述符上明確宣告可以輕鬆識別格式。entrypoint是套件的主匯出。 EmDash 在執行時期匯入它並呼叫預設匯出來建構解析後的外掛。options從描述符流向createPlugin。 使用者在註冊外掛時傳遞的所有內容(analyticsPlugin({ enabled: false }))都保留在描述符上並轉發給createPlugin。沙盒外掛沒有這個表面——它們改為從 KV 讀取設定。id、version和capabilities出現兩次。 描述符上一次,definePlugin()上一次。它們應該匹配。描述符的副本是astro.config.mjs在建置時看到的;definePlugin()的副本是在請求時執行的。- 原生路由處理器接受單一引數 —
(ctx: RouteContext)其中ctx.input、ctx.request和ctx.requestMeta與常規PluginContext屬性合併。這與標準格式的雙引數形式相反。完整的表面請參見 API 路由(其他所有內容都相同)。
外掛 ID 規則
id 欄位必須匹配 /^[a-z][a-z0-9_-]*$/ — 以小寫字母開頭,然後是字母、數字、連字號或底線。ID 用作外掛路由 URL 中的單一路徑段,以及作為外掛儲存索引產生的 SQL 識別符的一部分,因此超出此模式的任何內容在執行時期都會失敗。以下值顯示哪些 ID 被接受:
// 有效
"seo";
"audit-log";
"audit_log";
"plugin-forms";
// 無效
"@my-org/plugin-forms"; // 作用域形式在執行時期不允許
"MyPlugin"; // 不能有大寫字母
"42-plugin"; // 不能以數字開頭
"my.plugin"; // 不能有點號
將無作用域的 id 與 entrypoint 中的有作用域的 npm 套件名稱配對——套件名稱和外掛 ID 是不同的關注點。
版本格式
使用語意化版本控制。以下值顯示哪些版本字串被接受:
version: "1.0.0"; // 有效
version: "1.2.3-beta"; // 有效(預發布)
version: "1.0"; // 無效(缺少修補號)
註冊外掛
在網站的 astro.config.mjs 中,匯入描述符工廠並將其傳遞到 plugins: [] 陣列中——原生外掛始終在同一程序中執行,永遠不放在 sandboxed: [] 中:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { analyticsPlugin } from "@my-org/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [
analyticsPlugin({ enabled: true, maxEvents: 500 }),
],
}),
],
});
設定 UI
原生外掛可以使用 admin.settingsSchema 來產生自動設定表單,這是最簡單的方式:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
enabled: { type: "boolean", label: "Enabled", default: true },
maxItems: { type: "number", label: "Max items", min: 1, max: 1000, default: 100 },
},
},
欄位類型:string、number、boolean、select、secret、url、email。每種類型接受 label、description、default,以及類型特定的擴充如 min/max/options。設定持久化到沙盒外掛使用的同一個外掛級 KV 儲存——可以在任何地方使用 ctx.kv.get<T>("settings:<key>") 讀取。
產生的表單出現在 Plugins 中外掛卡片的齒輪圖示後面(僅管理員——編輯外掛設定需要 plugins:manage 權限)。密鑰欄位為唯寫:管理員永遠看不到儲存的值,只能看到是否已設定。
對於比 settingsSchema 提供的更豐富的設定 UI,請提供自訂 React 頁面——參見 React 管理頁面和小工具。
完整範例 — 稽核日誌外掛
以下外掛將每次內容的建立、更新和刪除記錄到索引儲存中,並公開一個最近活動路由:
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
interface AuditEntry {
timestamp: string;
action: "create" | "update" | "delete";
collection: string;
resourceId: string;
userId?: string;
}
export function auditLogPlugin(): PluginDescriptor {
return {
id: "audit-log",
version: "0.1.0",
format: "native",
entrypoint: "@emdash-cms/plugin-audit-log",
};
}
export function createPlugin() {
return definePlugin({
id: "audit-log",
version: "0.1.0",
storage: {
entries: {
indexes: [
"timestamp",
"action",
"collection",
["collection", "timestamp"],
["action", "timestamp"],
],
},
},
admin: {
settingsSchema: {
retentionDays: {
type: "number",
label: "Retention (days)",
description: "Days to keep entries. 0 = forever.",
default: 90,
min: 0,
max: 365,
},
},
pages: [{ path: "/history", label: "Audit History", icon: "history" }],
widgets: [{ id: "recent-activity", title: "Recent Activity", size: "half" }],
},
hooks: {
"content:afterSave": {
priority: 200,
handler: async (event, ctx) => {
const entry: AuditEntry = {
timestamp: new Date().toISOString(),
action: event.isNew ? "create" : "update",
collection: event.collection,
resourceId: event.content.id as string,
};
await ctx.storage.entries.put(`${Date.now()}-${event.content.id}`, entry);
},
},
"content:afterDelete": {
priority: 200,
handler: async (event, ctx) => {
await ctx.storage.entries.put(`${Date.now()}-${event.id}`, {
timestamp: new Date().toISOString(),
action: "delete",
collection: event.collection,
resourceId: event.id,
});
},
},
},
routes: {
recent: {
handler: async (ctx) => {
const result = await ctx.storage.entries.query({
orderBy: { timestamp: "desc" },
limit: 10,
});
return {
entries: result.items.map((item) => ({
id: item.id,
...(item.data as AuditEntry),
})),
};
},
},
},
});
}
export default createPlugin;
測試
透過建立一個註冊了外掛的最小 Astro 網站來測試原生外掛:
- 建立一個安裝了 EmDash 的測試網站。
- 在
astro.config.mjs中註冊你的外掛,直接從本地原始碼路徑匯入。 - 執行開發伺服器,透過建立、更新或刪除內容來觸發鉤子。
- 檢查主控台中的
ctx.log輸出,並透過 API 路由驗證儲存。
對於單元測試,模擬 PluginContext 介面並直接呼叫鉤子處理器。
下一步
- React 管理頁面和小工具 — 為管理面板提供自訂 React UI。
- Portable Text 呈現元件 — 提供呈現外掛定義的區塊類型的 Astro 元件。
- 頁面片段 — 將指令碼、樣式表或 HTML 注入公開頁面。
- 發佈原生外掛 — npm 打包和版本管理。