你的第一個原生外掛

本頁內容

原生外掛是一個 npm 套件,EmDash 會將它匯入到與 Astro 站台相同的行程中。本教學會建立一個記錄內容儲存的外掛,將其安裝到站台中,並在 astro.config.mjs 中註冊。

當外掛需要行程內功能時,例如 React 管理元件、Astro 算繪元件或受信任的頁面片段,請使用原生格式。如果鉤子、路由、儲存和 Block Kit 能滿足需求,請從沙箱外掛開始。選擇外掛格式對這兩種格式做了比較。

前置條件

從一個使用 pnpm 且能執行開發伺服器的 EmDash 站台開始。

這些指令將站台目錄稱為 my-emdash-site,並在它旁邊建立 plugin-activity。請將 my-emdash-site 替換為你的站台目錄名稱。

建立並註冊套件

  1. 在站台旁邊建立一個原生套件目錄。

    mkdir -p ../plugin-activity/src
    cd ../plugin-activity
  2. 新增套件中繼資料和建置腳本。

    {
        "name": "@example/plugin-activity",
        "version": "0.1.0",
        "type": "module",
        "main": "./dist/index.mjs",
        "exports": {
            ".": {
                "types": "./dist/index.d.mts",
                "import": "./dist/index.mjs"
            }
        },
        "files": ["dist"],
        "scripts": {
            "build": "tsdown src/index.ts --format esm --dts --clean",
            "dev": "tsdown src/index.ts --format esm --dts --watch",
            "typecheck": "tsc --noEmit"
        },
        "peerDependencies": {
            "emdash": "*"
        },
        "devDependencies": {
            "emdash": "*",
            "tsdown": "^0.20.0",
            "typescript": "^5.9.0"
        }
    }

    npm 套件名稱是 @example/plugin-activity。下面的執行階段程式碼使用不帶範圍的外掛 ID plugin-activity,以便它能放入外掛 API 路由的 URL 中。

  3. 新增 TypeScript 設定。

    {
        "compilerOptions": {
            "target": "ES2022",
            "module": "preserve",
            "moduleResolution": "bundler",
            "strict": true,
            "esModuleInterop": true,
            "declaration": true,
            "outDir": "./dist",
            "rootDir": "./src"
        },
        "include": ["src/**/*"],
        "exclude": ["node_modules", "dist"]
    }
  4. 安裝套件的相依套件。

    pnpm install
  5. 建立帶有內容儲存鉤子的 src/index.ts。

    import { definePlugin } from "emdash";
    import type { PluginDescriptor } from "emdash";
    
    export interface ActivityPluginOptions {
        logUpdates?: boolean;
    }
    
    export function activityPlugin(
        options: ActivityPluginOptions = {},
    ): PluginDescriptor<ActivityPluginOptions> {
        return {
            id: "plugin-activity",
            version: "0.1.0",
            format: "native",
            entrypoint: "@example/plugin-activity",
            options,
        };
    }
    
    export function createPlugin(options: ActivityPluginOptions = {}) {
        return definePlugin({
            id: "plugin-activity",
            version: "0.1.0",
            capabilities: ["content:read"],
            hooks: {
                "content:afterSave": async (event, ctx) => {
                    if (!event.isNew && options.logUpdates === false) return;
    
                    ctx.log.info("Content saved", {
                        collection: event.collection,
                        contentId: event.content.id,
                        isNew: event.isNew,
                    });
                },
            },
        });
    }
    
    export default createPlugin;

    content:afterSave 需要 content:read 能力。缺少該能力時,EmDash 會略過該鉤子。

  6. 建置該套件。

    pnpm build
  7. 在站台中安裝本機套件。

    cd ../my-emdash-site
    pnpm add ../plugin-activity
  8. 在 EmDash 整合中註冊描述符工廠。

    import { defineConfig } from "astro/config";
    import emdash from "emdash/astro";
    import { activityPlugin } from "@example/plugin-activity";
    
    export default defineConfig({
        integrations: [
            emdash({
                plugins: [activityPlugin({ logUpdates: true })],
            }),
        ],
    });

    原生描述符應放在 plugins 中,而不是 sandboxed 中。如果 sandboxed 陣列中出現原生描述符,EmDash 會拒絕它。

  9. 啟動站台,並在管理面板中儲存一個條目。

    pnpm dev

    伺服器記錄會包含 Content saved,以及集合、內容 ID 和該條目是否為新建。

描述符與執行階段的邊界

套件的匯出有兩項職責。EmDash 在不同階段使用它們:

  • 描述符工廠 activityPlugin() 在 Astro 評估其設定時執行。它回傳可序列化的建置時中繼資料:id、version、format、entrypoint 和 options。React 和 Astro 進入點也屬於這個描述符。
  • 具名匯出 createPlugin() 在 EmDash 初始化時執行。EmDash 從 entrypoint 匯入它,傳入序列化後的 options,並期望 definePlugin() 回傳一個已解析的外掛。

具名匯出 createPlugin 是必需的。預設匯出可能對套件的使用者有用,但 EmDash 的原生載入器是依名稱匯入 createPlugin 的。

請保持描述符和 definePlugin() 中的 id 與 version 一致。使用不帶範圍的短橫線命名外掛 ID,例如 plugin-activity;將 npm 範圍保留在套件名稱和 entrypoint 中。這樣可以讓該 ID 作為 API 路由 URL 中唯一的外掛區段使用。

外掛識別與版本控制列出了可接受的 ID 和版本格式。

執行階段行為屬於 definePlugin():

  • capabilities 和 allowedHosts
  • storage
  • hooks 和 routes
  • admin 設定、頁面、小工具和 Portable Text 宣告

描述符承載 Astro 必須在建置時匯入或公開的靜態條目。各專題指南會說明哪些管理欄位需要在描述符和執行階段中宣告相符的內容。

原生路由處理常式

原生路由處理常式接收一個 RouteContext。它將已驗證的輸入和請求資料與一般的 PluginContext 結合在一起:

routes: {
	status: {
		permission: "plugins:read",
		handler: async (ctx) => ({
			pluginId: ctx.plugin.id,
			callerId: ctx.user?.id ?? null,
		}),
	},
},

對應的沙箱處理常式以兩個參數接收 (routeCtx, ctx)。驗證、權限、輸入 schema 和路由 URL 在其他方面遵循共用的 API 路由約定。

當原生路由宣告了 request.body 時,請用 definePluginRoute() 包裝它;該輔助函式會根據請求本文模式推斷 ctx.input。帶有 response: "raw" 的原生路由回傳 pluginResponse()。 這兩個輔助函式都從 emdash 匯入。共用的 API 路由指南列出了請求本文模式、限制、回應 政策和相容性預設值。

讀取密鑰和繫結

原生外掛中的鉤子和路由處理常式接收的是外掛上下文,而不是 Astro 上下文,因此其中無法使用 Astro.locals。請從執行階段環境讀取密鑰和平台繫結。

從 process.env 讀取部署密鑰。在 Node.js 上,由代管平台的密鑰管理員提供。在 Cloudflare Workers 上,使用 wrangler secret put 設定的密鑰會透過相容日期為 2025-04-01 或更晚的 nodejs_compat 旗標進入 process.env,詳見 Worker 密鑰。

對於由站台管理員輸入的憑證,請改為在產生的設定表單中宣告 secret 欄位。EmDash 在儲存前會用 EMDASH_ENCRYPTION_KEY 加密該值,因此請在管理員儲存該欄位之前設定這個金鑰。外掛透過 ctx.settings.get() 讀取該值。

在 Cloudflare Workers 上,佇列、R2 儲存貯體或 send_email 等繫結是來自 cloudflare:workers 的 env 上的物件。Astro 在 Node.js 中載入 astro.config.mjs,而該檔案會為描述符工廠匯入外掛套件。Node.js 無法載入 cloudflare:workers,因此請在處理常式內部匯入它,而不是在模組頂部。下面的鉤子會將每個已儲存條目的 ID 傳送到繫結為 ACTIVITY_QUEUE 的佇列:

hooks: {
	"content:afterSave": async (event) => {
		const { env } = await import("cloudflare:workers");
		await env.ACTIVITY_QUEUE.send({ contentId: event.content.id });
	},
},

cloudflare:workers 模組只能在 Workers 執行階段中解析,因此匯入它的外掛只能在使用 Cloudflare 轉接器的站台上運作。

要對該匯入進行型別檢查,請將 @cloudflare/workers-types 新增到套件的 devDependencies 和 tsconfig.json 的 compilerOptions.types 中。然後在 Cloudflare.Env 介面上宣告外掛讀取的每個繫結。下面的宣告為上一個範例中的佇列加上型別:

declare namespace Cloudflare {
	interface Env {
		ACTIVITY_QUEUE: Queue;
	}
}

新增其他介面