前言

在前四章中,我们完成了一个功能完整的 Aurora 主题。本章将深入探讨 Hexo 插件系统,学习如何开发、使用和发布插件。

本章内容:

  • 🔌 插件机制 - 工作原理
  • 📦 插件开发 - 实战案例
  • 🛠️ 常用插件 - 生态精选
  • 🔗 协作模式 - 主题与插件
  • 📚 发布管理 - NPM 包
  • 🎯 最佳实践 - 开发规范

第一部分:插件机制深度解析

1.1 插件 vs 主题 Scripts

核心区别

特性主题 Scripts插件
作用域当前主题全局
加载时机主题启用时Hexo 启动时
适用场景主题特定功能通用功能
分发方式随主题NPM 独立包
版本管理跟随主题独立版本

何时使用插件

适合做成插件:

  • 通用功能(不依赖特定主题)
  • 可复用(多个项目都能用)
  • 独立维护(有独立版本管理)
  • 社区分享(希望其他人也能用)

不适合做成插件:

  • 主题特定功能
  • 简单定制(几行代码)
  • 临时需求(一次性使用)

1.2 插件加载流程

/**
 * Hexo 启动时的插件加载过程
 */

// 1. 初始化 Hexo 实例
const Hexo = require('hexo');
const hexo = new Hexo(process.cwd());

// 2. 加载配置
hexo.init()
  // 3. 扫描并加载插件
  .then(() => loadPlugins(hexo))
  // 4. 加载主题
  .then(() => hexo.loadTheme())
  // 5. 执行命令
  .then(() => hexo.call(command, args));

1.3 插件结构

标准插件目录:

hexo-plugin-example/
├── index.js              # 入口文件
├── lib/                  # 核心代码
│   ├── filter.js
│   ├── generator.js
│   └── helper.js
├── test/                 # 测试
├── package.json
└── README.md

入口文件模式:

// 模式 1:函数式(最常用)
module.exports = function(hexo) {
  const config = Object.assign({
    enable: true
  }, hexo.config.plugin_example);
  
  if (!config.enable) return;
  
  // 注册扩展
  require('./lib/filter')(hexo);
  require('./lib/generator')(hexo);
};

// 模式 2:对象式
module.exports = {
  init: function(hexo) {
    // 初始化逻辑
  },
  metadata: {
    name: 'hexo-plugin-example',
    version: '1.0.0'
  }
};

// 模式 3:类式(复杂插件)
class Plugin {
  constructor(hexo) {
    this.hexo = hexo;
    this.config = this.loadConfig();
  }
  
  init() {
    this.registerFilters();
  }
}

module.exports = function(hexo) {
  const plugin = new Plugin(hexo);
  plugin.init();
};

第二部分:插件开发实战

2.1 案例:图片优化插件

一个完整的图片压缩插件,支持 JPG/PNG 压缩、WebP 生成和响应式图片。

package.json

{
  "name": "hexo-image-optimizer",
  "version": "1.0.0",
  "description": "Optimize images for Hexo",
  "main": "index.js",
  "keywords": ["hexo", "plugin", "image"],
  "engines": {
    "node": ">=14.0.0",
    "hexo": ">=5.0.0"
  },
  "dependencies": {
    "sharp": "^0.32.0",
    "imagemin": "^8.0.0"
  }
}

index.js

'use strict';

module.exports = function(hexo) {
  const config = Object.assign({
    enable: true,
    jpg: { quality: 80 },
    png: { quality: [0.6, 0.8] },
    webp: { enable: false }
  }, hexo.config.image_optimizer);
  
  if (!config.enable) return;
  
  // 生成后优化图片
  hexo.extend.filter.register('after_generate', async function() {
    const images = getAllImages(hexo.public_dir);
    
    for (const img of images) {
      await optimizeImage(img, config);
    }
    
    hexo.log.info(`[image-optimizer] Optimized ${images.length} images`);
  });
  
  // Helper:响应式图片
  hexo.extend.helper.register('responsive_image', function(src, alt) {
    return `
      <picture>
        <source srcset="${src}.webp" type="image/webp">
        <img src="${src}" alt="${alt}" loading="lazy">
      </picture>
    `;
  });
};

2.2 案例:阅读统计插件

记录和展示文章阅读量。

核心功能:

module.exports = function(hexo) {
  const storage = require('./lib/storage')(hexo);
  
  // Helper:显示阅读量
  hexo.extend.helper.register('reading_count', function(path) {
    return storage.getCount(path);
  });
  
  // 注入客户端脚本
  hexo.extend.filter.register('after_render:html', function(str) {
    const script = '<script src="/reading-stats.js"></script>';
    return str.replace('</body>', script + '</body>');
  });
  
  // API 路由
  hexo.extend.generator.register('reading-api', function() {
    return {
      path: 'api/reading-stats.json',
      data: storage.getAll()
    };
  });
};

客户端脚本:

// public/reading-stats.js
(function() {
  const path = window.location.pathname;
  
  // 记录阅读(防重复)
  if (!localStorage.getItem(`read_${path}`)) {
    setTimeout(() => {
      localStorage.setItem(`read_${path}`, Date.now());
      updateCount(path);
    }, 5000); // 停留5秒后记录
  }
  
  // 更新显示
  async function updateCount(path) {
    const response = await fetch('/api/reading-stats.json');
    const data = await response.json();
    document.querySelector('.reading-count').textContent = data[path] || 0;
  }
})();

2.3 案例:代码复制插件

为代码块添加一键复制功能。

'use strict';

module.exports = function(hexo) {
  // 在代码块后添加复制按钮
  hexo.extend.filter.register('after_post_render', function(data) {
    const $ = require('cheerio').load(data.content);
    
    $('pre code').each(function() {
      const $code = $(this);
      const $pre = $code.parent();
      
      // 添加复制按钮
      $pre.prepend(`
        <button class="copy-btn" data-clipboard-text="${$code.text()}">
          复制代码
        </button>
      `);
    });
    
    data.content = $.html();
    return data;
  });
  
  // 注入 Clipboard.js
  hexo.extend.injector.register('head_end', `
    <link rel="stylesheet" href="/css/code-copy.css">
  `);
  
  hexo.extend.injector.register('body_end', `
    <script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
    <script>
      new ClipboardJS('.copy-btn').on('success', function(e) {
        e.trigger.textContent = '已复制!';
        setTimeout(() => e.trigger.textContent = '复制代码', 2000);
      });
    </script>
  `);
};

第三部分:Hexo 默认插件详解

Hexo 项目初始化时会自动安装一组核心插件,它们构成了 Hexo 的基础功能。让我们深入了解这些默认插件。

3.1 Generator 类插件

hexo-generator-index(首页生成器)

作用: 生成首页和分页

源码位置: node_modules/hexo-generator-index/

核心实现:

/**
 * hexo-generator-index 核心逻辑
 */

hexo.extend.generator.register('index', function(locals) {
  const config = this.config;
  const posts = locals.posts.sort(config.index_generator.order_by || '-date');
  const perPage = config.index_generator.per_page || 10;
  const paginationDir = config.pagination_dir || 'page';
  
  return pagination('', posts, {
    perPage: perPage,
    layout: ['index', 'archive'],
    format: paginationDir + '/%d/',
    data: {
      __index: true
    }
  });
});

配置选项:

# _config.yml
index_generator:
  path: ''           # 首页路径
  per_page: 10       # 每页文章数
  order_by: -date    # 排序方式(-date 降序)

生成的页面:

public/
├── index.html              # 第一页
├── page/2/index.html       # 第二页
├── page/3/index.html       # 第三页
└── ...

实际应用场景:

# 显示更多文章
index_generator:
  per_page: 15

# 按更新时间排序
index_generator:
  order_by: -updated

# 不分页(显示所有文章)
index_generator:
  per_page: 0

hexo-generator-archive(归档生成器)

作用: 生成归档页面(按年、按月)

核心实现:

/**
 * hexo-generator-archive 核心逻辑
 */

hexo.extend.generator.register('archive', function(locals) {
  const config = this.config;
  const archiveConfig = config.archive_generator;
  const posts = locals.posts.sort('-date');
  const perPage = archiveConfig.per_page || 10;
  
  const routes = [];
  
  // 1. 生成总归档页
  routes.push({
    path: archiveConfig.path || 'archives/',
    data: posts,
    layout: ['archive', 'index']
  });
  
  // 2. 按年归档
  if (archiveConfig.yearly) {
    const yearPosts = {};
    
    posts.forEach(post => {
      const year = post.date.year();
      if (!yearPosts[year]) yearPosts[year] = [];
      yearPosts[year].push(post);
    });
    
    Object.keys(yearPosts).forEach(year => {
      routes.push({
        path: `archives/${year}/`,
        data: yearPosts[year],
        layout: ['archive', 'index'],
        year: year
      });
    });
  }
  
  // 3. 按月归档
  if (archiveConfig.monthly) {
    const monthPosts = {};
    
    posts.forEach(post => {
      const year = post.date.year();
      const month = post.date.format('MM');
      const key = `${year}/${month}`;
      
      if (!monthPosts[key]) monthPosts[key] = [];
      monthPosts[key].push(post);
    });
    
    Object.keys(monthPosts).forEach(key => {
      const [year, month] = key.split('/');
      routes.push({
        path: `archives/${key}/`,
        data: monthPosts[key],
        layout: ['archive', 'index'],
        year: year,
        month: month
      });
    });
  }
  
  return routes;
});

配置选项:

# _config.yml
archive_generator:
  path: archives          # 归档页路径
  per_page: 10           # 每页文章数
  yearly: true           # 生成按年归档
  monthly: true          # 生成按月归档
  order_by: -date        # 排序方式

生成的页面:

public/
└── archives/
    ├── index.html              # 总归档
    ├── 2024/
    │   ├── index.html         # 2024年归档
    │   ├── 01/index.html      # 2024年1月
    │   ├── 02/index.html      # 2024年2月
    │   └── ...
    └── 2025/
        └── index.html

在模板中使用:

<!-- 显示归档列表 -->
<% if (is_archive()) { %>
  <% if (page.year) { %>
    <h1>归档:<%= page.year %>年
      <% if (page.month) { %>
        <%= page.month %>月
      <% } %>
    </h1>
  <% } else { %>
    <h1>全部归档</h1>
  <% } %>
  
  <!-- 文章列表 -->
  <% page.posts.each(function(post) { %>
    <%- partial('article', {post: post}) %>
  <% }) %>
<% } %>

hexo-generator-category(分类生成器)

作用: 为每个分类生成页面

核心实现:

/**
 * hexo-generator-category 核心逻辑
 */

hexo.extend.generator.register('category', function(locals) {
  const config = this.config;
  const categoryConfig = config.category_generator;
  const categories = locals.categories;
  
  return categories.reduce((result, category) => {
    if (!category.length) return result;
    
    const posts = category.posts.sort('-date');
    const path = category.path;
    
    // 生成分类页面
    result.push({
      path: path,
      data: category,
      layout: ['category', 'archive', 'index']
    });
    
    // 如果开启分页
    if (categoryConfig.per_page > 0) {
      const pages = Math.ceil(posts.length / categoryConfig.per_page);
      
      for (let i = 2; i <= pages; i++) {
        result.push({
          path: `${path}page/${i}/`,
          data: category,
          layout: ['category', 'archive', 'index'],
          current: i
        });
      }
    }
    
    return result;
  }, []);
});

配置选项:

# _config.yml
category_generator:
  path: categories       # 分类页路径
  per_page: 10          # 每页文章数

生成的页面:

public/
└── categories/
    ├── 技术/
    │   ├── index.html
    │   └── page/2/index.html
    ├── 生活/
    │   └── index.html
    └── ...

文章 Front-matter:

---
title: 文章标题
categories:
  - 技术
  - 前端
---

在模板中使用:

<!-- category.ejs -->
<% if (is_category()) { %>
  <h1>分类:<%= page.category %></h1>
  <p>共 <%= page.posts.length %> 篇文章</p>
  
  <% page.posts.each(function(post) { %>
    <%- partial('article', {post: post}) %>
  <% }) %>
<% } %>

<!-- 显示所有分类 -->
<%- list_categories() %>

<!-- 自定义分类列表 -->
<% site.categories.each(function(category) { %>
  <a href="<%- url_for(category.path) %>">
    <%= category.name %> (<%= category.length %>)
  </a>
<% }) %>

hexo-generator-tag(标签生成器)

作用: 为每个标签生成页面

核心实现:

/**
 * hexo-generator-tag 核心逻辑
 */

hexo.extend.generator.register('tag', function(locals) {
  const config = this.config;
  const tagConfig = config.tag_generator;
  const tags = locals.tags;
  
  return tags.reduce((result, tag) => {
    if (!tag.length) return result;
    
    const posts = tag.posts.sort('-date');
    const path = tag.path;
    
    // 生成标签页面
    result.push({
      path: path,
      data: tag,
      layout: ['tag', 'archive', 'index']
    });
    
    // 分页
    if (tagConfig.per_page > 0) {
      const pages = Math.ceil(posts.length / tagConfig.per_page);
      
      for (let i = 2; i <= pages; i++) {
        result.push({
          path: `${path}page/${i}/`,
          data: tag,
          layout: ['tag', 'archive', 'index'],
          current: i
        });
      }
    }
    
    return result;
  }, []);
});

配置选项:

# _config.yml
tag_generator:
  path: tags            # 标签页路径
  per_page: 10         # 每页文章数

生成的页面:

public/
└── tags/
    ├── JavaScript/
    │   └── index.html
    ├── React/
    │   └── index.html
    └── ...

文章 Front-matter:

---
title: 文章标题
tags:
  - JavaScript
  - React
  - 前端
---

在模板中使用:

<!-- tag.ejs -->
<% if (is_tag()) { %>
  <h1>标签:<%= page.tag %></h1>
  <p>共 <%= page.posts.length %> 篇文章</p>
  
  <% page.posts.each(function(post) { %>
    <%- partial('article', {post: post}) %>
  <% }) %>
<% } %>

<!-- 显示标签云 -->
<%- tagcloud() %>

<!-- 自定义标签列表 -->
<% site.tags.sort('length', -1).limit(10).each(function(tag) { %>
  <a href="<%- url_for(tag.path) %>">
    #<%= tag.name %> (<%= tag.length %>)
  </a>
<% }) %>

3.2 Renderer 类插件

hexo-renderer-ejs(EJS 模板渲染器)

作用: 渲染 .ejs 模板文件

核心实现:

/**
 * hexo-renderer-ejs 核心逻辑
 */

const ejs = require('ejs');

hexo.extend.renderer.register('ejs', 'html', function(data, options) {
  // 编译 EJS 模板
  return ejs.render(data.text, {
    ...options,
    filename: data.path
  });
}, true);

配置选项:

# _config.yml(主题配置)
ejs:
  compileDebug: false    # 关闭调试模式
  cache: true            # 启用缓存(生产环境)

模板语法:

<!-- 输出变量 -->
<%= title %>

<!-- 原始输出(不转义 HTML) -->
<%- content %>

<!-- JavaScript 代码 -->
<% if (is_post()) { %>
  <article>...</article>
<% } %>

<!-- 包含其他模板 -->
<%- partial('_partial/header') %>

<!-- 循环 -->
<% posts.each(function(post) { %>
  <div><%= post.title %></div>
<% }) %>

实战技巧:

<!-- 1. 条件渲染 -->
<% if (theme.sidebar.enable) { %>
  <%- partial('_partial/sidebar') %>
<% } %>

<!-- 2. 三元表达式 -->
<div class="<%= is_home() ? 'home' : 'page' %>">

<!-- 3. 数组操作 -->
<% page.tags.slice(0, 5).each(function(tag) { %>
  <span><%= tag.name %></span>
<% }) %>

<!-- 4. 默认值 -->
<%= page.title || config.title %>

<!-- 5. 安全访问 -->
<%= page.author?.name || 'Anonymous' %>

hexo-renderer-marked(Markdown 渲染器)

作用: 渲染 .md 文件为 HTML

核心实现:

/**
 * hexo-renderer-marked 核心逻辑
 */

const { marked } = require('marked');
const stripIndent = require('strip-indent');

hexo.extend.renderer.register('md', 'html', function(data, options) {
  const config = this.config.marked || {};
  
  // 配置 marked
  marked.setOptions({
    gfm: config.gfm !== false,
    breaks: config.breaks !== false,
    pedantic: config.pedantic || false,
    sanitize: config.sanitize || false,
    smartLists: config.smartLists !== false,
    smartypants: config.smartypants !== false,
    highlight: function(code, lang) {
      // 代码高亮
      return highlightCode(code, lang);
    }
  });
  
  // 渲染 Markdown
  return marked(stripIndent(data.text));
}, true);

配置选项:

# _config.yml
marked:
  gfm: true                    # GitHub Flavored Markdown
  breaks: true                 # 单行换行符转为 <br>
  pedantic: false              # 严格模式
  sanitize: false              # 是否过滤 HTML
  smartLists: true             # 优化列表
  smartypants: true            # 智能标点
  modifyAnchors: 0             # 标题锚点修改(0/1/2)
  autolink: true               # 自动链接
  
  # 外部链接
  external_link:
    enable: true
    exclude: []
    nofollow: true
  
  # 代码高亮(需要配合 highlight.js)
  highlight:
    enable: true
    line_number: true
    auto_detect: false
    tab_replace: '  '
    wrap: true
    hljs: false
  
  # 或使用 prismjs
  prismjs:
    enable: false
    line_number: true

Markdown 语法增强:

<!-- GFM 任务列表 -->
- [x] 已完成任务
- [ ] 未完成任务

<!-- 表格 -->
| Header 1 | Header 2 |
|----------|----------|
| Cell 1   | Cell 2   |

<!-- 删除线 -->
~~删除的文字~~

<!-- 代码块 -->
```javascript
console.log('Hello');

$$ E = mc^2 $$


**自定义渲染器:**

```javascript
// themes/aurora/scripts/renderer-custom.js

const marked = require('marked');
const renderer = new marked.Renderer();

// 自定义标题渲染
renderer.heading = function(text, level) {
  const slug = text.toLowerCase().replace(/\s+/g, '-');
  return `
    <h${level} id="${slug}">
      <a href="#${slug}" class="header-anchor">#</a>
      ${text}
    </h${level}>
  `;
};

// 自定义链接渲染
renderer.link = function(href, title, text) {
  const isExternal = /^https?:\/\//.test(href);
  const attrs = isExternal ? 
    'target="_blank" rel="noopener noreferrer"' : '';
  
  return `<a href="${href}" ${attrs}>${text}</a>`;
};

// 自定义图片渲染
renderer.image = function(href, title, text) {
  return `
    <figure>
      <img src="${href}" alt="${text}" loading="lazy">
      ${title ? `<figcaption>${title}</figcaption>` : ''}
    </figure>
  `;
};

hexo.extend.filter.register('marked:renderer', function(renderer) {
  // 应用自定义渲染器
  return renderer;
});

hexo-renderer-stylus(Stylus 渲染器)

作用: 渲染 .styl 文件为 CSS

核心实现:

/**
 * hexo-renderer-stylus 核心逻辑
 */

const stylus = require('stylus');
const nib = require('nib');

hexo.extend.renderer.register('styl', 'css', function(data, options) {
  const config = this.config.stylus || {};
  
  return new Promise((resolve, reject) => {
    stylus(data.text)
      .set('filename', data.path)
      .set('compress', config.compress || false)
      .set('include css', true)
      .use(nib())
      .render((err, css) => {
        if (err) reject(err);
        else resolve(css);
      });
  });
}, true);

配置选项:

# _config.yml
stylus:
  compress: true         # 压缩 CSS
  sourcemaps:
    comment: false       # 是否添加 sourcemap 注释
    inline: false        # 内联 sourcemap

Stylus 语法:

// themes/aurora/source/css/style.styl

// 变量
$primary-color = #4dabf7
$font-size = 16px

// Mixin
border-radius(n)
  -webkit-border-radius n
  -moz-border-radius n
  border-radius n

// 嵌套
.article
  padding 20px
  
  .title
    font-size 24px
    color $primary-color
    
  &:hover
    background #f5f5f5

// 函数
lighten($primary-color, 10%)
darken($primary-color, 10%)

// 导入
@import 'variables'
@import 'mixins'

替代方案(使用 SCSS):

# 卸载 Stylus
npm uninstall hexo-renderer-stylus

# 安装 SCSS
npm install hexo-renderer-scss --save
# _config.yml
node_sass:
  outputStyle: compressed
  precision: 5
  sourceComments: false

3.3 默认插件协作示例

这些插件如何共同工作:

/**
 * Hexo 生成流程中的插件协作
 */

// 1. 用户执行 hexo generate
hexo.call('generate')

// 2. hexo-renderer-marked 渲染 Markdown
//    source/_posts/hello.md → 渲染为 HTML

// 3. Generator 插件开始工作
//    hexo-generator-index → 生成首页
//    hexo-generator-archive → 生成归档页
//    hexo-generator-category → 生成分类页
//    hexo-generator-tag → 生成标签页

// 4. hexo-renderer-ejs 渲染模板
//    layout/index.ejs → 应用数据 → 生成 HTML

// 5. hexo-renderer-stylus 渲染样式
//    source/css/style.styl → 生成 CSS

// 6. 输出到 public 目录
public/
├── index.html
├── archives/
├── categories/
├── tags/
├── css/
└── ...

3.4 优化默认插件

禁用不需要的插件

# _config.yml

# 方法 1:通过配置禁用
archive_generator:
  enable: false

# 方法 2:在 package.json 中移除依赖
# 然后运行 npm install

扩展默认插件

// scripts/extend-generators.js

/**
 * 扩展 index generator,添加 RSS 链接
 */
hexo.extend.filter.register('before_generate', function() {
  const original = hexo.extend.generator.get('index');
  
  hexo.extend.generator.register('index', function(locals) {
    const result = original.call(this, locals);
    
    // 添加 RSS feed 到首页数据
    if (result && result[0]) {
      result[0].data.feed_url = '/atom.xml';
    }
    
    return result;
  });
});

性能优化

// scripts/optimize-generators.js

/**
 * 缓存 Generator 结果
 */
const cache = new Map();

hexo.extend.filter.register('before_generate', function() {
  cache.clear();
});

function cachedGenerator(name, fn) {
  return function(locals) {
    const key = JSON.stringify(locals);
    
    if (cache.has(key)) {
      hexo.log.debug(`[Cache] Hit: ${name}`);
      return cache.get(key);
    }
    
    const result = fn.call(this, locals);
    cache.set(key, result);
    return result;
  };
}

// 应用缓存
hexo.extend.generator.register('index', 
  cachedGenerator('index', originalIndexGenerator)
);

3.5 实战:自定义 Generator

基于默认 Generator 的原理,创建自定义生成器:

// scripts/generator-timeline.js

/**
 * 时间线生成器
 * 按时间轴展示所有文章
 */

hexo.extend.generator.register('timeline', function(locals) {
  const posts = locals.posts.sort('-date');
  const timeline = {};
  
  // 按年月分组
  posts.forEach(post => {
    const year = post.date.year();
    const month = post.date.format('MM');
    
    if (!timeline[year]) {
      timeline[year] = {};
    }
    
    if (!timeline[year][month]) {
      timeline[year][month] = [];
    }
    
    timeline[year][month].push({
      title: post.title,
      path: post.path,
      date: post.date.format('YYYY-MM-DD'),
      excerpt: post.excerpt
    });
  });
  
  // 生成页面
  return {
    path: 'timeline/index.html',
    layout: ['timeline', 'page'],
    data: {
      timeline: timeline,
      total: posts.length
    }
  };
});

对应的模板:

<!-- layout/timeline.ejs -->
<div class="timeline-page">
  <h1>时间线</h1>
  <p>共 <%= page.total %> 篇文章</p>
  
  <div class="timeline">
    <% 
      const years = Object.keys(page.timeline).sort((a, b) => b - a);
      years.forEach(year => {
    %>
      <div class="timeline-year">
        <h2><%= year %>年</h2>
        
        <%
          const months = Object.keys(page.timeline[year]).sort((a, b) => b - a);
          months.forEach(month => {
        %>
          <div class="timeline-month">
            <h3><%= parseInt(month) %>月</h3>
            
            <ul class="timeline-posts">
              <% page.timeline[year][month].forEach(post => { %>
                <li class="timeline-item">
                  <time><%= post.date %></time>
                  <a href="<%- url_for(post.path) %>">
                    <%= post.title %>
                  </a>
                </li>
              <% }) %>
            </ul>
          </div>
        <% }) %>
      </div>
    <% }) %>
  </div>
</div>

第四部分:常用插件生态

4.1 内容处理类

hexo-renderer-marked(Markdown 渲染)

# _config.yml
marked:
  gfm: true
  breaks: true
  smartLists: true
  smartypants: true

hexo-generator-feed(RSS Feed)

feed:
  type: atom
  path: atom.xml
  limit: 20

hexo-generator-sitemap(站点地图)

sitemap:
  path: sitemap.xml
  rel: false

3.2 部署类

hexo-deployer-git(Git 部署)

deploy:
  type: git
  repo: git@github.com:user/repo.git
  branch: gh-pages

hexo-deployer-rsync(Rsync 部署)

deploy:
  type: rsync
  host: example.com
  user: username
  root: /var/www/html

3.3 优化类

hexo-filter-optimize(资源优化)

filter_optimize:
  enable: true
  js:
    enable: true
    bundle: true
  css:
    enable: true
    bundle: true
  image:
    enable: true

hexo-lazyload-image(图片懒加载)

lazyload:
  enable: true
  onlypost: false
  loadingImg: /images/loading.gif

3.4 功能增强类

hexo-wordcount(字数统计)

<!-- 在模板中使用 -->
<span><%= wordcount(post.content) %> 字</span>
<span><%= min2read(post.content) %> 分钟</span>

hexo-admin(后台管理)

hexo server -d
# 访问 http://localhost:4000/admin/

第四部分:主题与插件协作

4.1 插件检测

在主题中检测插件是否存在:

// themes/aurora/scripts/plugin-detect.js

hexo.extend.filter.register('before_generate', function() {
  const plugins = {
    feed: 'hexo-generator-feed',
    sitemap: 'hexo-generator-sitemap',
    wordcount: 'hexo-wordcount'
  };
  
  Object.keys(plugins).forEach(key => {
    const pluginName = plugins[key];
    const hasPlugin = hexo.extend.helper.list()[key] !== undefined ||
                     hexo.extend.generator.list()[key] !== undefined;
    
    hexo.theme.config[`has_${key}`] = hasPlugin;
    
    if (!hasPlugin) {
      hexo.log.warn(`[Aurora] Plugin ${pluginName} not found`);
    }
  });
});

在模板中使用:

<% if (theme.has_wordcount) { %>
  <span>字数:<%= wordcount(page.content) %></span>
<% } %>

<% if (theme.has_feed) { %>
  <link rel="alternate" href="/atom.xml">
<% } %>

4.2 配置整合

# _config.yml

# 主题配置
theme_config:
  wordcount:
    enable: true
  feed:
    enable: true
    
# 插件配置
feed:
  type: atom
  path: atom.xml
  
wordcount:
  enable: true

4.3 功能扩展

主题提供接口,插件实现功能:

// 主题定义钩子
hexo.extend.filter.register('theme_custom_render', function(data) {
  // 插件可以在这里添加自定义处理
  return data;
});

// 插件使用钩子
hexo.extend.filter.register('theme_custom_render', function(data) {
  // 添加自定义功能
  data.custom = 'value';
  return data;
});

第五部分:插件测试与发布

5.1 单元测试

test/filter.test.js

const Hexo = require('hexo');
const { expect } = require('chai');

describe('Filter Tests', () => {
  let hexo;
  
  beforeEach(() => {
    hexo = new Hexo(__dirname);
    require('../index')(hexo);
  });
  
  it('should register filter', () => {
    const filters = hexo.extend.filter.list();
    expect(filters).to.have.property('after_post_render');
  });
  
  it('should process content', async () => {
    const data = {
      content: '<img src="test.jpg">'
    };
    
    const filter = hexo.extend.filter.get('after_post_render')[0];
    const result = await filter(data);
    
    expect(result.content).to.include('loading="lazy"');
  });
});

5.2 发布到 NPM

准备发布:

# 1. 登录 NPM
npm login

# 2. 检查包名
npm search hexo-image-optimizer

# 3. 发布
npm publish

# 4. 发布测试版
npm publish --tag beta

.npmignore

test/
*.test.js
.github/
.gitignore

版本管理:

# 补丁版本 1.0.0 -> 1.0.1
npm version patch

# 小版本 1.0.0 -> 1.1.0
npm version minor

# 大版本 1.0.0 -> 2.0.0
npm version major

5.3 文档编写

README.md 模板:

# hexo-image-optimizer

> Optimize images for Hexo

## Installation

\`\`\`bash
npm install hexo-image-optimizer --save
\`\`\`

## Usage

Add to `_config.yml`:

\`\`\`yaml
image_optimizer:
  enable: true
  jpg:
    quality: 80
\`\`\`

## API

### Helpers

- `responsive_image(src, alt)` - Generate responsive image

## License

MIT

第六部分:最佳实践

6.1 性能优化

// ✅ 使用缓存
const cache = new Map();

hexo.extend.helper.register('expensive_operation', function(data) {
  const key = JSON.stringify(data);
  
  if (!cache.has(key)) {
    cache.set(key, processData(data));
  }
  
  return cache.get(key);
});

// ✅ 异步处理
hexo.extend.filter.register('after_generate', async function() {
  await Promise.all(
    images.map(img => optimizeImage(img))
  );
});

// ❌ 避免同步阻塞
hexo.extend.filter.register('after_generate', function() {
  images.forEach(img => {
    fs.writeFileSync(img, data); // 阻塞
  });
});

6.2 错误处理

hexo.extend.filter.register('after_post_render', function(data) {
  try {
    return processData(data);
  } catch (err) {
    hexo.log.error('Processing failed:', err);
    return data; // 返回原数据,不中断流程
  }
});

6.3 配置验证

function validateConfig(config) {
  const required = ['apiKey', 'apiSecret'];
  
  for (const key of required) {
    if (!config[key]) {
      throw new Error(`Missing required config: ${key}`);
    }
  }
  
  return true;
}

6.4 日志规范

// 使用统一前缀
hexo.log.info('[plugin-name] Started');
hexo.log.warn('[plugin-name] Warning message');
hexo.log.error('[plugin-name] Error message');
hexo.log.debug('[plugin-name] Debug info');

6.5 版本兼容

// 检查 Hexo 版本
const hexoVersion = require('hexo/package.json').version;
const semver = require('semver');

if (!semver.satisfies(hexoVersion, '>=5.0.0')) {
  hexo.log.error('This plugin requires Hexo >= 5.0.0');
  return;
}

// 检查依赖插件
if (!hexo.extend.helper.get('markdown')) {
  hexo.log.warn('hexo-renderer-marked is required');
}

第七部分:高级主题

7.1 插件间通信

// Plugin A: 发布事件
hexo.extend.filter.register('after_generate', function() {
  hexo.emit('custom:event', { data: 'value' });
});

// Plugin B: 监听事件
hexo.on('custom:event', function(data) {
  console.log('Received:', data);
});

7.2 条件加载

module.exports = function(hexo) {
  // 只在生产环境加载
  if (process.env.NODE_ENV === 'production') {
    require('./lib/production')(hexo);
  }
  
  // 只在开发环境加载
  if (hexo.env.cmd === 'server') {
    require('./lib/development')(hexo);
  }
};

7.3 插件生态系统

插件管理器概念:

// hexo-plugin-manager
class PluginManager {
  constructor(hexo) {
    this.hexo = hexo;
    this.plugins = new Map();
  }
  
  register(name, plugin) {
    this.plugins.set(name, plugin);
  }
  
  get(name) {
    return this.plugins.get(name);
  }
  
  enable(name) {
    const plugin = this.get(name);
    if (plugin) plugin.enable();
  }
  
  disable(name) {
    const plugin = this.get(name);
    if (plugin) plugin.disable();
  }
}

推荐资源


本文是 Hexo 主题开发系列教程的第五章

完整系列:

🎉 系列完结!祝你开发出优秀的 Hexo 主题和插件!