你的第一個原生外掛程式

本頁內容

本指南將逐步介紹如何從零開始建立原生外掛程式。原生外掛程式與你的 Astro 網站在同一個處理程序中執行——沒有沙箱邊界,可以完全存取執行階段,並且能夠存取沙箱無法提供的功能(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          # 描述符工廠 + 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"
	}
}

emdashreact 保留為對等相依性,以便主機網站提供實際版本,避免重複打包。

撰寫描述符和執行階段

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

將無範圍的 identrypoint 中的範圍 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 },
	},
},

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

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

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

下一步