插件开发文档

博客系统 v1.0+ 支持可扩展的插件系统

概述

插件系统允许开发者扩展博客功能,无需修改核心代码。每个插件可以是纯前端插件(注入 CSS/JS)或全栈插件(包含后端 API)。

目录结构

plugins/
└── my-plugin/
    ├── manifest.json    # 插件配置(必需)
    ├── main.js         # 前端入口(可选)
    ├── server.js       # 后端入口(可选)
    ├── style.css       # 样式文件(可选)
    └── partials/       # Handlebars 片段(可选)
        └── my-partial.hbs

manifest.json

插件配置文件:

{
  "name": "my-plugin",
  "displayName": "我的插件",
  "version": "1.0.0",
  "description": "我的插件描述",
  "author": "作者名",
  "frontend": {
    "entry": "main.js",
    "style": "style.css"
  },
  "backend": {
    "entry": "server.js"
  },
  "hooks": ["head_css", "after_header", "body_js"],
  "hookContents": {
    "head_css": "<link rel='stylesheet' href='/plugins/my-plugin/style.css'>",
    "after_header": "<div class='plugin-banner'>欢迎访问我的博客</div>",
    "body_js": ""
  },
  "themeAware": false,
  "compatibility": {
    "minEngineVersion": "1.0.0",
    "themes": ["default"]
  }
}

字段说明

字段 类型 必需 说明
name string 插件唯一名称(目录名)
displayName string 插件显示名称
version string 版本号
description string 插件描述
author string 作者
frontend object 前端配置,包含 entry(入口文件)、style(样式文件)
backend object 后端配置,包含 entry(入口文件)
hooks string[] 声明使用的 Hook
hookContents object Hook 对应的内容
themeAware boolean 是否响应主题变更
compatibility object 兼容性配置,包含 minEngineVersionthemes

前端插件 (main.js)

纯前端逻辑,可在页面加载时执行:

// main.js - 前端入口

// 插件初始化
(function() {
  'use strict';

  // 获取插件上下文
  const pluginContext = window.__pluginRuntime;
  const pluginName = 'my-plugin';
  const themeName = pluginContext?.themeName || 'default';

  // 监听主题变更(如果启用 themeAware)
  if (pluginContext?.themeAware) {
    document.addEventListener('theme:changed', function(e) {
      console.log('Theme changed:', e.detail);
      init();
    });
  }

  function init() {
    console.log(`[${pluginName}] Initialized for theme: ${themeName}`);

    // 你的插件逻辑
    const container = document.querySelector('.plugin-hook-container[data-plugin-hook="after_header"]');
    if (container) {
      container.innerHTML += '<div class="my-plugin-element">插件内容</div>';
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();

后端插件 (server.js)

提供后端 API 路由和数据处理能力:

API 路径规则:插件的 API 自动挂载到 /plugin-api/{插件名}/ 前缀下。 例如 router.get('/data', ...) 最终可通过 GET /plugin-api/my-plugin/data 访问。

// server.js - 后端入口

/**
 * @param {express.Router} router - Express 路由实例
 * @param {Object} options - 插件选项
 * @param {Object} options.db - 数据库实例
 * @param {Object} options.config - 插件配置对象
 * @param {string} options.pluginDir - 插件目录路径
 * @param {string} options.pluginName - 插件名称
 */
export function registerRoutes(router, { db, config, pluginDir, pluginName }) {
  // 路由会自动挂载到 /plugin-api/{pluginName}/
  // 例如:GET /plugin-api/my-plugin/data
  
  router.get('/data', async (req, res) => {
    try {
      const data = await getPluginData(db);
      res.json({ success: true, data });
    } catch (err) {
      res.status(500).json({ success: false, error: err.message });
    }
  });

  router.post('/save', async (req, res) => {
    try {
      const { key, value } = req.body;
      await saveData(db, key, value);
      res.json({ success: true });
    } catch (err) {
      res.status(500).json({ success: false, error: err.message });
    }
  });

  console.log(`[${pluginName}] Routes registered`);
}

/**
 * 主题变更回调(可选)
 */
export async function onThemeChanged(oldTheme, newTheme) {
  console.log(`[my-plugin] Theme changed: ${oldTheme} -> ${newTheme}`);
}

/**
 * 插件停用回调(可选)
 */
export async function onDeactivate() {
  console.log('[my-plugin] Deactivating...');
}

Hook 系统

可用 Hook 列表

Hook 名称 位置 说明
head_css <head> 注入 CSS 链接
after_header <header> 紧跟 header 后
before_nav_end 导航内部末尾 导航栏末尾
before_content <main> 主内容区之前
after_content <main> 主内容区之后
after_post_content 文章内容后 文章正文之后
sidebar_top 侧边栏顶部 侧边栏开始处
sidebar_bottom 侧边栏底部 侧边栏结束处
before_footer <footer> Footer 之前
body_js <body> 末尾 注入 JS 脚本

Hook 注入方式

方式一:manifest.json 声明

{
  "name": "simple-plugin",
  "hooks": ["head_css", "after_header"],
  "hookContents": {
    "head_css": "<link rel='stylesheet' href='/plugins/simple-plugin/style.css'>",
    "after_header": "<div class='simple-plugin-banner'>欢迎访问</div>"
  }
}

方式二:前端动态注入

// main.js
function injectContent() {
  const headHook = document.querySelector('.plugin-hook-container[data-plugin-hook="head_css"]');
  if (headHook) {
    headHook.innerHTML += '<link rel="stylesheet" href="/plugins/my-plugin/dynamic.css">';
  }
}

主题感知 (themeAware)

如果插件需要适配不同主题,设置 themeAware: true

{
  "name": "adaptive-plugin",
  "themeAware": true,
  "compatibility": {
    "themes": ["default", "dark-mode", "minimal"]
  }
}

前端会自动收到主题变更事件:

document.addEventListener('theme:changed', function(e) {
  const { theme } = e.detail;
  // 重新渲染插件 UI
});

样式隔离

建议使用插件名前缀避免样式冲突:

.my-plugin-container {
  padding: 16px;
}

.my-plugin-title {
  font-size: 18px;
  font-weight: bold;
}

/* 使用 BEM 命名 */
.my-plugin__item {
  margin: 8px 0;
}

.my-plugin__item--active {
  background: var(--primary);
}

安装插件

方式一:直接放置

  1. 将插件目录复制到 plugins/ 目录
  2. 确保目录名与 manifest.json 中的 name 一致
  3. 在后台管理页面启用插件

方式二:上传安装

  1. 进入 后台管理 → 插件管理
  2. 点击「上传插件」按钮
  3. 选择插件 ZIP 包(包含完整插件目录)
  4. 系统自动解压并安装
  5. 启用插件

最佳实践

  1. 样式隔离:使用唯一前缀命名 CSS 类
  2. 资源路径:使用相对路径引用插件内部资源
  3. 错误处理:后端 API 必须有完整的错误处理
  4. 主题兼容:测试多个主题下的显示效果
  5. 清理资源:实现 onDeactivate 清理临时数据