TUTORIAL

钩子机制与插件系统

钩子机制与插件系统

钩子机制与插件系统

WordPress 为什么能撑起全球近半数网站?答案在于它的插件生态——任何人都能写插件扩展功能,而不必改动核心代码。这种"可扩展性"的底层支撑就是钩子机制。钩子(Hook)本质是"预埋的扩展点":核心代码在关键位置埋下钩子,插件把自己的函数挂上去,代码执行到此处时自动调用挂载的函数。本篇带你从零实现一套钩子与插件系统,理解建站框架的扩展原理。

一、钩子的两种形态:动作与过滤器

钩子分两类:动作(Action)和过滤器(Filter)。动作用于"在某个时机做点事",比如发布文章时发邮件通知,不关心返回值;过滤器用于"修改某个数据",比如把文章标题里的敏感词替换掉,必须返回修改后的值。两者底层实现几乎一样,区别只在于是否使用返回值。

// 钩子管理器(动作 + 过滤器统一实现)
class HookManager {
  constructor() {
    this.hooks = {};  // { hookName: [ {callback, priority} ] }
  }

  // 注册钩子:把回调挂到指定钩子上
  addHook(name, callback, priority = 10) {
    if (!this.hooks[name]) this.hooks[name] = [];
    this.hooks[name].push({ callback, priority });
    // 按 priority 升序排列,数字小的先执行
    this.hooks[name].sort((a, b) => a.priority - b.priority);
  }

  // 执行动作:依次调用所有回调,不关心返回值
  doAction(name, ...args) {
    const handlers = this.hooks[name] || [];
    for (const h of handlers) {
      h.callback(...args);
    }
  }

  // 应用过滤器:依次调用,把上一次返回值传给下一次
  applyFilters(name, value, ...args) {
    const handlers = this.hooks[name] || [];
    for (const h of handlers) {
      value = h.callback(value, ...args);  // 链式传递
    }
    return value;
  }
}

const hooks = new HookManager();

优先级参数让插件能控制执行顺序——尧图项目里"敏感词过滤"插件优先级设 5(先执行),"内容缓存"插件优先级设 20(后执行),保证缓存的是过滤后的干净内容。这种"优先级排序 + 链式传递"的设计,让多个插件互不冲突地协同工作。

二、在核心代码中埋钩子

钩子机制要发挥作用,核心代码必须"主动埋钩子"——在关键流程位置调用 doActionapplyFilters。埋点的位置就是扩展点,决定了插件能干预哪些环节。下面是一个文章发布流程的埋点示例:

// 文章发布流程(核心代码,预埋钩子)
async function publishPost(postData) {
  // 1. 过滤器:发布前允许插件修改文章内容
  postData.title   = hooks.applyFilters('pre_publish_title',   postData.title);
  postData.content = hooks.applyFilters('pre_publish_content', postData.content);

  // 2. 动作:保存前通知插件(如做内容校验、日志记录)
  hooks.doAction('before_publish', postData);

  // 3. 写入数据库
  const postId = await db.insert('posts', postData);

  // 4. 动作:发布完成后通知插件(如发邮件、推送、生成静态页)
  hooks.doAction('after_publish', postId, postData);

  return postId;
}

// 插件1:敏感词过滤(挂在过滤器上,修改内容)
hooks.addHook('pre_publish_content', (content) => {
  const badWords = ['违规词1', '违规词2'];
  badWords.forEach(w => content = content.replace(w, '***'));
  return content;  // 过滤器必须返回值
}, 5);

// 插件2:发布后发邮件通知(挂在动作上,不修改数据)
hooks.addHook('after_publish', (postId, postData) => {
  mailer.send({
    to: 'editor@ldpk.cn',
    subject: '新文章已发布:' + postData.title,
    body: '文章ID:' + postId
  });
}, 10);

// 插件3:发布后生成静态缓存页(挂在动作上)
hooks.addHook('after_publish', (postId) => {
  generateStaticPage(postId);
}, 20);  // 优先级20,邮件发送后再生成缓存

核心代码完全不知道插件的存在,它只是在固定位置"喊一嗓子",有没有人响应都不影响主流程。这种"发布-订阅"模式让核心与插件彻底解耦——卸载某个插件,核心照常运行;新增插件,核心无需改动。尧图在做 CMS 二开时,几乎从不改核心文件,全部用钩子挂载自定义逻辑,升级核心时不会丢失定制功能。

三、插件加载与生命周期

光有钩子还不够,还要有"插件管理"——自动扫描插件目录、按顺序加载、提供启用/禁用开关。一个规范的插件系统,每个插件是一个独立文件,包含元信息(名称、版本、作者)和初始化逻辑(注册钩子)。下面是尧图常用的插件加载器实现:

// 插件加载器
class PluginLoader {
  constructor(hookManager) {
    this.hooks = hookManager;
    this.plugins = {};  // { pluginName: { meta, enabled } }
  }

  // 扫描插件目录并加载
  loadAll(pluginDir) {
    const files = fs.readdirSync(pluginDir).filter(f => f.endsWith('.plugin.js'));
    for (const file of files) {
      const plugin = require(path.join(pluginDir, file));
      // 校验插件元信息
      if (!plugin.name || !plugin.init) {
        console.warn('插件格式非法:' + file);
        continue;
      }
      this.plugins[plugin.name] = {
        meta: { name: plugin.name, version: plugin.version, author: plugin.author },
        enabled: plugin.autoEnable !== false  // 默认启用
      };
      // 启用的插件执行初始化(注册钩子)
      if (this.plugins[plugin.name].enabled) {
        plugin.init(this.hooks);
        console.log('插件已加载:' + plugin.name + ' v' + plugin.version);
      }
    }
  }

  // 动态启用/禁用
  toggle(name, enabled) {
    if (!this.plugins[name]) return false;
    this.plugins[name].enabled = enabled;
    // 注意:禁用已加载的钩子需额外维护回调引用,此处从略
    return true;
  }
}

// 单个插件文件示例:email-notify.plugin.js
module.exports = {
  name: 'email-notify',
  version: '1.0.0',
  author: '尧图建站',
  autoEnable: true,
  init(hooks) {
    // 在初始化里注册钩子
    hooks.addHook('after_publish', (postId, post) => {
      mailer.send({ to: 'editor@ldpk.cn', subject: post.title });
    });
  }
};

插件加载顺序是个容易被忽视的坑——如果两个插件都过滤同一内容,执行顺序不同结果可能不同。尧图的实践是:在插件元信息里声明依赖(depends: ['sensitive-filter']),加载器先做拓扑排序再依次加载。另外插件不要直接操作数据库表结构,应该通过核心提供的 API,否则核心升级改了表结构,插件就崩了。理解钩子与插件原理,你就能在不碰核心代码的前提下,给任何建站系统"加料",这是从"用框架"到"玩转框架"的关键一步。

返回教程列表