Hexo 主题开发系列教程(五):插件系统与生态
前言
在前四章中,我们完成了一个功能完整的 Aurora 主题。本章将深入探讨 Hexo 插件系统,学习如何开发、使用和发布插件。
本章内容:
- 🔌 插件机制 - 工作原理
- 📦 插件开发 - 实战案例
- 🛠️ 常用插件 - 生态精选
- 🔗 协作模式 - 主题与插件
- 📚 发布管理 - NPM 包
- 🎯 最佳实践 - 开发规范
第一部分:插件机制深度解析
1.1 插件 vs 主题 Scripts
核心区别
| 特性 |
主题 Scripts |
插件 |
| 作用域 |
当前主题 |
全局 |
| 加载时机 |
主题启用时 |
Hexo 启动时 |
| 适用场景 |
主题特定功能 |
通用功能 |
| 分发方式 |
随主题 |
NPM 独立包 |
| 版本管理 |
跟随主题 |
独立版本 |
何时使用插件
✅ 适合做成插件:
- 通用功能(不依赖特定主题)
- 可复用(多个项目都能用)
- 独立维护(有独立版本管理)
- 社区分享(希望其他人也能用)
❌ 不适合做成插件:
- 主题特定功能
- 简单定制(几行代码)
- 临时需求(一次性使用)
1.2 插件加载流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
const Hexo = require('hexo'); const hexo = new Hexo(process.cwd());
hexo.init() .then(() => loadPlugins(hexo)) .then(() => hexo.loadTheme()) .then(() => hexo.call(command, args));
|
1.3 插件结构
标准插件目录:
1 2 3 4 5 6 7 8 9
| hexo-plugin-example/ ├── index.js # 入口文件 ├── lib/ # 核心代码 │ ├── filter.js │ ├── generator.js │ └── helper.js ├── test/ # 测试 ├── package.json └── README.md
|
入口文件模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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); };
module.exports = { init: function(hexo) { }, metadata: { name: 'hexo-plugin-example', version: '1.0.0' } };
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| '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`); }); 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 案例:阅读统计插件
记录和展示文章阅读量。
核心功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| module.exports = function(hexo) { const storage = require('./lib/storage')(hexo); 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>'); }); hexo.extend.generator.register('reading-api', function() { return { path: 'api/reading-stats.json', data: storage.getAll() }; }); };
|
客户端脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| (function() { const path = window.location.pathname; if (!localStorage.getItem(`read_${path}`)) { setTimeout(() => { localStorage.setItem(`read_${path}`, Date.now()); updateCount(path); }, 5000); } 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 案例:代码复制插件
为代码块添加一键复制功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| '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; }); 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/
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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 } }); });
|
配置选项:
1 2 3 4 5
| index_generator: path: '' per_page: 10 order_by: -date
|
生成的页面:
1 2 3 4 5
| public/ ├── index.html # 第一页 ├── page/2/index.html # 第二页 ├── page/3/index.html # 第三页 └── ...
|
实际应用场景:
1 2 3 4 5 6 7 8 9 10 11
| index_generator: per_page: 15
index_generator: order_by: -updated
index_generator: per_page: 0
|
hexo-generator-archive(归档生成器)
作用: 生成归档页面(按年、按月)
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
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 = []; routes.push({ path: archiveConfig.path || 'archives/', data: posts, layout: ['archive', 'index'] }); 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 }); }); } 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; });
|
配置选项:
1 2 3 4 5 6 7
| archive_generator: path: archives per_page: 10 yearly: true monthly: true order_by: -date
|
生成的页面:
1 2 3 4 5 6 7 8 9 10
| public/ └── archives/ ├── index.html # 总归档 ├── 2024/ │ ├── index.html # 2024年归档 │ ├── 01/index.html # 2024年1月 │ ├── 02/index.html # 2024年2月 │ └── ... └── 2025/ └── index.html
|
在模板中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!-- 显示归档列表 --> <% 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(分类生成器)
作用: 为每个分类生成页面
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
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; }, []); });
|
配置选项:
1 2 3 4
| category_generator: path: categories per_page: 10
|
生成的页面:
1 2 3 4 5 6 7 8
| public/ └── categories/ ├── 技术/ │ ├── index.html │ └── page/2/index.html ├── 生活/ │ └── index.html └── ...
|
文章 Front-matter:
1 2 3 4 5 6
| --- title: 文章标题 categories: - 技术 - 前端 ---
|
在模板中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!-- 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(标签生成器)
作用: 为每个标签生成页面
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
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; }, []); });
|
配置选项:
1 2 3 4
| tag_generator: path: tags per_page: 10
|
生成的页面:
1 2 3 4 5 6 7
| public/ └── tags/ ├── JavaScript/ │ └── index.html ├── React/ │ └── index.html └── ...
|
文章 Front-matter:
1 2 3 4 5 6 7
| --- title: 文章标题 tags: - JavaScript - React - 前端 ---
|
在模板中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!-- 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 模板文件
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
const ejs = require('ejs');
hexo.extend.renderer.register('ejs', 'html', function(data, options) { return ejs.render(data.text, { ...options, filename: data.path }); }, true);
|
配置选项:
1 2 3 4
| ejs: compileDebug: false cache: true
|
模板语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <!-- 输出变量 --> <%= title %>
<!-- 原始输出(不转义 HTML) --> <%- content %>
<!-- JavaScript 代码 --> <% if (is_post()) { %> <article>...</article> <% } %>
<!-- 包含其他模板 --> <%- partial('_partial/header') %>
<!-- 循环 --> <% posts.each(function(post) { %> <div><%= post.title %></div> <% }) %>
|
实战技巧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <!-- 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
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
const { marked } = require('marked'); const stripIndent = require('strip-indent');
hexo.extend.renderer.register('md', 'html', function(data, options) { const config = this.config.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); } }); return marked(stripIndent(data.text)); }, true);
|
配置选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| marked: gfm: true breaks: true pedantic: false sanitize: false smartLists: true smartypants: true modifyAnchors: 0 autolink: true external_link: enable: true exclude: [] nofollow: true highlight: enable: true line_number: true auto_detect: false tab_replace: ' ' wrap: true hljs: false prismjs: enable: false line_number: true
|
Markdown 语法增强:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!-- GFM 任务列表 --> - [x] 已完成任务 - [ ] 未完成任务
<!-- 表格 --> | Header 1 | Header 2 | |----------|----------| | Cell 1 | Cell 2 |
<!-- 删除线 --> ~~删除的文字~~
<!-- 代码块 --> ```javascript console.log('Hello');
|
$$
E = mc^2
$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| **自定义渲染器:**
```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
核心实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
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);
|
配置选项:
1 2 3 4 5 6
| stylus: compress: true sourcemaps: comment: false inline: false
|
Stylus 语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
$primary-color = #4dabf7 $font-size = 16px
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):
1 2 3 4 5
| npm uninstall hexo-renderer-stylus
npm install hexo-renderer-scss --save
|
1 2 3 4 5
| node_sass: outputStyle: compressed precision: 5 sourceComments: false
|
3.3 默认插件协作示例
这些插件如何共同工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
hexo.call('generate')
public/ ├── index.html ├── archives/ ├── categories/ ├── tags/ ├── css/ └── ...
|
3.4 优化默认插件
禁用不需要的插件
1 2 3 4 5 6 7 8
|
archive_generator: enable: false
|
扩展默认插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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); if (result && result[0]) { result[0].data.feed_url = '/atom.xml'; } return result; }); });
|
性能优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
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 的原理,创建自定义生成器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
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 } }; });
|
对应的模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <!-- 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 渲染)
1 2 3 4 5 6
| marked: gfm: true breaks: true smartLists: true smartypants: true
|
1 2 3 4
| feed: type: atom path: atom.xml limit: 20
|
hexo-generator-sitemap(站点地图)
1 2 3
| sitemap: path: sitemap.xml rel: false
|
3.2 部署类
hexo-deployer-git(Git 部署)
1 2 3 4
| deploy: type: git repo: git@github.com:user/repo.git branch: gh-pages
|
hexo-deployer-rsync(Rsync 部署)
1 2 3 4 5
| deploy: type: rsync host: example.com user: username root: /var/www/html
|
3.3 优化类
hexo-filter-optimize(资源优化)
1 2 3 4 5 6 7 8 9 10
| filter_optimize: enable: true js: enable: true bundle: true css: enable: true bundle: true image: enable: true
|
hexo-lazyload-image(图片懒加载)
1 2 3 4
| lazyload: enable: true onlypost: false loadingImg: /images/loading.gif
|
3.4 功能增强类
hexo-wordcount(字数统计)
1 2 3
| <!-- 在模板中使用 --> <span><%= wordcount(post.content) %> 字</span> <span><%= min2read(post.content) %> 分钟</span>
|
hexo-admin(后台管理)
第四部分:主题与插件协作
4.1 插件检测
在主题中检测插件是否存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
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`); } }); });
|
在模板中使用:
1 2 3 4 5 6 7
| <% if (theme.has_wordcount) { %> <span>字数:<%= wordcount(page.content) %></span> <% } %>
<% if (theme.has_feed) { %> <link rel="alternate" href="/atom.xml"> <% } %>
|
4.2 配置整合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
theme_config: wordcount: enable: true feed: enable: true
feed: type: atom path: atom.xml wordcount: enable: true
|
4.3 功能扩展
主题提供接口,插件实现功能:
1 2 3 4 5 6 7 8 9 10 11 12
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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 2 3 4 5 6 7 8 9 10 11
| npm login
npm search hexo-image-optimizer
npm publish
npm publish --tag beta
|
.npmignore
1 2 3 4
| test/ *.test.js .github/ .gitignore
|
版本管理:
1 2 3 4 5 6 7 8
| npm version patch
npm version minor
npm version major
|
5.3 文档编写
README.md 模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| # 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 性能优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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 错误处理
1 2 3 4 5 6 7 8
| 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 配置验证
1 2 3 4 5 6 7 8 9 10 11
| 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 日志规范
1 2 3 4 5
| 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 版本兼容
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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 插件间通信
1 2 3 4 5 6 7 8 9
| hexo.extend.filter.register('after_generate', function() { hexo.emit('custom:event', { data: 'value' }); });
hexo.on('custom:event', function(data) { console.log('Received:', data); });
|
7.2 条件加载
1 2 3 4 5 6 7 8 9 10 11
| 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 插件生态系统
插件管理器概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 插件系统:
核心内容
✅ 插件机制
✅ 开发实战
✅ 生态系统
✅ 协作模式
✅ 发布管理
✅ 最佳实践
学习成果
完成本章后,你将能够:
✅ 理解插件的工作机制
✅ 开发功能完整的插件
✅ 测试和发布插件到 NPM
✅ 在主题中优雅地使用插件
✅ 遵循插件开发最佳实践
附录:插件开发清单
开发前
开发中
发布前
发布后
推荐资源
本文是 Hexo 主题开发系列教程的第五章
完整系列:
🎉 系列完结!祝你开发出优秀的 Hexo 主题和插件!