RustForge 插件开发完全指南
基于 Wasm 的插件系统,以五运六气插件为实战案例
Rust
Wasm
Axum
插件系统
多语言
钩子系统
📖 概述
RustForge 的插件系统基于 WebAssembly (Wasm),允许您使用 Rust 安全地扩展网站功能。本指南以五运六气插件为实战案例,演示从零到一构建一个完整插件的全流程。
通过本指南,你将学习到:
- 插件的基本结构和必需的导出函数
- 如何使用
plugin_settings存储插件数据(无需修改主程序) - 编写前端视图并实现多语言支持(
{{ key }}语法) - 实现
is_page_protected进行登录保护 - 插件页面路由访问规范(
/plugins/:name/index) - 钩子系统:在主题任意位置注入插件内容(导航栏、侧边栏、页脚等),并支持多语言切换
- 利用扫描功能一键安装插件
💡 设计理念:插件仅负责前端展示和轻量逻辑,数据持久化通过主程序提供的
/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。同时,插件设置页支持为每个语言版本单独编辑钩子内容,实现了精细化的多语言管理。
🔌 钩子系统
钩子系统允许插件在主题的任意位置注入内容,无需修改主题模板。支持导航栏、侧边栏、页脚、头部等任何位置,并且钩子内容可以按语言版本独立设置。
工作原理
- 主题模板中放置钩子占位符:
{{ hooks.nav | safe }} - 插件通过管理 API 创建钩子记录,指定
hook_name和lang(语言代码) - 主程序渲染时,会根据当前语言加载对应版本的钩子;若不存在,则回退到通用(
lang='')钩子 - 钩子内容支持
{{ key }}多语言占位符,由主程序自动翻译 - 若插件被禁用,其所有钩子将不会在前台显示
钩子数据结构
| 字段 | 说明 |
|---|---|
plugin_name | 插件名称(标识来源) |
hook_name | 钩子名称(如 nav, sidebar, footer) |
content | HTML 内容,支持 {{ key }} 多语言占位符 |
sort_order | 排序(多个钩子按此顺序合并) |
lang | 语言代码(空字符串表示通用,所有语言均显示) |
enabled | 是否启用 |
钩子的多语言策略
系统加载钩子时遵循以下优先级:
- 优先加载当前语言(
lang='zh')的钩子。 - 若不存在,则回退到通用(
lang='')钩子。 - 若仍不存在,该钩子位置为空。
- 多个钩子按
sort_order排序后合并输出。 - 若插件被禁用,其所有钩子均被忽略。
💡 推荐做法:为常用语言(如 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 插件
- 创建项目
cargo new my-plugin --lib - 配置
Cargo.toml,设置crate-type = ["cdylib"] - 编写
src/lib.rs,实现 4 个导出函数 - 编译为 wasm:
cargo build --target wasm32-unknown-unknown --release - 复制
target/wasm32-unknown-unknown/release/my_plugin.wasm到插件根目录,命名为my-plugin.wasm
2. 创建前端文件
views/:存放 HTML 页面片段(如index.html,new.html)static/:存放 JS、CSS 等静态资源locales/:存放zh.json和en.json语言包- 钩子支持:在设置页提供钩子管理功能,支持为不同语言版本添加/编辑钩子
3. 安装与启用
- 将插件文件夹放入
plugins/目录 - 登录管理后台 → 插件管理 → 点击 "扫描可用插件"
- 在列表中找到插件,点击 "安装"
- 安装完成后,确保插件状态为 "启用"
4. 访问测试
- 访问
/plugins/my-plugin/index查看列表页 - 访问
/plugins/my-plugin/new测试新建功能 - 访问
/plugins/my-plugin/settings管理钩子(测试多语言钩子切换) - 切换语言验证多语言翻译是否生效
- 检查主题页面中钩子内容是否正确显示
⚠️ 注意事项与最佳实践
- 视图模板:使用
{{ key }}而非{{ t(...) }},后者不会被解析 - 数据存储:优先使用
plugin_settings接口存储 JSON 数据 - 钩子内容:支持
{{ key }}多语言,语言包由插件提供 - 钩子语言:
lang字段留空表示通用钩子,所有语言均显示;指定语言则仅在该语言下显示 - 钩子与插件状态:禁用插件后,其钩子不会在前台出现
- 设置页语言选择:从系统设置的
/api/admin/lang-settings接口动态获取可用语言列表,保持与站点配置一致 - Wasm 轻量化:将业务逻辑放在前端 JavaScript 中,减少 Wasm 复杂度
- 语言包:JSON 文件末尾不能有多余逗号
- 静态资源路径:使用
/plugins/{name}/static/{file} - 页面入口:建议使用
/plugins/{name}/index作为默认首页 - 钩子渲染:主题模板中需使用
{{ hooks.xxx | safe }}输出 HTML - 安装机制:系统会复制 wasm 文件,卸载时删除副本,原始文件保留