跳转到内容

移植 WordPress 插件

许多 WordPress 插件可以移植到 EmDash。插件模型有所不同——使用 TypeScript 而非 PHP,使用钩子而非动作/过滤器,使用结构化存储而非 wp_options——但大多数功能都能清晰地映射。

并非所有插件都适合移植。在开始前请评估候选插件。

适合移植的插件

自定义字段、SEO 插件、内容处理器、管理界面扩展、分析工具、社交分享、表单

不适合移植的插件

多站点功能、WooCommerce/Gutenberg 集成、修补 WordPress 核心内部机制的插件

wp-content/plugins/my-plugin/
├── my-plugin.php # Main file with plugin header
├── includes/
│ ├── class-admin.php
│ └── class-api.php
└── admin/
└── js/

WordPress 使用带有字符串钩子名称的 add_action() 和 add_filter()。EmDash 使用在插件定义中声明的类型化钩子。

WordPressEmDash说明
register_activation_hook()plugin:install首次安装时运行一次
插件启用plugin:activate启用时运行
插件禁用plugin:deactivate禁用时运行
register_uninstall_hook()plugin:uninstallevent.deleteData 指示用户选择
WordPressEmDash说明
wp_insert_post_datacontent:beforeSave返回修改后的内容或抛出异常以取消
save_postcontent:afterSave保存后的副作用操作
before_delete_postcontent:beforeDelete返回 false 以取消删除
deleted_postcontent:afterDelete删除后的清理操作
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);
WordPressEmDash说明
wp_handle_upload_prefiltermedia:beforeUpload验证或转换
add_attachmentmedia:afterUpload上传后响应
$api_key = get_option('my_plugin_api_key', '');
update_option('my_plugin_api_key', 'abc123');
delete_option('my_plugin_api_key');
global $wpdb;
$table = $wpdb->prefix . 'my_plugin_items';
// Insert
$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);
// Query
$items = $wpdb->get_results(
"SELECT \* FROM $table WHERE status = 'active' LIMIT 10"
);

WordPress 使用 Settings API 来管理面板表单。EmDash 使用声明式模式,可自动生成用户界面。

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');
});

WordPress 管理页面使用 PHP。EmDash 使用 React 组件。

src/admin.tsx
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>Total items: {count}</div>;
},
};
export const pages = {
settings: function SettingsPage() {
// React component for settings page
return <div>Settings content</div>;
},
};

在插件定义中注册:

src/index.ts
admin: {
entry: "@my-org/my-plugin/admin",
pages: [{ path: "/settings", label: "Dashboard" }],
widgets: [{ id: "summary", title: "Summary", size: "half" }],
},
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/api/plugins/{plugin-id}/{route-name} 访问。

  1. 分析 WordPress 插件

    记录其功能:钩子、数据库操作、管理页面、REST 端点。

  2. 映射到 EmDash 概念

    WordPress 钩子 → EmDash 钩子。wp_options → ctx.kv。自定义表 → 存储集合。管理页面 → React 组件。REST 端点 → 插件路由。

  3. 创建插件骨架

    src/index.ts
    import { definePlugin } from "emdash";
    export function createPlugin() {
    return definePlugin({
    id: "my-ported-plugin",
    version: "1.0.0",
    capabilities: [],
    storage: {},
    hooks: {},
    routes: {},
    admin: {},
    });
    }
  4. 按顺序实现

    存储 → 钩子 → 管理界面 → 路由

  5. 全面测试

    验证钩子是否正确触发、存储是否正常工作以及管理界面是否正确渲染。

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);

插件必须声明所需的能力,以实现安全沙箱:

能力提供功能使用场景
network:fetchctx.http.fetch()外部 API 调用
read:contentctx.content.get(), list()读取 CMS 内容
write:contentctx.content.create() 等修改内容
read:mediactx.media.get(), list()读取媒体
write:mediactx.media.getUploadUrl()上传媒体

无全局状态 — 使用存储而非全局变量。

一切都是异步的 — 始终 await 存储和 API 调用。

无直接 SQL — 使用结构化存储集合。

无文件系统访问 — 使用媒体 API 处理文件。