移植 WordPress 外掛程式

本頁內容

許多 WordPress 外掛程式可以移植到 EmDash。外掛程式模型有所不同——TypeScript 而非 PHP,hooks 而非 actions/filters,結構化儲存而非 wp_options——但大多數功能都能完美映射。

可移植性評估

並非所有外掛程式都適合移植。在開始前評估候選外掛程式。

適合的候選外掛程式

自訂欄位、SEO 外掛程式、內容處理器、管理 UI 擴充、分析、社交分享、表單

不適合的候選外掛程式

多站點功能、WooCommerce/Gutenberg 整合、修補 WordPress 核心內部的外掛程式

外掛程式結構對比

WordPress

wp-content/plugins/my-plugin/
├── my-plugin.php       # 帶有外掛程式標頭的主檔案
├── includes/
│   ├── class-admin.php
│   └── class-api.php
└── admin/
    └── js/

EmDash

my-plugin/
├── src/
│   ├── index.ts    # 外掛程式定義 (definePlugin)
│   └── admin.tsx   # 管理 UI 匯出 (React)
├── package.json
└── tsconfig.json

Hooks 映射

WordPress 使用帶字串 hook 名稱的 add_action()add_filter()。EmDash 使用在外掛程式定義中宣告的型別化 hooks。

生命週期 Hooks

WordPressEmDash備註
register_activation_hook()plugin:install首次安裝時執行一次
外掛程式啟用plugin:activate啟用時執行
外掛程式停用plugin:deactivate停用時執行
register_uninstall_hook()plugin:uninstallevent.deleteData 表示使用者的選擇

內容 Hooks

WordPressEmDash備註
wp_insert_post_datacontent:beforeSave傳回修改的內容或拋出錯誤以取消
save_postcontent:afterSave儲存後的副作用
before_delete_postcontent:beforeDelete傳回 false 以取消
deleted_postcontent:afterDelete刪除後的清理

WordPress

add_action('save_post', function($post_id, $post, $update) {
    if ($post->post_type !== 'product') return;

    $price = get_post_meta($post_id, 'price', true);
    if ($price > 1000) {
        update_post_meta($post_id, 'is_premium', true);
    }

}, 10, 3);

EmDash

hooks: {
    "content:afterSave": async (event, ctx) => {
        if (event.collection !== "products") return;

        const price = event.content.price as number;
        if (price > 1000) {
            await ctx.kv.set(`premium:${event.content.id}`, true);
        }
    },
}

媒體 Hooks

WordPressEmDash備註
wp_handle_upload_prefiltermedia:beforeUpload驗證或轉換
add_attachmentmedia:afterUpload上傳後回應

儲存映射

Options API → KV Store

WordPress

$api_key = get_option('my_plugin_api_key', '');
update_option('my_plugin_api_key', 'abc123');
delete_option('my_plugin_api_key');

EmDash

const apiKey = await ctx.kv.get<string>("settings:apiKey") ?? "";
await ctx.kv.set("settings:apiKey", "abc123");
await ctx.kv.delete("settings:apiKey");

自訂表格 → 儲存集合

WordPress

global $wpdb;
$table = $wpdb->prefix . 'my_plugin_items';

// 插入
$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);

// 查詢
$items = $wpdb->get_results(
"SELECT \* FROM $table WHERE status = 'active' LIMIT 10"
);

EmDash

// 在外掛程式定義中宣告
storage: {
    items: {
        indexes: ["status", "createdAt"],
    },
},

// 在 hooks 或 routes 中:
await ctx.storage.items.put("item-1", {
    name: "Item 1",
    status: "active",
    createdAt: new Date().toISOString(),
});

const result = await ctx.storage.items.query({
    where: { status: "active" },
    limit: 10,
});

設定 Schema

WordPress 使用 Settings API 處理管理表單。EmDash 使用宣告式 schema 自動產生 UI。

WordPress

add_action('admin_init', function() {
    register_setting('my_plugin', 'my_plugin_api_key');
    add_settings_section('main', 'Settings', null, 'my-plugin');
    add_settings_field('api_key', 'API Key', function() {
        $value = get_option('my_plugin_api_key');
        echo '<input type="text" name="my_plugin_api_key"
              value="' . esc_attr($value) . '">';
    }, 'my-plugin', 'main');
});

EmDash

admin: {
    settingsSchema: {
        apiKey: {
            type: "secret",
            label: "API 金鑰",
            description: "來自儀表板的 API 金鑰",
        },
        enabled: {
            type: "boolean",
            label: "已啟用",
            default: true,
        },
        limit: {
            type: "number",
            label: "項目限制",
            default: 100,
            min: 1,
            max: 1000,
        },
    },
}

管理 UI

WordPress 管理頁面是 PHP。EmDash 使用 React 元件。

import { useState, useEffect } from "react";

export const widgets = {
	summary: function SummaryWidget() {
		const [count, setCount] = useState(0);

		useEffect(() => {
			fetch("/_emdash/api/plugins/my-plugin/status")
				.then((r) => r.json())
				.then((data) => setCount(data.count));
		}, []);

		return <div>總項目數: {count}</div>;
	},
};

export const pages = {
	settings: function SettingsPage() {
		// 設定頁面的 React 元件
		return <div>設定內容</div>;
	},
};

在外掛程式定義中註冊:

admin: {
    entry: "@my-org/my-plugin/admin",
    pages: [{ path: "/settings", label: "儀表板" }],
    widgets: [{ id: "summary", title: "摘要", size: "half" }],
},

REST API → 外掛程式路由

WordPress

register_rest_route('my-plugin/v1', '/items', [
    'methods' => 'GET',
    'callback' => function($request) {
        global $wpdb;
        $items = $wpdb->get_results("SELECT * FROM items LIMIT 50");
        return new WP_REST_Response($items);
    },
]);

EmDash

routes: {
    items: {
        handler: async (ctx) => {
            const result = await ctx.storage.items.query({ limit: 50 });
            return { items: result.items };
        },
    },
},

路由可在 /_emdash/api/plugins/{plugin-id}/{route-name} 存取。

移植流程

  1. 分析 WordPress 外掛程式

    記錄它的功能:hooks、資料庫操作、管理頁面、REST 端點。

  2. 映射到 EmDash 概念

    WordPress hooks → EmDash hooks。wp_optionsctx.kv。自訂表格 → 儲存集合。管理頁面 → React 元件。REST 端點 → 外掛程式路由。

  3. 建立外掛程式骨架

    import { definePlugin } from "emdash";
    
    export function createPlugin() {
    	return definePlugin({
    		id: "my-ported-plugin",
    		version: "1.0.0",
    		capabilities: [],
    		storage: {},
    		hooks: {},
    		routes: {},
    		admin: {},
    	});
    }
  4. 按順序實作

    儲存 → Hooks → 管理 UI → 路由

  5. 徹底測試

    驗證 hooks 是否正確觸發,儲存是否運作,以及管理 UI 是否呈現。

範例:閱讀時間外掛程式

WordPress

add_filter('wp_insert_post_data', function($data, $postarr) {
    if ($data['post_type'] !== 'post') return $data;

    $content = strip_tags($data['post_content']);
    $word_count = str_word_count($content);
    $read_time = ceil($word_count / 200);

    if (!empty($postarr['ID'])) {
        update_post_meta($postarr['ID'], '_read_time', $read_time);
    }
    return $data;

}, 10, 2);

EmDash

export function createPlugin() {
    return definePlugin({
        id: "read-time",
        version: "1.0.0",

        admin: {
            settingsSchema: {
                wordsPerMinute: {
                    type: "number",
                    label: "每分鐘字數",
                    default: 200,
                    min: 100,
                    max: 400,
                },
            },
        },

        hooks: {
            "content:beforeSave": async (event, ctx) => {
                if (event.collection !== "posts") return;

                const wpm = await ctx.kv.get<number>("settings:wordsPerMinute") ?? 200;
                const text = JSON.stringify(event.content.body || "");
                const readTime = Math.ceil(text.split(/\s+/).length / wpm);

                return { ...event.content, readTime };
            },
        },
    });
}

權限

外掛程式必須宣告安全沙箱所需的權限:

權限提供用例
network:requestctx.http.fetch()外部 API 呼叫
content:readctx.content.get(), list()讀取 CMS 內容
content:writectx.content.create(), etc.修改內容
media:readctx.media.get(), list()讀取媒體
media:writectx.media.getUploadUrl()上傳媒體

常見陷阱

無全域狀態 — 使用儲存而非全域變數。

一切非同步 — 始終對儲存和 API 呼叫使用 await

無直接 SQL — 使用結構化儲存集合。

無檔案系統 — 使用媒體 API 處理檔案。

後續步驟