插件概述
Fastdotnet 的插件系统是其核心特性之一,允许开发者在不修改主程序的情况下扩展功能。
🎯 什么是插件?
插件是一个独立的功能模块,可以:
- ✅ 动态加载/卸载 - 无需重启应用
- ✅ 独立开发 - 与主程序解耦
- ✅ 按需启用 - 根据业务需求选择
- ✅ 版本管理 - 支持多版本共存
- ✅ 热更新 - 运行时更新插件
🏗️ 插件架构
整体架构
┌─────────────────────────────────────┐
│ Fastdotnet 主程序 │
│ (WebApi + Core + Service) │
└──────────┬──────────────────────────┘
│ 插件接口
┌──────┴──────┬──────────┐
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐
│Plugin A│ │Plugin B│ │Plugin C│
└────────┘ └────────┘ └────────┘插件项目结构
MyPlugin/
├── Backend/ # 后端项目 (.NET 10)
│ ├── MyPlugin.csproj
│ ├── MyPluginPlugin.cs # 插件入口类
│ ├── plugin.json # 插件元数据
│ ├── Controllers/ # API 控制器
│ ├── Services/ # 业务服务
│ ├── Dto/ # 数据传输对象
│ ├── Models/ # 数据模型
│ └── Initializers/ # 初始化器(菜单、配置等)
│
├── Frontend/ # 前端项目 (Vue 3)
│ ├── MyPlugin.Admin/ # 管理端
│ │ ├── src/
│ │ ├── vite.config.ts
│ │ └── micro-index.html # 微前端入口
│ │
│ └── MyPlugin.App/ # 应用端
│ ├── src/
│ ├── vite.config.ts
│ └── micro-index.html
│
└── publish/ # 发布目录(自动生成)
└── {PluginId}/
├── dependencies/ # 后端 DLL 文件
├── wwwroot/
│ ├── admin/ # 管理端静态资源
│ ├── app/ # 应用端静态资源
│ └── publish/ # 其他资源
└── plugin.json🔧 技术栈
后端技术
| 技术 | 说明 |
|---|---|
| .NET 10 | 最新版本的 .NET 运行时 |
| SqlSugar ORM | 轻量级高性能 ORM |
| OIDC / OpenIddict | 身份认证和授权 |
| Autofac | 依赖注入容器 |
| Swagger/OpenAPI | API 文档 |
前端技术
| 技术 | 说明 |
|---|---|
| Vue 3 | 渐进式 JavaScript 框架 |
| TypeScript | 类型安全的 JavaScript |
| Element Plus | Vue 3 UI 组件库 |
| Vite | 下一代构建工具 |
| qiankun | 微前端框架 |
| Pinia | 状态管理 |
📦 插件元数据 (plugin.json)
每个插件都包含一个 plugin.json 文件,描述插件的基本信息:
json
{
"PluginId": "12345678901234567",
"Name": "MyPlugin",
"DisplayName": "我的插件",
"Version": "1.0.0",
"Description": "这是一个示例插件",
"Author": "Your Name",
"Website": "https://example.com",
"MinHostVersion": "1.0.0",
"Enabled": true
}字段说明:
PluginId: 从官网申请的唯一标识(必需)Name: 插件名称(英文,用于命名空间)DisplayName: 显示名称(中文)Version: 版本号(语义化版本)Description: 插件描述Author: 作者Website: 官方网站MinHostVersion: 最低宿主版本要求Enabled: 是否启用
🔄 插件生命周期
1. 创建阶段
使用 CLI 工具生成项目骨架:
bash
fd-plugin create --name MyPlugin --plugin-id 123456789012345672. 开发阶段
- 后端:添加控制器、服务、数据模型
- 前端:开发页面、组件、路由
- 调试:本地测试功能
3. 打包阶段
bash
# 构建后端
dotnet publish -c Release
# 构建前端
pnpm build
# 打包为 ZIP
zip -r MyPlugin.zip publish/4. 发布阶段
上传到插件市场或手动部署到服务器。
5. 安装阶段
通过插件市场安装或手动复制到 Plugins/ 目录。
6. 运行阶段
插件被加载到内存中,提供服务。
🎨 前端双端架构
Fastdotnet 插件的前端分为两个独立的项目:
管理端 (Admin)
- 目标用户: 系统管理员、运营人员
- 功能: 配置管理、数据管理、权限控制
- 路由前缀:
/micro/{PluginId}/admin/ - 端口: 开发时通常使用 8099
应用端 (App)
- 目标用户: 最终用户
- 功能: 业务操作、数据展示、工作流程
- 路由前缀:
/micro/{PluginId}/app/ - 端口: 开发时通常使用 8100
为什么分两端?
- 职责分离: 管理和业务逻辑清晰分开
- 权限控制: 不同用户看到不同界面
- 性能优化: 可以独立加载和优化
- 用户体验: 针对不同场景定制 UI
🔌 插件通信机制
1. API 调用
前端通过 HTTP 请求调用后端 API:
typescript
// 前端调用
import axios from 'axios'
const response = await axios.get('/api/sample/data')2. 事件总线
插件之间通过事件进行松耦合通信:
csharp
// 发布事件
await _eventBus.PublishAsync(new OrderCreatedEvent(orderId));
// 订阅事件
[EventHandler]
public async Task HandleOrderCreated(OrderCreatedEvent evt)
{
// 处理订单创建事件
}3. 依赖注入
插件可以注册自己的服务,也可以访问主程序的服务:
csharp
public override void ConfigureServices(IServiceCollection services)
{
// 注册插件服务
services.AddScoped<IMyService, MyService>();
// 访问主程序服务
var dbContext = services.BuildServiceProvider()
.GetService<AppDbContext>();
}⚡ 热插拔原理
AssemblyLoadContext (ALC)
Fastdotnet 使用 .NET 的 AssemblyLoadContext 技术实现插件隔离:
- 独立上下文: 每个插件在独立的 ALC 中加载
- 版本隔离: 不同插件可以使用同一库的不同版本
- 故障隔离: 插件崩溃不影响主程序
- 动态卸载: 可以卸载插件释放资源
加载流程
1. 扫描 Plugins 目录
2. 读取 plugin.json
3. 创建 AssemblyLoadContext
4. 加载插件 DLL
5. 调用 InitializeAsync()
6. 注册路由和服务
7. 插件就绪📊 插件 vs 模块
| 特性 | 插件 | 模块 |
|---|---|---|
| 编译时机 | 运行时加载 | 编译时确定 |
| 独立性 | 完全独立 | 依赖主程序 |
| 更新方式 | 热更新 | 需要重新编译 |
| 适用场景 | 可选功能 | 核心功能 |
| 版本管理 | 独立版本 | 跟随主程序 |
🎯 最佳实践
1. 命名规范
- 插件名称: PascalCase(例如:
MyPlugin) - Plugin ID: 小写字母+数字+连字符(例如:
my-plugin-123) - 命名空间:
{PluginName}(例如:MyPlugin.Controllers)
2. 版本管理
使用语义化版本:MAJOR.MINOR.PATCH
MAJOR: 不兼容的 API 变更MINOR: 向后兼容的功能新增PATCH: 向后兼容的问题修正
3. 错误处理
csharp
try
{
// 业务逻辑
}
catch (Exception ex)
{
_logger.LogError(ex, "操作失败");
return StatusCode(500, new { error = "Internal server error" });
}4. 日志记录
csharp
public class SampleController : ControllerBase
{
private readonly ILogger<SampleController> _logger;
public SampleController(ILogger<SampleController> logger)
{
_logger = logger;
}
public IActionResult GetData()
{
_logger.LogInformation("获取数据");
return Ok(data);
}
}