本指南將引導您從頭開始建構原生外掛程式。原生外掛程式與您的 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 # Descriptor factory + createPlugin
│ ├── admin.tsx # React admin components (optional)
│ └── astro/ # Astro components for PT block rendering (optional)
│ └── 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 是可接受的:
// Valid
"seo";
"audit-log";
"audit_log";
"plugin-forms";
// Invalid
"@my-org/plugin-forms"; // scoped form not allowed at runtime
"MyPlugin"; // no uppercase
"42-plugin"; // can't start with a digit
"my.plugin"; // no dots
將無作用域的 id 與 entrypoint 中的有作用域的 npm 套件名稱配對 — 套件名稱和外掛程式 ID 是獨立的關注點。
版本格式
使用語義版本控制。以下值顯示了哪些版本字串是可接受的:
version: "1.0.0"; // valid
version: "1.2.3-beta"; // valid (prerelease)
version: "1.0"; // invalid (missing patch)
註冊外掛程式
在您網站的 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>") 讀取它們。
對於比 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 打包和版本控制。