本指南將逐步介紹如何從零開始建立原生外掛程式。原生外掛程式與你的 Astro 網站在同一個處理程序中執行——沒有沙箱邊界,可以完全存取執行階段,並且能夠存取沙箱無法提供的功能(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
設定套件
{
"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 保留為對等相依性,以便主機網站提供實際版本,避免重複打包。
撰寫描述符和執行階段
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: "追蹤 ID" },
enabled: { type: "boolean", label: "已啟用", default: options.enabled ?? true },
},
pages: [{ path: "/dashboard", label: "儀表板", icon: "chart" }],
widgets: [{ id: "events-today", title: "今日事件", size: "third" }],
},
hooks: {
"plugin:install": async (_event, ctx) => {
ctx.log.info("分析外掛程式已安裝", { 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 識別碼的一部分,因此超出該模式的任何內容都會在執行階段失敗。
// 有效
"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 }),
],
}),
],
});
設定介面
原生外掛程式可以使用 admin.settingsSchema 來自動產生設定表單,這是最簡單的方式:
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API 金鑰" },
enabled: { type: "boolean", label: "已啟用", default: true },
maxItems: { type: "number", label: "最大項目數", 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 提供的更豐富的設定介面,可以提供自訂 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: "保留期限(天)",
description: "保留條目的天數。0 = 永久。",
default: 90,
min: 0,
max: 365,
},
},
pages: [{ path: "/history", label: "稽核歷史", icon: "history" }],
widgets: [{ id: "recent-activity", title: "最近活動", 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 打包和版本控制。