RustForge 插件开发完全指南

基于 Wasm 的插件系统,以五运六气插件为实战案例

Rust Wasm Axum 插件系统 多语言 钩子系统

📖 概述

RustForge 的插件系统基于 WebAssembly (Wasm),允许您使用 Rust 安全地扩展网站功能。本指南以五运六气插件为实战案例,演示从零到一构建一个完整插件的全流程。

通过本指南,你将学习到:

💡 设计理念:插件仅负责前端展示和轻量逻辑,数据持久化通过主程序提供的 /api/admin/plugins/{name}/settings 接口以 JSON 格式存储,完全无需修改主程序。

📁 插件文件结构

plugins/wuyun-plugin/
├── Cargo.toml                 # Rust 项目配置
├── src/
│   └── lib.rs                 # 插件核心逻辑 (Wasm,极简)
├── locales/                   # 多语言翻译文件
│   ├── zh.json                # 中文翻译
│   └── en.json                # 英文翻译
├── views/                     # 前端视图片段
│   ├── index.html             # 列表页
│   ├── new.html               # 新建页
│   ├── edit.html              # 编辑页
│   └── settings.html          # 设置页(钩子管理 + 多语言配置)
└── static/                    # 静态资源
    ├── nav.html               # 导航栏片段
    ├── nav-loader.js          # 导航加载器
    └── wuyun.js               # 业务逻辑(纯前端计算)

最终编译产物为 wuyun-plugin.wasm,放在插件根目录即可被系统扫描识别。

📌 注意:插件数据通过 plugin_hooks 表和 plugin_settings 表存储,由主程序统一管理,插件无需关心数据库操作。

⚙️ 插件 Rust 源码解析

极简 lib.rs(只做元数据和页面保护)

对于纯前端展示类插件,Rust 部分只需实现必需的 4 个导出函数,业务逻辑可在前端 JavaScript 中完成。

use std::alloc::{Layout, alloc};

const PLUGIN_METADATA: &str = r#"{
    "name": "wuyun-plugin",
    "version": "1.0.0",
    "author": "RustForge Team",
    "description": "五运六气管理插件"
}"#;

static mut LAST_LEN: usize = 0;

#[no_mangle]
pub extern "C" fn plugin_metadata() -> *mut u8 {
    let bytes = PLUGIN_METADATA.as_bytes();
    let len = bytes.len();
    unsafe {
        let layout = Layout::array::(len).unwrap();
        let ptr = alloc(layout) as *mut u8;
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len);
        LAST_LEN = len;
        ptr
    }
}

#[no_mangle]
pub extern "C" fn get_last_result_len() -> usize {
    unsafe { LAST_LEN }
}

#[no_mangle]
pub extern "C" fn execute(_ptr: *const u8, _len: usize) -> *mut u8 {
    let output = "{}".to_string();
    let bytes = output.into_bytes();
    let len = bytes.len();
    unsafe {
        let layout = Layout::array::(len).unwrap();
        let ptr = alloc(layout) as *mut u8;
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len);
        LAST_LEN = len;
        ptr
    }
}

#[no_mangle]
pub extern "C" fn is_page_protected(_ptr: *const u8, _len: usize) -> *mut u8 {
    // 所有页面都需要登录
    let output = "true".to_string();
    let bytes = output.into_bytes();
    let len = bytes.len();
    unsafe {
        let layout = Layout::array::(len).unwrap();
        let ptr = alloc(layout) as *mut u8;
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len);
        LAST_LEN = len;
        ptr
    }
}
💡 为什么可以如此精简? 因为数据通过 /api/admin/plugins/{name}/settings 读写,Wasm 不需要直接操作数据库,因此可以非常轻量。

Cargo.toml 配置

[package]
name = "wuyun-plugin"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

🎨 前端实现

视图模板语法(重要)

⚠️ 关键点:插件视图使用 {{ key }} 语法,而非 {{ t(key="xxx", lang=lang) }}。主程序的 plugin_page_handler 只会进行简单的字符串替换,不解析 Tera 函数。
<!-- ✅ 正确写法 -->
<h2>{{ wuyun_title }}</h2>
<button>{{ save }}</button>

<!-- ❌ 错误写法(不会被解析) -->
<h2>{{ t(key="wuyun_title", lang=lang) }}</h2>

导航加载器(nav-loader.js)

支持多语言的导航加载,自动替换 {{ key }} 占位符。

function loadNav(currentPage) {
    return fetch('/plugins/wuyun-plugin/static/nav.html')
        .then(res => res.text())
        .then(html => {
            const locales = window.__plugin_locales__ || {};
            for (const [key, value] of Object.entries(locales)) {
                html = html.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), value);
            }
            document.getElementById('user-center-nav').innerHTML = html;
            // 高亮当前页面
            document.querySelectorAll('[data-nav]').forEach(link => {
                if (link.getAttribute('data-nav') === currentPage) {
                    link.classList.add('bg-gray-100', 'font-semibold');
                }
            });
        });
}

JavaScript 多语言辅助

const __t = (key, fallback) => (window.__plugin_locales__ && window.__plugin_locales__[key]) || fallback || key;

🌐 多语言支持

插件只需在根目录下创建 locales/ 文件夹,放入对应语言的 JSON 文件即可实现多语言。钩子内容中的 {{ key }} 也会被自动翻译,并且插件设置页可以按语言版本分别管理钩子内容。

语言包文件格式

locales/zh.json

{
    "wuyun_title": "五运六气",
    "add_record": "添加记录",
    "name": "姓名",
    "year": "年份",
    "time": "时间 (MM-DD)",
    "save": "保存",
    "cancel": "取消",
    "edit": "编辑",
    "delete": "删除"
}

locales/en.json

{
    "wuyun_title": "Five Yun and Six Qi",
    "add_record": "Add Record",
    "name": "Name",
    "year": "Year",
    "time": "Time (MM-DD)",
    "save": "Save",
    "cancel": "Cancel",
    "edit": "Edit",
    "delete": "Delete"
}
💡 自动注入:主程序在渲染插件页面时,会自动将 window.__plugin_locales__ 注入到页面中,包含当前语言的所有翻译键值对。
✅ 钩子多语言:钩子内容中的 {{ key }} 会在服务端渲染时自动替换为翻译,无需前端 JS。同时,插件设置页支持为每个语言版本单独编辑钩子内容,实现了精细化的多语言管理。

🔌 钩子系统

钩子系统允许插件在主题的任意位置注入内容,无需修改主题模板。支持导航栏、侧边栏、页脚、头部等任何位置,并且钩子内容可以按语言版本独立设置。

工作原理

钩子数据结构

字段说明
plugin_name插件名称(标识来源)
hook_name钩子名称(如 nav, sidebar, footer
contentHTML 内容,支持 {{ key }} 多语言占位符
sort_order排序(多个钩子按此顺序合并)
lang语言代码(空字符串表示通用,所有语言均显示)
enabled是否启用

钩子的多语言策略

系统加载钩子时遵循以下优先级:

  1. 优先加载当前语言(lang='zh')的钩子。
  2. 若不存在,则回退到通用(lang='')钩子。
  3. 若仍不存在,该钩子位置为空。
  4. 多个钩子按 sort_order 排序后合并输出。
  5. 若插件被禁用,其所有钩子均被忽略。
💡 推荐做法:为常用语言(如 zh、en)分别创建钩子,同时保留一个通用版本作为 fallback,确保所有语言都有内容。

在主题模板中使用

在主题模板的任意位置添加钩子占位符,使用 | safe 防止转义:

<!-- 导航栏 -->
<ul>
    <li><a href="/">首页</a></li>
    {{ hooks.nav | safe }}
    <li><a href="/about">关于我们</a></li>
</ul>

<!-- 侧边栏 -->
<div class="sidebar">
    {{ hooks.sidebar | safe }}
</div>

<!-- 页脚 -->
<footer>
    {{ hooks.footer | safe }}
</footer>

插件管理钩子

在插件设置页面中,为不同语言版本分别创建或编辑钩子内容。建议从系统设置动态获取可用语言列表,生成下拉选择框。

// 获取系统可用语言
const resp = await fetch('/api/admin/lang-settings');
const data = await resp.json();
const availableLangs = data.available_langs || [];

// 创建钩子时指定语言
fetch('/api/admin/plugins/wuyun/hooks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        hook_name: 'nav',
        content: '
  • {{ wuyun_title }}
  • ', sort_order: 10, lang: 'zh', // 或 '' 表示通用 enabled: true }) });
    ✅ 最佳实践
    • 钩子名称应语义化(如 nav, sidebar, footer
    • 多个插件可向同一钩子添加内容,按 sort_order 排序
    • 钩子内容支持 {{ key }} 多语言,语言包由插件提供
    • 在设置页提供“一键添加”功能,并为每个语言版本独立管理,方便多语言站点
    • 定期清理不再使用的钩子,避免数据库膨胀

    钩子 API

    方法路径说明
    GET/api/admin/plugins/{name}/hooks获取插件钩子列表(可传 ?lang=zh 过滤语言)
    POST/api/admin/plugins/{name}/hooks创建钩子(需指定 lang
    PUT/api/admin/plugins/{name}/hooks/:id更新钩子(可更新 lang
    DELETE/api/admin/plugins/{name}/hooks/:id删除钩子

    💾 插件数据存储

    插件数据通过 plugin_settings 表以 JSON 格式存储,完全无需修改主程序。

    读写接口

    方法路径说明
    GET/api/admin/plugins/{name}/settings读取插件设置
    PUT/api/admin/plugins/{name}/settings写入插件设置

    使用示例

    // 读取数据
    fetch(`/api/admin/plugins/${PLUGIN_NAME}/settings`)
        .then(r => r.json())
        .then(data => {
            const records = data.records || [];
            // 渲染列表
        });
    
    // 写入数据
    fetch(`/api/admin/plugins/${PLUGIN_NAME}/settings`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ records: [...] })
    });
    ✅ 优势:无需修改主程序,数据存储与插件绑定,卸载插件时数据自动保留(需手动清理)。
    📌 与钩子的区别
    • plugin_settings:存储插件自己的配置数据(如记录列表)
    • plugin_hooks:存储注入到主题中的 HTML 内容(如导航链接),且支持按语言版本分离
    • 两者互补,共同构成插件的数据层

    🧭 插件路由访问

    访问规范

    URL说明
    /plugins/{name}/index插件默认首页(推荐)
    /plugins/{name}/new新建页面
    /plugins/{name}/edit?id=xxx编辑页面
    /plugins/{name}/settings插件设置页面(需管理员权限,通常包含钩子管理)
    /plugins/{name}/static/*file静态资源(CSS/JS/图片)
    💡 路径规则:主程序路由定义为 /plugins/:plugin_name/:page,因此访问时必须包含页面名。建议使用 /plugins/{name}/index 作为首页入口。

    页面保护

    lib.rs 中实现 is_page_protected 函数,返回 "true" 的页面需要登录才能访问。

    #[no_mangle]
    pub extern "C" fn is_page_protected(_ptr: *const u8, _len: usize) -> *mut u8 {
        // 所有页面都需要登录
        alloc_string("true")
    }

    未登录用户访问受保护页面会被重定向到 /plugins/{name}/login(可修改为全局后台登录页)。

    🚀 开发与部署流程

    1. 编写 Rust 插件

    1. 创建项目 cargo new my-plugin --lib
    2. 配置 Cargo.toml,设置 crate-type = ["cdylib"]
    3. 编写 src/lib.rs,实现 4 个导出函数
    4. 编译为 wasm:cargo build --target wasm32-unknown-unknown --release
    5. 复制 target/wasm32-unknown-unknown/release/my_plugin.wasm 到插件根目录,命名为 my-plugin.wasm

    2. 创建前端文件

    3. 安装与启用

    1. 将插件文件夹放入 plugins/ 目录
    2. 登录管理后台 → 插件管理 → 点击 "扫描可用插件"
    3. 在列表中找到插件,点击 "安装"
    4. 安装完成后,确保插件状态为 "启用"

    4. 访问测试

    ⚠️ 注意事项与最佳实践