mirror of
https://github.com/RRQM/TouchSocket.git
synced 2025-12-20 02:16:42 +08:00
在多个 `.mdx` 文件中添加对 `TouchSocketCoreDefinition`、`TouchSocketHttpDefinition`、`TouchSocketDmtpDefinition`、`TouchSocketModbusDefinition`、`TouchSocketProModbusDefinition`、`TouchSocketNamedPipeDefinition`、`TouchSocketWebApiDefinition`、`TouchSocketJsonRpcDefinition` 和 `TouchSocketXmlRpcDefinition` 的导入。增加解析和处理定义部分的 JavaScript 脚本,确保导入的有效性和一致性。新增功能包括精确匹配定义部分的正则表达式、解析定义内容的函数、生成定义组件的函数、递归查找 `.mdx` 文件的功能,以及验证导入的有效性
332 lines
10 KiB
Plaintext
332 lines
10 KiB
Plaintext
---
|
||
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.0,netcore3.1,net4.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) |