您的第一個原生外掛程式

本頁內容

本指南將引導您從頭開始建構原生外掛程式。原生外掛程式與您的 Astro 網站在同一程序中執行,可完全存取執行時期,包括 React 管理頁面、Portable Text 元件和頁面片段。

如果您還沒有決定是使用原生外掛程式還是沙盒外掛程式,請先閱讀選擇外掛程式格式。原生格式適用於需要 React 管理頁面、Portable Text 渲染元件或頁面片段的外掛程式。

一個或兩個檔案中的兩個部分

與沙盒外掛程式類似,原生外掛程式包含兩個部分:

  1. 描述符工廠 — 傳回帶有 format: "native" 和管理相關進入點的 PluginDescriptor。在建置時由 astro.config.mjs 匯入。
  2. 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"
	}
}

emdashreact 保持為對等相依性,以便主機網站提供實際版本,避免您發送重複的套件。

編寫描述符和執行時期

以下 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 讀取設定。
  • idversioncapabilities 出現兩次。 一次在描述符上,一次在 definePlugin() 上。它們應該匹配。描述符的副本是 astro.config.mjs 在建置時看到的;definePlugin() 的副本是在請求時執行的。
  • 原生路由處理常式接受單個參數(ctx: RouteContext),其中 ctx.inputctx.requestctx.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

將無作用域的 identrypoint 中的有作用域的 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 },
	},
},

欄位類型:stringnumberbooleanselectsecreturlemail。每個都接受 labeldescriptiondefault,以及特定於類型的額外項目,如 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 網站來測試原生外掛程式:

  1. 建立一個安裝了 EmDash 的測試網站。
  2. astro.config.mjs 中註冊您的外掛程式,直接從您的本機來源路徑匯入。
  3. 執行開發伺服器並透過建立、更新或刪除內容來觸發掛鉤。
  4. 檢查主控台的 ctx.log 輸出並透過 API 路由驗證儲存。

對於單元測試,模擬 PluginContext 介面並直接呼叫掛鉤處理常式。

下一步