侧边栏壁纸
博主头像
分享你我博主等级

行动起来,活在当下

  • 累计撰写 112 篇文章
  • 累计创建 13 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

nodejs 实现一个插件式框架,支持热插热拔

管理员
2024-12-16 / 0 评论 / 0 点赞 / 19 阅读 / 9398 字

目录结构

project/

├── plugins/

│ ├── shopSignIn.js // 示例插件

├── PluginFramework.js // 插件框架

├── HotReloadPluginFramework.js // 插件框架

├── PluginParser.js // 规则解析器

└── index.js // 启动文件

PluginFramework.js插件管理器

const fs = require('fs');
const path = require('path');
const PluginParser = require('./PluginParser');

class PluginFramework {
  constructor(pluginDirectory) {
    this.plugins = {}; // 存储插件
    this.pluginDirectory = pluginDirectory || path.join(__dirname, 'plugins'); // 插件目录
    this.loadPlugins(); // 加载插件
  }
  // 加载插件
  loadPlugins() {
    const pluginFiles = fs.readdirSync(this.pluginDirectory);
    pluginFiles.forEach(file => {
      if (file.endsWith('.js')) {
        const pluginName = path.basename(file, '.js');
        const pluginPath = path.join(this.pluginDirectory, file);

        // 如果插件已加载,跳过
        if (this.plugins[pluginName]) {
          console.log(`插件 ${pluginName} 已经加载,跳过`);
          return;
        }
        const plugin = require(pluginPath);
        this.plugins[pluginName] = this.loadPluginRules(pluginName, pluginPath);
        console.log(`插件 ${pluginName} 已加载`);
      }
    });
  }

  // 解析插件规则
  loadPluginRules(pluginName, pluginPath) {
    const pluginContent = fs.readFileSync(pluginPath, 'utf-8');
    // 创建解析器实例并解析插件注释
    const parser = new PluginParser();
    const pluginMetadata = parser.parse(pluginContent);
    // console.log(`插件 ${pluginName} 的元数据:`, pluginMetadata);
    // 确保返回插件的实际函数
    const pluginFunction = require(pluginPath);  // 这里是加载插件文件并获得其导出的函数
    return {
      pluginFunction,  // 保存实际插件函数
      rules: pluginMetadata.rules// 保存规则
    };
  }

  // 从插件文件内容中提取规则
  extractRules(content) {
    const ruleRegex = /@rule\s+([^\n]+)/g;  // 匹配 @rule 后面的一行规则
    const rules = [];
    let match;
    while ((match = ruleRegex.exec(content)) !== null) {
      // 去除规则字符串中的多余空格和换行符
      const rule = match[1].replace(/[\r\n]+/g, '').trim();
      rules.push(new RegExp(rule)); // 转换为正则表达式并存储
    }
    return rules;
  }

  // 触发插件处理消息事件
  triggerEvent(s) {
    if (!s || !s.message) {
      console.error("消息对象格式错误,缺少 message 属性");
      return;
    }

    // 插件链的执行
    let currentIndex = 0;  // 插件索引

    const next = () => {
      if (currentIndex < Object.keys(this.plugins).length) {
        const plugin = this.plugins[Object.keys(this.plugins)[currentIndex]];
        currentIndex++;
        // 遍历插件的规则
        let matched = false;
        plugin.rules.forEach(rule => {
          if (!matched && rule.test(s.message)) {
            // 匹配成功,执行插件的处理函数
            plugin.pluginFunction(s);
            matched = true;  // 标记已匹配
          }
        });

        //如果没有匹配到规则,传递到下一个插件
        if (!matched) {
          next();  // 查找下一个符合规则的插件
        }
      } else {
        console.log("所有插件均未匹配到规则,消息处理结束");
      }
    };
    s.next = next;  // 保存继续传递的函数
    next();  // 启动第一个插件
  }
}

module.exports = PluginFramework;

PluginParser.js插件规则解析器实现

class PluginParser {
    // 解析注释内容
    parse(content) {
        const metadata = {};

        // 提取规则
        metadata.rules = this.extractRules(content);

        // 提取其他字段
        metadata.description = this.extractField(content, '@description');
        metadata.team = this.extractField(content, '@team');
        metadata.author = this.extractField(content, '@author');
        metadata.version = this.extractField(content, '@version');
        metadata.name = this.extractField(content, '@name');
        metadata.priority = this.extractField(content, '@priority');
        metadata.admin = this.extractField(content, '@admin');
        metadata.disable = this.extractField(content, '@disable');
        metadata.public = this.extractField(content, '@public');
        metadata.authentication = this.extractField(content, '@authentication');
        metadata.classification = this.extractField(content, '@classification');

        return metadata;
    }

    // 提取特定字段的值
    extractField(content, field) {
        const regex = new RegExp(`\\${field}\\s+([^\n]+)`);
        const match = content.match(regex);
        return match ? match[1].trim() : null;
    }
    // 从插件文件内容中提取规则
    extractRules(content) {
        const ruleRegex = /@rule\s+([^\n]+)/g;  // 匹配 @rule 后面的一行规则
        const rules = [];
        let match;
        while ((match = ruleRegex.exec(content)) !== null) {
            // 去除规则字符串中的多余空格和换行符
            const rule = match[1].replace(/[\r\n]+/g, '').trim();
            rules.push(new RegExp(rule)); // 转换为正则表达式并存储
        }
        return rules;
    }
}
module.exports = PluginParser;

扩展PluginFramework实现热插热拔

// HotReloadPluginFramework.js
const PluginFramework = require('./PluginFramework');  // 导入基础插件框架
const chokidar = require('chokidar');
const path = require('path');
const fs = require('fs');

class HotReloadPluginFramework extends PluginFramework {
  constructor(pluginDirectory) {
    super(pluginDirectory);  // 调用父类构造函数
    this.pluginFileCache = {}; // 用来缓存插件的文件内容
    this.watchPluginDirectory(); // 开始监听插件目录
  }

  // 监听插件目录的变化
  watchPluginDirectory() {
    const watcher = chokidar.watch(this.pluginDirectory, {
      ignored: /^\./, // 忽略隐藏文件
      persistent: true,
    });

    watcher.on('change', (filePath) => {
      const pluginName = path.basename(filePath, '.js');
      // 只有当文件内容真正发生变化时,才重新加载
      this.reloadPluginIfChanged(pluginName, filePath);
    });

    watcher.on('add', (filePath) => {
      const pluginName = path.basename(filePath, '.js');
      // 只有插件没有加载过时,才重新加载
      if (this.plugins[pluginName]) {
        return;
      }
      console.log(`插件文件 ${pluginName} 被添加`);
      this.reloadPlugin(pluginName, filePath);
    });

    watcher.on('unlink', (filePath) => {
      const pluginName = path.basename(filePath, '.js');
      console.log(`插件文件 ${pluginName} 被删除`);
      this.unloadPlugin(pluginName);  // 卸载被删除的插件
    });
  }

  // 判断插件文件是否发生变化
  reloadPluginIfChanged(pluginName, filePath) {
    try {
      // 检查 filePath 是否有效
      if (!filePath) {
        throw new Error(`插件文件路径无效: ${filePath}`);
      }
      // 读取文件内容
      const newContent = fs.readFileSync(filePath, 'utf-8');
      // 比较缓存的内容和当前内容
      const cachedContent = this.pluginFileCache[pluginName];
      if (cachedContent !== newContent) {
        this.reloadPlugin(pluginName, filePath);
        this.pluginFileCache[pluginName] = newContent; // 更新缓存的内容
      }
    } catch (err) {
      console.error(`加载插件 ${pluginName} 时发生错误: ${err.message}`);
    }
  }

  // 重新加载插件
  reloadPlugin(pluginName, filePath) {
    try {
      console.log(`重新加载插件 ${pluginName}`);
      this.unloadPlugin(pluginName);  // 卸载旧插件

      // 检查文件路径是否有效
      if (!filePath || !fs.existsSync(filePath)) {
        throw new Error(`插件文件 ${filePath} 不存在`);
      }

      // 清除缓存,确保插件被重新加载
      delete require.cache[require.resolve(filePath)];

      // 重新加载插件
      const plugin = require(filePath);
      this.plugins[pluginName] = plugin;

      console.log(`插件 ${pluginName} 已重新加载`);
    } catch (err) {
      console.error(`加载插件 ${pluginName} 时发生错误: ${err.message}`);
    }
  }

  // 卸载插件
  unloadPlugin(pluginName) {
    if (this.plugins[pluginName]) {
      // 删除插件缓存
      delete this.plugins[pluginName];
      console.log(`插件 ${pluginName} 已卸载`);
    }
  }
}

module.exports = HotReloadPluginFramework;  // 导出 HotReloadPluginFramework 类

添加一个插件测试一下效果

/**
 * @description 监听签到的店铺
 * @team youfak
 * @author youfak
 * @version 2.0.1
 * @name 店铺签到
 * @rule ([A-Fa-f0-9]{32})&(\d+)&(\d+)
 * @rule ^(qd [0-9A-Fa-f]{32})
 * @priority 100000
 * @admin false
 * @disable false
 * @public false
 * @authentication true
 * @classification ["工具"]
 */
module.exports = async s => {
    const message = s.message.trim();
    console.log(message);
    // 如果消息匹配店铺签到规则
    if (/([A-Fa-f0-9]{32})&(\d+)&(\d+)/.test(message)) {
        s.text("匹配到店铺签到规则!");
        return; // 如果匹配,直接响应并退出
    }
    
    // 如果没有匹配规则,传递消息给下一个插件
    s.next();
};

启动程序

const fs = require('fs');
const path = require('path');
// 引入 PluginFramework 类
// const PluginFramework = require('./PluginFramework');
// 创建框架实例
// const framework = new PluginFramework(path.join(__dirname, 'plugins')); // 这里指定了插件目录

// 引入 HotReloadPluginFramework 类 扩展PluginFramework实现热重载
const HotReloadPluginFramework = require('./HotReloadPluginFramework');
// 创建 HotReloadPluginFramework 实例,指定插件目录
const pluginDirectory = path.join(__dirname, 'plugins');  // 插件目录
const framework = new HotReloadPluginFramework(pluginDirectory);
// 模拟消息对象
const s = {
    message: "qd 1234567890abcdef1234567890abcdef&123&456",  // 模拟消息
    text: (msg) => { console.log(msg); },  // 输出返回消息
    next: () => { console.log("消息未匹配到规则,继续传递"); }  // 继续传递
};

// 确保 `s.message` 存在且为字符串
if (s.message && typeof s.message === 'string') {
    // 触发事件,处理消息
    framework.triggerEvent(s);
} else {
    console.error("消息内容为空或格式不正确");
}

0

评论区