Files
TouchSocket/handbook/docs/pluginsmanager.mdx
若汝棋茗 72e0a65742 feat(docs): 添加多个组件的导入和解析功能
在多个 `.mdx` 文件中添加对 `TouchSocketCoreDefinition`、`TouchSocketHttpDefinition`、`TouchSocketDmtpDefinition`、`TouchSocketModbusDefinition`、`TouchSocketProModbusDefinition`、`TouchSocketNamedPipeDefinition`、`TouchSocketWebApiDefinition`、`TouchSocketJsonRpcDefinition` 和 `TouchSocketXmlRpcDefinition` 的导入。增加解析和处理定义部分的 JavaScript 脚本,确保导入的有效性和一致性。新增功能包括精确匹配定义部分的正则表达式、解析定义内容的函数、生成定义组件的函数、递归查找 `.mdx` 文件的功能,以及验证导入的有效性
2025-07-09 22:22:17 +08:00

332 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: pluginsmanager
title: 插件系统
---
import { TouchSocketCoreDefinition } from "@site/src/components/Definition.js";
### 定义
<TouchSocketCoreDefinition />
## 一、说明
插件系统是一组能实现多播订阅的,可中断的触发器,其主要功能就是实现类似事件、委托的通知消息。其设计核心来自于`AspNetCore`的中间件,它有着和中间件一样的使用体验,同时也有着更高的灵活性和自由度。
## 二、产品特点
- 简单易用。
- 易扩展。
## 三、产品应用场景
- 所有可以使用事件。委托的场景。
## 四、与事件、委托相比
1. 订阅的时候可以不用知道被订阅方是谁,只需要知道要订阅什么通知即可。
2. 订阅可以随时中断。例如:当事件多播的时候,即使其中一个订阅方已经处理,触发也不会停止,这样会造成资源浪费。
3. 订阅回调。例如:第一个订阅方,想知道本次触发的最终结果是否已被处理时,委托则做不到。而插件则可以。
4. 插件可以被继承,可以被扩展。
5. 插件可以被注入。
6. 插件可以独立负责相关功能,实现功能独立。可模块化功能。
## 五、创建插件
#### 5.1 新建插件接口及事件类
```csharp showLineNumbers
public class MyPluginEventArgs : PluginEventArgs
{
public string Words { get; set; }
}
/// <summary>
/// 定义一个插件接口,使其继承<see cref="IPlugin"/>
/// </summary>
public interface ISayPlugin : IPlugin
{
/// <summary>
/// Say。定义一个插件方法必须遵循
/// 1.必须是两个参数,第一个参数可以是任意类型,一般表示触发源。第二个参数必须继承<see cref="PluginEventArgs"/>
/// 2.返回值必须是Task。
/// </summary>
/// <param name="sender">触发主体</param>
/// <param name="e">传递参数</param>
/// <returns></returns>
Task Say(object sender, MyPluginEventArgs e);
}
```
:::caution 注意事项
确定插件唯一的是插件中的`类型`,所以要求一个插件中只允许有一个插件方法。
:::
#### 5.2 实现插件接口
新建一个类,实现`ISayPlugin`接口即可。不过为了方便,也可先继承`PluginBase`基类,然后实现所需的插件接口。
```csharp showLineNumbers
public class SayHelloPlugin : PluginBase, ISayPlugin
{
public async Task Say(object sender, MyPluginEventArgs e)
{
Console.WriteLine($"{this.GetType().Name}------Enter");
if (e.Words == "hello")
{
Console.WriteLine($"{this.GetType().Name}------Say");
//当满足的时候输出,且不在调用下一个插件。
//亦或者设置e.Handled = true即使调用下一个插件也会无效
return;
}
await e.InvokeNext();
Console.WriteLine($"{this.GetType().Name}------Leave");
}
}
public class SayHiPlugin : PluginBase, ISayPlugin
{
public async Task Say(object sender, MyPluginEventArgs e)
{
Console.WriteLine($"{this.GetType().Name}------Enter");
if (e.Words == "hi")
{
Console.WriteLine($"{this.GetType().Name}------Say");
//当满足的时候输出,且不在调用下一个插件。
//亦或者设置e.Handled = true即使调用下一个插件也会无效
return;
}
await e.InvokeNext();
Console.WriteLine($"{this.GetType().Name}------Leave");
}
}
internal class LastSayPlugin : PluginBase, ISayPlugin
{
public async Task Say(object sender, MyPluginEventArgs e)
{
Console.WriteLine($"{this.GetType().Name}------Enter");
Console.WriteLine($"您输入的{e.Words}似乎不被任何插件处理");
await e.InvokeNext();
Console.WriteLine($"{this.GetType().Name}------Leave");
}
}
```
:::tip 提示
实现的插件,建议继承`PluginBase`,然后实现所需的插件接口,这样能简化实现过程。但是如果该类型已经拥有基类,则直接实现所需插件接口的全部内容即可。
:::
## 六、订阅插件
在插件触发前,需要先订阅插件。这样才知道哪些插件可以处理该数据。
### 6.1 创建插件管理器
```csharp showLineNumbers
IPluginManager pluginManager = new PluginManager(new Container())
{
Enable = true//必须启用
};
```
### 6.2 添加订阅插件
#### 6.2.1 按类型添加
```csharp showLineNumbers
//添加订阅插件
pluginManager.Add<SayHelloPlugin>();
pluginManager.Add<SayHiPlugin>();
pluginManager.Add<LastSayPlugin>();
```
#### 6.2.2 按实例添加
```csharp showLineNumbers
pluginManager.Add(new SayHelloPlugin());
pluginManager.Add(new SayHiPlugin());
pluginManager.Add(new LastSayPlugin());
```
#### 6.2.3 按委托添加
委托添加插件有多个重载,下面一一为例:
```csharp showLineNumbers
pluginManager.Add(typeof(ISayPlugin), () =>
{
//无参委托,一般做通知
Console.WriteLine("在Action1中获得");
});
pluginManager.Add(typeof(ISayPlugin), async (MyPluginEventArgs e) =>
{
//只1个指定参数当参数是事件参数时需要主动InvokeNext
Console.WriteLine("在Action2中获得");
await e.InvokeNext();
});
pluginManager.Add(typeof(ISayPlugin), async (client, e) =>
{
//2个不指定参数需要主动InvokeNext
Console.WriteLine("在Action3中获得");
await e.InvokeNext();
});
pluginManager.Add(typeof(ISayPlugin), async (object client, MyPluginEventArgs e) =>
{
//2个指定参数需要主动InvokeNext
Console.WriteLine("在Action3中获得");
await e.InvokeNext();
});
```
:::tip 提示
需不需要`InvokeNext`,只需要记住一点,委托中是否接收了`PluginEventArgs`派生的事件参数,如果是,则需要主动调用。
:::
## 七、触发插件
直接使用`RaiseAsync`方法即可触发插件。
在触发时,`sender`参数和`PluginEventArgs`参数必须和插件中定义的方法参数一致,不然会抛出异常。
```csharp {1}
await pluginManager.RaiseAsync(typeof(ISayPlugin), new object(), new MyPluginEventArgs()
{
Words = Console.ReadLine()
});
```
### 7.1 执行结果
按照上述代码代码逻辑,我们声明了一个名为`ISayPlugin`的插件接口,里面包含一个`Say`的方法。然后分别创建了`SayHelloPlugin`、`SayHiPlugin`、`LastSayPlugin`三个类去实现`ISayPlugin`接口。然后将该三个类都添加至插件管理器中,然后触发`Say`方法。同时传入不同的参数。
当Words=test时`SayHelloPlugin`和`SayHiPlugin`均不满足处理条件,所以会将数据转至下一个插件,直到`LastSayPlugin`插件。然后当`LastSayPlugin`处理结束以后,处理结果又按照`LastSayPlugin`、`SayHiPlugin`、`SayHelloPlugin`的顺序退出插件。这样,即使`SayHelloPlugin`无法处理该数据,也能得知该数据最终有没有被处理。
当Words=hello时`SayHelloPlugin`满足处理条件,并且终止插件的继续传递。
```
请输入hello或者hi
test
SayHelloPlugin------Enter
SayHiPlugin------Enter
LastSayPlugin------Enter
您输入的test似乎不被任何插件处理
LastSayPlugin------Leave
SayHiPlugin------Leave
SayHelloPlugin------Leave
请输入hello或者hi
hello
SayHelloPlugin------Enter
SayHelloPlugin------Say
请输入hello或者hi
hi
SayHelloPlugin------Enter
SayHiPlugin------Enter
SayHiPlugin------Say
SayHelloPlugin------Leave
请输入hello或者hi
```
## 八、插件特性
### 8.1 中断传递
当某个插件在响应时,如果设置`e.Handled=true`,或者没有调用下一个插件`e.InvokeNext`,则该数据将**不会**再触发后续的插件。
## 九、提升插件性能
### 9.1 插件性能测试
如下图所示添加10个插件并且调用10000次。即单个插件方法被调用10w次。
分别在net6.0netcore3.1net4.6.1上进行测试。测试项依次为:
1. DirectRun直接调用
2. ActionRun委托调用
3. MethodInfoRun反射调用
4. ExpressionRun表达式树调用
5. PluginRun插件调用
6. PluginActionRun插件委托调用
实际上插件内部使用的是IL调用所以即使调用上有迭代性能上也和直接调用的表达式树差不多这里插件是采用递归的方式迭代而表达式树测试则是直接for迭代。但是总体而言性能上是有缺失的。
但是当使用插件委托调用时性能上会有所提升。和直接调用相比虽然性能降低了20%。但是这已经是委托调用的极限且没有任何动态代码的生成。这意味着在unity调用也是完全可行的。
<img src={require('@site/static/img/docs/pluginsmanager-1.png').default} />
### 9.2 注册委托
注册委托,使用委托,可以提升插件性能。
```csharp showLineNumbers
//订阅插件,不仅可以使用声明插件的方式,还可以使用委托。
pluginsManager.Add(typeof(ISayPlugin), () =>
{
Console.WriteLine("在Action1中获得");
});
```
其次,不仅可以直接使用委托,还可以在插件里面注册方法为委托。
```csharp showLineNumbers
public class SayHelloAction: PluginBase
{
protected override void Loaded(IPluginsManager pluginsManager)
{
base.Loaded(pluginsManager);
//注册本地方法为委托
pluginsManager.Add<object,MyPluginEventArgs>(typeof(ISayPlugin),this.Say);
}
public async Task Say(object sender, MyPluginEventArgs e)
{
Console.WriteLine($"{this.GetType().Name}------Enter");
if (e.Words == "helloaction")
{
Console.WriteLine($"{this.GetType().Name}------Say");
//当满足的时候输出,且不在调用下一个插件。
//亦或者设置e.Handled = true即使调用下一个插件也会无效
return;
}
await e.InvokeNext();
Console.WriteLine($"{this.GetType().Name}------Leave");
}
}
```
:::caution 注意事项
注册方法为委托时,不要再实现接口,避免重复调用。
:::
### 9.3 源生成插件
使用源生成器直接可以生成委托调用但是这要求你的编译器支持一般vs2022以上、或者Rider等
一般的,这不需要你的关系,因为当源生成可用时,会自动使用源生成器。
[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/Core/PluginConsoleApp)